From 9f74e47ecfec1c1f98c0d4cd78a0ad3ff1c16dc2 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Mon, 16 Mar 2026 20:03:38 +0100 Subject: [PATCH] fix: use correct role-based JWT tokens in all integration tests Co-Authored-By: Claude Opus 4.6 (1M context) --- .../server/app/TestSecurityHelper.java | 32 +++++++++++++++++-- .../controller/AgentCommandControllerIT.java | 20 ++++++------ .../AgentRegistrationControllerIT.java | 8 +++-- .../app/controller/AgentSseControllerIT.java | 4 ++- .../app/controller/DetailControllerIT.java | 4 ++- .../controller/DiagramRenderControllerIT.java | 10 +++--- .../app/controller/SearchControllerIT.java | 4 ++- .../server/app/security/JwtRefreshIT.java | 4 +-- .../app/security/RegistrationSecurityIT.java | 4 +-- .../server/app/security/SecurityFilterIT.java | 4 ++- .../server/app/security/SseSigningIT.java | 10 ++++-- 11 files changed, 76 insertions(+), 28 deletions(-) diff --git a/cameleer3-server-app/src/test/java/com/cameleer3/server/app/TestSecurityHelper.java b/cameleer3-server-app/src/test/java/com/cameleer3/server/app/TestSecurityHelper.java index 9867cd7a..97df1b83 100644 --- a/cameleer3-server-app/src/test/java/com/cameleer3/server/app/TestSecurityHelper.java +++ b/cameleer3-server-app/src/test/java/com/cameleer3/server/app/TestSecurityHelper.java @@ -27,11 +27,39 @@ public class TestSecurityHelper { } /** - * Registers a test agent and returns a valid JWT access token for it. + * Registers a test agent and returns a valid JWT access token with AGENT role. */ public String registerTestAgent(String agentId) { agentRegistryService.register(agentId, "test", "test-group", "1.0", List.of(), Map.of()); - return jwtService.createAccessToken(agentId, "test-group"); + return jwtService.createAccessToken(agentId, "test-group", List.of("AGENT")); + } + + /** + * Returns a valid JWT access token with the given roles (no agent registration). + */ + public String createToken(String subject, String group, List roles) { + return jwtService.createAccessToken(subject, group, roles); + } + + /** + * Returns a valid JWT access token with OPERATOR role. + */ + public String operatorToken() { + return jwtService.createAccessToken("test-operator", "user", List.of("OPERATOR")); + } + + /** + * Returns a valid JWT access token with ADMIN role. + */ + public String adminToken() { + return jwtService.createAccessToken("test-admin", "user", List.of("ADMIN")); + } + + /** + * Returns a valid JWT access token with VIEWER role. + */ + public String viewerToken() { + return jwtService.createAccessToken("test-viewer", "user", List.of("VIEWER")); } /** diff --git a/cameleer3-server-app/src/test/java/com/cameleer3/server/app/controller/AgentCommandControllerIT.java b/cameleer3-server-app/src/test/java/com/cameleer3/server/app/controller/AgentCommandControllerIT.java index 4ba36c5d..b6d791d7 100644 --- a/cameleer3-server-app/src/test/java/com/cameleer3/server/app/controller/AgentCommandControllerIT.java +++ b/cameleer3-server-app/src/test/java/com/cameleer3/server/app/controller/AgentCommandControllerIT.java @@ -29,11 +29,13 @@ class AgentCommandControllerIT extends AbstractPostgresIT { @Autowired private TestSecurityHelper securityHelper; - private String jwt; + private String agentJwt; + private String operatorJwt; @BeforeEach void setUp() { - jwt = securityHelper.registerTestAgent("test-agent-command-it"); + agentJwt = securityHelper.registerTestAgent("test-agent-command-it"); + operatorJwt = securityHelper.operatorToken(); } private ResponseEntity registerAgent(String agentId, String name, String group) { @@ -65,7 +67,7 @@ class AgentCommandControllerIT extends AbstractPostgresIT { ResponseEntity response = restTemplate.postForEntity( "/api/v1/agents/" + agentId + "/commands", - new HttpEntity<>(commandJson, securityHelper.authHeaders(jwt)), + new HttpEntity<>(commandJson, securityHelper.authHeaders(operatorJwt)), String.class); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.ACCEPTED); @@ -88,7 +90,7 @@ class AgentCommandControllerIT extends AbstractPostgresIT { ResponseEntity response = restTemplate.postForEntity( "/api/v1/agents/groups/" + group + "/commands", - new HttpEntity<>(commandJson, securityHelper.authHeaders(jwt)), + new HttpEntity<>(commandJson, securityHelper.authHeaders(operatorJwt)), String.class); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.ACCEPTED); @@ -110,7 +112,7 @@ class AgentCommandControllerIT extends AbstractPostgresIT { ResponseEntity response = restTemplate.postForEntity( "/api/v1/agents/commands", - new HttpEntity<>(commandJson, securityHelper.authHeaders(jwt)), + new HttpEntity<>(commandJson, securityHelper.authHeaders(operatorJwt)), String.class); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.ACCEPTED); @@ -131,7 +133,7 @@ class AgentCommandControllerIT extends AbstractPostgresIT { ResponseEntity cmdResponse = restTemplate.postForEntity( "/api/v1/agents/" + agentId + "/commands", - new HttpEntity<>(commandJson, securityHelper.authHeaders(jwt)), + new HttpEntity<>(commandJson, securityHelper.authHeaders(operatorJwt)), String.class); JsonNode cmdBody = objectMapper.readTree(cmdResponse.getBody()); @@ -140,7 +142,7 @@ class AgentCommandControllerIT extends AbstractPostgresIT { ResponseEntity ackResponse = restTemplate.exchange( "/api/v1/agents/" + agentId + "/commands/" + commandId + "/ack", HttpMethod.POST, - new HttpEntity<>(securityHelper.authHeadersNoBody(jwt)), + new HttpEntity<>(securityHelper.authHeadersNoBody(agentJwt)), Void.class); assertThat(ackResponse.getStatusCode()).isEqualTo(HttpStatus.OK); @@ -154,7 +156,7 @@ class AgentCommandControllerIT extends AbstractPostgresIT { ResponseEntity response = restTemplate.exchange( "/api/v1/agents/" + agentId + "/commands/nonexistent-cmd-id/ack", HttpMethod.POST, - new HttpEntity<>(securityHelper.authHeadersNoBody(jwt)), + new HttpEntity<>(securityHelper.authHeadersNoBody(agentJwt)), Void.class); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); @@ -168,7 +170,7 @@ class AgentCommandControllerIT extends AbstractPostgresIT { ResponseEntity response = restTemplate.postForEntity( "/api/v1/agents/nonexistent-agent-xyz/commands", - new HttpEntity<>(commandJson, securityHelper.authHeaders(jwt)), + new HttpEntity<>(commandJson, securityHelper.authHeaders(operatorJwt)), String.class); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); diff --git a/cameleer3-server-app/src/test/java/com/cameleer3/server/app/controller/AgentRegistrationControllerIT.java b/cameleer3-server-app/src/test/java/com/cameleer3/server/app/controller/AgentRegistrationControllerIT.java index 763646b9..12cbf02e 100644 --- a/cameleer3-server-app/src/test/java/com/cameleer3/server/app/controller/AgentRegistrationControllerIT.java +++ b/cameleer3-server-app/src/test/java/com/cameleer3/server/app/controller/AgentRegistrationControllerIT.java @@ -28,10 +28,12 @@ class AgentRegistrationControllerIT extends AbstractPostgresIT { private TestSecurityHelper securityHelper; private String jwt; + private String viewerJwt; @BeforeEach void setUp() { jwt = securityHelper.registerTestAgent("test-agent-registration-it"); + viewerJwt = securityHelper.viewerToken(); } private ResponseEntity registerAgent(String agentId, String name) { @@ -114,7 +116,7 @@ class AgentRegistrationControllerIT extends AbstractPostgresIT { ResponseEntity response = restTemplate.exchange( "/api/v1/agents", HttpMethod.GET, - new HttpEntity<>(securityHelper.authHeadersNoBody(jwt)), + new HttpEntity<>(securityHelper.authHeadersNoBody(viewerJwt)), String.class); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); @@ -131,7 +133,7 @@ class AgentRegistrationControllerIT extends AbstractPostgresIT { ResponseEntity response = restTemplate.exchange( "/api/v1/agents?status=LIVE", HttpMethod.GET, - new HttpEntity<>(securityHelper.authHeadersNoBody(jwt)), + new HttpEntity<>(securityHelper.authHeadersNoBody(viewerJwt)), String.class); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); @@ -148,7 +150,7 @@ class AgentRegistrationControllerIT extends AbstractPostgresIT { ResponseEntity response = restTemplate.exchange( "/api/v1/agents?status=INVALID", HttpMethod.GET, - new HttpEntity<>(securityHelper.authHeadersNoBody(jwt)), + new HttpEntity<>(securityHelper.authHeadersNoBody(viewerJwt)), String.class); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); diff --git a/cameleer3-server-app/src/test/java/com/cameleer3/server/app/controller/AgentSseControllerIT.java b/cameleer3-server-app/src/test/java/com/cameleer3/server/app/controller/AgentSseControllerIT.java index fddc7152..78a3743f 100644 --- a/cameleer3-server-app/src/test/java/com/cameleer3/server/app/controller/AgentSseControllerIT.java +++ b/cameleer3-server-app/src/test/java/com/cameleer3/server/app/controller/AgentSseControllerIT.java @@ -45,10 +45,12 @@ class AgentSseControllerIT extends AbstractPostgresIT { private int port; private String jwt; + private String operatorJwt; @BeforeEach void setUp() { jwt = securityHelper.registerTestAgent("test-agent-sse-it"); + operatorJwt = securityHelper.operatorToken(); } private ResponseEntity registerAgent(String agentId, String name, String group) { @@ -76,7 +78,7 @@ class AgentSseControllerIT extends AbstractPostgresIT { return restTemplate.postForEntity( "/api/v1/agents/" + agentId + "/commands", - new HttpEntity<>(json, securityHelper.authHeaders(jwt)), + new HttpEntity<>(json, securityHelper.authHeaders(operatorJwt)), String.class); } diff --git a/cameleer3-server-app/src/test/java/com/cameleer3/server/app/controller/DetailControllerIT.java b/cameleer3-server-app/src/test/java/com/cameleer3/server/app/controller/DetailControllerIT.java index 83fa17b1..f0cef246 100644 --- a/cameleer3-server-app/src/test/java/com/cameleer3/server/app/controller/DetailControllerIT.java +++ b/cameleer3-server-app/src/test/java/com/cameleer3/server/app/controller/DetailControllerIT.java @@ -34,6 +34,7 @@ class DetailControllerIT extends AbstractPostgresIT { private final ObjectMapper objectMapper = new ObjectMapper(); private String jwt; + private String viewerJwt; private String seededExecutionId; /** @@ -43,6 +44,7 @@ class DetailControllerIT extends AbstractPostgresIT { @BeforeAll void seedTestData() { jwt = securityHelper.registerTestAgent("test-agent-detail-it"); + viewerJwt = securityHelper.viewerToken(); String json = """ { @@ -217,7 +219,7 @@ class DetailControllerIT extends AbstractPostgresIT { } private ResponseEntity detailGet(String path) { - HttpHeaders headers = securityHelper.authHeadersNoBody(jwt); + HttpHeaders headers = securityHelper.authHeadersNoBody(viewerJwt); return restTemplate.exchange( "/api/v1/executions" + path, HttpMethod.GET, diff --git a/cameleer3-server-app/src/test/java/com/cameleer3/server/app/controller/DiagramRenderControllerIT.java b/cameleer3-server-app/src/test/java/com/cameleer3/server/app/controller/DiagramRenderControllerIT.java index af0b8668..416dc78c 100644 --- a/cameleer3-server-app/src/test/java/com/cameleer3/server/app/controller/DiagramRenderControllerIT.java +++ b/cameleer3-server-app/src/test/java/com/cameleer3/server/app/controller/DiagramRenderControllerIT.java @@ -29,6 +29,7 @@ class DiagramRenderControllerIT extends AbstractPostgresIT { private TestSecurityHelper securityHelper; private String jwt; + private String viewerJwt; private String contentHash; /** @@ -37,6 +38,7 @@ class DiagramRenderControllerIT extends AbstractPostgresIT { @BeforeEach void seedDiagram() { jwt = securityHelper.registerTestAgent("test-agent-diagram-render-it"); + viewerJwt = securityHelper.viewerToken(); String json = """ { @@ -73,7 +75,7 @@ class DiagramRenderControllerIT extends AbstractPostgresIT { @Test void getSvg_withAcceptHeader_returnsSvg() { - HttpHeaders headers = securityHelper.authHeadersNoBody(jwt); + HttpHeaders headers = securityHelper.authHeadersNoBody(viewerJwt); headers.set("Accept", "image/svg+xml"); ResponseEntity response = restTemplate.exchange( @@ -90,7 +92,7 @@ class DiagramRenderControllerIT extends AbstractPostgresIT { @Test void getJson_withAcceptHeader_returnsJson() { - HttpHeaders headers = securityHelper.authHeadersNoBody(jwt); + HttpHeaders headers = securityHelper.authHeadersNoBody(viewerJwt); headers.set("Accept", "application/json"); ResponseEntity response = restTemplate.exchange( @@ -107,7 +109,7 @@ class DiagramRenderControllerIT extends AbstractPostgresIT { @Test void getNonExistentHash_returns404() { - HttpHeaders headers = securityHelper.authHeadersNoBody(jwt); + HttpHeaders headers = securityHelper.authHeadersNoBody(viewerJwt); headers.set("Accept", "image/svg+xml"); ResponseEntity response = restTemplate.exchange( @@ -122,7 +124,7 @@ class DiagramRenderControllerIT extends AbstractPostgresIT { @Test void getWithNoAcceptHeader_defaultsToSvg() { - HttpHeaders headers = securityHelper.authHeadersNoBody(jwt); + HttpHeaders headers = securityHelper.authHeadersNoBody(viewerJwt); ResponseEntity response = restTemplate.exchange( "/api/v1/diagrams/{hash}/render", diff --git a/cameleer3-server-app/src/test/java/com/cameleer3/server/app/controller/SearchControllerIT.java b/cameleer3-server-app/src/test/java/com/cameleer3/server/app/controller/SearchControllerIT.java index 95f42b2a..439bfa5a 100644 --- a/cameleer3-server-app/src/test/java/com/cameleer3/server/app/controller/SearchControllerIT.java +++ b/cameleer3-server-app/src/test/java/com/cameleer3/server/app/controller/SearchControllerIT.java @@ -35,6 +35,7 @@ class SearchControllerIT extends AbstractPostgresIT { private final ObjectMapper objectMapper = new ObjectMapper(); private String jwt; + private String viewerJwt; /** * Seed test data: Insert executions with varying statuses, times, durations, @@ -43,6 +44,7 @@ class SearchControllerIT extends AbstractPostgresIT { @BeforeAll void seedTestData() { jwt = securityHelper.registerTestAgent("test-agent-search-it"); + viewerJwt = securityHelper.viewerToken(); // Execution 1: COMPLETED, short duration, no errors ingest(""" @@ -376,7 +378,7 @@ class SearchControllerIT extends AbstractPostgresIT { return restTemplate.exchange( "/api/v1/search/executions", HttpMethod.POST, - new HttpEntity<>(jsonBody, securityHelper.authHeaders(jwt)), + new HttpEntity<>(jsonBody, securityHelper.authHeaders(viewerJwt)), String.class); } } diff --git a/cameleer3-server-app/src/test/java/com/cameleer3/server/app/security/JwtRefreshIT.java b/cameleer3-server-app/src/test/java/com/cameleer3/server/app/security/JwtRefreshIT.java index 87ddf25e..af033318 100644 --- a/cameleer3-server-app/src/test/java/com/cameleer3/server/app/security/JwtRefreshIT.java +++ b/cameleer3-server-app/src/test/java/com/cameleer3/server/app/security/JwtRefreshIT.java @@ -153,13 +153,13 @@ class JwtRefreshIT extends AbstractPostgresIT { JsonNode refreshBody2 = objectMapper.readTree(refreshResponse.getBody()); String newAccessToken = refreshBody2.get("accessToken").asText(); - // Use the new access token to hit a protected endpoint + // Use the new access token to hit a protected endpoint accessible by AGENT role HttpHeaders authHeaders = new HttpHeaders(); authHeaders.set("Authorization", "Bearer " + newAccessToken); authHeaders.set("X-Cameleer-Protocol-Version", "1"); ResponseEntity response = restTemplate.exchange( - "/api/v1/agents", + "/api/v1/search/executions", HttpMethod.GET, new HttpEntity<>(authHeaders), String.class); diff --git a/cameleer3-server-app/src/test/java/com/cameleer3/server/app/security/RegistrationSecurityIT.java b/cameleer3-server-app/src/test/java/com/cameleer3/server/app/security/RegistrationSecurityIT.java index e4ee5da4..54c17e71 100644 --- a/cameleer3-server-app/src/test/java/com/cameleer3/server/app/security/RegistrationSecurityIT.java +++ b/cameleer3-server-app/src/test/java/com/cameleer3/server/app/security/RegistrationSecurityIT.java @@ -81,13 +81,13 @@ class RegistrationSecurityIT extends AbstractPostgresIT { JsonNode regBody = objectMapper.readTree(regResponse.getBody()); String accessToken = regBody.get("accessToken").asText(); - // Use the access token to hit a protected endpoint + // Use the access token to hit a protected endpoint accessible by AGENT role HttpHeaders headers = new HttpHeaders(); headers.set("Authorization", "Bearer " + accessToken); headers.set("X-Cameleer-Protocol-Version", "1"); ResponseEntity response = restTemplate.exchange( - "/api/v1/agents", + "/api/v1/search/executions", HttpMethod.GET, new HttpEntity<>(headers), String.class); diff --git a/cameleer3-server-app/src/test/java/com/cameleer3/server/app/security/SecurityFilterIT.java b/cameleer3-server-app/src/test/java/com/cameleer3/server/app/security/SecurityFilterIT.java index ba8dfcbb..a55c7190 100644 --- a/cameleer3-server-app/src/test/java/com/cameleer3/server/app/security/SecurityFilterIT.java +++ b/cameleer3-server-app/src/test/java/com/cameleer3/server/app/security/SecurityFilterIT.java @@ -28,10 +28,12 @@ class SecurityFilterIT extends AbstractPostgresIT { private TestSecurityHelper securityHelper; private String jwt; + private String viewerJwt; @BeforeEach void setUp() { jwt = securityHelper.registerTestAgent("test-agent-security-filter-it"); + viewerJwt = securityHelper.viewerToken(); } @Test @@ -53,7 +55,7 @@ class SecurityFilterIT extends AbstractPostgresIT { ResponseEntity response = restTemplate.exchange( "/api/v1/agents", HttpMethod.GET, - new HttpEntity<>(securityHelper.authHeadersNoBody(jwt)), + new HttpEntity<>(securityHelper.authHeadersNoBody(viewerJwt)), String.class); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); diff --git a/cameleer3-server-app/src/test/java/com/cameleer3/server/app/security/SseSigningIT.java b/cameleer3-server-app/src/test/java/com/cameleer3/server/app/security/SseSigningIT.java index d611520b..11e0ed6b 100644 --- a/cameleer3-server-app/src/test/java/com/cameleer3/server/app/security/SseSigningIT.java +++ b/cameleer3-server-app/src/test/java/com/cameleer3/server/app/security/SseSigningIT.java @@ -1,6 +1,7 @@ package com.cameleer3.server.app.security; import com.cameleer3.server.app.AbstractPostgresIT; +import com.cameleer3.server.app.TestSecurityHelper; import com.cameleer3.server.core.security.Ed25519SigningService; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; @@ -52,6 +53,9 @@ class SseSigningIT extends AbstractPostgresIT { @Autowired private ObjectMapper objectMapper; + @Autowired + private TestSecurityHelper securityHelper; + @Autowired private Ed25519SigningService ed25519SigningService; @@ -165,6 +169,7 @@ class SseSigningIT extends AbstractPostgresIT { String agentId = "sse-sign-it-" + UUID.randomUUID().toString().substring(0, 8); JsonNode registration = registerAgentWithAuth(agentId); String accessToken = registration.get("accessToken").asText(); + String operatorToken = securityHelper.operatorToken(); String serverPublicKey = registration.get("serverPublicKey").asText(); SseStream stream = openSseStream(agentId, accessToken); @@ -177,7 +182,7 @@ class SseSigningIT extends AbstractPostgresIT { await().atMost(10, TimeUnit.SECONDS).pollInterval(200, TimeUnit.MILLISECONDS) .ignoreExceptions() .until(() -> { - sendCommand(agentId, "config-update", originalPayload, accessToken); + sendCommand(agentId, "config-update", originalPayload, operatorToken); List lines = stream.snapshot(); return lines.stream().anyMatch(l -> l.contains("event:config-update")); }); @@ -221,6 +226,7 @@ class SseSigningIT extends AbstractPostgresIT { String agentId = "sse-sign-trace-" + UUID.randomUUID().toString().substring(0, 8); JsonNode registration = registerAgentWithAuth(agentId); String accessToken = registration.get("accessToken").asText(); + String operatorToken = securityHelper.operatorToken(); String serverPublicKey = registration.get("serverPublicKey").asText(); SseStream stream = openSseStream(agentId, accessToken); @@ -232,7 +238,7 @@ class SseSigningIT extends AbstractPostgresIT { await().atMost(10, TimeUnit.SECONDS).pollInterval(200, TimeUnit.MILLISECONDS) .ignoreExceptions() .until(() -> { - sendCommand(agentId, "deep-trace", originalPayload, accessToken); + sendCommand(agentId, "deep-trace", originalPayload, operatorToken); List lines = stream.snapshot(); return lines.stream().anyMatch(l -> l.contains("event:deep-trace")); });