test(04-01): add failing tests for security services

- JwtService: 7 tests for access/refresh token creation and validation
- Ed25519SigningService: 5 tests for keypair, signing, verification
- BootstrapTokenValidator: 6 tests for token matching and rotation
- Core interfaces and stub implementations (all throw UnsupportedOperationException)
- Added nimbus-jose-jwt and spring-boot-starter-security dependencies

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-03-11 19:58:59 +01:00
parent b7c35037e6
commit 51a02700dd
11 changed files with 482 additions and 0 deletions

View File

@@ -0,0 +1,70 @@
package com.cameleer3.server.app.security;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
/**
* Unit tests for {@link BootstrapTokenValidator}.
* No Spring context needed — implementation constructed directly.
*/
class BootstrapTokenValidatorTest {
@Test
void validate_correctToken_returnsTrue() {
SecurityProperties props = new SecurityProperties();
props.setBootstrapToken("my-secret-token");
BootstrapTokenValidator validator = new BootstrapTokenValidator(props);
assertTrue(validator.validate("my-secret-token"));
}
@Test
void validate_wrongToken_returnsFalse() {
SecurityProperties props = new SecurityProperties();
props.setBootstrapToken("my-secret-token");
BootstrapTokenValidator validator = new BootstrapTokenValidator(props);
assertFalse(validator.validate("wrong-token"));
}
@Test
void validate_previousToken_returnsTrueWhenSet() {
SecurityProperties props = new SecurityProperties();
props.setBootstrapToken("new-token");
props.setBootstrapTokenPrevious("old-token");
BootstrapTokenValidator validator = new BootstrapTokenValidator(props);
assertTrue(validator.validate("old-token"), "Previous token should be accepted during rotation");
}
@Test
void validate_nullToken_returnsFalse() {
SecurityProperties props = new SecurityProperties();
props.setBootstrapToken("my-secret-token");
BootstrapTokenValidator validator = new BootstrapTokenValidator(props);
assertFalse(validator.validate(null));
}
@Test
void validate_blankToken_returnsFalse() {
SecurityProperties props = new SecurityProperties();
props.setBootstrapToken("my-secret-token");
BootstrapTokenValidator validator = new BootstrapTokenValidator(props);
assertFalse(validator.validate(""));
assertFalse(validator.validate(" "));
}
@Test
void validate_previousTokenNotSet_onlyCurrentAccepted() {
SecurityProperties props = new SecurityProperties();
props.setBootstrapToken("current-token");
// bootstrapTokenPrevious is null by default
BootstrapTokenValidator validator = new BootstrapTokenValidator(props);
assertTrue(validator.validate("current-token"));
assertFalse(validator.validate("some-old-token"));
}
}

View File

@@ -0,0 +1,88 @@
package com.cameleer3.server.app.security;
import com.cameleer3.server.core.security.Ed25519SigningService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.security.KeyFactory;
import java.security.PublicKey;
import java.security.Signature;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import static org.junit.jupiter.api.Assertions.*;
/**
* Unit tests for {@link Ed25519SigningServiceImpl}.
* No Spring context needed — implementation constructed directly.
*/
class Ed25519SigningServiceTest {
private Ed25519SigningService signingService;
@BeforeEach
void setUp() {
signingService = new Ed25519SigningServiceImpl();
}
@Test
void getPublicKeyBase64_returnsNonNullBase64String() {
String publicKeyBase64 = signingService.getPublicKeyBase64();
assertNotNull(publicKeyBase64);
assertFalse(publicKeyBase64.isBlank());
// Verify it's valid Base64
assertDoesNotThrow(() -> Base64.getDecoder().decode(publicKeyBase64));
}
@Test
void sign_returnsBase64SignatureString() {
String signature = signingService.sign("test payload");
assertNotNull(signature);
assertFalse(signature.isBlank());
assertDoesNotThrow(() -> Base64.getDecoder().decode(signature));
}
@Test
void sign_signatureVerifiesAgainstPublicKey() throws Exception {
String payload = "important config data";
String signatureBase64 = signingService.sign(payload);
String publicKeyBase64 = signingService.getPublicKeyBase64();
// Reconstruct public key from Base64
byte[] publicKeyBytes = Base64.getDecoder().decode(publicKeyBase64);
KeyFactory keyFactory = KeyFactory.getInstance("Ed25519");
PublicKey publicKey = keyFactory.generatePublic(new X509EncodedKeySpec(publicKeyBytes));
// Verify signature
Signature verifier = Signature.getInstance("Ed25519");
verifier.initVerify(publicKey);
verifier.update(payload.getBytes(java.nio.charset.StandardCharsets.UTF_8));
assertTrue(verifier.verify(Base64.getDecoder().decode(signatureBase64)),
"Signature should verify against the public key");
}
@Test
void sign_differentPayloadsProduceDifferentSignatures() {
String sig1 = signingService.sign("payload one");
String sig2 = signingService.sign("payload two");
assertNotEquals(sig1, sig2, "Different payloads should produce different signatures");
}
@Test
void sign_tamperedPayloadFailsVerification() throws Exception {
String payload = "original payload";
String signatureBase64 = signingService.sign(payload);
String publicKeyBase64 = signingService.getPublicKeyBase64();
byte[] publicKeyBytes = Base64.getDecoder().decode(publicKeyBase64);
KeyFactory keyFactory = KeyFactory.getInstance("Ed25519");
PublicKey publicKey = keyFactory.generatePublic(new X509EncodedKeySpec(publicKeyBytes));
// Verify against tampered payload
Signature verifier = Signature.getInstance("Ed25519");
verifier.initVerify(publicKey);
verifier.update("tampered payload".getBytes(java.nio.charset.StandardCharsets.UTF_8));
assertFalse(verifier.verify(Base64.getDecoder().decode(signatureBase64)),
"Tampered payload should fail verification");
}
}

View File

@@ -0,0 +1,86 @@
package com.cameleer3.server.app.security;
import com.cameleer3.server.core.security.InvalidTokenException;
import com.cameleer3.server.core.security.JwtService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
/**
* Unit tests for {@link JwtServiceImpl}.
* No Spring context needed — implementations are constructed directly.
*/
class JwtServiceTest {
private JwtService jwtService;
@BeforeEach
void setUp() {
SecurityProperties props = new SecurityProperties();
props.setAccessTokenExpiryMs(3_600_000); // 1 hour
props.setRefreshTokenExpiryMs(604_800_000); // 7 days
jwtService = new JwtServiceImpl(props);
}
@Test
void createAccessToken_returnsSignedJwtWithCorrectClaims() {
String token = jwtService.createAccessToken("agent-1", "group-a");
assertNotNull(token);
assertFalse(token.isBlank());
// JWT format: header.payload.signature
assertEquals(3, token.split("\\.").length, "JWT should have 3 parts");
}
@Test
void createAccessToken_canBeValidated() {
String token = jwtService.createAccessToken("agent-1", "group-a");
String agentId = jwtService.validateAndExtractAgentId(token);
assertEquals("agent-1", agentId);
}
@Test
void createRefreshToken_returnsSignedJwt() {
String token = jwtService.createRefreshToken("agent-2", "group-b");
assertNotNull(token);
assertFalse(token.isBlank());
assertEquals(3, token.split("\\.").length, "JWT should have 3 parts");
}
@Test
void createRefreshToken_canBeValidatedWithRefreshMethod() {
String token = jwtService.createRefreshToken("agent-2", "group-b");
String agentId = jwtService.validateRefreshToken(token);
assertEquals("agent-2", agentId);
}
@Test
void validateAndExtractAgentId_rejectsRefreshToken() {
String refreshToken = jwtService.createRefreshToken("agent-3", "group-c");
assertThrows(InvalidTokenException.class, () ->
jwtService.validateAndExtractAgentId(refreshToken),
"Access validation should reject refresh tokens");
}
@Test
void validateRefreshToken_rejectsAccessToken() {
String accessToken = jwtService.createAccessToken("agent-4", "group-d");
assertThrows(InvalidTokenException.class, () ->
jwtService.validateRefreshToken(accessToken),
"Refresh validation should reject access tokens");
}
@Test
void validateAndExtractAgentId_rejectsExpiredToken() {
// Create a service with 0ms expiry to produce already-expired tokens
SecurityProperties shortProps = new SecurityProperties();
shortProps.setAccessTokenExpiryMs(0);
shortProps.setRefreshTokenExpiryMs(604_800_000);
JwtService shortLivedService = new JwtServiceImpl(shortProps);
String token = shortLivedService.createAccessToken("agent-5", "group-e");
assertThrows(InvalidTokenException.class, () ->
jwtService.validateAndExtractAgentId(token),
"Should reject expired tokens");
}
}