fix(test): rewrite Execution/Metrics ControllerITs to chunks + REST verify

Same pattern as DetailControllerIT:
- ExecutionControllerIT: all four tests now post ExecutionChunk envelopes
  (chunkSeq=0, final=true) carrying instanceId/applicationId. Flush
  visibility check pivoted from PG SELECT → env-scoped search REST.
- MetricsControllerIT: postMetrics_dataAppearsAfterFlush now stamps
  collectedAt at now() and verifies through GET /environments/{env}/
  agents/{id}/metrics with the default 1h lookback, looking for a
  non-zero bucket on the metric name.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-21 22:07:25 +02:00
parent dfacedb0ca
commit 87bada1fc7
2 changed files with 84 additions and 22 deletions

View File

@@ -2,12 +2,15 @@ package com.cameleer.server.app.controller;
import com.cameleer.server.app.AbstractPostgresIT; import com.cameleer.server.app.AbstractPostgresIT;
import com.cameleer.server.app.TestSecurityHelper; import com.cameleer.server.app.TestSecurityHelper;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.web.client.TestRestTemplate; import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.HttpEntity; import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
@@ -15,6 +18,11 @@ import static java.util.concurrent.TimeUnit.SECONDS;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await; import static org.awaitility.Awaitility.await;
/**
* POST /api/v1/data/executions is owned by ChunkIngestionController (the
* legacy ExecutionController is @ConditionalOnMissingBean(ChunkAccumulator)
* and never binds). All payloads here are ExecutionChunk envelopes.
*/
class ExecutionControllerIT extends AbstractPostgresIT { class ExecutionControllerIT extends AbstractPostgresIT {
@Autowired @Autowired
@@ -23,27 +31,33 @@ class ExecutionControllerIT extends AbstractPostgresIT {
@Autowired @Autowired
private TestSecurityHelper securityHelper; private TestSecurityHelper securityHelper;
private final ObjectMapper objectMapper = new ObjectMapper();
private HttpHeaders authHeaders; private HttpHeaders authHeaders;
private HttpHeaders viewerHeaders;
@BeforeEach @BeforeEach
void setUp() { void setUp() {
String jwt = securityHelper.registerTestAgent("test-agent-execution-it"); String jwt = securityHelper.registerTestAgent("test-agent-execution-it");
authHeaders = securityHelper.authHeaders(jwt); authHeaders = securityHelper.authHeaders(jwt);
viewerHeaders = securityHelper.authHeadersNoBody(securityHelper.viewerToken());
} }
@Test @Test
void postSingleExecution_returns202() { void postSingleExecution_returns202() {
String json = """ String json = """
{ {
"routeId": "route-1",
"exchangeId": "exchange-1", "exchangeId": "exchange-1",
"applicationId": "test-group",
"instanceId": "test-agent-execution-it",
"routeId": "route-1",
"correlationId": "corr-1", "correlationId": "corr-1",
"status": "COMPLETED", "status": "COMPLETED",
"startTime": "2026-03-11T10:00:00Z", "startTime": "2026-03-11T10:00:00Z",
"endTime": "2026-03-11T10:00:01Z", "endTime": "2026-03-11T10:00:01Z",
"durationMs": 1000, "durationMs": 1000,
"errorMessage": "", "chunkSeq": 0,
"errorStackTrace": "", "final": true,
"processors": [] "processors": []
} }
"""; """;
@@ -60,22 +74,30 @@ class ExecutionControllerIT extends AbstractPostgresIT {
void postArrayOfExecutions_returns202() { void postArrayOfExecutions_returns202() {
String json = """ String json = """
[{ [{
"routeId": "route-2",
"exchangeId": "exchange-2", "exchangeId": "exchange-2",
"applicationId": "test-group",
"instanceId": "test-agent-execution-it",
"routeId": "route-2",
"status": "COMPLETED", "status": "COMPLETED",
"startTime": "2026-03-11T10:00:00Z", "startTime": "2026-03-11T10:00:00Z",
"endTime": "2026-03-11T10:00:01Z", "endTime": "2026-03-11T10:00:01Z",
"durationMs": 1000, "durationMs": 1000,
"chunkSeq": 0,
"final": true,
"processors": [] "processors": []
}, },
{ {
"routeId": "route-3",
"exchangeId": "exchange-3", "exchangeId": "exchange-3",
"applicationId": "test-group",
"instanceId": "test-agent-execution-it",
"routeId": "route-3",
"status": "FAILED", "status": "FAILED",
"startTime": "2026-03-11T10:00:00Z", "startTime": "2026-03-11T10:00:00Z",
"endTime": "2026-03-11T10:00:02Z", "endTime": "2026-03-11T10:00:02Z",
"durationMs": 2000, "durationMs": 2000,
"errorMessage": "Something went wrong", "errorMessage": "Something went wrong",
"chunkSeq": 0,
"final": true,
"processors": [] "processors": []
}] }]
"""; """;
@@ -92,13 +114,17 @@ class ExecutionControllerIT extends AbstractPostgresIT {
void postExecution_dataAppearsAfterFlush() { void postExecution_dataAppearsAfterFlush() {
String json = """ String json = """
{ {
"routeId": "flush-test-route",
"exchangeId": "flush-exchange-1", "exchangeId": "flush-exchange-1",
"applicationId": "test-group",
"instanceId": "test-agent-execution-it",
"routeId": "flush-test-route",
"correlationId": "flush-corr-1", "correlationId": "flush-corr-1",
"status": "COMPLETED", "status": "COMPLETED",
"startTime": "2026-03-11T10:00:00Z", "startTime": "2026-03-11T10:00:00Z",
"endTime": "2026-03-11T10:00:01Z", "endTime": "2026-03-11T10:00:01Z",
"durationMs": 1000, "durationMs": 1000,
"chunkSeq": 0,
"final": true,
"processors": [] "processors": []
} }
"""; """;
@@ -108,11 +134,17 @@ class ExecutionControllerIT extends AbstractPostgresIT {
new HttpEntity<>(json, authHeaders), new HttpEntity<>(json, authHeaders),
String.class); String.class);
await().atMost(10, SECONDS).untilAsserted(() -> { // Executions live in ClickHouse; drive the visibility check through
Integer count = jdbcTemplate.queryForObject( // the REST search API (env-scoped), never through raw SQL.
"SELECT count(*) FROM executions WHERE route_id = 'flush-test-route'", await().atMost(15, SECONDS).untilAsserted(() -> {
Integer.class); ResponseEntity<String> r = restTemplate.exchange(
assertThat(count).isGreaterThanOrEqualTo(1); "/api/v1/environments/default/executions?correlationId=flush-corr-1",
HttpMethod.GET,
new HttpEntity<>(viewerHeaders),
String.class);
assertThat(r.getStatusCode()).isEqualTo(HttpStatus.OK);
JsonNode body = objectMapper.readTree(r.getBody());
assertThat(body.get("total").asLong()).isGreaterThanOrEqualTo(1);
}); });
} }
@@ -120,11 +152,15 @@ class ExecutionControllerIT extends AbstractPostgresIT {
void postExecution_unknownFieldsAccepted() { void postExecution_unknownFieldsAccepted() {
String json = """ String json = """
{ {
"routeId": "route-unk",
"exchangeId": "exchange-unk", "exchangeId": "exchange-unk",
"applicationId": "test-group",
"instanceId": "test-agent-execution-it",
"routeId": "route-unk",
"status": "COMPLETED", "status": "COMPLETED",
"startTime": "2026-03-11T10:00:00Z", "startTime": "2026-03-11T10:00:00Z",
"durationMs": 500, "durationMs": 500,
"chunkSeq": 0,
"final": true,
"unknownField": "should-be-ignored", "unknownField": "should-be-ignored",
"anotherUnknown": 42, "anotherUnknown": 42,
"processors": [] "processors": []

View File

@@ -2,12 +2,15 @@ package com.cameleer.server.app.controller;
import com.cameleer.server.app.AbstractPostgresIT; import com.cameleer.server.app.AbstractPostgresIT;
import com.cameleer.server.app.TestSecurityHelper; import com.cameleer.server.app.TestSecurityHelper;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.web.client.TestRestTemplate; import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.HttpEntity; import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
@@ -23,12 +26,18 @@ class MetricsControllerIT extends AbstractPostgresIT {
@Autowired @Autowired
private TestSecurityHelper securityHelper; private TestSecurityHelper securityHelper;
private final ObjectMapper objectMapper = new ObjectMapper();
private HttpHeaders authHeaders; private HttpHeaders authHeaders;
private HttpHeaders viewerHeaders;
private String agentId;
@BeforeEach @BeforeEach
void setUp() { void setUp() {
String jwt = securityHelper.registerTestAgent("test-agent-metrics-it"); agentId = "test-agent-metrics-it";
String jwt = securityHelper.registerTestAgent(agentId);
authHeaders = securityHelper.authHeaders(jwt); authHeaders = securityHelper.authHeaders(jwt);
viewerHeaders = securityHelper.authHeadersNoBody(securityHelper.viewerToken());
} }
@Test @Test
@@ -53,26 +62,43 @@ class MetricsControllerIT extends AbstractPostgresIT {
@Test @Test
void postMetrics_dataAppearsAfterFlush() { void postMetrics_dataAppearsAfterFlush() {
// Post fresh now-stamped metrics so the default 1h lookback window of
// GET /agents/{id}/metrics sees them deterministically.
java.time.Instant now = java.time.Instant.now();
String json = """ String json = """
[{ [{
"instanceId": "agent-flush-test", "instanceId": "%s",
"collectedAt": "2026-03-11T10:00:00Z", "collectedAt": "%s",
"metricName": "memory.used", "metricName": "memory.used",
"metricValue": 1024.0, "metricValue": 1024.0,
"tags": {} "tags": {}
}] }]
"""; """.formatted(agentId, now.toString());
restTemplate.postForEntity( ResponseEntity<String> ingestResponse = restTemplate.postForEntity(
"/api/v1/data/metrics", "/api/v1/data/metrics",
new HttpEntity<>(json, authHeaders), new HttpEntity<>(json, authHeaders),
String.class); String.class);
assertThat(ingestResponse.getStatusCode()).isEqualTo(HttpStatus.ACCEPTED);
await().atMost(10, SECONDS).untilAsserted(() -> { // agent_metrics lives in ClickHouse; drive the visibility check through
Integer count = jdbcTemplate.queryForObject( // the env-scoped REST metrics endpoint, never through raw SQL.
"SELECT count(*) FROM agent_metrics WHERE instance_id = 'agent-flush-test'", await().atMost(15, SECONDS).untilAsserted(() -> {
Integer.class); ResponseEntity<String> r = restTemplate.exchange(
assertThat(count).isGreaterThanOrEqualTo(1); "/api/v1/environments/default/agents/" + agentId
+ "/metrics?names=memory.used",
HttpMethod.GET,
new HttpEntity<>(viewerHeaders),
String.class);
assertThat(r.getStatusCode()).isEqualTo(HttpStatus.OK);
JsonNode body = objectMapper.readTree(r.getBody());
JsonNode series = body.path("metrics").path("memory.used");
assertThat(series.isArray()).isTrue();
long nonZero = 0;
for (JsonNode bucket : series) {
if (bucket.get("value").asDouble() > 0) nonZero++;
}
assertThat(nonZero).isGreaterThanOrEqualTo(1);
}); });
} }
} }