From 40e6d6635c0b633e37730610855cda8ddf487c58 Mon Sep 17 00:00:00 2001 From: Dylan De Faoite Date: Thu, 15 Oct 2026 20:47:58 +0100 Subject: [PATCH] add user unit tests --- .../vodsystem/services/UserServiceTest.java | 290 +++++++++++++++++- 1 file changed, 289 insertions(+), 1 deletion(-) diff --git a/src/test/java/com/ddf/vodsystem/services/UserServiceTest.java b/src/test/java/com/ddf/vodsystem/services/UserServiceTest.java index 0b5eb3e..e6249ce 100644 --- a/src/test/java/com/ddf/vodsystem/services/UserServiceTest.java +++ b/src/test/java/com/ddf/vodsystem/services/UserServiceTest.java @@ -1,7 +1,295 @@ package com.ddf.vodsystem.services; +import com.ddf.vodsystem.dto.GoogleUser; +import com.ddf.vodsystem.dto.TokenPackage; +import com.ddf.vodsystem.entities.User; +import com.ddf.vodsystem.exceptions.NotAuthenticated; +import com.ddf.vodsystem.repositories.UserRepository; +import com.ddf.vodsystem.security.GoogleVerifierService; +import com.ddf.vodsystem.security.JwtService; +import com.ddf.vodsystem.security.TokenService; +import org.junit.jupiter.api.AfterEach; +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; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; + +import java.util.List; +import java.util.Optional; + import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; +@ExtendWith(MockitoExtension.class) class UserServiceTest { + @Mock private GoogleVerifierService googleVerifierService; + @Mock private JwtService jwtService; + @Mock private UserRepository userRepository; + @Mock private TokenService tokenService; -} \ No newline at end of file + @Captor ArgumentCaptor userCaptor; + + private static final String ID_TOKEN = "google-id-token"; + private static final String REFRESH_TOKEN = "deadbeefdeadbeefdeadbeefdeadbeef"; + private static final String NEW_REFRESH_TOKEN = "cafebabecafebabecafebabecafebabe"; + private static final String JWT = "header.payload.signature"; + private static final String GOOGLE_ID = "google-id"; + + UserService userService; + + User createUser(Long id, String username, String email) { + User user = new User(); + user.setId(id); + user.setUsername(username); + user.setEmail(email); + return user; + } + + GoogleUser googleUser() { + return new GoogleUser( + GOOGLE_ID, + "user@example.com", + "My User", + "https://lol.com/1" + ); + } + + @BeforeEach + void setUp() { + userService = new UserService( + userRepository, + jwtService, + googleVerifierService, + tokenService + ); + } + + @AfterEach + void clearSecurityContext() { + SecurityContextHolder.clearContext(); + } + + @Nested + @DisplayName("getUserById()") + class getUserById { + @Test + void existingId_returnsUser() { + User user = createUser(1L, "user", "user@example.com"); + when(userRepository.findById(1L)).thenReturn(Optional.of(user)); + + Optional result = userService.getUserById(1L); + + assertTrue(result.isPresent()); + assertSame(user, result.get()); + } + + @Test + void unknownId_returnsEmpty() { + when(userRepository.findById(1L)).thenReturn(Optional.empty()); + + assertTrue(userService.getUserById(1L).isEmpty()); + } + } + + @Nested + @DisplayName("getLoggedInUser()") + class getLoggedInUser { + @Test + void authenticatedUserPrincipal_returnsUser() { + User user = createUser(1L, "user", "user@example.com"); + SecurityContextHolder.getContext().setAuthentication( + new UsernamePasswordAuthenticationToken(user, null, List.of()) + ); + + Optional result = userService.getLoggedInUser(); + + assertTrue(result.isPresent()); + assertSame(user, result.get()); + } + + @Test + void noAuthentication_returnsEmpty() { + assertTrue(userService.getLoggedInUser().isEmpty()); + } + + @Test + void unauthenticated_returnsEmpty() { + User user = createUser(1L, "user", "user@example.com"); + SecurityContextHolder.getContext().setAuthentication( + new UsernamePasswordAuthenticationToken(user, null) + ); + + assertTrue(userService.getLoggedInUser().isEmpty()); + } + + @Test + void nonUserPrincipal_returnsEmpty() { + SecurityContextHolder.getContext().setAuthentication( + new UsernamePasswordAuthenticationToken("anonymous", null, List.of()) + ); + + assertTrue(userService.getLoggedInUser().isEmpty()); + } + } + + @Nested + @DisplayName("login()") + class login { + @Test + void newUser_savesUserToRepo() { + when(googleVerifierService.verify(ID_TOKEN)).thenReturn(googleUser()); + when(userRepository.findByGoogleId(GOOGLE_ID)).thenReturn(Optional.empty()); + when(userRepository.save(any())).thenAnswer(inv -> inv.getArgument(0)); + + userService.login(ID_TOKEN); + + verify(userRepository).save(userCaptor.capture()); + User saved = userCaptor.getValue(); + assertEquals(GOOGLE_ID, saved.getGoogleId()); + assertEquals("user@example.com", saved.getEmail()); + assertEquals("user@example.com", saved.getUsername()); + assertEquals("My User", saved.getName()); + assertEquals("https://lol.com/1", saved.getProfilePictureUrl()); + assertEquals(0, saved.getRole()); + assertNotNull(saved.getCreatedAt()); + } + + @Test + void newUser_generatesHexStreamKeyOf48Characters() { + when(googleVerifierService.verify(ID_TOKEN)).thenReturn(googleUser()); + when(userRepository.findByGoogleId(GOOGLE_ID)).thenReturn(Optional.empty()); + when(userRepository.save(any())).thenAnswer(inv -> inv.getArgument(0)); + + userService.login(ID_TOKEN); + + verify(userRepository).save(userCaptor.capture()); + String streamKey = userCaptor.getValue().getStreamKey(); + assertEquals(48, streamKey.length()); + assertTrue(streamKey.matches("[0-9a-f]+"), () -> "not hex: " + streamKey); + } + + @Test + void existingUser_updatesUserInfo() { + User existingUser = createUser(1L, "old-username", "old@example.com"); + existingUser.setGoogleId(GOOGLE_ID); + existingUser.setName("Old Name"); + existingUser.setProfilePictureUrl("https://lol.com/old"); + existingUser.setStreamKey("123"); + + when(googleVerifierService.verify(ID_TOKEN)).thenReturn(googleUser()); + when(userRepository.findByGoogleId(GOOGLE_ID)).thenReturn(Optional.of(existingUser)); + when(userRepository.save(any())).thenAnswer(inv -> inv.getArgument(0)); + + userService.login(ID_TOKEN); + + verify(userRepository).save(userCaptor.capture()); + User saved = userCaptor.getValue(); + assertSame(existingUser, saved); + assertEquals(1L, saved.getId()); + assertEquals(GOOGLE_ID, saved.getGoogleId()); + assertEquals("user@example.com", saved.getEmail()); + assertEquals("My User", saved.getName()); + assertEquals("https://lol.com/1", saved.getProfilePictureUrl()); + assertEquals("old-username", saved.getUsername()); + assertEquals("123", saved.getStreamKey()); + } + + @Test + void validToken_returnsRefreshTokenAndJwt() { + when(googleVerifierService.verify(ID_TOKEN)).thenReturn(googleUser()); + when(userRepository.findByGoogleId(GOOGLE_ID)).thenReturn(Optional.empty()); + when(userRepository.save(any())).thenAnswer(inv -> inv.getArgument(0)); + when(tokenService.createSession(any())).thenReturn(REFRESH_TOKEN); + when(jwtService.generateToken(any())).thenReturn(JWT); + + TokenPackage tokenPackage = userService.login(ID_TOKEN); + + assertEquals(REFRESH_TOKEN, tokenPackage.refreshToken()); + assertEquals(JWT, tokenPackage.accessToken()); + } + + @Test + void validToken_createsSessionForSavedUser() { + User savedUser = createUser(42L, "user@example.com", "user@example.com"); + + when(googleVerifierService.verify(ID_TOKEN)).thenReturn(googleUser()); + when(userRepository.findByGoogleId(GOOGLE_ID)).thenReturn(Optional.empty()); + when(userRepository.save(any())).thenReturn(savedUser); + + userService.login(ID_TOKEN); + + verify(tokenService).createSession(savedUser); + verify(jwtService).generateToken(42L); + } + + @Test + void invalidToken_throwsNotAuthenticated() { + when(googleVerifierService.verify(ID_TOKEN)) + .thenThrow(new NotAuthenticated("Invalid ID token")); + + assertThrows( + NotAuthenticated.class, + () -> userService.login(ID_TOKEN) + ); + + verifyNoInteractions(userRepository, tokenService, jwtService); + } + } + + @Nested + @DisplayName("refresh()") + class refresh { + @Test + void validToken_returnsRotatedTokenAndJwt() { + when(tokenService.rotate(REFRESH_TOKEN)) + .thenReturn(new TokenService.Rotation(1L, NEW_REFRESH_TOKEN)); + when(jwtService.generateToken(1L)).thenReturn(JWT); + + TokenPackage tokenPackage = userService.refresh(REFRESH_TOKEN); + + assertEquals(NEW_REFRESH_TOKEN, tokenPackage.refreshToken()); + assertEquals(JWT, tokenPackage.accessToken()); + } + + @Test + void invalidToken_throwsNotAuthenticated() { + when(tokenService.rotate(REFRESH_TOKEN)) + .thenThrow(new NotAuthenticated("Invalid refresh token")); + + assertThrows( + NotAuthenticated.class, + () -> userService.refresh(REFRESH_TOKEN) + ); + + verifyNoInteractions(jwtService); + } + } + + @Nested + @DisplayName("logout()") + class logout { + @Test + void revokesRefreshToken() { + userService.logout(REFRESH_TOKEN); + + verify(tokenService).revoke(REFRESH_TOKEN); + } + + @Test + void doesNotIssueTokens() { + userService.logout(REFRESH_TOKEN); + + verify(tokenService, never()).createSession(any()); + verify(tokenService, never()).rotate(any()); + verifyNoInteractions(jwtService); + } + } +}