fix(test): split AgentCommandEnforcementIT into two discoverable top-level IT classes

Failsafe discovers classes by name pattern (*IT, *ITCase, IT*). The
nested static classes EnforcementDisabled/EnforcementEnabled inside the
outer AgentCommandEnforcementIT were invisible to the test runner — the
outer class matched *IT but had no @Test methods; the inner classes did
not match the naming convention and were silently skipped.

Split into AgentCommandEnforcementDisabledIT and
AgentCommandEnforcementEnabledIT — identical logic, each a proper
top-level class extending AbstractPostgresIT. All 6 enforcement tests
(3 signing-failure + 1 disabled + 2 enabled) now execute and pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-29 12:02:28 +02:00
parent ccf31a4067
commit bf79a6f5ae
3 changed files with 150 additions and 137 deletions

View File

@@ -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<String> response = sendConfigUpdate(agentId);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.ACCEPTED);
JsonNode body = objectMapper.readTree(response.getBody());
assertThat(body.has("commandId")).isTrue();
}
private ResponseEntity<String> sendConfigUpdate(String agentId) {
return restTemplate.postForEntity(
"/api/v1/agents/" + agentId + "/commands",
new HttpEntity<>("""
{"type": "config-update", "payload": {"key": "value"}}
""", securityHelper.authHeaders(operatorJwt)),
String.class);
}
}

View File

@@ -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<String> 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<String> response = sendConfigUpdate(agentId);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.ACCEPTED);
JsonNode body = objectMapper.readTree(response.getBody());
assertThat(body.has("commandId")).isTrue();
}
private ResponseEntity<String> sendConfigUpdate(String agentId) {
return restTemplate.postForEntity(
"/api/v1/agents/" + agentId + "/commands",
new HttpEntity<>("""
{"type": "config-update", "payload": {"key": "value"}}
""", securityHelper.authHeaders(operatorJwt)),
String.class);
}
}

View File

@@ -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.
* <p>
* 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:
* <ul>
* <li>{@link EnforcementDisabled} — flag off (default): non-capable agent gets 202.</li>
* <li>{@link EnforcementEnabled} — flag on: non-capable gets 409, capable gets 202.</li>
* </ul>
*/
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<String> response = sendConfigUpdate(agentId);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.ACCEPTED);
JsonNode body = objectMapper.readTree(response.getBody());
assertThat(body.has("commandId")).isTrue();
}
private ResponseEntity<String> 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<String> 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<String> response = sendConfigUpdate(agentId);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.ACCEPTED);
JsonNode body = objectMapper.readTree(response.getBody());
assertThat(body.has("commandId")).isTrue();
}
private ResponseEntity<String> sendConfigUpdate(String agentId) {
return restTemplate.postForEntity(
"/api/v1/agents/" + agentId + "/commands",
new HttpEntity<>("""
{"type": "config-update", "payload": {"key": "value"}}
""", securityHelper.authHeaders(operatorJwt)),
String.class);
}
}
}