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.TestSecurityHelper;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
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.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 {
@Autowired
@@ -23,27 +31,33 @@ class ExecutionControllerIT extends AbstractPostgresIT {
@Autowired
private TestSecurityHelper securityHelper;
private final ObjectMapper objectMapper = new ObjectMapper();
private HttpHeaders authHeaders;
private HttpHeaders viewerHeaders;
@BeforeEach
void setUp() {
String jwt = securityHelper.registerTestAgent("test-agent-execution-it");
authHeaders = securityHelper.authHeaders(jwt);
viewerHeaders = securityHelper.authHeadersNoBody(securityHelper.viewerToken());
}
@Test
void postSingleExecution_returns202() {
String json = """
{
"routeId": "route-1",
"exchangeId": "exchange-1",
"applicationId": "test-group",
"instanceId": "test-agent-execution-it",
"routeId": "route-1",
"correlationId": "corr-1",
"status": "COMPLETED",
"startTime": "2026-03-11T10:00:00Z",
"endTime": "2026-03-11T10:00:01Z",
"durationMs": 1000,
"errorMessage": "",
"errorStackTrace": "",
"chunkSeq": 0,
"final": true,
"processors": []
}
""";
@@ -60,22 +74,30 @@ class ExecutionControllerIT extends AbstractPostgresIT {
void postArrayOfExecutions_returns202() {
String json = """
[{
"routeId": "route-2",
"exchangeId": "exchange-2",
"applicationId": "test-group",
"instanceId": "test-agent-execution-it",
"routeId": "route-2",
"status": "COMPLETED",
"startTime": "2026-03-11T10:00:00Z",
"endTime": "2026-03-11T10:00:01Z",
"durationMs": 1000,
"chunkSeq": 0,
"final": true,
"processors": []
},
{
"routeId": "route-3",
"exchangeId": "exchange-3",
"applicationId": "test-group",
"instanceId": "test-agent-execution-it",
"routeId": "route-3",
"status": "FAILED",
"startTime": "2026-03-11T10:00:00Z",
"endTime": "2026-03-11T10:00:02Z",
"durationMs": 2000,
"errorMessage": "Something went wrong",
"chunkSeq": 0,
"final": true,
"processors": []
}]
""";
@@ -92,13 +114,17 @@ class ExecutionControllerIT extends AbstractPostgresIT {
void postExecution_dataAppearsAfterFlush() {
String json = """
{
"routeId": "flush-test-route",
"exchangeId": "flush-exchange-1",
"applicationId": "test-group",
"instanceId": "test-agent-execution-it",
"routeId": "flush-test-route",
"correlationId": "flush-corr-1",
"status": "COMPLETED",
"startTime": "2026-03-11T10:00:00Z",
"endTime": "2026-03-11T10:00:01Z",
"durationMs": 1000,
"chunkSeq": 0,
"final": true,
"processors": []
}
""";
@@ -108,11 +134,17 @@ class ExecutionControllerIT extends AbstractPostgresIT {
new HttpEntity<>(json, authHeaders),
String.class);
await().atMost(10, SECONDS).untilAsserted(() -> {
Integer count = jdbcTemplate.queryForObject(
"SELECT count(*) FROM executions WHERE route_id = 'flush-test-route'",
Integer.class);
assertThat(count).isGreaterThanOrEqualTo(1);
// Executions live in ClickHouse; drive the visibility check through
// the REST search API (env-scoped), never through raw SQL.
await().atMost(15, SECONDS).untilAsserted(() -> {
ResponseEntity<String> r = restTemplate.exchange(
"/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() {
String json = """
{
"routeId": "route-unk",
"exchangeId": "exchange-unk",
"applicationId": "test-group",
"instanceId": "test-agent-execution-it",
"routeId": "route-unk",
"status": "COMPLETED",
"startTime": "2026-03-11T10:00:00Z",
"durationMs": 500,
"chunkSeq": 0,
"final": true,
"unknownField": "should-be-ignored",
"anotherUnknown": 42,
"processors": []

View File

@@ -2,12 +2,15 @@ package com.cameleer.server.app.controller;
import com.cameleer.server.app.AbstractPostgresIT;
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.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
@@ -23,12 +26,18 @@ class MetricsControllerIT extends AbstractPostgresIT {
@Autowired
private TestSecurityHelper securityHelper;
private final ObjectMapper objectMapper = new ObjectMapper();
private HttpHeaders authHeaders;
private HttpHeaders viewerHeaders;
private String agentId;
@BeforeEach
void setUp() {
String jwt = securityHelper.registerTestAgent("test-agent-metrics-it");
agentId = "test-agent-metrics-it";
String jwt = securityHelper.registerTestAgent(agentId);
authHeaders = securityHelper.authHeaders(jwt);
viewerHeaders = securityHelper.authHeadersNoBody(securityHelper.viewerToken());
}
@Test
@@ -53,26 +62,43 @@ class MetricsControllerIT extends AbstractPostgresIT {
@Test
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 = """
[{
"instanceId": "agent-flush-test",
"collectedAt": "2026-03-11T10:00:00Z",
"instanceId": "%s",
"collectedAt": "%s",
"metricName": "memory.used",
"metricValue": 1024.0,
"tags": {}
}]
""";
""".formatted(agentId, now.toString());
restTemplate.postForEntity(
ResponseEntity<String> ingestResponse = restTemplate.postForEntity(
"/api/v1/data/metrics",
new HttpEntity<>(json, authHeaders),
String.class);
assertThat(ingestResponse.getStatusCode()).isEqualTo(HttpStatus.ACCEPTED);
await().atMost(10, SECONDS).untilAsserted(() -> {
Integer count = jdbcTemplate.queryForObject(
"SELECT count(*) FROM agent_metrics WHERE instance_id = 'agent-flush-test'",
Integer.class);
assertThat(count).isGreaterThanOrEqualTo(1);
// agent_metrics lives in ClickHouse; drive the visibility check through
// the env-scoped REST metrics endpoint, never through raw SQL.
await().atMost(15, SECONDS).untilAsserted(() -> {
ResponseEntity<String> r = restTemplate.exchange(
"/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);
});
}
}