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