diff --git a/cameleer-server-app/src/test/java/io/cameleer/server/app/controller/AgentCommandEnforcementDisabledIT.java b/cameleer-server-app/src/test/java/io/cameleer/server/app/controller/AgentCommandEnforcementDisabledIT.java new file mode 100644 index 00000000..9aeb3dfa --- /dev/null +++ b/cameleer-server-app/src/test/java/io/cameleer/server/app/controller/AgentCommandEnforcementDisabledIT.java @@ -0,0 +1,70 @@ +package io.cameleer.server.app.controller; + +import io.cameleer.server.app.AbstractPostgresIT; +import io.cameleer.server.app.TestSecurityHelper; +import io.cameleer.server.core.agent.AgentRegistryService; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.AfterEach; +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.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.TestPropertySource; + +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * E4/E5 integration test — enforce-signed-commands=false (default). + * Non-capable agents must receive a 202 and the command must be enqueued. + */ +@TestPropertySource(properties = "cameleer.server.security.enforce-signed-commands=false") +class AgentCommandEnforcementDisabledIT extends AbstractPostgresIT { + + @Autowired private TestRestTemplate restTemplate; + @Autowired private ObjectMapper objectMapper; + @Autowired private TestSecurityHelper securityHelper; + @Autowired private AgentRegistryService registryService; + + private String operatorJwt; + + @BeforeEach + void setUp() { + securityHelper.installSyntheticUnsignedLicense(Map.of("max_agents", 100)); + operatorJwt = securityHelper.operatorToken(); + } + + @AfterEach + void tearDown() { + securityHelper.clearTestLicense(); + } + + @Test + void sendCommand_nonCapableAgent_returns202WhenEnforcementDisabled() throws Exception { + String agentId = "enforce-off-" + UUID.randomUUID().toString().substring(0, 8); + // requireSignedCommands=false (the default overload) + registryService.register(agentId, agentId, "test-app", "default", "1.0", List.of(), Map.of()); + + ResponseEntity response = sendConfigUpdate(agentId); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.ACCEPTED); + JsonNode body = objectMapper.readTree(response.getBody()); + assertThat(body.has("commandId")).isTrue(); + } + + private ResponseEntity sendConfigUpdate(String agentId) { + return restTemplate.postForEntity( + "/api/v1/agents/" + agentId + "/commands", + new HttpEntity<>(""" + {"type": "config-update", "payload": {"key": "value"}} + """, securityHelper.authHeaders(operatorJwt)), + String.class); + } +} diff --git a/cameleer-server-app/src/test/java/io/cameleer/server/app/controller/AgentCommandEnforcementEnabledIT.java b/cameleer-server-app/src/test/java/io/cameleer/server/app/controller/AgentCommandEnforcementEnabledIT.java new file mode 100644 index 00000000..f1390899 --- /dev/null +++ b/cameleer-server-app/src/test/java/io/cameleer/server/app/controller/AgentCommandEnforcementEnabledIT.java @@ -0,0 +1,80 @@ +package io.cameleer.server.app.controller; + +import io.cameleer.server.app.AbstractPostgresIT; +import io.cameleer.server.app.TestSecurityHelper; +import io.cameleer.server.core.agent.AgentRegistryService; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.AfterEach; +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.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.TestPropertySource; + +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * E4/E5 integration test — enforce-signed-commands=true (hard gate). + * Non-capable agents must receive 409; capable agents must still receive 202. + */ +@TestPropertySource(properties = "cameleer.server.security.enforce-signed-commands=true") +class AgentCommandEnforcementEnabledIT extends AbstractPostgresIT { + + @Autowired private TestRestTemplate restTemplate; + @Autowired private ObjectMapper objectMapper; + @Autowired private TestSecurityHelper securityHelper; + @Autowired private AgentRegistryService registryService; + + private String operatorJwt; + + @BeforeEach + void setUp() { + securityHelper.installSyntheticUnsignedLicense(Map.of("max_agents", 100)); + operatorJwt = securityHelper.operatorToken(); + } + + @AfterEach + void tearDown() { + securityHelper.clearTestLicense(); + } + + @Test + void sendCommand_nonCapableAgent_returns409WhenEnforcementEnabled() { + String agentId = "enforce-on-noncap-" + UUID.randomUUID().toString().substring(0, 8); + registryService.register(agentId, agentId, "test-app", "default", "1.0", List.of(), Map.of()); + + ResponseEntity response = sendConfigUpdate(agentId); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CONFLICT); + } + + @Test + void sendCommand_capableAgent_returns202WhenEnforcementEnabled() throws Exception { + String agentId = "enforce-on-cap-" + UUID.randomUUID().toString().substring(0, 8); + // requireSignedCommands=true — agent has been upgraded to verify signatures + registryService.register(agentId, agentId, "test-app", "default", "2.0", List.of(), Map.of(), true); + + ResponseEntity response = sendConfigUpdate(agentId); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.ACCEPTED); + JsonNode body = objectMapper.readTree(response.getBody()); + assertThat(body.has("commandId")).isTrue(); + } + + private ResponseEntity sendConfigUpdate(String agentId) { + return restTemplate.postForEntity( + "/api/v1/agents/" + agentId + "/commands", + new HttpEntity<>(""" + {"type": "config-update", "payload": {"key": "value"}} + """, securityHelper.authHeaders(operatorJwt)), + String.class); + } +} diff --git a/cameleer-server-app/src/test/java/io/cameleer/server/app/controller/AgentCommandEnforcementIT.java b/cameleer-server-app/src/test/java/io/cameleer/server/app/controller/AgentCommandEnforcementIT.java deleted file mode 100644 index 706995f9..00000000 --- a/cameleer-server-app/src/test/java/io/cameleer/server/app/controller/AgentCommandEnforcementIT.java +++ /dev/null @@ -1,137 +0,0 @@ -package io.cameleer.server.app.controller; - -import io.cameleer.server.app.AbstractPostgresIT; -import io.cameleer.server.app.TestSecurityHelper; -import io.cameleer.server.core.agent.AgentRegistryService; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.jupiter.api.AfterEach; -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.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.test.context.TestPropertySource; - -import java.util.List; -import java.util.Map; -import java.util.UUID; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Integration tests for E4 (operator warning) and E5 (hard enforcement) of the SSE signing handoff. - *

- * Two top-level classes share this file. Each spins up its own application context via - * {@code @TestPropertySource} to flip the {@code enforce-signed-commands} flag: - *

    - *
  • {@link EnforcementDisabled} — flag off (default): non-capable agent gets 202.
  • - *
  • {@link EnforcementEnabled} — flag on: non-capable gets 409, capable gets 202.
  • - *
- */ -class AgentCommandEnforcementIT { - - // ── Flag OFF ───────────────────────────────────────────────────────────── - - @TestPropertySource(properties = "cameleer.server.security.enforce-signed-commands=false") - static class EnforcementDisabled extends AbstractPostgresIT { - - @Autowired private TestRestTemplate restTemplate; - @Autowired private ObjectMapper objectMapper; - @Autowired private TestSecurityHelper securityHelper; - @Autowired private AgentRegistryService registryService; - - private String operatorJwt; - - @BeforeEach - void setUp() { - securityHelper.installSyntheticUnsignedLicense(Map.of("max_agents", 100)); - operatorJwt = securityHelper.operatorToken(); - } - - @AfterEach - void tearDown() { - securityHelper.clearTestLicense(); - } - - @Test - void sendCommand_nonCapableAgent_returns202WhenEnforcementDisabled() throws Exception { - String agentId = "enforce-off-" + UUID.randomUUID().toString().substring(0, 8); - // requireSignedCommands=false (the default overload) - registryService.register(agentId, agentId, "test-app", "default", "1.0", List.of(), Map.of()); - - ResponseEntity response = sendConfigUpdate(agentId); - - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.ACCEPTED); - JsonNode body = objectMapper.readTree(response.getBody()); - assertThat(body.has("commandId")).isTrue(); - } - - private ResponseEntity sendConfigUpdate(String agentId) { - return restTemplate.postForEntity( - "/api/v1/agents/" + agentId + "/commands", - new HttpEntity<>(""" - {"type": "config-update", "payload": {"key": "value"}} - """, securityHelper.authHeaders(operatorJwt)), - String.class); - } - } - - // ── Flag ON ────────────────────────────────────────────────────────────── - - @TestPropertySource(properties = "cameleer.server.security.enforce-signed-commands=true") - static class EnforcementEnabled extends AbstractPostgresIT { - - @Autowired private TestRestTemplate restTemplate; - @Autowired private ObjectMapper objectMapper; - @Autowired private TestSecurityHelper securityHelper; - @Autowired private AgentRegistryService registryService; - - private String operatorJwt; - - @BeforeEach - void setUp() { - securityHelper.installSyntheticUnsignedLicense(Map.of("max_agents", 100)); - operatorJwt = securityHelper.operatorToken(); - } - - @AfterEach - void tearDown() { - securityHelper.clearTestLicense(); - } - - @Test - void sendCommand_nonCapableAgent_returns409WhenEnforcementEnabled() { - String agentId = "enforce-on-noncap-" + UUID.randomUUID().toString().substring(0, 8); - registryService.register(agentId, agentId, "test-app", "default", "1.0", List.of(), Map.of()); - - ResponseEntity response = sendConfigUpdate(agentId); - - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CONFLICT); - } - - @Test - void sendCommand_capableAgent_returns202WhenEnforcementEnabled() throws Exception { - String agentId = "enforce-on-cap-" + UUID.randomUUID().toString().substring(0, 8); - // requireSignedCommands=true — agent has been upgraded to verify signatures - registryService.register(agentId, agentId, "test-app", "default", "2.0", List.of(), Map.of(), true); - - ResponseEntity response = sendConfigUpdate(agentId); - - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.ACCEPTED); - JsonNode body = objectMapper.readTree(response.getBody()); - assertThat(body.has("commandId")).isTrue(); - } - - private ResponseEntity sendConfigUpdate(String agentId) { - return restTemplate.postForEntity( - "/api/v1/agents/" + agentId + "/commands", - new HttpEntity<>(""" - {"type": "config-update", "payload": {"key": "value"}} - """, securityHelper.authHeaders(operatorJwt)), - String.class); - } - } -}