add rotate tests
All checks were successful
CI / Maven unit tests (push) Successful in 15s
CI / Build and push images (push) Has been skipped
CI / Deploy to server (push) Has been skipped

This commit is contained in:
2026-10-09 17:44:30 +01:00
parent 4b5e6ed8f6
commit 4c7c2f0916

View File

@@ -4,9 +4,12 @@ import com.ddf.vodsystem.dto.properties.AuthProperties;
import com.ddf.vodsystem.entities.RefreshToken; import com.ddf.vodsystem.entities.RefreshToken;
import com.ddf.vodsystem.entities.TokenFamily; import com.ddf.vodsystem.entities.TokenFamily;
import com.ddf.vodsystem.entities.User; import com.ddf.vodsystem.entities.User;
import com.ddf.vodsystem.exceptions.NotAuthenticated;
import com.ddf.vodsystem.repositories.RefreshTokenRepository; import com.ddf.vodsystem.repositories.RefreshTokenRepository;
import com.ddf.vodsystem.repositories.TokenFamilyRepository; import com.ddf.vodsystem.repositories.TokenFamilyRepository;
import org.junit.jupiter.api.BeforeEach; 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.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor; import org.mockito.ArgumentCaptor;
@@ -16,9 +19,11 @@ import org.mockito.junit.jupiter.MockitoExtension;
import java.time.*; import java.time.*;
import java.util.HexFormat; import java.util.HexFormat;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.*; 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 { class MutableClock extends Clock {
private Instant now; private Instant now;
@@ -46,6 +51,7 @@ class TokenServiceTest {
@Captor ArgumentCaptor<RefreshToken> tokenCaptor; @Captor ArgumentCaptor<RefreshToken> tokenCaptor;
private static final HexFormat HEX = HexFormat.of(); private static final HexFormat HEX = HexFormat.of();
private static final String rawToken = "deadbeefdeadbeefdeadbeefdeadbeef";
AuthProperties authProperties; AuthProperties authProperties;
TokenService tokenService; TokenService tokenService;
@@ -61,6 +67,7 @@ class TokenServiceTest {
@BeforeEach @BeforeEach
void setUp() { void setUp() {
authProperties = new AuthProperties( authProperties = new AuthProperties(
"a-secret", "a-secret",
new AuthProperties.Expiration( new AuthProperties.Expiration(
@@ -81,6 +88,9 @@ class TokenServiceTest {
); );
} }
@Nested
@DisplayName("createSession()")
class createSession {
@Test @Test
void createSession_createsTokenFamily() { void createSession_createsTokenFamily() {
User user = createUser(1L, "user", "user@gmail.com"); User user = createUser(1L, "user", "user@gmail.com");
@@ -138,5 +148,113 @@ class TokenServiceTest {
assertNotEquals(refreshToken.getTokenHash(), HEX.parseHex(token)); assertNotEquals(refreshToken.getTokenHash(), HEX.parseHex(token));
} }
}
// Rotate
@Nested
@DisplayName("rotate()")
class rotate {
// Input val
@Test
void nullToken_throwsNotAuthenticated() {
assertThrows(
NotAuthenticated.class,
() -> tokenService.rotate(null)
);
}
@Test
void blankToken_throwsNotAuthenticated() {
assertThrows(
NotAuthenticated.class,
() -> tokenService.rotate("")
);
}
// Invalid cases
@Test
void unknownToken_throwsNotAuthenticated() {
when(refreshTokenRepository.findByHash(any())).thenReturn(
Optional.empty()
);
assertThrows(
NotAuthenticated.class,
() -> tokenService.rotate(rawToken)
);
}
@Test
void revokedFamily_throwsNotAuthenticated() {
TokenFamily tokenFamily = new TokenFamily();
tokenFamily.setId(1L);
tokenFamily.setRevokedAt(Instant.now(clock));
RefreshToken refreshToken = new RefreshToken();
refreshToken.setTokenFamily(tokenFamily);
clock.advance(Duration.ofMinutes(1));
when(refreshTokenRepository.findByHash(any())).thenReturn(
Optional.of(refreshToken)
);
assertThrows(
NotAuthenticated.class,
() -> tokenService.rotate(rawToken)
);
}
// 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());
}
}
} }