+ * Automatically starts the correct database container based on active Spring profile. + * Uses @ServiceConnection to automatically configure Spring DataSource. + *
+ * Usage: + *
+ * mvn test -Dspring.profiles.active=test,mysql -Dtest=DatabaseConfigurationSmokeTest + * mvn test -Dspring.profiles.active=test,mssql -Dtest=DatabaseConfigurationSmokeTest + *+ */ +@TestConfiguration +public class DatabaseTestConfiguration { + + @Bean + @ServiceConnection + @Profile("mysql") + public MySQLContainer> mysqlContainer() { + System.out.println("DatabaseTestConfiguration: Creating MySQL container bean..."); + MySQLContainer> container = new MySQLContainer<>(DockerImageName.parse("mysql:8.0")) + .withDatabaseName("lcc_test") + .withUsername("test") + .withPassword("test"); + System.out.println("DatabaseTestConfiguration: MySQL container bean created"); + return container; + } + + @Bean + @ServiceConnection + @Profile("mssql") + public MSSQLServerContainer> mssqlContainer() { + System.out.println("DatabaseTestConfiguration: Creating MSSQL container bean..."); + MSSQLServerContainer> container = new MSSQLServerContainer<>( + DockerImageName.parse("mcr.microsoft.com/mssql/server:2022-latest")) + .acceptLicense() + .withPassword("YourStrong!Passw0rd123"); + System.out.println("DatabaseTestConfiguration: MSSQL container bean created"); + return container; + } +} diff --git a/src/test/java/de/avatic/lcc/config/RepositoryTestConfig.java b/src/test/java/de/avatic/lcc/config/RepositoryTestConfig.java new file mode 100644 index 0000000..6bab904 --- /dev/null +++ b/src/test/java/de/avatic/lcc/config/RepositoryTestConfig.java @@ -0,0 +1,64 @@ +package de.avatic.lcc.config; + +import de.avatic.lcc.database.dialect.MySQLDialectProvider; +import de.avatic.lcc.database.dialect.MSSQLDialectProvider; +import de.avatic.lcc.database.dialect.SqlDialectProvider; +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.FilterType; +import org.springframework.context.annotation.Profile; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; + +import javax.sql.DataSource; + +/** + * Test configuration that provides only the beans needed for repository tests. + * Does NOT load the full LccApplication context. + * + * Uses @SpringBootConfiguration to prevent Spring Boot from searching for and loading LccApplication. + * + * Excludes repositories with external dependencies (transformers/services) since we're only testing JDBC layer. + */ +@SpringBootConfiguration +@EnableAutoConfiguration +@ComponentScan( + basePackages = "de.avatic.lcc.repositories", + excludeFilters = @ComponentScan.Filter( + type = FilterType.ASSIGNABLE_TYPE, + classes = { + de.avatic.lcc.repositories.error.DumpRepository.class, + de.avatic.lcc.repositories.NomenclatureRepository.class, + de.avatic.lcc.repositories.premise.DestinationRepository.class + } + ) +) +public class RepositoryTestConfig { + + @Bean + public JdbcTemplate jdbcTemplate(DataSource dataSource) { + return new JdbcTemplate(dataSource); + } + + @Bean + public NamedParameterJdbcTemplate namedParameterJdbcTemplate(DataSource dataSource) { + return new NamedParameterJdbcTemplate(dataSource); + } + + @Bean + @Profile("mysql") + public SqlDialectProvider mysqlDialectProvider() { + System.out.println("RepositoryTestConfig: Creating MySQLDialectProvider"); + return new MySQLDialectProvider(); + } + + @Bean + @Profile("mssql") + public SqlDialectProvider mssqlDialectProvider() { + System.out.println("RepositoryTestConfig: Creating MSSQLDialectProvider"); + return new MSSQLDialectProvider(); + } +} diff --git a/src/test/java/de/avatic/lcc/database/dialect/MSSQLDialectProviderTest.java b/src/test/java/de/avatic/lcc/database/dialect/MSSQLDialectProviderTest.java new file mode 100644 index 0000000..c3d1538 --- /dev/null +++ b/src/test/java/de/avatic/lcc/database/dialect/MSSQLDialectProviderTest.java @@ -0,0 +1,301 @@ +package de.avatic.lcc.database.dialect; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for {@link MSSQLDialectProvider}. + */ +@DisplayName("MSSQLDialectProvider Tests") +class MSSQLDialectProviderTest { + + private MSSQLDialectProvider provider; + + @BeforeEach + void setUp() { + provider = new MSSQLDialectProvider(); + } + + @Nested + @DisplayName("Metadata Tests") + class MetadataTests { + + @Test + @DisplayName("Should return correct dialect name") + void shouldReturnCorrectDialectName() { + assertEquals("Microsoft SQL Server", provider.getDialectName()); + } + + @Test + @DisplayName("Should return correct driver class name") + void shouldReturnCorrectDriverClassName() { + assertEquals("com.microsoft.sqlserver.jdbc.SQLServerDriver", provider.getDriverClassName()); + } + } + + @Nested + @DisplayName("Pagination Tests") + class PaginationTests { + + @Test + @DisplayName("Should build correct pagination clause with OFFSET/FETCH") + void shouldBuildCorrectPaginationClause() { + String result = provider.buildPaginationClause(10, 20); + assertEquals("OFFSET ? ROWS FETCH NEXT ? ROWS ONLY", result); + } + + @Test + @DisplayName("Should return pagination parameters in correct order (offset, limit)") + void shouldReturnPaginationParametersInCorrectOrder() { + Object[] params = provider.getPaginationParameters(10, 20); + // MSSQL: offset first, then limit (reversed from MySQL) + assertArrayEquals(new Object[]{20, 10}, params); + } + } + + @Nested + @DisplayName("Upsert Operation Tests") + class UpsertOperationTests { + + @Test + @DisplayName("Should build correct MERGE statement") + void shouldBuildCorrectMergeStatement() { + List
+ * Provides TestContainers-based database setup for both MySQL and MSSQL. + * Tests extending this class will run against the database specified by the active profile. + * Flyway migrations from db/migration/{mysql|mssql}/ will be automatically applied. + *
+ * Only loads Repository and JDBC beans, not the full application context (no Controllers, no API Services). + *
+ * Usage: + *
+ * // Run against MySQL + * mvn test -Dspring.profiles.active=test,mysql -Dtest=NodeRepositoryIntegrationTest + * + * // Run against MSSQL + * mvn test -Dspring.profiles.active=test,mssql -Dtest=NodeRepositoryIntegrationTest + *+ */ +@SpringBootTest( + classes = {RepositoryTestConfig.class}, + properties = { + "spring.main.web-application-type=none", + "spring.autoconfigure.exclude=" + + "org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration," + + "org.springframework.boot.autoconfigure.security.oauth2.client.servlet.OAuth2ClientAutoConfiguration," + + "org.springframework.boot.autoconfigure.security.oauth2.resource.servlet.OAuth2ResourceServerAutoConfiguration," + + "org.springframework.boot.autoconfigure.webservices.WebServicesAutoConfiguration," + + "org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration" + } +) +@Testcontainers +@Import(DatabaseTestConfiguration.class) +// NOTE: No @ActiveProfiles - profiles come from command line: -Dspring.profiles.active=test,mysql +@Transactional // Rollback after each test for isolation +public abstract class AbstractRepositoryIntegrationTest { + + @Autowired + protected JdbcTemplate jdbcTemplate; + + @Autowired + protected SqlDialectProvider dialectProvider; + + /** + * Gets the active database profile (mysql or mssql). + * Useful for profile-specific test assertions. + */ + protected String getDatabaseProfile() { + return System.getProperty("spring.profiles.active", "mysql"); + } + + /** + * Checks if tests are running against MSSQL. + */ + protected boolean isMssql() { + return getDatabaseProfile().contains("mssql"); + } + + /** + * Checks if tests are running against MySQL. + */ + protected boolean isMysql() { + return getDatabaseProfile().contains("mysql"); + } + + @BeforeEach + void baseSetup() { + // Common setup logic if needed + // Flyway migrations are automatically applied by Spring Boot + } + + /** + * Executes a raw SQL query for test data setup. + * Use with caution - prefer using repositories where possible. + */ + protected void executeRawSql(String sql, Object... params) { + jdbcTemplate.update(sql, params); + } + + /** + * Counts rows in a table. + */ + protected int countRows(String tableName) { + return jdbcTemplate.queryForObject("SELECT COUNT(*) FROM " + tableName, Integer.class); + } +} diff --git a/src/test/java/de/avatic/lcc/repositories/DatabaseConfigurationSmokeTest.java b/src/test/java/de/avatic/lcc/repositories/DatabaseConfigurationSmokeTest.java new file mode 100644 index 0000000..1e73eda --- /dev/null +++ b/src/test/java/de/avatic/lcc/repositories/DatabaseConfigurationSmokeTest.java @@ -0,0 +1,128 @@ +package de.avatic.lcc.repositories; + +import de.avatic.lcc.database.dialect.SqlDialectProvider; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jdbc.core.JdbcTemplate; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Smoke test to verify TestContainers and Flyway setup. + *
+ * Validates: + * - TestContainers starts correctly + * - Flyway migrations run successfully + * - Database contains expected test data + * - Correct SqlDialectProvider is loaded + *
+ * Run with: + *
+ * mvn test -Dspring.profiles.active=test,mysql -Dtest=DatabaseConfigurationSmokeTest + * mvn test -Dspring.profiles.active=test,mssql -Dtest=DatabaseConfigurationSmokeTest + *+ */ +class DatabaseConfigurationSmokeTest extends AbstractRepositoryIntegrationTest { + + @Autowired + private JdbcTemplate jdbcTemplate; + + @Autowired + private SqlDialectProvider dialectProvider; + + @Test + void testDatabaseConnectionIsEstablished() { + // When: Query database + Integer result = jdbcTemplate.queryForObject("SELECT 1", Integer.class); + + // Then: Connection works + assertNotNull(result); + assertEquals(1, result); + } + + @Test + void testFlywayMigrationsRanSuccessfully() { + // When: Check if core tables exist + Integer propertySetCount = jdbcTemplate.queryForObject( + "SELECT COUNT(*) FROM property_set", Integer.class); + + // Then: Table exists (migrations ran) + assertNotNull(propertySetCount); + } + + @Test + void testCountriesWereLoadedFromMigrations() { + // When: Count countries + Integer countryCount = jdbcTemplate.queryForObject( + "SELECT COUNT(*) FROM country", Integer.class); + + // Then: Countries exist (V4__Country.sql ran) + assertNotNull(countryCount); + assertTrue(countryCount > 0, "Countries should be loaded from V4__Country.sql migration"); + System.out.println("Found " + countryCount + " countries in database"); + } + + @Test + void testNodesWereLoadedFromMigrations() { + // When: Count nodes + Integer nodeCount = jdbcTemplate.queryForObject( + "SELECT COUNT(*) FROM node", Integer.class); + + // Then: Nodes exist (V5__Nodes.sql ran) + assertNotNull(nodeCount); + assertTrue(nodeCount > 0, "Nodes should be loaded from V5__Nodes.sql migration"); + System.out.println("Found " + nodeCount + " nodes in database"); + } + + @Test + void testCorrectSqlDialectProviderIsLoaded() { + // Debug: Print active profiles + String[] activeProfiles = jdbcTemplate.getDataSource() != null ? + new String[]{getDatabaseProfile()} : new String[]{}; + System.out.println("Active Spring profiles from getDatabaseProfile(): " + getDatabaseProfile()); + System.out.println("System property spring.profiles.active: " + System.getProperty("spring.profiles.active")); + + // When: Check which dialect provider is active + String booleanTrue = dialectProvider.getBooleanTrue(); + + // Then: Correct provider based on profile + if (isMysql()) { + assertEquals("TRUE", booleanTrue, "MySQL should use TRUE literal"); + } else if (isMssql()) { + assertEquals("1", booleanTrue, "MSSQL should use 1 literal"); + } + + System.out.println("Active database profile: " + getDatabaseProfile()); + System.out.println("Dialect provider class: " + dialectProvider.getClass().getSimpleName()); + } + + @Test + void testBooleanLiteralInQuery() { + // When: Query with boolean literal from dialect provider + String query = "SELECT COUNT(*) FROM node WHERE is_deprecated = " + + dialectProvider.getBooleanFalse(); + Integer activeNodeCount = jdbcTemplate.queryForObject(query, Integer.class); + + // Then: Query executes without syntax error + assertNotNull(activeNodeCount); + System.out.println("Active (non-deprecated) nodes: " + activeNodeCount); + } + + @Test + void testPaginationQuery() { + // When: Execute query with pagination (requires ORDER BY in MSSQL) + String paginationClause = dialectProvider.buildPaginationClause(5, 0); + Object[] paginationParams = dialectProvider.getPaginationParameters(5, 0); + + String query = "SELECT id FROM node ORDER BY id " + paginationClause; + var nodeIds = jdbcTemplate.query(query, + (rs, rowNum) -> rs.getInt("id"), + paginationParams[0], paginationParams[1]); + + // Then: Query executes successfully and returns up to 5 results + assertNotNull(nodeIds); + assertFalse(nodeIds.isEmpty(), "Should return at least one node"); + assertTrue(nodeIds.size() <= 5, "Should return at most 5 nodes"); + System.out.println("Returned " + nodeIds.size() + " nodes with pagination: " + nodeIds); + } +} diff --git a/src/test/java/de/avatic/lcc/repositories/NodeRepositoryIntegrationTest.java b/src/test/java/de/avatic/lcc/repositories/NodeRepositoryIntegrationTest.java new file mode 100644 index 0000000..472b02b --- /dev/null +++ b/src/test/java/de/avatic/lcc/repositories/NodeRepositoryIntegrationTest.java @@ -0,0 +1,208 @@ +package de.avatic.lcc.repositories; + +import de.avatic.lcc.dto.generic.NodeType; +import de.avatic.lcc.repositories.pagination.SearchQueryPagination; +import de.avatic.lcc.repositories.pagination.SearchQueryResult; +import de.avatic.lcc.model.db.nodes.Node; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Integration tests for NodeRepository. + *
+ * Tests critical functionality across both MySQL and MSSQL: + * - Basic CRUD operations + * - Pagination with ORDER BY (MSSQL requirement) + * - Haversine distance calculations + * - Complex search queries + *
+ * Run with: + *
+ * mvn test -Dspring.profiles.active=test,mysql -Dtest=NodeRepositoryIntegrationTest + * mvn test -Dspring.profiles.active=test,mssql -Dtest=NodeRepositoryIntegrationTest + *+ */ +class NodeRepositoryIntegrationTest extends AbstractRepositoryIntegrationTest { + + @Autowired + private NodeRepository nodeRepository; + + @Test + void testInsertAndRetrieveNode() { + // Given + Node node = new Node(); + node.setName("Test Node"); + node.setAddress("Test Address 123"); + node.setGeoLat(new BigDecimal("52.5200")); + node.setGeoLng(new BigDecimal("13.4050")); + node.setDeprecated(false); + node.setCountryId(1); // Assuming country with id=1 exists in Flyway migrations + + // When + Integer nodeId = nodeRepository.insert(node); + + // Then + assertNotNull(nodeId, "Node ID should not be null"); + assertTrue(nodeId > 0, "Node ID should be positive"); + + Optional