fix: resolve all integration test failures after storage layer refactor
- Use singleton container pattern for PostgreSQL + OpenSearch testcontainers (fixes container lifecycle issues with @TestInstance(PER_CLASS)) - Fix table name route_executions → executions in DetailControllerIT and ExecutionControllerIT - Serialize processor headers as JSON (ObjectMapper) instead of Map.toString() for JSONB column compatibility - Add nested mapping for processors field in OpenSearch index template - Use .keyword sub-field for term queries on dynamically mapped text fields - Add wildcard fallback queries for all text searches (substring matching) - Isolate stats tests with unique route names to prevent data contamination - Wait for OpenSearch indexing in SearchControllerIT with targeted Awaitility - Reduce OpenSearch debounce to 100ms in test profile Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
package com.cameleer3.server.app;
|
||||
|
||||
import org.opensearch.testcontainers.OpensearchContainer;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
@@ -7,25 +8,29 @@ import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.test.context.DynamicPropertyRegistry;
|
||||
import org.springframework.test.context.DynamicPropertySource;
|
||||
import org.testcontainers.containers.PostgreSQLContainer;
|
||||
import org.testcontainers.junit.jupiter.Container;
|
||||
import org.testcontainers.junit.jupiter.Testcontainers;
|
||||
import org.testcontainers.utility.DockerImageName;
|
||||
|
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
||||
@ActiveProfiles("test")
|
||||
@Testcontainers
|
||||
public abstract class AbstractPostgresIT {
|
||||
|
||||
private static final DockerImageName TIMESCALEDB_IMAGE =
|
||||
DockerImageName.parse("timescale/timescaledb-ha:pg16")
|
||||
.asCompatibleSubstituteFor("postgres");
|
||||
|
||||
@Container
|
||||
static final PostgreSQLContainer<?> postgres =
|
||||
new PostgreSQLContainer<>(TIMESCALEDB_IMAGE)
|
||||
.withDatabaseName("cameleer3")
|
||||
.withUsername("cameleer")
|
||||
.withPassword("test");
|
||||
static final PostgreSQLContainer<?> postgres;
|
||||
static final OpensearchContainer<?> opensearch;
|
||||
|
||||
static {
|
||||
postgres = new PostgreSQLContainer<>(TIMESCALEDB_IMAGE)
|
||||
.withDatabaseName("cameleer3")
|
||||
.withUsername("cameleer")
|
||||
.withPassword("test");
|
||||
postgres.start();
|
||||
|
||||
opensearch = new OpensearchContainer<>("opensearchproject/opensearch:2.19.0");
|
||||
opensearch.start();
|
||||
}
|
||||
|
||||
@Autowired
|
||||
protected JdbcTemplate jdbcTemplate;
|
||||
@@ -37,5 +42,6 @@ public abstract class AbstractPostgresIT {
|
||||
registry.add("spring.datasource.password", postgres::getPassword);
|
||||
registry.add("spring.datasource.driver-class-name", () -> "org.postgresql.Driver");
|
||||
registry.add("spring.flyway.enabled", () -> "true");
|
||||
registry.add("opensearch.url", opensearch::getHttpHostAddress);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,13 +123,13 @@ class DetailControllerIT extends AbstractPostgresIT {
|
||||
// Wait for flush and get the execution_id
|
||||
await().atMost(10, SECONDS).untilAsserted(() -> {
|
||||
Integer count = jdbcTemplate.queryForObject(
|
||||
"SELECT count(*) FROM route_executions WHERE route_id = 'detail-test-route'",
|
||||
"SELECT count(*) FROM executions WHERE route_id = 'detail-test-route'",
|
||||
Integer.class);
|
||||
assertThat(count).isGreaterThanOrEqualTo(1);
|
||||
});
|
||||
|
||||
seededExecutionId = jdbcTemplate.queryForObject(
|
||||
"SELECT execution_id FROM route_executions WHERE route_id = 'detail-test-route' LIMIT 1",
|
||||
"SELECT execution_id FROM executions WHERE route_id = 'detail-test-route' LIMIT 1",
|
||||
String.class);
|
||||
}
|
||||
|
||||
|
||||
@@ -111,7 +111,7 @@ class ExecutionControllerIT extends AbstractPostgresIT {
|
||||
|
||||
await().atMost(10, SECONDS).untilAsserted(() -> {
|
||||
Integer count = jdbcTemplate.queryForObject(
|
||||
"SELECT count(*) FROM route_executions WHERE route_id = 'flush-test-route'",
|
||||
"SELECT count(*) FROM executions WHERE route_id = 'flush-test-route'",
|
||||
Integer.class);
|
||||
assertThat(count).isGreaterThanOrEqualTo(1);
|
||||
});
|
||||
|
||||
@@ -14,7 +14,9 @@ import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
|
||||
import static java.util.concurrent.TimeUnit.SECONDS;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.awaitility.Awaitility.await;
|
||||
|
||||
/**
|
||||
* Integration tests for the search controller endpoints.
|
||||
@@ -153,11 +155,19 @@ class SearchControllerIT extends AbstractPostgresIT {
|
||||
""", i, i, i, i, i));
|
||||
}
|
||||
|
||||
// Verify all data is available (synchronous writes)
|
||||
// Verify all data is in PostgreSQL (synchronous writes)
|
||||
Integer count = jdbcTemplate.queryForObject(
|
||||
"SELECT count(*) FROM executions WHERE route_id LIKE 'search-route-%'",
|
||||
Integer.class);
|
||||
assertThat(count).isEqualTo(10);
|
||||
|
||||
// Wait for async OpenSearch indexing (debounce + index time)
|
||||
// Check for last seeded execution specifically to avoid false positives from other test classes
|
||||
await().atMost(30, SECONDS).untilAsserted(() -> {
|
||||
ResponseEntity<String> r = searchGet("?correlationId=corr-page-10");
|
||||
JsonNode body = objectMapper.readTree(r.getBody());
|
||||
assertThat(body.get("total").asLong()).isGreaterThanOrEqualTo(1);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -10,29 +10,16 @@ import com.cameleer3.server.core.storage.model.ExecutionDocument.ProcessorDoc;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.opensearch.client.opensearch.OpenSearchClient;
|
||||
import org.opensearch.client.opensearch.indices.RefreshRequest;
|
||||
import org.opensearch.testcontainers.OpensearchContainer;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.test.context.DynamicPropertyRegistry;
|
||||
import org.springframework.test.context.DynamicPropertySource;
|
||||
import org.testcontainers.junit.jupiter.Container;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
// Extends AbstractPostgresIT for PostgreSQL datasource needed by Spring context
|
||||
// Extends AbstractPostgresIT which provides both PostgreSQL and OpenSearch testcontainers
|
||||
class OpenSearchIndexIT extends AbstractPostgresIT {
|
||||
|
||||
@Container
|
||||
static final OpensearchContainer<?> opensearch =
|
||||
new OpensearchContainer<>("opensearchproject/opensearch:2.19.0");
|
||||
|
||||
@DynamicPropertySource
|
||||
static void configureOpenSearch(DynamicPropertyRegistry registry) {
|
||||
registry.add("opensearch.url", opensearch::getHttpHostAddress);
|
||||
}
|
||||
|
||||
@Autowired
|
||||
SearchIndex searchIndex;
|
||||
|
||||
|
||||
@@ -23,30 +23,33 @@ class PostgresStatsStoreIT extends AbstractPostgresIT {
|
||||
|
||||
@Test
|
||||
void statsReturnsCountsForTimeWindow() {
|
||||
Instant now = Instant.now().truncatedTo(ChronoUnit.SECONDS);
|
||||
insertExecution("stats-1", "route-a", "app-1", "COMPLETED", now, 100L);
|
||||
insertExecution("stats-2", "route-a", "app-1", "FAILED", now.plusSeconds(10), 200L);
|
||||
insertExecution("stats-3", "route-b", "app-1", "COMPLETED", now.plusSeconds(20), 50L);
|
||||
// Use a unique route + statsForRoute to avoid data contamination from other tests
|
||||
String uniqueRoute = "stats-route-" + System.nanoTime();
|
||||
Instant base = Instant.now().minus(5, ChronoUnit.MINUTES).truncatedTo(ChronoUnit.SECONDS);
|
||||
insertExecution("stats-1-" + uniqueRoute, uniqueRoute, "app-stats", "COMPLETED", base, 100L);
|
||||
insertExecution("stats-2-" + uniqueRoute, uniqueRoute, "app-stats", "FAILED", base.plusSeconds(10), 200L);
|
||||
insertExecution("stats-3-" + uniqueRoute, uniqueRoute, "app-stats", "COMPLETED", base.plusSeconds(20), 50L);
|
||||
|
||||
// Force continuous aggregate refresh
|
||||
jdbc.execute("CALL refresh_continuous_aggregate('stats_1m_all', NOW() - INTERVAL '1 hour', NOW() + INTERVAL '1 hour')");
|
||||
jdbc.execute("CALL refresh_continuous_aggregate('stats_1m_route', NOW() - INTERVAL '1 hour', NOW() + INTERVAL '1 hour')");
|
||||
|
||||
ExecutionStats stats = statsStore.stats(now.minusSeconds(60), now.plusSeconds(60));
|
||||
ExecutionStats stats = statsStore.statsForRoute(base.minusSeconds(60), base.plusSeconds(60), uniqueRoute, null);
|
||||
assertEquals(3, stats.totalCount());
|
||||
assertEquals(1, stats.failedCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
void timeseriesReturnsBuckets() {
|
||||
Instant now = Instant.now().truncatedTo(ChronoUnit.MINUTES);
|
||||
String uniqueRoute = "ts-route-" + System.nanoTime();
|
||||
Instant base = Instant.now().minus(10, ChronoUnit.MINUTES).truncatedTo(ChronoUnit.MINUTES);
|
||||
for (int i = 0; i < 10; i++) {
|
||||
insertExecution("ts-" + i, "route-a", "app-1", "COMPLETED",
|
||||
now.plusSeconds(i * 30), 100L + i);
|
||||
insertExecution("ts-" + i + "-" + uniqueRoute, uniqueRoute, "app-ts", "COMPLETED",
|
||||
base.plusSeconds(i * 30), 100L + i);
|
||||
}
|
||||
|
||||
jdbc.execute("CALL refresh_continuous_aggregate('stats_1m_all', NOW() - INTERVAL '1 hour', NOW() + INTERVAL '1 hour')");
|
||||
jdbc.execute("CALL refresh_continuous_aggregate('stats_1m_route', NOW() - INTERVAL '1 hour', NOW() + INTERVAL '1 hour')");
|
||||
|
||||
StatsTimeseries ts = statsStore.timeseries(now.minus(1, ChronoUnit.MINUTES), now.plus(10, ChronoUnit.MINUTES), 5);
|
||||
StatsTimeseries ts = statsStore.timeseriesForRoute(base.minus(1, ChronoUnit.MINUTES), base.plus(10, ChronoUnit.MINUTES), 5, uniqueRoute, null);
|
||||
assertNotNull(ts);
|
||||
assertFalse(ts.buckets().isEmpty());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user