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:
@@ -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": []
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user