From 4c7c2f0916112ddef6530cf2aae56e9a9e876b61 Mon Sep 17 00:00:00 2001 From: Dylan De Faoite Date: Fri, 9 Oct 2026 17:44:30 +0100 Subject: [PATCH] add rotate tests --- .../vodsystem/security/TokenServiceTest.java | 200 ++++++++++++++---- 1 file changed, 159 insertions(+), 41 deletions(-) diff --git a/src/test/java/com/ddf/vodsystem/security/TokenServiceTest.java b/src/test/java/com/ddf/vodsystem/security/TokenServiceTest.java index 7cee129..22e9fff 100644 --- a/src/test/java/com/ddf/vodsystem/security/TokenServiceTest.java +++ b/src/test/java/com/ddf/vodsystem/security/TokenServiceTest.java @@ -4,9 +4,12 @@ import com.ddf.vodsystem.dto.properties.AuthProperties; import com.ddf.vodsystem.entities.RefreshToken; import com.ddf.vodsystem.entities.TokenFamily; import com.ddf.vodsystem.entities.User; +import com.ddf.vodsystem.exceptions.NotAuthenticated; import com.ddf.vodsystem.repositories.RefreshTokenRepository; import com.ddf.vodsystem.repositories.TokenFamilyRepository; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; @@ -16,9 +19,11 @@ import org.mockito.junit.jupiter.MockitoExtension; import java.time.*; import java.util.HexFormat; +import java.util.Optional; import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.verify; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; class MutableClock extends Clock { private Instant now; @@ -46,6 +51,7 @@ class TokenServiceTest { @Captor ArgumentCaptor tokenCaptor; private static final HexFormat HEX = HexFormat.of(); + private static final String rawToken = "deadbeefdeadbeefdeadbeefdeadbeef"; AuthProperties authProperties; TokenService tokenService; @@ -61,6 +67,7 @@ class TokenServiceTest { @BeforeEach void setUp() { + authProperties = new AuthProperties( "a-secret", new AuthProperties.Expiration( @@ -81,62 +88,173 @@ class TokenServiceTest { ); } - @Test - void createSession_createsTokenFamily() { - User user = createUser(1L, "user", "user@gmail.com"); + @Nested + @DisplayName("createSession()") + class createSession { + @Test + void createSession_createsTokenFamily() { + User user = createUser(1L, "user", "user@gmail.com"); - tokenService.createSession(user); + tokenService.createSession(user); - verify(tokenFamilyRepository).save(familyCaptor.capture()); - verify(refreshTokenRepository).save(tokenCaptor.capture()); + verify(tokenFamilyRepository).save(familyCaptor.capture()); + verify(refreshTokenRepository).save(tokenCaptor.capture()); - TokenFamily tokenFamily = familyCaptor.getValue(); - RefreshToken refreshToken = tokenCaptor.getValue(); + TokenFamily tokenFamily = familyCaptor.getValue(); + RefreshToken refreshToken = tokenCaptor.getValue(); - assertSame(tokenFamily.getUser(), user); - assertSame(refreshToken.getTokenFamily(), tokenFamily); - assertEquals( - tokenFamily.getExpiresAt(), - clock.instant().plusMillis(authProperties.expiration().tokenFamily().toMillis()) - ); + assertSame(tokenFamily.getUser(), user); + assertSame(refreshToken.getTokenFamily(), tokenFamily); + assertEquals( + tokenFamily.getExpiresAt(), + clock.instant().plusMillis(authProperties.expiration().tokenFamily().toMillis()) + ); + } + + @Test + void createSession_savesRefreshTokenCreatedAtAndExpires() { + User user = createUser(1L, "user", "user"); + + tokenService.createSession(user); + + verify(refreshTokenRepository).save(tokenCaptor.capture()); + RefreshToken refreshToken = tokenCaptor.getValue(); + + assertEquals(refreshToken.getCreatedAt(), clock.instant()); + assertEquals( + refreshToken.getExpiresAt(), + clock.instant().plusMillis(authProperties.expiration().refreshToken().toMillis()) + ); + } + + @Test + void createSession_returnsHexTokenOf64Characters() { + User user = createUser(1L, "user", "user"); + + String token = tokenService.createSession(user); + + assertEquals(64, token.length()); + assertTrue(token.matches("[0-9a-fA-F]+"), () -> "not hex: " + token); + } + + @Test + void createSession_doesNotStoreRawToken() { + User user = createUser(1L, "user", "user"); + + String token = tokenService.createSession(user); + + verify(refreshTokenRepository).save(tokenCaptor.capture()); + RefreshToken refreshToken = tokenCaptor.getValue(); + + assertNotEquals(refreshToken.getTokenHash(), HEX.parseHex(token)); + } } - @Test - void createSession_savesRefreshTokenCreatedAtAndExpires() { - User user = createUser(1L, "user", "user"); + // Rotate + @Nested + @DisplayName("rotate()") + class rotate { + // Input val + @Test + void nullToken_throwsNotAuthenticated() { + assertThrows( + NotAuthenticated.class, + () -> tokenService.rotate(null) + ); + } - tokenService.createSession(user); + @Test + void blankToken_throwsNotAuthenticated() { + assertThrows( + NotAuthenticated.class, + () -> tokenService.rotate("") + ); + } - verify(refreshTokenRepository).save(tokenCaptor.capture()); - RefreshToken refreshToken = tokenCaptor.getValue(); + // Invalid cases + @Test + void unknownToken_throwsNotAuthenticated() { + when(refreshTokenRepository.findByHash(any())).thenReturn( + Optional.empty() + ); - assertEquals(refreshToken.getCreatedAt(), clock.instant()); - assertEquals( - refreshToken.getExpiresAt(), - clock.instant().plusMillis(authProperties.expiration().refreshToken().toMillis()) - ); - } + assertThrows( + NotAuthenticated.class, + () -> tokenService.rotate(rawToken) + ); + } - @Test - void createSession_returnsHexTokenOf64Characters() { - User user = createUser(1L, "user", "user"); + @Test + void revokedFamily_throwsNotAuthenticated() { + TokenFamily tokenFamily = new TokenFamily(); + tokenFamily.setId(1L); + tokenFamily.setRevokedAt(Instant.now(clock)); - String token = tokenService.createSession(user); + RefreshToken refreshToken = new RefreshToken(); + refreshToken.setTokenFamily(tokenFamily); - assertEquals(64, token.length()); - assertTrue(token.matches("[0-9a-fA-F]+"), () -> "not hex: " + token); - } + clock.advance(Duration.ofMinutes(1)); - @Test - void createSession_doesNotStoreRawToken() { - User user = createUser(1L, "user", "user"); + when(refreshTokenRepository.findByHash(any())).thenReturn( + Optional.of(refreshToken) + ); - String token = tokenService.createSession(user); + assertThrows( + NotAuthenticated.class, + () -> tokenService.rotate(rawToken) + ); + } - verify(refreshTokenRepository).save(tokenCaptor.capture()); - RefreshToken refreshToken = tokenCaptor.getValue(); + // Happy Path + @Test + void unusedToken_returnsUserIdAndRefreshToken() { + TokenFamily tokenFamily = new TokenFamily(); + tokenFamily.setId(1L); + tokenFamily.setUser(createUser(1L, "user", "user")); + tokenFamily.setExpiresAt(Instant.now(clock).plusSeconds(10)); + + RefreshToken refreshToken = new RefreshToken(); + refreshToken.setTokenFamily(tokenFamily); + refreshToken.setExpiresAt(Instant.now(clock).plusSeconds(10)); + + when(refreshTokenRepository.findByHash(any())) + .thenReturn(Optional.of(refreshToken)); + + TokenService.Rotation rotation = tokenService.rotate(rawToken); + + assertEquals(1L, rotation.userId()); + assertNotEquals(rawToken, rotation.newRefreshToken()); + } + + @Test + void unsedToken_savesRefreshToken() { + TokenFamily tokenFamily = new TokenFamily(); + tokenFamily.setId(1L); + tokenFamily.setUser(createUser(1L, "user", "user")); + tokenFamily.setExpiresAt(Instant.now(clock).plusSeconds(10)); + + RefreshToken refreshToken = new RefreshToken(); + refreshToken.setTokenFamily(tokenFamily); + refreshToken.setExpiresAt(Instant.now(clock).plusSeconds(10)); + + when(refreshTokenRepository.findByHash(any())) + .thenReturn(Optional.of(refreshToken)); + + tokenService.rotate(rawToken); + + verify(refreshTokenRepository, times(2)).save(tokenCaptor.capture()); + + RefreshToken newToken = tokenCaptor.getAllValues().stream() + .filter(t -> t.getUsedAt() == null) + .reduce((a, b) -> { throw new AssertionError("expected exactly one unused token saved"); }) + .orElseThrow(() -> new AssertionError("no unused token was saved")); + + assertNotEquals(refreshToken.getTokenHash(), newToken.getTokenHash()); + assertEquals(refreshToken.getTokenFamily().getUser(), newToken.getTokenFamily().getUser()); // same owner + assertEquals(refreshToken.getTokenFamily(), newToken.getTokenFamily()); // same rotation family, if you model one + assertNull(newToken.getUsedAt()); + } - assertNotEquals(refreshToken.getTokenHash(), HEX.parseHex(token)); } } \ No newline at end of file