diff --git a/src/test/java/de/avatic/lcc/repositories/rates/ContainerRateRepositoryIntegrationTest.java b/src/test/java/de/avatic/lcc/repositories/rates/ContainerRateRepositoryIntegrationTest.java
new file mode 100644
index 0000000..d23b354
--- /dev/null
+++ b/src/test/java/de/avatic/lcc/repositories/rates/ContainerRateRepositoryIntegrationTest.java
@@ -0,0 +1,358 @@
+package de.avatic.lcc.repositories.rates;
+
+import de.avatic.lcc.dto.generic.TransportType;
+import de.avatic.lcc.model.db.rates.ContainerRate;
+import de.avatic.lcc.model.db.rates.ValidityPeriodState;
+import de.avatic.lcc.repositories.AbstractRepositoryIntegrationTest;
+import de.avatic.lcc.repositories.pagination.SearchQueryPagination;
+import de.avatic.lcc.repositories.pagination.SearchQueryResult;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+
+import java.math.BigDecimal;
+import java.sql.Timestamp;
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Optional;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Integration tests for ContainerRateRepository.
+ *
+ * Tests critical functionality across both MySQL and MSSQL:
+ * - Pagination (LIMIT/OFFSET vs OFFSET/FETCH)
+ * - UPSERT operations (ON DUPLICATE KEY UPDATE vs MERGE)
+ * - Complex JOIN queries with filtering
+ * - Transport type filtering (SEA, RAIL, POST_RUN, ROAD)
+ * - Boolean literals (TRUE/FALSE vs 1/0)
+ *
+ * Run with:
+ *
+ * mvn test -Dspring.profiles.active=test,mysql -Dtest=ContainerRateRepositoryIntegrationTest
+ * mvn test -Dspring.profiles.active=test,mssql -Dtest=ContainerRateRepositoryIntegrationTest
+ *
+ */
+class ContainerRateRepositoryIntegrationTest extends AbstractRepositoryIntegrationTest {
+
+ @Autowired
+ private ContainerRateRepository containerRateRepository;
+
+ private Integer testValidPeriodId;
+ private Integer testCountryDeId;
+ private Integer testCountryUsId;
+ private Integer testNodeHamburgId;
+ private Integer testNodeBremenId;
+ private Integer testNodeNewYorkId;
+
+ @BeforeEach
+ void setupTestData() {
+ // Clean up in correct order (foreign key constraints)
+ jdbcTemplate.update("DELETE FROM container_rate");
+ jdbcTemplate.update("DELETE FROM country_property");
+ jdbcTemplate.update("DELETE FROM country_matrix_rate");
+ jdbcTemplate.update("DELETE FROM node_predecessor_entry");
+ jdbcTemplate.update("DELETE FROM node_predecessor_chain");
+ jdbcTemplate.update("DELETE FROM node");
+ jdbcTemplate.update("DELETE FROM validity_period");
+
+ // Use existing countries from migrations
+ testCountryDeId = jdbcTemplate.queryForObject(
+ "SELECT id FROM country WHERE iso_code = 'DE'", Integer.class);
+ testCountryUsId = jdbcTemplate.queryForObject(
+ "SELECT id FROM country WHERE iso_code = 'US'", Integer.class);
+
+ // Create test validity period
+ testValidPeriodId = createTestValidityPeriod(ValidityPeriodState.VALID,
+ LocalDateTime.now().minusDays(1), null);
+
+ // Create test nodes
+ testNodeHamburgId = createTestNode("Hamburg Port", "HAM", testCountryDeId, false, 53.5, 10.0);
+ testNodeBremenId = createTestNode("Bremen Port", "BRE", testCountryDeId, false, 53.1, 8.8);
+ testNodeNewYorkId = createTestNode("New York Port", "NYC", testCountryUsId, false, 40.7, -74.0);
+
+ // Create test container rates
+ createTestContainerRate(testNodeHamburgId, testNodeNewYorkId, TransportType.SEA,
+ new BigDecimal("2000"), new BigDecimal("1000"), new BigDecimal("2200"), 14, testValidPeriodId);
+ createTestContainerRate(testNodeBremenId, testNodeNewYorkId, TransportType.SEA,
+ new BigDecimal("2100"), new BigDecimal("1050"), new BigDecimal("2300"), 15, testValidPeriodId);
+ createTestContainerRate(testNodeHamburgId, testNodeBremenId, TransportType.RAIL,
+ new BigDecimal("300"), new BigDecimal("150"), new BigDecimal("350"), 1, testValidPeriodId);
+ }
+
+ @Test
+ void testListRatesByPeriodId() {
+ // Given: Valid period ID
+ SearchQueryPagination pagination = new SearchQueryPagination(1, 10);
+
+ // When: List rates by period
+ SearchQueryResult result = containerRateRepository.listRatesByPeriodId(null, pagination, testValidPeriodId);
+
+ // Then: Should return all 3 rates
+ assertNotNull(result);
+ assertEquals(3, result.getTotalElements());
+ assertTrue(result.toList().stream()
+ .allMatch(rate -> rate.getValidityPeriodId().equals(testValidPeriodId)));
+ }
+
+ @Test
+ void testListRatesByPeriodIdWithFilter() {
+ // Given: Filter for "Hamburg"
+ SearchQueryPagination pagination = new SearchQueryPagination(1, 10);
+
+ // When: List rates with filter
+ SearchQueryResult result = containerRateRepository.listRatesByPeriodId("Hamburg", pagination, testValidPeriodId);
+
+ // Then: Should return rates involving Hamburg
+ assertNotNull(result);
+ assertTrue(result.getTotalElements() >= 2, "Should find at least 2 rates with Hamburg");
+ }
+
+ @Test
+ void testListRatesByPeriodIdWithExternalMappingIdFilter() {
+ // Given: Filter for "HAM"
+ SearchQueryPagination pagination = new SearchQueryPagination(1, 10);
+
+ // When: List rates with external mapping ID filter
+ SearchQueryResult result = containerRateRepository.listRatesByPeriodId("HAM", pagination, testValidPeriodId);
+
+ // Then: Should return rates involving Hamburg
+ assertNotNull(result);
+ assertTrue(result.getTotalElements() >= 2, "Should find at least 2 rates with HAM");
+ }
+
+ @Test
+ void testListRatesByPeriodIdPagination() {
+ // Given: Pagination with limit 2
+ SearchQueryPagination pagination = new SearchQueryPagination(1, 2);
+
+ // When: List rates
+ SearchQueryResult result = containerRateRepository.listRatesByPeriodId(null, pagination, testValidPeriodId);
+
+ // Then: Should respect limit
+ assertNotNull(result);
+ assertEquals(2, result.toList().size());
+ assertEquals(3, result.getTotalElements());
+ }
+
+ @Test
+ void testGetById() {
+ // Given: Get first rate ID
+ SearchQueryPagination pagination = new SearchQueryPagination(1, 1);
+ SearchQueryResult result = containerRateRepository.listRatesByPeriodId(null, pagination, testValidPeriodId);
+ Integer rateId = result.toList().getFirst().getId();
+
+ // When: Get by ID
+ Optional rate = containerRateRepository.getById(rateId);
+
+ // Then: Should retrieve correct rate
+ assertTrue(rate.isPresent());
+ assertEquals(rateId, rate.get().getId());
+ assertNotNull(rate.get().getRateFeu());
+ assertNotNull(rate.get().getRateTeu());
+ assertNotNull(rate.get().getFromNodeId());
+ assertNotNull(rate.get().getToNodeId());
+ }
+
+ @Test
+ void testGetByIdNotFound() {
+ // When: Get non-existent ID
+ Optional rate = containerRateRepository.getById(99999);
+
+ // Then: Should not find
+ assertFalse(rate.isPresent());
+ }
+
+ @Test
+ void testListAllRatesByPeriodId() {
+ // When: List all rates for valid period
+ List rates = containerRateRepository.listAllRatesByPeriodId(testValidPeriodId);
+
+ // Then: Should return all 3 rates
+ assertNotNull(rates);
+ assertEquals(3, rates.size());
+ assertTrue(rates.stream().allMatch(rate -> rate.getValidityPeriodId().equals(testValidPeriodId)));
+ }
+
+ @Test
+ void testFindRoutesByStartNodeIdAndDestinationCountryId() {
+ // When: Find routes from Hamburg to US
+ List routes = containerRateRepository.findRoutesByStartNodeIdAndDestinationCountryId(
+ testNodeHamburgId, List.of(testCountryUsId));
+
+ // Then: Should find Hamburg -> New York route
+ assertNotNull(routes);
+ assertEquals(1, routes.size());
+ assertEquals(testNodeHamburgId, routes.getFirst().getFromNodeId());
+ assertEquals(testNodeNewYorkId, routes.getFirst().getToNodeId());
+ assertEquals(TransportType.SEA, routes.getFirst().getType());
+ }
+
+ @Test
+ void testFindRoutesByStartNodeIdAndDestinationCountryIdMultiple() {
+ // When: Find routes from Hamburg to DE or US
+ List routes = containerRateRepository.findRoutesByStartNodeIdAndDestinationCountryId(
+ testNodeHamburgId, List.of(testCountryDeId, testCountryUsId));
+
+ // Then: Should find both routes (Hamburg -> Bremen and Hamburg -> New York)
+ assertNotNull(routes);
+ assertEquals(2, routes.size());
+ assertTrue(routes.stream().allMatch(r -> r.getFromNodeId().equals(testNodeHamburgId)));
+ }
+
+ @Test
+ void testFindRoutesByStartNodeIdAndDestinationCountryIdEmpty() {
+ // When: Find routes with empty destination list
+ List routes = containerRateRepository.findRoutesByStartNodeIdAndDestinationCountryId(
+ testNodeHamburgId, List.of());
+
+ // Then: Should return empty list
+ assertNotNull(routes);
+ assertTrue(routes.isEmpty());
+ }
+
+ @Test
+ void testGetPostRunsFor() {
+ // Given: Create a main run and post-run
+ Integer testNodeWarehouseId = createTestNode("Warehouse", "WH1", testCountryUsId, false, 40.8, -74.1);
+ createTestContainerRate(testNodeNewYorkId, testNodeWarehouseId, TransportType.POST_RUN,
+ new BigDecimal("100"), new BigDecimal("50"), new BigDecimal("120"), 1, testValidPeriodId);
+
+ ContainerRate mainRun = new ContainerRate();
+ mainRun.setToNodeId(testNodeNewYorkId);
+
+ // When: Get post runs
+ List postRuns = containerRateRepository.getPostRunsFor(mainRun);
+
+ // Then: Should find the post-run
+ assertNotNull(postRuns);
+ assertEquals(1, postRuns.size());
+ assertEquals(testNodeNewYorkId, postRuns.getFirst().getFromNodeId());
+ assertEquals(TransportType.POST_RUN, postRuns.getFirst().getType());
+ }
+
+ @Test
+ void testFindRouteWithPeriodId() {
+ // When: Find route Hamburg -> New York SEA in valid period
+ Optional route = containerRateRepository.findRoute(
+ testNodeHamburgId, testNodeNewYorkId, testValidPeriodId, TransportType.SEA);
+
+ // Then: Should find route
+ assertTrue(route.isPresent());
+ assertEquals(testNodeHamburgId, route.get().getFromNodeId());
+ assertEquals(testNodeNewYorkId, route.get().getToNodeId());
+ assertEquals(TransportType.SEA, route.get().getType());
+ assertEquals(0, new BigDecimal("2000").compareTo(route.get().getRateFeu()));
+ }
+
+ @Test
+ void testFindRouteWithPeriodIdNotFound() {
+ // When: Find route with wrong transport type
+ Optional route = containerRateRepository.findRoute(
+ testNodeHamburgId, testNodeNewYorkId, testValidPeriodId, TransportType.ROAD);
+
+ // Then: Should not find
+ assertFalse(route.isPresent());
+ }
+
+ @Test
+ void testFindRouteWithoutPeriodId() {
+ // When: Find route Hamburg -> New York SEA (uses VALID period)
+ Optional route = containerRateRepository.findRoute(
+ testNodeHamburgId, testNodeNewYorkId, TransportType.SEA);
+
+ // Then: Should find route
+ assertTrue(route.isPresent());
+ assertEquals(testNodeHamburgId, route.get().getFromNodeId());
+ assertEquals(testNodeNewYorkId, route.get().getToNodeId());
+ assertEquals(TransportType.SEA, route.get().getType());
+ }
+
+ @Test
+ void testInsertNewRate() {
+ // Given: New container rate
+ ContainerRate newRate = new ContainerRate();
+ newRate.setFromNodeId(testNodeBremenId);
+ newRate.setToNodeId(testNodeHamburgId);
+ newRate.setType(TransportType.ROAD);
+ newRate.setRateFeu(new BigDecimal("200"));
+ newRate.setRateTeu(new BigDecimal("100"));
+ newRate.setRateHc(new BigDecimal("220"));
+ newRate.setLeadTime(1);
+ newRate.setValidityPeriodId(testValidPeriodId);
+
+ // When: Insert
+ containerRateRepository.insert(newRate);
+
+ // Then: Should be inserted
+ Optional inserted = containerRateRepository.findRoute(
+ testNodeBremenId, testNodeHamburgId, testValidPeriodId, TransportType.ROAD);
+ assertTrue(inserted.isPresent());
+ assertEquals(0, new BigDecimal("200").compareTo(inserted.get().getRateFeu()));
+ }
+
+ @Test
+ void testInsertUpsertExisting() {
+ // Given: Existing rate Hamburg -> New York
+ ContainerRate updateRate = new ContainerRate();
+ updateRate.setFromNodeId(testNodeHamburgId);
+ updateRate.setToNodeId(testNodeNewYorkId);
+ updateRate.setType(TransportType.SEA);
+ updateRate.setRateFeu(new BigDecimal("2500")); // Different rate
+ updateRate.setRateTeu(new BigDecimal("1250"));
+ updateRate.setRateHc(new BigDecimal("2700"));
+ updateRate.setLeadTime(12);
+ updateRate.setValidityPeriodId(testValidPeriodId);
+
+ // When: Insert (should upsert)
+ containerRateRepository.insert(updateRate);
+
+ // Then: Rate should be updated
+ Optional updated = containerRateRepository.findRoute(
+ testNodeHamburgId, testNodeNewYorkId, testValidPeriodId, TransportType.SEA);
+ assertTrue(updated.isPresent());
+ assertEquals(0, new BigDecimal("2500").compareTo(updated.get().getRateFeu()));
+ assertEquals(12, updated.get().getLeadTime());
+
+ // Should still have only 3 rates total
+ List allRates = containerRateRepository.listAllRatesByPeriodId(testValidPeriodId);
+ assertEquals(3, allRates.size());
+ }
+
+ // ========== Helper Methods ==========
+
+ private Integer createTestValidityPeriod(ValidityPeriodState state, LocalDateTime startDate, LocalDateTime endDate) {
+ String sql = "INSERT INTO validity_period (state, start_date, end_date) VALUES (?, ?, ?)";
+ Timestamp startTs = Timestamp.valueOf(startDate);
+ Timestamp endTs = endDate != null ? Timestamp.valueOf(endDate) : null;
+ executeRawSql(sql, state.name(), startTs, endTs);
+
+ String selectSql = isMysql() ? "SELECT LAST_INSERT_ID()" : "SELECT CAST(@@IDENTITY AS INT)";
+ return jdbcTemplate.queryForObject(selectSql, Integer.class);
+ }
+
+ private Integer createTestNode(String name, String externalMappingId, Integer countryId, boolean isDeprecated,
+ double geoLat, double geoLng) {
+ String isDeprecatedValue = isDeprecated ? dialectProvider.getBooleanTrue() : dialectProvider.getBooleanFalse();
+ String sql = String.format(
+ "INSERT INTO node (name, external_mapping_id, country_id, is_deprecated, is_source, is_destination, is_intermediate, address, geo_lat, geo_lng) " +
+ "VALUES (?, ?, ?, %s, %s, %s, %s, 'Test Address', ?, ?)",
+ isDeprecatedValue,
+ dialectProvider.getBooleanTrue(),
+ dialectProvider.getBooleanTrue(),
+ dialectProvider.getBooleanTrue());
+ executeRawSql(sql, name, externalMappingId, countryId, geoLat, geoLng);
+
+ String selectSql = isMysql() ? "SELECT LAST_INSERT_ID()" : "SELECT CAST(@@IDENTITY AS INT)";
+ return jdbcTemplate.queryForObject(selectSql, Integer.class);
+ }
+
+ private void createTestContainerRate(Integer fromNodeId, Integer toNodeId, TransportType type,
+ BigDecimal rateFeu, BigDecimal rateTeu, BigDecimal rateHc,
+ int leadTime, Integer validityPeriodId) {
+ String sql = "INSERT INTO container_rate (from_node_id, to_node_id, container_rate_type, rate_feu, rate_teu, rate_hc, lead_time, validity_period_id) " +
+ "VALUES (?, ?, ?, ?, ?, ?, ?, ?)";
+ executeRawSql(sql, fromNodeId, toNodeId, type.name(), rateFeu, rateTeu, rateHc, leadTime, validityPeriodId);
+ }
+}
diff --git a/src/test/java/de/avatic/lcc/repositories/users/GroupRepositoryIntegrationTest.java b/src/test/java/de/avatic/lcc/repositories/users/GroupRepositoryIntegrationTest.java
new file mode 100644
index 0000000..1744cca
--- /dev/null
+++ b/src/test/java/de/avatic/lcc/repositories/users/GroupRepositoryIntegrationTest.java
@@ -0,0 +1,215 @@
+package de.avatic.lcc.repositories.users;
+
+import de.avatic.lcc.model.db.users.Group;
+import de.avatic.lcc.repositories.AbstractRepositoryIntegrationTest;
+import de.avatic.lcc.repositories.pagination.SearchQueryPagination;
+import de.avatic.lcc.repositories.pagination.SearchQueryResult;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Integration tests for GroupRepository.
+ *
+ * Tests critical functionality across both MySQL and MSSQL:
+ * - Pagination (LIMIT/OFFSET vs OFFSET/FETCH)
+ * - UPSERT operations (ON DUPLICATE KEY UPDATE vs MERGE)
+ * - IN clause with dynamic parameters
+ *
+ * Run with:
+ *
+ * mvn test -Dspring.profiles.active=test,mysql -Dtest=GroupRepositoryIntegrationTest
+ * mvn test -Dspring.profiles.active=test,mssql -Dtest=GroupRepositoryIntegrationTest
+ *
+ */
+class GroupRepositoryIntegrationTest extends AbstractRepositoryIntegrationTest {
+
+ @Autowired
+ private GroupRepository groupRepository;
+
+ @BeforeEach
+ void setupTestData() {
+ // Clean up groups
+ jdbcTemplate.update("DELETE FROM sys_user_group_mapping");
+ jdbcTemplate.update("DELETE FROM sys_group");
+
+ // Create test groups
+ createTestGroup("Administrators", "Admin users with full access");
+ createTestGroup("Developers", "Software developers");
+ createTestGroup("Analysts", "Data analysts");
+ createTestGroup("Viewers", "Read-only users");
+ }
+
+ @Test
+ void testListGroups() {
+ // Given: Pagination
+ SearchQueryPagination pagination = new SearchQueryPagination(1, 10);
+
+ // When: List groups
+ SearchQueryResult result = groupRepository.listGroups(pagination);
+
+ // Then: Should return all groups
+ assertNotNull(result);
+ assertEquals(4, result.getTotalElements());
+ assertFalse(result.toList().isEmpty());
+ }
+
+ @Test
+ void testListGroupsPagination() {
+ // Given: Pagination with limit 2
+ SearchQueryPagination pagination = new SearchQueryPagination(1, 2);
+
+ // When: List groups
+ SearchQueryResult result = groupRepository.listGroups(pagination);
+
+ // Then: Should respect limit
+ assertNotNull(result);
+ assertEquals(2, result.toList().size());
+ assertEquals(4, result.getTotalElements());
+ }
+
+ @Test
+ void testListGroupsOrdering() {
+ // Given: Pagination
+ SearchQueryPagination pagination = new SearchQueryPagination(1, 10);
+
+ // When: List groups
+ SearchQueryResult result = groupRepository.listGroups(pagination);
+
+ // Then: Should be ordered by group_name
+ assertNotNull(result);
+ List groups = result.toList();
+ for (int i = 1; i < groups.size(); i++) {
+ assertTrue(groups.get(i - 1).getName().compareTo(groups.get(i).getName()) <= 0,
+ "Groups should be ordered alphabetically by name");
+ }
+ }
+
+ @Test
+ void testFindGroupIds() {
+ // When: Find group IDs by names
+ List ids = groupRepository.findGroupIds(List.of("Administrators", "Developers"));
+
+ // Then: Should find 2 groups
+ assertNotNull(ids);
+ assertEquals(2, ids.size());
+ }
+
+ @Test
+ void testFindGroupIdsSingle() {
+ // When: Find single group ID
+ List ids = groupRepository.findGroupIds(List.of("Administrators"));
+
+ // Then: Should find 1 group
+ assertNotNull(ids);
+ assertEquals(1, ids.size());
+ }
+
+ @Test
+ void testFindGroupIdsNotFound() {
+ // When: Find non-existent group
+ List ids = groupRepository.findGroupIds(List.of("NonExistent"));
+
+ // Then: Should return empty list
+ assertNotNull(ids);
+ assertTrue(ids.isEmpty());
+ }
+
+ @Test
+ void testFindGroupIdsEmptyList() {
+ // When: Find with empty list
+ List ids = groupRepository.findGroupIds(List.of());
+
+ // Then: Should return empty list
+ assertNotNull(ids);
+ assertTrue(ids.isEmpty());
+ }
+
+ @Test
+ void testFindGroupIdsNull() {
+ // When: Find with null
+ List ids = groupRepository.findGroupIds(null);
+
+ // Then: Should return empty list
+ assertNotNull(ids);
+ assertTrue(ids.isEmpty());
+ }
+
+ @Test
+ void testUpdateGroupInsert() {
+ // Given: New group
+ Group newGroup = new Group();
+ newGroup.setName("Testers");
+ newGroup.setDescription("QA testers");
+
+ // When: Update (insert)
+ groupRepository.updateGroup(newGroup);
+
+ // Then: Should be inserted
+ SearchQueryPagination pagination = new SearchQueryPagination(1, 10);
+ SearchQueryResult result = groupRepository.listGroups(pagination);
+ assertEquals(5, result.getTotalElements());
+
+ // Verify the new group exists
+ List ids = groupRepository.findGroupIds(List.of("Testers"));
+ assertEquals(1, ids.size());
+ }
+
+ @Test
+ void testUpdateGroupUpsert() {
+ // Given: Existing group name
+ Group updateGroup = new Group();
+ updateGroup.setName("Administrators");
+ updateGroup.setDescription("Updated admin description");
+
+ // When: Update (upsert)
+ groupRepository.updateGroup(updateGroup);
+
+ // Then: Should update description
+ SearchQueryPagination pagination = new SearchQueryPagination(1, 10);
+ SearchQueryResult result = groupRepository.listGroups(pagination);
+
+ // Should still have 4 groups
+ assertEquals(4, result.getTotalElements());
+
+ // Find the updated group
+ Group updated = result.toList().stream()
+ .filter(g -> "Administrators".equals(g.getName()))
+ .findFirst()
+ .orElseThrow();
+ assertEquals("Updated admin description", updated.getDescription());
+ }
+
+ @Test
+ void testFindGroupIdsMultiple() {
+ // When: Find multiple group IDs
+ List ids = groupRepository.findGroupIds(
+ List.of("Administrators", "Developers", "Analysts"));
+
+ // Then: Should find 3 groups
+ assertNotNull(ids);
+ assertEquals(3, ids.size());
+ }
+
+ @Test
+ void testFindGroupIdsPartialMatch() {
+ // When: Find mix of existing and non-existing groups
+ List ids = groupRepository.findGroupIds(
+ List.of("Administrators", "NonExistent", "Developers"));
+
+ // Then: Should find only existing groups
+ assertNotNull(ids);
+ assertEquals(2, ids.size());
+ }
+
+ // ========== Helper Methods ==========
+
+ private void createTestGroup(String name, String description) {
+ String sql = "INSERT INTO sys_group (group_name, group_description) VALUES (?, ?)";
+ executeRawSql(sql, name, description);
+ }
+}
diff --git a/src/test/java/de/avatic/lcc/repositories/users/UserRepositoryIntegrationTest.java b/src/test/java/de/avatic/lcc/repositories/users/UserRepositoryIntegrationTest.java
new file mode 100644
index 0000000..297e1bd
--- /dev/null
+++ b/src/test/java/de/avatic/lcc/repositories/users/UserRepositoryIntegrationTest.java
@@ -0,0 +1,350 @@
+package de.avatic.lcc.repositories.users;
+
+import de.avatic.lcc.model.db.users.Group;
+import de.avatic.lcc.model.db.users.User;
+import de.avatic.lcc.repositories.AbstractRepositoryIntegrationTest;
+import de.avatic.lcc.repositories.pagination.SearchQueryPagination;
+import de.avatic.lcc.repositories.pagination.SearchQueryResult;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+
+import java.util.List;
+import java.util.Optional;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Integration tests for UserRepository.
+ *
+ * Tests critical functionality across both MySQL and MSSQL:
+ * - Pagination (LIMIT/OFFSET vs OFFSET/FETCH)
+ * - INSERT IGNORE (MySQL) vs MERGE (MSSQL)
+ * - Complex group mapping operations
+ * - User lookup by various fields
+ *
+ * Run with:
+ *
+ * mvn test -Dspring.profiles.active=test,mysql -Dtest=UserRepositoryIntegrationTest
+ * mvn test -Dspring.profiles.active=test,mssql -Dtest=UserRepositoryIntegrationTest
+ *
+ */
+class UserRepositoryIntegrationTest extends AbstractRepositoryIntegrationTest {
+
+ @Autowired
+ private UserRepository userRepository;
+
+ @Autowired
+ private GroupRepository groupRepository;
+
+ private Integer testGroupAdminId;
+ private Integer testGroupDevId;
+
+ @BeforeEach
+ void setupTestData() {
+ // Clean up in correct order
+ jdbcTemplate.update("DELETE FROM sys_user_group_mapping");
+ jdbcTemplate.update("DELETE FROM sys_user");
+ jdbcTemplate.update("DELETE FROM sys_group");
+
+ // Create test groups
+ createTestGroup("Administrators", "Admin users");
+ createTestGroup("Developers", "Dev users");
+ createTestGroup("Viewers", "Read-only users");
+
+ // Get group IDs
+ testGroupAdminId = groupRepository.findGroupIds(List.of("Administrators")).getFirst();
+ testGroupDevId = groupRepository.findGroupIds(List.of("Developers")).getFirst();
+
+ // Create test users
+ createTestUser("WD001", "john.doe@example.com", "John", "Doe", true);
+ createTestUser("WD002", "jane.smith@example.com", "Jane", "Smith", true);
+ createTestUser("WD003", "bob.inactive@example.com", "Bob", "Inactive", false);
+
+ // Create group mappings
+ createUserGroupMapping(getUserIdByWorkday("WD001"), testGroupAdminId);
+ createUserGroupMapping(getUserIdByWorkday("WD002"), testGroupDevId);
+ }
+
+ @Test
+ void testListUsers() {
+ // Given: Pagination
+ SearchQueryPagination pagination = new SearchQueryPagination(1, 10);
+
+ // When: List users
+ SearchQueryResult result = userRepository.listUsers(pagination);
+
+ // Then: Should return all users
+ assertNotNull(result);
+ assertEquals(3, result.getTotalElements());
+ assertFalse(result.toList().isEmpty());
+ }
+
+ @Test
+ void testListUsersPagination() {
+ // Given: Pagination with limit 2
+ SearchQueryPagination pagination = new SearchQueryPagination(1, 2);
+
+ // When: List users
+ SearchQueryResult result = userRepository.listUsers(pagination);
+
+ // Then: Should respect limit
+ assertNotNull(result);
+ assertEquals(2, result.toList().size());
+ assertEquals(3, result.getTotalElements());
+ }
+
+ @Test
+ void testListUsersOrdering() {
+ // Given: Pagination
+ SearchQueryPagination pagination = new SearchQueryPagination(1, 10);
+
+ // When: List users
+ SearchQueryResult result = userRepository.listUsers(pagination);
+
+ // Then: Should be ordered by workday_id
+ assertNotNull(result);
+ List users = result.toList();
+ for (int i = 1; i < users.size(); i++) {
+ assertTrue(users.get(i - 1).getWorkdayId().compareTo(users.get(i).getWorkdayId()) <= 0,
+ "Users should be ordered by workday_id");
+ }
+ }
+
+ @Test
+ void testUpdateInsertNewUser() {
+ // Given: New user
+ User newUser = new User();
+ newUser.setWorkdayId("WD004");
+ newUser.setEmail("new.user@example.com");
+ newUser.setFirstName("New");
+ newUser.setLastName("User");
+ newUser.setActive(true);
+ newUser.setGroups(List.of());
+
+ // When: Update (insert)
+ Integer userId = userRepository.update(newUser);
+
+ // Then: Should be inserted
+ assertNotNull(userId);
+ assertTrue(userId > 0);
+
+ User inserted = userRepository.getById(userId);
+ assertNotNull(inserted);
+ assertEquals("WD004", inserted.getWorkdayId());
+ assertEquals("new.user@example.com", inserted.getEmail());
+ }
+
+ @Test
+ void testUpdateExistingUser() {
+ // Given: Existing user
+ User user = userRepository.getByWorkdayId("WD001").orElseThrow();
+ user.setEmail("john.updated@example.com");
+ user.setFirstName("Johnny");
+
+ // When: Update
+ Integer userId = userRepository.update(user);
+
+ // Then: Should be updated
+ assertNotNull(userId);
+
+ User updated = userRepository.getById(userId);
+ assertEquals("john.updated@example.com", updated.getEmail());
+ assertEquals("Johnny", updated.getFirstName());
+ assertEquals("Doe", updated.getLastName()); // Unchanged
+ }
+
+ @Test
+ void testUpdateUserWithGroups() {
+ // Given: New user with groups
+ User newUser = new User();
+ newUser.setWorkdayId("WD005");
+ newUser.setEmail("grouped.user@example.com");
+ newUser.setFirstName("Grouped");
+ newUser.setLastName("User");
+ newUser.setActive(true);
+
+ Group adminGroup = new Group();
+ adminGroup.setName("Administrators");
+ Group devGroup = new Group();
+ devGroup.setName("Developers");
+ newUser.setGroups(List.of(adminGroup, devGroup));
+
+ // When: Update (insert)
+ Integer userId = userRepository.update(newUser);
+
+ // Then: Should have groups
+ User inserted = userRepository.getById(userId);
+ assertNotNull(inserted.getGroups());
+ assertEquals(2, inserted.getGroups().size());
+ }
+
+ @Test
+ void testUpdateUserRemoveGroups() {
+ // Given: User with groups
+ User user = userRepository.getByWorkdayId("WD001").orElseThrow();
+ assertEquals(1, user.getGroups().size());
+
+ // When: Update with empty groups
+ user.setGroups(List.of());
+ userRepository.update(user);
+
+ // Then: Groups should be removed
+ User updated = userRepository.getById(user.getId());
+ assertTrue(updated.getGroups().isEmpty());
+ }
+
+ @Test
+ void testUpdateUserChangeGroups() {
+ // Given: User with Admin group
+ User user = userRepository.getByWorkdayId("WD001").orElseThrow();
+ assertEquals("Administrators", user.getGroups().getFirst().getName());
+
+ // When: Change to Dev group
+ Group devGroup = new Group();
+ devGroup.setName("Developers");
+ user.setGroups(List.of(devGroup));
+ userRepository.update(user);
+
+ // Then: Should have Dev group
+ User updated = userRepository.getById(user.getId());
+ assertEquals(1, updated.getGroups().size());
+ assertEquals("Developers", updated.getGroups().getFirst().getName());
+ }
+
+ @Test
+ void testCount() {
+ // When: Count users
+ Integer count = userRepository.count();
+
+ // Then: Should return 3
+ assertEquals(3, count);
+ }
+
+ @Test
+ void testGetUserIdByWorkdayId() {
+ // When: Get user ID by workday ID
+ Integer userId = userRepository.getUserIdByWorkdayId("WD001");
+
+ // Then: Should find user
+ assertNotNull(userId);
+ assertTrue(userId > 0);
+ }
+
+ @Test
+ void testGetUserIdByWorkdayIdNotFound() {
+ // When: Get non-existent user
+ Integer userId = userRepository.getUserIdByWorkdayId("NONEXISTENT");
+
+ // Then: Should return null
+ assertNull(userId);
+ }
+
+ @Test
+ void testGetByWorkdayId() {
+ // When: Get user by workday ID
+ Optional user = userRepository.getByWorkdayId("WD001");
+
+ // Then: Should find user
+ assertTrue(user.isPresent());
+ assertEquals("WD001", user.get().getWorkdayId());
+ assertEquals("john.doe@example.com", user.get().getEmail());
+ assertEquals("John", user.get().getFirstName());
+ }
+
+ @Test
+ void testGetByWorkdayIdNotFound() {
+ // When: Get non-existent user
+ Optional user = userRepository.getByWorkdayId("NONEXISTENT");
+
+ // Then: Should not find
+ assertFalse(user.isPresent());
+ }
+
+ @Test
+ void testGetById() {
+ // Given: User ID
+ Integer userId = userRepository.getUserIdByWorkdayId("WD001");
+
+ // When: Get by ID
+ User user = userRepository.getById(userId);
+
+ // Then: Should find user
+ assertNotNull(user);
+ assertEquals("WD001", user.getWorkdayId());
+ }
+
+ @Test
+ void testGetByIdNotFound() {
+ // When: Get non-existent ID
+ User user = userRepository.getById(99999);
+
+ // Then: Should return null
+ assertNull(user);
+ }
+
+ @Test
+ void testGetByEmail() {
+ // When: Get user by email
+ User user = userRepository.getByEmail("john.doe@example.com");
+
+ // Then: Should find user
+ assertNotNull(user);
+ assertEquals("WD001", user.getWorkdayId());
+ assertEquals("john.doe@example.com", user.getEmail());
+ }
+
+ @Test
+ void testGetByEmailNotFound() {
+ // When: Get non-existent email
+ User user = userRepository.getByEmail("nonexistent@example.com");
+
+ // Then: Should return null
+ assertNull(user);
+ }
+
+ @Test
+ void testUserWithGroupMemberships() {
+ // When: Get user with groups
+ User user = userRepository.getByWorkdayId("WD001").orElseThrow();
+
+ // Then: Should have group memberships
+ assertNotNull(user.getGroups());
+ assertEquals(1, user.getGroups().size());
+ assertEquals("Administrators", user.getGroups().getFirst().getName());
+ }
+
+ @Test
+ void testUserWithoutGroupMemberships() {
+ // When: Get user without groups
+ User user = userRepository.getByWorkdayId("WD003").orElseThrow();
+
+ // Then: Should have empty groups
+ assertNotNull(user.getGroups());
+ assertTrue(user.getGroups().isEmpty());
+ }
+
+ // ========== Helper Methods ==========
+
+ private void createTestGroup(String name, String description) {
+ String sql = "INSERT INTO sys_group (group_name, group_description) VALUES (?, ?)";
+ executeRawSql(sql, name, description);
+ }
+
+ private void createTestUser(String workdayId, String email, String firstName, String lastName, boolean isActive) {
+ String isActiveValue = isActive ? dialectProvider.getBooleanTrue() : dialectProvider.getBooleanFalse();
+ String sql = String.format(
+ "INSERT INTO sys_user (workday_id, email, firstname, lastname, is_active) VALUES (?, ?, ?, ?, %s)",
+ isActiveValue);
+ executeRawSql(sql, workdayId, email, firstName, lastName);
+ }
+
+ private Integer getUserIdByWorkday(String workdayId) {
+ return jdbcTemplate.queryForObject("SELECT id FROM sys_user WHERE workday_id = ?", Integer.class, workdayId);
+ }
+
+ private void createUserGroupMapping(Integer userId, Integer groupId) {
+ String sql = "INSERT INTO sys_user_group_mapping (user_id, group_id) VALUES (?, ?)";
+ executeRawSql(sql, userId, groupId);
+ }
+}