Added TestContainers-based testing configuration for MySQL and MSSQL integration tests. Added module test for DialectProviders, Smoketests for TestContainers-based integration tests. NodeRepositoryIntegrationTest. Other Repository integration tests still missing.

This commit is contained in:
Jan 2026-01-27 18:04:08 +01:00
parent 5fb025e4b3
commit 8e6cc8cf07
13 changed files with 942 additions and 15 deletions

11
pom.xml
View file

@ -206,6 +206,11 @@
</dependency>
<!-- TestContainers for multi-database integration testing -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-testcontainers</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
@ -224,6 +229,12 @@
<version>1.19.7</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<version>1.19.7</version>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>

View file

@ -128,11 +128,12 @@ public class MySQLDialectProvider implements SqlDialectProvider {
@Override
public String buildHaversineDistance(String lat1, String lng1, String lat2, String lng2) {
// Haversine formula: 6371000 meters (Earth radius) * acos(...)
// Haversine formula: 6371 km (Earth radius) * acos(...)
// Formula: d = 2R * arcsin(sqrt(sin²((lat2-lat1)/2) + cos(lat1)*cos(lat2)*sin²((lon2-lon1)/2)))
// Simplified: R * acos(cos(lat1)*cos(lat2)*cos(lng2-lng1) + sin(lat1)*sin(lat2))
// Returns distance in KILOMETERS
return String.format(
"6371000 * ACOS(COS(RADIANS(%s)) * COS(RADIANS(%s)) * " +
"6371 * ACOS(COS(RADIANS(%s)) * COS(RADIANS(%s)) * " +
"COS(RADIANS(%s) - RADIANS(%s)) + SIN(RADIANS(%s)) * SIN(RADIANS(%s)))",
lat1, lat2, lng2, lng1, lat1, lat2
);

View file

@ -157,10 +157,10 @@ public class NodeRepository {
@Transactional
public Optional<Integer> setDeprecatedById(Integer id) {
String query = "UPDATE node SET is_deprecated = TRUE WHERE id = ?";
String query = "UPDATE node SET is_deprecated = " + dialectProvider.getBooleanTrue() + " WHERE id = ?";
// Mark all linked RouteNodes as outdated
jdbcTemplate.update("UPDATE premise_route_node SET is_outdated = TRUE WHERE node_id = ?", id);
jdbcTemplate.update("UPDATE premise_route_node SET is_outdated = " + dialectProvider.getBooleanTrue() + " WHERE node_id = ?", id);
return Optional.ofNullable(jdbcTemplate.update(query, id) == 0 ? null : id);
@ -261,7 +261,7 @@ public class NodeRepository {
}
// Mark all linked RouteNodes as outdated
jdbcTemplate.update("UPDATE premise_route_node SET is_outdated = TRUE WHERE node_id = ?", node.getId());
jdbcTemplate.update("UPDATE premise_route_node SET is_outdated = " + dialectProvider.getBooleanTrue() + " WHERE node_id = ?", node.getId());
// Mark all distance matrix entries as stale
jdbcTemplate.update("UPDATE distance_matrix SET state = 'STALE' WHERE ((from_node_id = ?) OR (to_node_id = ?))", node.getId(), node.getId());

View file

@ -0,0 +1,51 @@
package de.avatic.lcc.config;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Profile;
import org.testcontainers.containers.MSSQLServerContainer;
import org.testcontainers.containers.MySQLContainer;
import org.testcontainers.utility.DockerImageName;
/**
* TestContainers configuration for multi-database integration testing.
* <p>
* Automatically starts the correct database container based on active Spring profile.
* Uses @ServiceConnection to automatically configure Spring DataSource.
* <p>
* Usage:
* <pre>
* mvn test -Dspring.profiles.active=test,mysql -Dtest=DatabaseConfigurationSmokeTest
* mvn test -Dspring.profiles.active=test,mssql -Dtest=DatabaseConfigurationSmokeTest
* </pre>
*/
@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;
}
}

View file

@ -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();
}
}

View file

@ -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<String> uniqueCols = Arrays.asList("id", "user_id");
List<String> insertCols = Arrays.asList("id", "user_id", "name", "value");
List<String> updateCols = Arrays.asList("name", "value");
String result = provider.buildUpsertStatement("test_table", uniqueCols, insertCols, updateCols);
assertTrue(result.contains("MERGE INTO test_table AS target"));
assertTrue(result.contains("USING (SELECT"));
assertTrue(result.contains("ON target.id = source.id AND target.user_id = source.user_id"));
assertTrue(result.contains("WHEN MATCHED THEN UPDATE SET"));
assertTrue(result.contains("WHEN NOT MATCHED THEN INSERT"));
assertTrue(result.contains("name = source.name"));
assertTrue(result.contains("value = source.value"));
}
@Test
@DisplayName("Should build correct conditional INSERT statement")
void shouldBuildCorrectInsertIgnoreStatement() {
List<String> columns = Arrays.asList("user_id", "group_id");
List<String> uniqueCols = Arrays.asList("user_id", "group_id");
String result = provider.buildInsertIgnoreStatement("mapping_table", columns, uniqueCols);
assertTrue(result.contains("IF NOT EXISTS"));
assertTrue(result.contains("SELECT 1 FROM mapping_table"));
assertTrue(result.contains("WHERE user_id = ? AND group_id = ?"));
assertTrue(result.contains("INSERT INTO mapping_table (user_id, group_id) VALUES (?, ?)"));
}
}
@Nested
@DisplayName("Locking Strategy Tests")
class LockingStrategyTests {
@Test
@DisplayName("Should build WITH (UPDLOCK, READPAST) for SKIP LOCKED equivalent")
void shouldBuildSelectForUpdateSkipLocked() {
String baseQuery = "SELECT * FROM calculation_job WHERE state = 'CREATED'";
String result = provider.buildSelectForUpdateSkipLocked(baseQuery);
assertTrue(result.contains("WITH (UPDLOCK, READPAST)"));
assertTrue(result.contains("FROM calculation_job WITH (UPDLOCK, READPAST)"));
}
@Test
@DisplayName("Should build WITH (UPDLOCK, ROWLOCK) for standard locking")
void shouldBuildSelectForUpdate() {
String baseQuery = "SELECT * FROM calculation_job WHERE id = ?";
String result = provider.buildSelectForUpdate(baseQuery);
assertTrue(result.contains("WITH (UPDLOCK, ROWLOCK)"));
assertTrue(result.contains("FROM calculation_job WITH (UPDLOCK, ROWLOCK)"));
assertFalse(result.contains("READPAST"));
}
}
@Nested
@DisplayName("Date/Time Function Tests")
class DateTimeFunctionTests {
@Test
@DisplayName("Should return GETDATE() for current timestamp")
void shouldReturnGetDateForCurrentTimestamp() {
assertEquals("GETDATE()", provider.getCurrentTimestamp());
}
@Test
@DisplayName("Should build date subtraction with GETDATE() using DATEADD")
void shouldBuildDateSubtractionWithGetDate() {
String result = provider.buildDateSubtraction(null, "3", SqlDialectProvider.DateUnit.DAY);
assertEquals("DATEADD(DAY, -3, GETDATE())", result);
}
@Test
@DisplayName("Should build date subtraction with custom base date")
void shouldBuildDateSubtractionWithCustomBaseDate() {
String result = provider.buildDateSubtraction("calculation_date", "60", SqlDialectProvider.DateUnit.MINUTE);
assertEquals("DATEADD(MINUTE, -60, calculation_date)", result);
}
@Test
@DisplayName("Should build date addition with GETDATE() using DATEADD")
void shouldBuildDateAdditionWithGetDate() {
String result = provider.buildDateAddition(null, "7", SqlDialectProvider.DateUnit.DAY);
assertEquals("DATEADD(DAY, 7, GETDATE())", result);
}
@Test
@DisplayName("Should build date addition with custom base date")
void shouldBuildDateAdditionWithCustomBaseDate() {
String result = provider.buildDateAddition("start_date", "1", SqlDialectProvider.DateUnit.MONTH);
assertEquals("DATEADD(MONTH, 1, start_date)", result);
}
@Test
@DisplayName("Should extract date from column using CAST")
void shouldExtractDateFromColumn() {
String result = provider.extractDate("created_at");
assertEquals("CAST(created_at AS DATE)", result);
}
@Test
@DisplayName("Should extract date from expression using CAST")
void shouldExtractDateFromExpression() {
String result = provider.extractDate("GETDATE()");
assertEquals("CAST(GETDATE() AS DATE)", result);
}
}
@Nested
@DisplayName("Auto-increment Reset Tests")
class AutoIncrementResetTests {
@Test
@DisplayName("Should build DBCC CHECKIDENT reset statement")
void shouldBuildAutoIncrementResetStatement() {
String result = provider.buildAutoIncrementReset("test_table");
assertEquals("DBCC CHECKIDENT ('test_table', RESEED, 0)", result);
}
}
@Nested
@DisplayName("Geospatial Distance Tests")
class GeospatialDistanceTests {
@Test
@DisplayName("Should build Haversine distance calculation in kilometers")
void shouldBuildHaversineDistanceCalculation() {
String result = provider.buildHaversineDistance("50.1", "8.6", "node.geo_lat", "node.geo_lng");
// MSSQL uses 6371 km (not 6371000 m like MySQL)
assertTrue(result.contains("6371"));
assertFalse(result.contains("6371000")); // Should NOT be in meters
assertTrue(result.contains("ACOS"));
assertTrue(result.contains("COS"));
assertTrue(result.contains("SIN"));
assertTrue(result.contains("RADIANS"));
assertTrue(result.contains("50.1"));
assertTrue(result.contains("8.6"));
assertTrue(result.contains("node.geo_lat"));
assertTrue(result.contains("node.geo_lng"));
}
}
@Nested
@DisplayName("String/Type Function Tests")
class StringTypeFunctionTests {
@Test
@DisplayName("Should build CONCAT with multiple expressions")
void shouldBuildConcatWithMultipleExpressions() {
String result = provider.buildConcat("first_name", "' '", "last_name");
assertEquals("CONCAT(first_name, ' ', last_name)", result);
}
@Test
@DisplayName("Should build CONCAT with single expression")
void shouldBuildConcatWithSingleExpression() {
String result = provider.buildConcat("column_name");
assertEquals("CONCAT(column_name)", result);
}
@Test
@DisplayName("Should cast to string using VARCHAR")
void shouldCastToString() {
String result = provider.castToString("user_id");
assertEquals("CAST(user_id AS VARCHAR(MAX))", result);
}
}
@Nested
@DisplayName("Bulk Operation Tests")
class BulkOperationTests {
@Test
@DisplayName("Should return INT max value for MSSQL")
void shouldReturnMSSQLIntMaxValue() {
// MSSQL returns INT max value (not BIGINT)
assertEquals("2147483647", provider.getMaxLimitValue());
}
@Test
@DisplayName("Should support RETURNING clause via OUTPUT")
void shouldSupportReturningClause() {
assertTrue(provider.supportsReturningClause());
}
@Test
@DisplayName("Should build OUTPUT clause for RETURNING")
void shouldBuildOutputClause() {
String result = provider.buildReturningClause("id", "name", "created_at");
assertEquals("OUTPUT INSERTED.id, INSERTED.name, INSERTED.created_at", result);
}
}
@Nested
@DisplayName("Schema/DDL Tests")
class SchemaDDLTests {
@Test
@DisplayName("Should return IDENTITY definition")
void shouldReturnIdentityDefinition() {
String result = provider.getAutoIncrementDefinition();
assertEquals("IDENTITY(1,1)", result);
}
@Test
@DisplayName("Should return DATETIME2 with default for timestamp")
void shouldReturnDateTimeWithDefaultDefinition() {
String result = provider.getTimestampDefinition();
assertEquals("DATETIME2 DEFAULT GETDATE()", result);
}
}
@Nested
@DisplayName("Boolean Literal Tests")
class BooleanLiteralTests {
@Test
@DisplayName("Should return '1' for boolean true")
void shouldReturnOneForBooleanTrue() {
assertEquals("1", provider.getBooleanTrue());
}
@Test
@DisplayName("Should return '0' for boolean false")
void shouldReturnZeroForBooleanFalse() {
assertEquals("0", provider.getBooleanFalse());
}
}
}

View file

@ -187,11 +187,13 @@ class MySQLDialectProviderTest {
class GeospatialDistanceTests {
@Test
@DisplayName("Should build Haversine distance calculation")
@DisplayName("Should build Haversine distance calculation in kilometers")
void shouldBuildHaversineDistanceCalculation() {
String result = provider.buildHaversineDistance("50.1", "8.6", "node.geo_lat", "node.geo_lng");
assertTrue(result.contains("6371000"));
// MySQL now uses 6371 km (not 6371000 m) for consistency with MSSQL
assertTrue(result.contains("6371"));
assertFalse(result.contains("6371000")); // Should NOT be in meters
assertTrue(result.contains("ACOS"));
assertTrue(result.contains("COS"));
assertTrue(result.contains("SIN"));

View file

@ -0,0 +1,99 @@
package de.avatic.lcc.repositories;
import de.avatic.lcc.config.DatabaseTestConfiguration;
import de.avatic.lcc.config.RepositoryTestConfig;
import de.avatic.lcc.database.dialect.SqlDialectProvider;
import org.junit.jupiter.api.BeforeEach;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.transaction.annotation.Transactional;
import org.testcontainers.junit.jupiter.Testcontainers;
/**
* Abstract base class for repository integration tests.
* <p>
* 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.
* <p>
* Only loads Repository and JDBC beans, not the full application context (no Controllers, no API Services).
* <p>
* Usage:
* <pre>
* // Run against MySQL
* mvn test -Dspring.profiles.active=test,mysql -Dtest=NodeRepositoryIntegrationTest
*
* // Run against MSSQL
* mvn test -Dspring.profiles.active=test,mssql -Dtest=NodeRepositoryIntegrationTest
* </pre>
*/
@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);
}
}

View file

@ -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.
* <p>
* Validates:
* - TestContainers starts correctly
* - Flyway migrations run successfully
* - Database contains expected test data
* - Correct SqlDialectProvider is loaded
* <p>
* Run with:
* <pre>
* mvn test -Dspring.profiles.active=test,mysql -Dtest=DatabaseConfigurationSmokeTest
* mvn test -Dspring.profiles.active=test,mssql -Dtest=DatabaseConfigurationSmokeTest
* </pre>
*/
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);
}
}

View file

@ -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.
* <p>
* Tests critical functionality across both MySQL and MSSQL:
* - Basic CRUD operations
* - Pagination with ORDER BY (MSSQL requirement)
* - Haversine distance calculations
* - Complex search queries
* <p>
* Run with:
* <pre>
* mvn test -Dspring.profiles.active=test,mysql -Dtest=NodeRepositoryIntegrationTest
* mvn test -Dspring.profiles.active=test,mssql -Dtest=NodeRepositoryIntegrationTest
* </pre>
*/
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<Node> retrieved = nodeRepository.getById(nodeId);
assertTrue(retrieved.isPresent(), "Node should be retrievable after creation");
assertEquals("Test Node", retrieved.get().getName());
assertEquals("Test Address 123", retrieved.get().getAddress());
}
@Test
void testUpdateNode() {
// Given: Create a node first
Node node = createTestNode("Original Name", "Original Address", "50.0", "10.0");
Integer nodeId = nodeRepository.insert(node);
// When: Update the node
Node updatedNode = nodeRepository.getById(nodeId).orElseThrow();
updatedNode.setName("Updated Name");
updatedNode.setAddress("Updated Address");
nodeRepository.update(updatedNode);
// Then: Verify update
Node result = nodeRepository.getById(nodeId).orElseThrow();
assertEquals("Updated Name", result.getName());
assertEquals("Updated Address", result.getAddress());
}
@Test
void testDeprecateNode() {
// Given: Create a node
Node node = createTestNode("Node to Deprecate", "Address", "50.0", "10.0");
Integer nodeId = nodeRepository.insert(node);
// When: Deprecate the node
nodeRepository.setDeprecatedById(nodeId);
// Then: Verify node is deprecated
Node deprecated = nodeRepository.getById(nodeId).orElseThrow();
assertTrue(deprecated.getDeprecated(), "Node should be marked as deprecated");
}
@Test
void testListNodesWithPagination() {
// Given: Create multiple nodes
for (int i = 1; i <= 5; i++) {
Node node = createTestNode("Pagination Node " + i, "Address " + i, "50." + i, "10." + i);
nodeRepository.insert(node);
}
// When: List nodes with pagination (page 1, size 3)
SearchQueryPagination pagination = new SearchQueryPagination(1, 3);
SearchQueryResult<Node> result = nodeRepository.listNodes(null, false, pagination);
// Then: Verify pagination works (ORDER BY is required for MSSQL)
assertNotNull(result);
assertNotNull(result.toList());
assertTrue(result.toList().size() <= 3, "Should return at most 3 nodes per page");
}
@Test
void testSearchNodeWithFilter() {
// Given: Create nodes with different names
Node node1 = createTestNode("Berlin Node Test", "Berlin Street 1", "52.5200", "13.4050");
Node node2 = createTestNode("Munich Node Test", "Munich Street 1", "48.1351", "11.5820");
Node node3 = createTestNode("Hamburg Node Test", "Hamburg Street 1", "53.5511", "9.9937");
nodeRepository.insert(node1);
nodeRepository.insert(node2);
nodeRepository.insert(node3);
// When: Search for nodes containing "Berlin"
List<Node> results = nodeRepository.searchNode("Berlin", 10, null, false);
// Then: Should find Berlin node
assertFalse(results.isEmpty(), "Should find at least one node");
assertTrue(results.stream().anyMatch(n -> n.getName().contains("Berlin")),
"Should contain Berlin node");
}
@Test
void testGetByDistanceWithHaversineFormula() {
// Given: Create a reference node (Berlin)
Node referenceNode = createTestNode("Berlin Distance Test", "Berlin Center", "52.5200", "13.4050");
referenceNode.setUserNode(false);
Integer refId = nodeRepository.insert(referenceNode);
referenceNode.setId(refId);
// Create a nearby node (Potsdam, ~30km from Berlin)
Node nearbyNode = createTestNode("Potsdam Distance Test", "Potsdam Center", "52.3906", "13.0645");
nodeRepository.insert(nearbyNode);
// Create a far node (Munich, ~500km from Berlin)
Node farNode = createTestNode("Munich Distance Test", "Munich Center", "48.1351", "11.5820");
nodeRepository.insert(farNode);
// When: Get nodes within 100km radius
// The Haversine formula returns distance in kilometers for both MySQL and MSSQL
List<Node> nodesWithin100km = nodeRepository.getByDistance(referenceNode, 100);
// Then: Should find nearby node but not far node
assertNotNull(nodesWithin100km);
assertTrue(nodesWithin100km.stream().anyMatch(n -> n.getName().contains("Potsdam")),
"Should find Potsdam (30km away)");
assertFalse(nodesWithin100km.stream().anyMatch(n -> n.getName().contains("Munich")),
"Should not find Munich (500km away)");
}
@Test
void testGetByDistanceExcludingReferenceNode() {
// Given: Create reference node
Node referenceNode = createTestNode("Reference Node Distance", "Ref Address", "50.0", "10.0");
referenceNode.setUserNode(false);
Integer refId = nodeRepository.insert(referenceNode);
referenceNode.setId(refId);
// Create nearby node
Node nearbyNode = createTestNode("Nearby Node Distance", "Nearby Address", "50.1", "10.1");
nodeRepository.insert(nearbyNode);
// When: Get nodes within large radius
List<Node> results = nodeRepository.getByDistance(referenceNode, 1000);
// Then: Reference node itself should be excluded (via id != ?)
assertFalse(results.stream().anyMatch(n -> n.getId().equals(refId)),
"Reference node should be excluded from results");
}
@Test
void testBooleanLiteralCompatibility() {
// Given: Create deprecated and non-deprecated nodes
Node deprecatedNode = createTestNode("Deprecated Boolean Test", "Addr1", "50.0", "10.0");
Integer depId = nodeRepository.insert(deprecatedNode);
nodeRepository.setDeprecatedById(depId);
Node activeNode = createTestNode("Active Boolean Test", "Addr2", "50.1", "10.1");
nodeRepository.insert(activeNode);
// When: Search excluding deprecated nodes
List<Node> activeNodes = nodeRepository.searchNode("Boolean Test", 100, null, true);
// Then: Should not include deprecated node
assertFalse(activeNodes.stream().anyMatch(n -> n.getId().equals(depId)),
"Should exclude deprecated nodes when excludeDeprecated=true");
}
// ========== Helper Methods ==========
private Node createTestNode(String name, String address, String lat, String lng) {
Node node = new Node();
node.setName(name);
node.setAddress(address);
node.setGeoLat(new BigDecimal(lat));
node.setGeoLng(new BigDecimal(lng));
node.setDeprecated(false);
node.setCountryId(1); // Assuming country id=1 exists
node.setUserNode(false);
return node;
}
}

View file

@ -0,0 +1,17 @@
# MSSQL Test Profile Configuration
# Activate with: mvn test -Dspring.profiles.active=test,mssql
# Flyway Migration - MSSQL specific
spring.flyway.locations=classpath:db/migration/mssql
# TestContainers will automatically provide:
# - spring.datasource.url
# - spring.datasource.username
# - spring.datasource.password
# - spring.datasource.driver-class-name
# Disable calculation job processor in tests
calculation.job.processor.enabled=false
# Disable Azure Maps API calls in tests
# (Mock these services in test classes if needed)

View file

@ -0,0 +1,17 @@
# MySQL Test Profile Configuration
# Activate with: mvn test -Dspring.profiles.active=test,mysql
# Flyway Migration - MySQL specific
spring.flyway.locations=classpath:db/migration/mysql
# TestContainers will automatically provide:
# - spring.datasource.url
# - spring.datasource.username
# - spring.datasource.password
# - spring.datasource.driver-class-name
# Disable calculation job processor in tests
calculation.job.processor.enabled=false
# Disable Azure Maps API calls in tests
# (Mock these services in test classes if needed)

View file

@ -1,12 +1,40 @@
spring.config.import=classpath:env.properties
# Test Configuration for TestContainers-based Integration Tests
# DataSource is automatically configured by @ServiceConnection from TestContainers
spring.application.name=lcc
spring.datasource.url=jdbc:mysql://localhost:3306/${DB_DATABASE}
spring.datasource.username=${DB_USER}
spring.datasource.password=${DB_PASSWORD}
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.sql.init.mode=always
spring.sql.init.continue-on-error=false
spring.sql.init.schema-locations=classpath:schema.sql
# Disable env.properties import for tests (TestContainers provides DB config)
# spring.config.import=classpath:env.properties
# Disable schema.sql - we use Flyway migrations instead
spring.sql.init.mode=never
# Flyway configuration - migrations will run automatically
spring.flyway.enabled=true
spring.flyway.baseline-on-migrate=true
spring.flyway.clean-disabled=false
# Note: spring.flyway.locations is set in application-mysql.properties or application-mssql.properties
# Security disabled for integration tests
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
# Disable Azure AD for tests
spring.cloud.azure.active-directory.enabled=false
# Disable async processing in tests (EUTaxationApiService uses @Async)
spring.task.execution.pool.core-size=0
spring.task.scheduling.enabled=false
# Logging
logging.level.org.flywaydb=INFO
logging.level.org.testcontainers=INFO
logging.level.de.avatic.lcc=DEBUG
logging.level.org.springframework.jdbc=DEBUG