diff --git a/src/main/java/com/ddf/vodsystem/security/TokenService.java b/src/main/java/com/ddf/vodsystem/security/TokenService.java index 3c6abab..fbc68c3 100644 --- a/src/main/java/com/ddf/vodsystem/security/TokenService.java +++ b/src/main/java/com/ddf/vodsystem/security/TokenService.java @@ -28,8 +28,8 @@ public class TokenService { private final TokenFamilyRepository tokenFamilyRepository; private final RefreshTokenRepository refreshTokenRepository; - private final Duration tokenFamilyExpirationMs; - private final Duration refreshTokenExpirationMs; + private final Duration tokenFamilyExpiration; + private final Duration refreshTokenExpiration; private final Duration refreshGracePeriod; public TokenService ( @@ -42,8 +42,8 @@ public class TokenService { this.refreshTokenRepository = refreshTokenRepository; this.clock = clock; - this.refreshTokenExpirationMs = props.expiration().refreshToken(); - this.tokenFamilyExpirationMs = props.expiration().tokenFamily(); + this.refreshTokenExpiration = props.expiration().refreshToken(); + this.tokenFamilyExpiration = props.expiration().tokenFamily(); this.refreshGracePeriod = props.expiration().gracePeriod(); } @@ -54,7 +54,7 @@ public class TokenService { TokenFamily tokenFamily = new TokenFamily(); tokenFamily.setUser(user); tokenFamily.setCreatedAt(now); - tokenFamily.setExpiresAt(now.plusMillis(tokenFamilyExpirationMs.toMillis())); + tokenFamily.setExpiresAt(now.plusMillis(tokenFamilyExpiration.toMillis())); tokenFamilyRepository.save(tokenFamily); byte[] token = generateRandomBytes(32); @@ -150,7 +150,7 @@ public class TokenService { refreshToken.setTokenFamily(tokenFamily); refreshToken.setTokenHash(sha256(token)); refreshToken.setCreatedAt(now); - refreshToken.setExpiresAt(now.plusMillis(refreshTokenExpirationMs.toMillis())); + refreshToken.setExpiresAt(now.plusMillis(refreshTokenExpiration.toMillis())); return refreshTokenRepository.save(refreshToken); } diff --git a/src/test/java/com/ddf/vodsystem/security/TokenServiceTest.java b/src/test/java/com/ddf/vodsystem/security/TokenServiceTest.java new file mode 100644 index 0000000..7cee129 --- /dev/null +++ b/src/test/java/com/ddf/vodsystem/security/TokenServiceTest.java @@ -0,0 +1,142 @@ +package com.ddf.vodsystem.security; + +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.repositories.RefreshTokenRepository; +import com.ddf.vodsystem.repositories.TokenFamilyRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.*; +import java.util.HexFormat; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.verify; + +class MutableClock extends Clock { + private Instant now; + + MutableClock(Instant start) { + this.now = start; + } + + void advance(Duration d) { + now = now.plus(d); + } + + @Override public Instant instant() { return now; } + @Override public ZoneId getZone() { return ZoneOffset.UTC; } + @Override public Clock withZone(ZoneId zone) { return this; } +} + + +@ExtendWith(MockitoExtension.class) +class TokenServiceTest { + @Mock private TokenFamilyRepository tokenFamilyRepository; + @Mock private RefreshTokenRepository refreshTokenRepository; + + @Captor ArgumentCaptor familyCaptor; + @Captor ArgumentCaptor tokenCaptor; + + private static final HexFormat HEX = HexFormat.of(); + + AuthProperties authProperties; + TokenService tokenService; + MutableClock clock; + + User createUser(Long id, String username, String email) { + User user = new User(); + user.setId(id); + user.setUsername(username); + user.setEmail(email); + return user; + } + + @BeforeEach + void setUp() { + authProperties = new AuthProperties( + "a-secret", + new AuthProperties.Expiration( + Duration.ofSeconds(10), + Duration.ofSeconds(10), + Duration.ofSeconds(10), + Duration.ofSeconds(10) + ) + ); + + clock = new MutableClock(Instant.parse("2026-01-01T00:00:00Z")); + + tokenService = new TokenService( + tokenFamilyRepository, + refreshTokenRepository, + authProperties, + clock + ); + } + + @Test + void createSession_createsTokenFamily() { + User user = createUser(1L, "user", "user@gmail.com"); + + tokenService.createSession(user); + + verify(tokenFamilyRepository).save(familyCaptor.capture()); + verify(refreshTokenRepository).save(tokenCaptor.capture()); + + 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()) + ); + } + + @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)); + } + +} \ No newline at end of file