**Manual Builder Approach** (host2): Sets each property explicitly using the fluent builder
+ * pattern. Useful when you need fine-grained control or custom configurations.
+ *
+ *
Both approaches produce valid hosts tracked by the HostStateManager.
+ */
+ @Test
+ void testCanInsertMultipleHosts() {
+ // Given: No specific setup needed beyond @BeforeEach
+
+ // When: Inserting multiple hosts into HBI database
+ SeededHost host1 = hostManager.createRhsmHost(orgId).physicalRhel1Socket0Cores().insert();
+
+ SeededHost host2 =
+ hostManager
+ .createRhsmHost(orgId)
+ .infrastructureType("physical")
+ .arch("x86_64")
+ .rhsmFact("IS_VIRTUAL", "false")
+ .rhsmFact("RH_PROD", List.of(RHEL_PRODUCT_ID))
+ .rhsmFact("ARCHITECTURE", "x86_64")
+ .sockets(1)
+ .cores(0)
+ .displayName("Test Host - 1")
+ .insert();
+
+ // Then: Hosts are tracked with expected metadata
+ assertNotNull(host1.hostId(), "Host ID should be generated");
+ assertNotNull(host2.hostId(), "Host ID should be generated");
+ }
+
+ /**
+ * - **Description**: Verify that we can insert a host in the HBI database with multiple
+ * reporters- **Setup**: Component test environment with swatch-tally is running and an instance
+ * of insights db is up - **Action**: Insert a host into the HBI database with multiple
+ * conflicting reporters, satellite facts and rhsm facts ( rhsm facts will overwrite satellite
+ * facts ) - - **Verification**: - a host was returned from the insert - the host returned has a
+ * inventory id - the host returned has an id - the host returned has the expected orgId -
+ * **Expected Result**: The host was inserted into the HBI database
+ */
+ @Test
+ void testCanCreateHostWithMultipleReporters() {
+ // Given: The org has opted in
+ service.createOptInConfig(orgId);
+
+ // When: Inserting multiple hosts into HBI database
+ SeededHost host =
+ hostManager
+ .createSatelliteHost(orgId)
+ // Satellite Facts replicated from prod data
+ .satelliteFact("satellite_version", "6.17.6.2")
+ .satelliteFact("virtual_host_name", "virt-name-for-testing.com")
+ .satelliteFact("virtual_host_uuid", UUID.randomUUID().toString())
+ .satelliteFact("system_purpose_sla", "Standard")
+ .satelliteFact("system_purpose_role", "Red Hat Enterprise Linux Server")
+ .satelliteFact("system_purpose_usage", "Component Testing")
+ .addRhsmReporter()
+ // RHEL Facts replicated from prod data
+ .infrastructureType("physical")
+ .cloudProvider("aws")
+ .arch("x86_64")
+ .providerId(UUID.randomUUID().toString())
+ .rhsmFact("IS_VIRTUAL", "false")
+ .rhsmFact("RH_PROD", List.of("69"))
+ .rhsmFact("ARCHITECTURE", "x86_64")
+ .rhsmFact("Memory", 63)
+ // Set to envoke the nomalization logic that will overwrite the normalize from satellite
+ // facts with the
+ .rhsmFact("SYSPURPOSE_SLA", "Premium")
+ .rhsmFact("SYSPURPOSE_ROLE", "Red Hat Enterprise Linux Server")
+ .rhsmFact("SYSPURPOSE_USAGE", "Component Testing")
+ .sockets(1)
+ .cores(8)
+ .arch("x86_64")
+ .insert();
+
+ assertNotNull(host.hostId(), "Host ID should be generated");
+
+ // When: Nightly tally runs
+ service.tallyOrg(orgId);
+
+ // Then: The instance appears in the instances API
+ OffsetDateTime beginning = OffsetDateTime.now().minusDays(1);
+ OffsetDateTime ending = OffsetDateTime.now().plusDays(1);
+
+ var instanceResponse =
+ service.getInstancesByProduct(orgId, RHEL_FOR_X86.productTag(), beginning, ending);
+
+ assertNotNull(instanceResponse.getData(), "Instance response should have data");
+ assertFalse(instanceResponse.getData().isEmpty(), "Instance response should not be empty");
+
+ assertThatExpectedReportPopulated(orgId, RHEL_FOR_X86.productTag(), beginning, ending, true);
+ }
+
+ /**
+ * - **Description**: Verify that we can insert a host in the HBI database with multiple
+ * reporters- **Setup**: Component test environment with swatch-tally is running and an instance
+ * of insights db is up - **Action**: Insert a host into the HBI database with multiple
+ * conflicting reporters, satellite facts and rhsm facts ( rhsm facts will WILL NOT overwrite the
+ * satellite facts ) - - **Verification**: - a host was returned from the insert - the host
+ * returned has a inventory id - the host returned has a an id - the host returned has the
+ * expected orgId - **Expected Result**: The host was inserted into the HBI database
+ */
+ @Test
+ void testCanCreateHostWithMultipleReportersOverrideRhsm() {
+ // Given: The org has opted in
+ service.createOptInConfig(orgId);
+
+ // When: Inserting multiple hosts into HBI database
+ SeededHost host =
+ hostManager
+ .createSatelliteHost(orgId)
+ // Satellite Facts replicated from prod data
+ .satelliteFact("satellite_version", "6.17.6.2")
+ .satelliteFact("virtual_host_name", "virt-name-for-testing.com")
+ .satelliteFact("virtual_host_uuid", UUID.randomUUID().toString())
+ .satelliteFact("system_purpose_sla", "Standard")
+ .satelliteFact("system_purpose_role", "Red Hat Enterprise Linux Server")
+ .satelliteFact("system_purpose_usage", "Component Testing")
+ .addRhsmReporter()
+ // RHEL Facts replicated from prod data
+ .infrastructureType("physical")
+ .cloudProvider("aws")
+ .arch("x86_64")
+ .providerId(UUID.randomUUID().toString())
+ .rhsmFact("IS_VIRTUAL", "false")
+ .rhsmFact("RH_PROD", List.of("69"))
+ .rhsmFact("ARCHITECTURE", "x86_64")
+ .rhsmFact("Memory", 63)
+ // Set to envoke the skiprshm logic that will use the satellite facts with the
+ .rhsmFact("SYNC_TIMESTAMP", "2026-08-24T00:32:05.179356374Z")
+ .rhsmFact("SYSPURPOSE_SLA", "Premium")
+ .rhsmFact("SYSPURPOSE_ROLE", "Red Hat Enterprise Linux Server")
+ .rhsmFact("SYSPURPOSE_USAGE", "Component Testing")
+ .sockets(1)
+ .cores(8)
+ .arch("x86_64")
+ .insert();
+
+ assertNotNull(host.hostId(), "Host ID should be generated");
+
+ // When: Nightly tally runs
+ service.tallyOrg(orgId);
+
+ // Then: The instance appears in the instances API
+ OffsetDateTime beginning = OffsetDateTime.now().minusDays(1);
+ OffsetDateTime ending = OffsetDateTime.now().plusDays(1);
+
+ var instanceResponse =
+ service.getInstancesByProduct(orgId, RHEL_FOR_X86.productTag(), beginning, ending);
+
+ assertNotNull(instanceResponse.getData(), "Instance response should have data");
+ assertFalse(instanceResponse.getData().isEmpty(), "Instance response should not be empty");
+
+ assertThatExpectedReportPopulated(orgId, RHEL_FOR_X86.productTag(), beginning, ending, false);
}
/**
@@ -106,15 +260,15 @@ void testHbiSeederCanInsert() {
@Test
void testHbiSeederCanDelete() {
// Given: A host is inserted into HBI database
- SeededHost host = hbiSeeder.insertRhelHost(orgId);
+ SeededHost host = hostManager.createRhsmHost(orgId).physicalRhel1Socket0Cores().insert();
assertNotNull(host.hostId(), "Host ID should be generated");
// When: Deleting the host
- hbiSeeder.deleteHost(host.hostId());
+ hostManager.cleanup(host.hostId());
// Then: Host is removed from tracking and database
- assertEquals(0, hbiSeeder.getInsertedHostCount(), "Seeder should track zero hosts");
- assertFalse(hbiSeeder.hostExists(host.hostId()), "Host should not exist in HBI database");
+ assertEquals(0, hostManager.getTrackedCount(), "Seeder should track zero hosts");
+ assertFalse(hostManager.hostExists(host.hostId()), "Host should not exist in HBI database");
}
/**
@@ -127,44 +281,65 @@ void testHbiSeederCanDelete() {
@Test
void testHbiSeederRollbackDeletesAllHosts() {
// Given: Multiple hosts are inserted (mix of RHEL and cloud)
- SeededHost host1 = hbiSeeder.insertRhelHost(orgId);
- SeededHost host2 = hbiSeeder.insertCloudHost(orgId);
- assertEquals(2, hbiSeeder.getInsertedHostCount(), "Seeder should track two hosts");
- assertTrue(hbiSeeder.hostExists(host1.hostId()), "Host 1 should exist in HBI database");
- assertTrue(hbiSeeder.hostExists(host2.hostId()), "Host 2 should exist in HBI database");
+ SeededHost host1 = hostManager.createRhsmHost(orgId).insert();
+ SeededHost host2 = hostManager.createRhsmHost(orgId).insert();
+ assertEquals(2, hostManager.getTrackedCount(), "Seeder should track two hosts");
+ assertTrue(hostManager.hostExists(host1.hostId()), "Host 1 should exist in HBI database");
+ assertTrue(hostManager.hostExists(host2.hostId()), "Host 2 should exist in HBI database");
// When: Rolling back all inserted hosts
- hbiSeeder.deleteAllInsertedHosts();
+ hostManager.cleanupAll();
// Then: All hosts are removed from tracking and database
- assertEquals(0, hbiSeeder.getInsertedHostCount(), "Seeder should track zero hosts");
- assertFalse(hbiSeeder.hostExists(host1.hostId()), "Host 1 should not exist in HBI database");
- assertFalse(hbiSeeder.hostExists(host2.hostId()), "Host 2 should not exist in HBI database");
+ assertEquals(0, hostManager.getTrackedCount(), "Seeder should track zero hosts");
+ assertFalse(hostManager.hostExists(host1.hostId()), "Host 1 should not exist in HBI database");
+ assertFalse(hostManager.hostExists(host2.hostId()), "Host 2 should not exist in HBI database");
}
/**
* - **Description**: Verify that we can insert a RHEL product into the the HBI database -
* **Setup**: Component test environment with swatch-tally is running, an instance of insights db
* - **Action**: Create a host with a product that is a RHEL product - **Verification**: - verify
- * that a tally Report for the RHEL product is not null - verify that the total sockets value in
- * the tally Report is greather that or equal to the 2 sockets - **Expected Result**: All the host
- * inserted into the db have been deleted from the database
+ * that a tally Report for the RHEL product is not null - verify that the socket count increased
+ * by 2 - **Expected Result**: The tally socket count increased by 2
*/
@Test
void testNightlyTallyRhelProduct() {
- // Given: Org is opted in and RHEL host exists with known capacity
+ // Given: Org is opted in
service.createOptInConfig(orgId);
- SeededHost host = hbiSeeder.rhelHost(orgId).cores(8).sockets(2).insert();
- assertNotNull(host.hostId(), "Host should be created");
- // When: Nightly tally runs
- service.tallyOrg(orgId);
-
- // Then: Tally report contains expected socket count
+ // And: Define time range (today only)
OffsetDateTime beginning = OffsetDateTime.now().minusDays(1);
OffsetDateTime ending = OffsetDateTime.now().plusDays(1);
- var reportData =
+ // When: Capture initial socket count
+ var initialReportData =
+ service.getTallyReportData(
+ orgId,
+ RHEL_FOR_X86.productTag(),
+ "Sockets",
+ Map.of(
+ "granularity", "Daily",
+ "beginning", beginning.toString(),
+ "ending", ending.toString()));
+
+ double initialSockets =
+ initialReportData.getData() != null
+ ? initialReportData.getData().stream()
+ .mapToDouble(point -> point.getValue() != null ? point.getValue() : 0.0)
+ .sum()
+ : 0.0;
+
+ // And: RHEL host with 2 sockets and 8 cores is created
+ SeededHost host =
+ hostManager.createRhsmHost(orgId).physicalRhel2Socket2Cores().cores(8).insert();
+ assertNotNull(host.hostId(), "Host should be created");
+
+ // And: Nightly tally runs
+ service.tallyOrg(orgId);
+
+ // Then: Socket count increased by 2
+ var currentReportData =
service.getTallyReportData(
orgId,
RHEL_FOR_X86.productTag(),
@@ -174,40 +349,129 @@ void testNightlyTallyRhelProduct() {
"beginning", beginning.toString(),
"ending", ending.toString()));
- assertNotNull(reportData, "Tally report should be created");
- assertNotNull(reportData.getData(), "Report should have data");
- assertFalse(reportData.getData().isEmpty(), "Report should not be empty");
+ assertNotNull(currentReportData, "Tally report should be created");
+ assertNotNull(currentReportData.getData(), "Report should have data");
- boolean hasExpectedSockets =
- reportData.getData().stream()
- .anyMatch(point -> point.getValue() != null && point.getValue() == 2.0);
- assertTrue(hasExpectedSockets, "Report should contain a data point with exactly 2 sockets");
+ double currentSockets =
+ currentReportData.getData().stream()
+ .mapToDouble(point -> point.getValue() != null ? point.getValue() : 0.0)
+ .sum();
+
+ assertEquals(
+ initialSockets + 2.0,
+ currentSockets,
+ "Socket count should increase by 2 after adding host with 2 sockets");
}
/**
- * - **Description**: Verify that a cloud host with RHEL product produces a TallySummary Kafka
- * message - **Setup**: Component test environment with swatch-tally is running, an instance of
- * insights db - **Action**: Create a cloud host with RHEL product facts and run nightly tally -
- * **Verification**: - verify that the host exists in HBI - verify that a TallySummary message
- * appears on the tally Kafka topic with the expected product and metric - **Expected Result**: A
- * TallySummary message is produced, proving the host was accepted by the tally process
+ * - **Description**: Verify that a cloud host with RHEL product appears in the instance report -
+ * **Setup**: Component test environment with swatch-tally is running, an instance of insights db
+ * - **Action**: Create a cloud host with RHEL product facts and run nightly tally -
+ * **Verification**: - verify that the host exists in HBI - verify that the instance appears in
+ * the instances API - **Expected Result**: The instance is visible in the instance report
*/
@Test
void testNightlyTallyCloudProduct() {
// Given: Org is opted in and cloud host with RHEL product exists in HBI database
service.createOptInConfig(orgId);
- SeededHost host = hbiSeeder.rhelHost(orgId).cores(8).sockets(2).cloudProvider("aws").insert();
+ SeededHost host = hostManager.createRhsmHost(orgId).awsRhelSockets1Cores1().insert();
assertNotNull(host.hostId(), "Cloud host should be created");
- assertTrue(hbiSeeder.hostExists(host.hostId()), "Cloud host should exist in HBI database");
+ assertTrue(hostManager.hostExists(host.hostId()), "Cloud host should exist in HBI database");
// When: Nightly tally runs
service.tallyOrg(orgId);
- // Then: A TallySummary message is produced on the tally Kafka topic
- kafkaBridge.waitForKafkaMessage(
- TALLY,
- MessageValidators.tallySummaryMatches(
- orgId, RHEL_FOR_X86.productTag(), "Sockets", Granularity.DAILY),
- 1);
+ // Then: The instance appears in the instances API
+ OffsetDateTime beginning = OffsetDateTime.now().minusDays(1);
+ OffsetDateTime ending = OffsetDateTime.now().plusDays(1);
+
+ var instanceResponse =
+ service.getInstancesByProduct(orgId, RHEL_FOR_X86.productTag(), beginning, ending);
+
+ assertNotNull(instanceResponse.getData(), "Instance response should have data");
+ assertFalse(instanceResponse.getData().isEmpty(), "Instance response should not be empty");
+ assertEquals(
+ 1,
+ instanceResponse.getData().size(),
+ "Should have exactly one instance for this org and product");
+ }
+
+ private void assertThatExpectedReportPopulated(
+ String orgId,
+ String productTag,
+ OffsetDateTime beginning,
+ OffsetDateTime ending,
+ Boolean isPremium) {
+ var premiumData =
+ service.getTallyReportData(
+ orgId,
+ productTag,
+ "Sockets",
+ Map.of(
+ "granularity",
+ "Daily",
+ "beginning",
+ beginning.toString(),
+ "ending",
+ ending.toString(),
+ "sla",
+ "Premium"));
+
+ var standardData =
+ service.getTallyReportData(
+ orgId,
+ productTag,
+ "Sockets",
+ Map.of(
+ "granularity",
+ "Daily",
+ "beginning",
+ beginning.toString(),
+ "ending",
+ ending.toString(),
+ "sla",
+ "Standard"));
+
+ if (isPremium) {
+ // Verify Premium has data (RHSM facts won)
+ assertNotNull(premiumData.getData(), "Premium SLA should have data");
+ assertTrue(
+ premiumData.getData().stream().anyMatch(point -> point.getHasData()),
+ "Premium SLA should have at least one data point with hasData=true");
+ assertEquals(
+ ServiceLevelType.PREMIUM,
+ premiumData.getMeta().getServiceLevel(),
+ "Meta should show Premium SLA");
+
+ // Verify Standard has NO data (Satellite facts were overridden)
+ assertNotNull(standardData.getData(), "Standard SLA should return data structure");
+ assertTrue(
+ standardData.getData().stream().noneMatch(point -> point.getHasData()),
+ "Standard SLA should have NO actual data (Satellite SLA was overridden by RHSM Premium)");
+ assertEquals(
+ ServiceLevelType.STANDARD,
+ standardData.getMeta().getServiceLevel(),
+ "Meta should show Standard SLA");
+ } else {
+ // Verify Standard has data (Standard facts being used)
+ assertNotNull(standardData.getData(), "Standard SLA should have data");
+ assertTrue(
+ standardData.getData().stream().anyMatch(point -> point.getHasData()),
+ "Standard SLA should have at least one data point with hasData=true");
+ assertEquals(
+ ServiceLevelType.STANDARD,
+ standardData.getMeta().getServiceLevel(),
+ "Meta should show Standard SLA");
+
+ // Verify Premium has NO data
+ assertNotNull(premiumData.getData(), "Premium SLA should return data structure");
+ assertTrue(
+ premiumData.getData().stream().noneMatch(point -> point.getHasData()),
+ "Premium SLA should have NO actual data");
+ assertEquals(
+ ServiceLevelType.PREMIUM,
+ premiumData.getMeta().getServiceLevel(),
+ "Meta should show Premium SLA");
+ }
}
}
diff --git a/swatch-tally/ct/java/tests/TallyRhelTest.java b/swatch-tally/ct/java/tests/TallyNightlyTest.java
similarity index 80%
rename from swatch-tally/ct/java/tests/TallyRhelTest.java
rename to swatch-tally/ct/java/tests/TallyNightlyTest.java
index 0ed79700c0..b2e5c308d6 100644
--- a/swatch-tally/ct/java/tests/TallyRhelTest.java
+++ b/swatch-tally/ct/java/tests/TallyNightlyTest.java
@@ -28,19 +28,21 @@
import static utils.TallyTestProducts.RHEL_FOR_X86;
import com.redhat.swatch.component.tests.api.TestPlanName;
+import com.redhat.swatch.component.tests.api.hbi.HbiDbConnector;
+import com.redhat.swatch.component.tests.api.hbi.HostConnector.SeededHost;
+import com.redhat.swatch.component.tests.api.hbi.HostStateManager;
import com.redhat.swatch.component.tests.logging.Log;
import com.redhat.swatch.tally.test.model.InstanceData;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.List;
+import java.util.Map;
import java.util.stream.Stream;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
-import utils.TallyHbiDbSeeder;
-import utils.TallyHbiDbSeeder.SeededHost;
/**
* Component tests for RHEL physical host tally with socket increase mapping.
@@ -50,30 +52,35 @@
*
*
Matches IQE test: test_validate_tally_on_physical_rhel_sockets
*/
-public class TallyRhelTest extends BaseTallyComponentTest {
+public class TallyNightlyTest extends BaseTallyComponentTest {
- private TallyHbiDbSeeder hbiSeeder;
+ private HostStateManager hostManager;
+
+ /**
+ * Socket increase mapping for RHEL physical hosts. Maps actual socket count -> reported socket
+ * count for tally.
+ */
+ private static final Map RHEL_PER_SOCKET_INCREASE =
+ Map.of(1, 2, 2, 2, 4, 4, 7, 8);
/**
* Provider for socket increase mapping test parameters. Matches
* IQE's @pytest.mark.parametrize("sockets", rhel_per_socket_increase.keys())
*/
static Stream socketMappingProvider() {
- return TallyHbiDbSeeder.getRhelPerSocketIncreaseMap().entrySet().stream()
+ return RHEL_PER_SOCKET_INCREASE.entrySet().stream()
.map(entry -> Arguments.of(entry.getKey(), entry.getValue()));
}
@BeforeEach
- void setupHbiSeeder() {
- // Initialize HBI seeder with database service (auto-configured for local/OpenShift)
- hbiSeeder = new TallyHbiDbSeeder(hbiDatabase);
+ void setupHostManager() {
+ hostManager = new HostStateManager(new HbiDbConnector(hbiDatabase));
}
@AfterEach
- void cleanupHbiHosts() {
- // Rollback: delete all HBI hosts inserted during test
- if (hbiSeeder != null) {
- hbiSeeder.deleteAllInsertedHosts();
+ void cleanupHosts() {
+ if (hostManager != null) {
+ hostManager.cleanupAll();
}
}
@@ -88,14 +95,11 @@ void cleanupHbiHosts() {
* correct display_name, category, and labeled_measurements
*/
@TestPlanName("nightly-tally-TC001")
- @ParameterizedTest(name = "Physical RHEL: {0} actual sockets -> {1} reported sockets")
+ @ParameterizedTest(name = "Physical RHEL: {0} starting sockets -> {1} reported sockets")
@MethodSource("socketMappingProvider")
void test_validate_tally_on_physical_rhel_sockets(
- int actualSockets, int expectedReportedSockets) {
- String inventoryId = helpers.generateUUIDOfSize(false, 5) + "-" + actualSockets;
- String subscriptionManagerId = helpers.generateUUIDOfSize(false, 5) + "-" + actualSockets;
- String displayName =
- "RHEL Host " + helpers.generateUUIDOfSize(false, 5) + actualSockets + " sockets";
+ int startingSockets, int expectedReportedSockets) {
+
// Given: Org is opted in
service.createOptInConfig(orgId);
@@ -110,19 +114,20 @@ void test_validate_tally_on_physical_rhel_sockets(
Log.info("Initial sockets: %.0f", initialSockets);
// And: Create RHEL host
- int cores = actualSockets; // 1 core per socket (matches IQE)
+ int cores = startingSockets; // 1 core per socket (matches IQE)
+ String displayName = String.format("RHEL-Physical-%dsockets-%dcores", startingSockets, cores);
SeededHost host =
- hbiSeeder
- .rhelHost(orgId)
- .inventoryId("inventory-" + actualSockets)
- .subscriptionManagerId("subman-" + actualSockets)
+ hostManager
+ .createRhsmHost(orgId)
.displayName(displayName)
+ .rhsmFact("RH_PROD", List.of("69"))
+ .rhsmFact("ARCHITECTURE", "x86_64")
.cores(cores)
- .sockets(actualSockets)
+ .sockets(startingSockets)
.insert();
- Log.info("Inserted host %s: %d cores, %d sockets", host.hostId(), cores, actualSockets);
+ Log.info("Inserted host %s: %d cores, %d sockets", host.hostId(), cores, startingSockets);
// And: Run tally
service.tallyOrg(orgId);
@@ -169,6 +174,6 @@ void test_validate_tally_on_physical_rhel_sockets(
instance.getMeasurements().get(socketsIndex),
String.format(
"Labeled measurement should show %d sockets (increased from %d)",
- expectedReportedSockets, actualSockets));
+ expectedReportedSockets, startingSockets));
}
}
diff --git a/swatch-tally/ct/java/utils/TallyHbiDbSeeder.java b/swatch-tally/ct/java/utils/TallyHbiDbSeeder.java
index 3074a58822..72ff27a186 100644
--- a/swatch-tally/ct/java/utils/TallyHbiDbSeeder.java
+++ b/swatch-tally/ct/java/utils/TallyHbiDbSeeder.java
@@ -60,6 +60,19 @@
*/
public final class TallyHbiDbSeeder {
+ // separate functionality of the Seeding, DB connection, and HBI into different classes
+ // Need to be specific about what kind of RHEL host we are creating, so we can use the correct one
+ // Need to specify what facts per RHEL host ( factory for the RHEL host )
+ /*example for Kartik
+ * public HostBuilder rhelHost(String orgId) {
+ return new HostBuilder(orgId, true);
+ }
+
+ public HostBuilder cloudHost(String orgId) {
+ return new HostBuilder(orgId, false).cloudProvider("aws");
+ }
+ * */
+
private final DatabaseService hbiDatabase;
// Default values for test hosts
@@ -192,6 +205,7 @@ public CloudHostBuilder cloudHost(String orgId) {
* Builder for RHEL hosts. Defaults to physical infrastructure; call {@link #cloudProvider} to
* create a "RHEL on cloud" host (virtual infrastructure with cloud provider metadata).
*/
+ // the host
public class RhelHostBuilder {
private final String orgId;
private String inventoryId;
diff --git a/swatch-test-framework/src/main/java/com/redhat/swatch/component/tests/api/hbi/HbiDbConnector.java b/swatch-test-framework/src/main/java/com/redhat/swatch/component/tests/api/hbi/HbiDbConnector.java
new file mode 100644
index 0000000000..6f0725a15a
--- /dev/null
+++ b/swatch-test-framework/src/main/java/com/redhat/swatch/component/tests/api/hbi/HbiDbConnector.java
@@ -0,0 +1,337 @@
+/*
+ * Copyright Red Hat, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * Red Hat trademarks are not licensed under GPLv3. No permission is
+ * granted to use or replicate Red Hat trademarks that are incorporated
+ * in this software or its documentation.
+ */
+package com.redhat.swatch.component.tests.api.hbi;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.redhat.swatch.component.tests.api.db.DatabaseService;
+import com.redhat.swatch.component.tests.api.hbi.HostConnector.SeededHost;
+import com.redhat.swatch.component.tests.logging.Log;
+import com.redhat.swatch.component.tests.utils.AwaitilitySettings;
+import com.redhat.swatch.component.tests.utils.AwaitilityUtils;
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.SQLException;
+import java.time.Duration;
+import java.time.OffsetDateTime;
+import java.time.ZoneOffset;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.UUID;
+import java.util.stream.Collectors;
+
+/**
+ * HBI database connector - inserts hosts directly into the HBI database.
+ *
+ *
This is the fastest approach for component tests but bypasses the Kafka ingestion pipeline.
+ *
+ *
+ */
+public class HbiDbConnector implements HostConnector {
+
+ private final DatabaseService hbiDatabase;
+ private final ObjectMapper objectMapper = new ObjectMapper();
+ private boolean schemaVerified = false;
+
+ public HbiDbConnector(DatabaseService hbiDatabase) {
+ this.hbiDatabase = Objects.requireNonNull(hbiDatabase, "hbiDatabase is required");
+ }
+
+ @Override
+ public SeededHost seed(Host host) {
+
+ verifySchema();
+
+ UUID insightsId =
+ host.getInsightsId() != null ? UUID.fromString(host.getInsightsId()) : UUID.randomUUID();
+ OffsetDateTime now = OffsetDateTime.now(ZoneOffset.UTC);
+
+ // Use provided timestamps or default to now
+ OffsetDateTime createdOn = host.getCreatedOn() != null ? host.getCreatedOn() : now;
+ OffsetDateTime modifiedOn = host.getModifiedOn() != null ? host.getModifiedOn() : now;
+ OffsetDateTime lastCheckIn = host.getLastCheckIn() != null ? host.getLastCheckIn() : now;
+
+ String subscriptionManagerId = host.getSubscriptionManagerId(); // Can be null
+
+ // Validate sockets before INSERT
+ if (host.getSockets() != null && host.getSockets() == 0) {
+ throw new IllegalArgumentException(
+ "Sockets cannot be 0 (would cause division by zero when calculating cores_per_socket)");
+ }
+
+ UUID hostId = null;
+ try (Connection conn = hbiDatabase.getConnection()) {
+ // Insert host and get the database-generated ID
+ hostId = insertHost(conn, insightsId, host, createdOn, modifiedOn, lastCheckIn);
+ insertSystemProfile(conn, hostId, host);
+ } catch (SQLException e) {
+ if (hostId != null && hostExists(hostId)) {
+ // Host already exists, but we failed to insert the system profile
+ // This is a rare case, but it can happen if the host is being re-inserted
+ // after a previous failure.
+ // In this case, we can safely ignore the error and continue.
+ cleanup(hostId);
+ }
+ throw new RuntimeException("Failed to seed host into HBI database", e);
+ }
+
+ // Use the host ID as the inventory ID
+ // In HBI, the hosts.id column serves as the inventory identifier.
+ // There is no separate inventory_id column in the schema.
+ String inventoryId = hostId.toString();
+
+ return new SeededHost(hostId, inventoryId, subscriptionManagerId, host.getOrgId());
+ }
+
+ @Override
+ public List seedBatch(List hosts) {
+ return hosts.stream().map(this::seed).collect(Collectors.toList());
+ }
+
+ @Override
+ public void cleanup(UUID hostId) {
+ try (Connection conn = hbiDatabase.getConnection()) {
+ deleteSystemProfile(conn, hostId);
+ deleteHost(conn, hostId);
+ Log.debug("Cleaned up host: %s", hostId);
+ } catch (SQLException e) {
+ throw new RuntimeException("Failed to cleanup host: " + hostId, e);
+ }
+ }
+
+ @Override
+ public boolean hostExists(UUID hostId) {
+ String sql = "SELECT 1 FROM hbi.hosts WHERE id = ? LIMIT 1";
+ try (Connection conn = hbiDatabase.getConnection();
+ PreparedStatement ps = conn.prepareStatement(sql)) {
+ ps.setObject(1, hostId);
+ var rs = ps.executeQuery();
+ return rs.next();
+ } catch (SQLException e) {
+ throw new RuntimeException("Failed to check if host exists: " + hostId, e);
+ }
+ }
+
+ // ===== Private Helper Methods =====
+
+ private void verifySchema() {
+ if (schemaVerified) {
+ return;
+ }
+ Log.info("Waiting for HBI schema to be ready...");
+ AwaitilityUtils.untilIsTrue(
+ () -> {
+ try (Connection conn = hbiDatabase.getConnection();
+ PreparedStatement ps =
+ conn.prepareStatement(
+ "SELECT COUNT(*) FROM information_schema.tables"
+ + " WHERE table_schema = 'hbi'"
+ + " AND table_name IN ('hosts', 'system_profiles_static')")) {
+ var rs = ps.executeQuery();
+ return rs.next() && rs.getInt(1) == 2;
+ }
+ },
+ AwaitilitySettings.using(Duration.ofSeconds(2), Duration.ofSeconds(120))
+ .timeoutMessage(
+ "HBI schema not ready after 120s - migration job may not have completed"));
+ schemaVerified = true;
+ Log.info("HBI schema is ready.");
+ }
+
+ private UUID insertHost(
+ Connection conn,
+ UUID insightsId,
+ Host host,
+ OffsetDateTime createdOn,
+ OffsetDateTime modifiedOn,
+ OffsetDateTime lastCheckIn)
+ throws SQLException {
+
+ String sql =
+ """
+ INSERT INTO hbi.hosts
+ (id, org_id, display_name, insights_id, subscription_manager_id, provider_id,
+ created_on, modified_on, last_check_in,
+ facts, groups, reporter, reporters)
+ VALUES
+ (gen_random_uuid(), ?, ?, ?, ?, ?,
+ ?, ?, ?,
+ ?::jsonb, ?::jsonb, ?, ?)
+ RETURNING id
+ """;
+
+ try (PreparedStatement ps = conn.prepareStatement(sql)) {
+ ps.setString(1, host.getOrgId());
+ ps.setString(
+ 2, host.getDisplayName() != null ? host.getDisplayName() : "Test Host (auto-generated)");
+ ps.setObject(3, insightsId);
+ ps.setString(4, host.getSubscriptionManagerId());
+ ps.setString(5, host.getProviderId());
+ ps.setObject(6, createdOn);
+ ps.setObject(7, modifiedOn);
+ ps.setObject(8, lastCheckIn);
+ ps.setString(9, buildFactsJson(host));
+ ps.setString(10, "[]"); // groups
+ ps.setString(11, host.getReporter());
+ ps.setArray(12, conn.createArrayOf("varchar", host.getReporters()));
+
+ // Execute query and retrieve the database-generated ID
+ var rs = ps.executeQuery();
+ if (!rs.next()) {
+ throw new SQLException("Failed to retrieve generated host ID from database");
+ }
+ UUID hostId = (UUID) rs.getObject("id");
+ return hostId;
+ } catch (SQLException e) {
+ String errorMessage = "Failed to insert host into hbi.hosts";
+ if (e.getMessage() != null && e.getMessage().contains("does not exist")) {
+ errorMessage +=
+ "\n\nSCHEMA ERROR: Required HBI table or column is missing."
+ + "\nThis usually means:"
+ + "\n 1. HBI database migrations haven't been run (local: see README for setup)"
+ + "\n 2. Schema is outdated (EE: check if host-inventory-run-db-migrations job"
+ + " completed)"
+ + "\n 3. Wrong database selected (verify you're connecting to 'insights'"
+ + " database)"
+ + "\n\nOriginal error: "
+ + e.getMessage();
+ }
+ throw new SQLException(errorMessage, e);
+ }
+ }
+
+ private void insertSystemProfile(Connection conn, UUID hostId, Host host) throws SQLException {
+ String sql =
+ """
+ INSERT INTO hbi.system_profiles_static
+ (org_id, host_id, cores_per_socket, number_of_sockets, number_of_cpus,
+ threads_per_core, infrastructure_type, cloud_provider, arch, is_marketplace,
+ virtual_host_uuid, host_type)
+ VALUES
+ (?, ?, ?, ?, ?,
+ ?, ?, ?, ?, ?,
+ ?::uuid, ?)
+ """;
+
+ try (PreparedStatement ps = conn.prepareStatement(sql)) {
+ ps.setString(1, host.getOrgId());
+ ps.setObject(2, hostId);
+
+ // Calculate cores_per_socket if not explicitly set
+ Integer coresPerSocket = host.getCoresPerSocket();
+
+ // Set nullable integers
+ if (coresPerSocket != null) {
+ ps.setInt(3, coresPerSocket);
+ } else {
+ ps.setNull(3, java.sql.Types.INTEGER);
+ }
+
+ if (host.getSockets() != null) {
+ ps.setInt(4, host.getSockets());
+ } else {
+ ps.setNull(4, java.sql.Types.INTEGER);
+ }
+
+ if (host.getCores() != null) {
+ ps.setInt(5, host.getCores());
+ } else {
+ ps.setNull(5, java.sql.Types.INTEGER);
+ }
+
+ if (host.getThreadsPerCore() != null) {
+ ps.setInt(6, host.getThreadsPerCore());
+ } else {
+ ps.setNull(6, java.sql.Types.INTEGER);
+ }
+
+ ps.setString(7, host.getInfrastructureType());
+ ps.setString(8, host.getCloudProvider());
+ ps.setString(9, host.getArch());
+
+ if (host.getIsMarketplace() != null) {
+ ps.setBoolean(10, host.getIsMarketplace());
+ } else {
+ ps.setNull(10, java.sql.Types.BOOLEAN);
+ }
+
+ ps.setString(11, host.getVirtualHostUuid());
+ ps.setString(12, host.getHostType());
+
+ ps.executeUpdate();
+ } catch (SQLException e) {
+ String errorMessage = "Failed to insert into hbi.system_profiles_static";
+ if (e.getMessage() != null && e.getMessage().contains("does not exist")) {
+ errorMessage +=
+ "\n\nSCHEMA ERROR: system_profiles_static table or column missing."
+ + "\nOriginal error: "
+ + e.getMessage();
+ }
+ throw new SQLException(errorMessage, e);
+ }
+ }
+
+ private void deleteSystemProfile(Connection conn, UUID hostId) throws SQLException {
+ String sql = "DELETE FROM hbi.system_profiles_static WHERE host_id = ?";
+ try (PreparedStatement ps = conn.prepareStatement(sql)) {
+ ps.setObject(1, hostId);
+ ps.executeUpdate();
+ }
+ }
+
+ private void deleteHost(Connection conn, UUID hostId) throws SQLException {
+ String sql = "DELETE FROM hbi.hosts WHERE id = ?";
+ try (PreparedStatement ps = conn.prepareStatement(sql)) {
+ ps.setObject(1, hostId);
+ ps.executeUpdate();
+ }
+ }
+
+ private String buildFactsJson(Host host) {
+ Map facts = new HashMap<>();
+
+ // Add fact groups if they have content
+ if (!host.getRhsmFacts().isEmpty()) {
+ facts.put("rhsm", host.getRhsmFacts());
+ }
+ if (!host.getQpcFacts().isEmpty()) {
+ facts.put("qpc", host.getQpcFacts());
+ facts.put("yupana", host.getYupanaFacts());
+ }
+ if (!host.getSatelliteFacts().isEmpty()) {
+ facts.put("satellite", host.getSatelliteFacts());
+ facts.put("yupana", host.getYupanaFacts());
+ }
+
+ try {
+ return objectMapper.writeValueAsString(facts);
+ } catch (JsonProcessingException e) {
+ throw new RuntimeException("Failed to serialize facts to JSON", e);
+ }
+ }
+}
diff --git a/swatch-test-framework/src/main/java/com/redhat/swatch/component/tests/api/hbi/Host.java b/swatch-test-framework/src/main/java/com/redhat/swatch/component/tests/api/hbi/Host.java
new file mode 100644
index 0000000000..b33d5e4cea
--- /dev/null
+++ b/swatch-test-framework/src/main/java/com/redhat/swatch/component/tests/api/hbi/Host.java
@@ -0,0 +1,469 @@
+/*
+ * Copyright Red Hat, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * Red Hat trademarks are not licensed under GPLv3. No permission is
+ * granted to use or replicate Red Hat trademarks that are incorporated
+ * in this software or its documentation.
+ */
+package com.redhat.swatch.component.tests.api.hbi;
+
+import java.time.OffsetDateTime;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.UUID;
+
+/**
+ * Host data container with fluent setters.
+ *
+ *
Represents host data that will be seeded into the HBI database. Fields align with the
+ * InventoryHost query to support different tally normalization scenarios.
+ *
+ *