package se.bilhalsning.service; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.util.Optional; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.security.crypto.password.PasswordEncoder; import se.bilhalsning.entity.Subscription; import se.bilhalsning.entity.User; import se.bilhalsning.exception.EmailAlreadyExistsException; import se.bilhalsning.exception.InvalidCredentialsException; import se.bilhalsning.repository.UserRepository; @ExtendWith(MockitoExtension.class) class UserServiceTest { @Mock private UserRepository userRepository; @Mock private PasswordEncoder passwordEncoder; @InjectMocks private UserService userService; @Test void shouldCreateUserWhenEmailIsNew() { when(userRepository.existsByEmail("new@example.com")).thenReturn(false); when(passwordEncoder.encode("password123")).thenReturn("hashed"); when(userRepository.save(any(User.class))).thenAnswer(inv -> inv.getArgument(0)); User result = userService.createUser("new@example.com", "password123"); assertNotNull(result); assertEquals("new@example.com", result.getEmail()); assertEquals("hashed", result.getPasswordHash()); assertEquals(Subscription.NONE, result.getSubscription()); assertEquals("user", result.getRole()); verify(userRepository).save(any(User.class)); } @Test void shouldThrowWhenEmailAlreadyExists() { when(userRepository.existsByEmail("taken@example.com")).thenReturn(true); assertThrows(EmailAlreadyExistsException.class, () -> userService.createUser("taken@example.com", "password123")); verify(userRepository, never()).save(any(User.class)); } @Test void shouldNormalizeEmailToLowercase() { when(userRepository.existsByEmail("user@example.com")).thenReturn(false); when(passwordEncoder.encode(any())).thenReturn("hashed"); when(userRepository.save(any(User.class))).thenAnswer(inv -> inv.getArgument(0)); User result = userService.createUser("User@Example.COM", "password123"); assertEquals("user@example.com", result.getEmail()); verify(userRepository).existsByEmail("user@example.com"); } @Test void shouldTrimWhitespaceFromEmail() { when(userRepository.existsByEmail("user@example.com")).thenReturn(false); when(passwordEncoder.encode(any())).thenReturn("hashed"); when(userRepository.save(any(User.class))).thenAnswer(inv -> inv.getArgument(0)); User result = userService.createUser(" user@example.com ", "password123"); assertEquals("user@example.com", result.getEmail()); verify(userRepository).existsByEmail("user@example.com"); } @Test void shouldHashPasswordBeforeSave() { when(userRepository.existsByEmail("test@example.com")).thenReturn(false); when(passwordEncoder.encode("myPassword")).thenReturn("bcryptHash"); when(userRepository.save(any(User.class))).thenAnswer(inv -> inv.getArgument(0)); userService.createUser("test@example.com", "myPassword"); ArgumentCaptor captor = ArgumentCaptor.forClass(User.class); verify(userRepository).save(captor.capture()); assertEquals("bcryptHash", captor.getValue().getPasswordHash()); } @Test void shouldFindByEmailWhenExists() { User user = new User(); user.setEmail("found@example.com"); when(userRepository.findByEmail("found@example.com")).thenReturn(Optional.of(user)); Optional result = userService.findByEmail("found@example.com"); assertTrue(result.isPresent()); assertEquals("found@example.com", result.get().getEmail()); } @Test void shouldFindByEmailNormalizesInput() { when(userRepository.findByEmail("user@example.com")).thenReturn(Optional.empty()); userService.findByEmail(" User@Example.COM "); verify(userRepository).findByEmail("user@example.com"); } @Test void shouldReturnUserWhenCredentialsAreValid() { User user = new User(); user.setEmail("user@example.com"); user.setPasswordHash("hashed"); when(userRepository.findByEmail("user@example.com")).thenReturn(Optional.of(user)); when(passwordEncoder.matches("password123", "hashed")).thenReturn(true); User result = userService.authenticate("user@example.com", "password123"); assertNotNull(result); assertEquals("user@example.com", result.getEmail()); } @Test void shouldThrowWhenEmailNotFoundOnAuthenticate() { when(userRepository.findByEmail("unknown@example.com")).thenReturn(Optional.empty()); assertThrows(InvalidCredentialsException.class, () -> userService.authenticate("unknown@example.com", "password123")); } @Test void shouldThrowWhenPasswordDoesNotMatch() { User user = new User(); user.setEmail("user@example.com"); user.setPasswordHash("hashed"); when(userRepository.findByEmail("user@example.com")).thenReturn(Optional.of(user)); when(passwordEncoder.matches("wrongpassword", "hashed")).thenReturn(false); assertThrows(InvalidCredentialsException.class, () -> userService.authenticate("user@example.com", "wrongpassword")); } @Test void shouldNormalizeEmailBeforeAuthenticating() { User user = new User(); user.setEmail("user@example.com"); user.setPasswordHash("hashed"); when(userRepository.findByEmail("user@example.com")).thenReturn(Optional.of(user)); when(passwordEncoder.matches("password123", "hashed")).thenReturn(true); userService.authenticate(" User@Example.COM ", "password123"); verify(userRepository).findByEmail("user@example.com"); } @Test void shouldChangePasswordWhenCurrentPasswordMatches() { User user = new User(); user.setEmail("admin@bilhej.se"); user.setPasswordHash("old-hash"); when(userRepository.findByEmail("admin@bilhej.se")).thenReturn(Optional.of(user)); when(passwordEncoder.matches("test1234", "old-hash")).thenReturn(true); when(passwordEncoder.encode("newpassword123")).thenReturn("new-hash"); when(userRepository.save(user)).thenReturn(user); userService.changePassword("admin@bilhej.se", "test1234", "newpassword123"); assertEquals("new-hash", user.getPasswordHash()); verify(userRepository).save(user); } @Test void shouldRejectChangePasswordWhenCurrentPasswordWrong() { User user = new User(); user.setEmail("admin@bilhej.se"); user.setPasswordHash("old-hash"); when(userRepository.findByEmail("admin@bilhej.se")).thenReturn(Optional.of(user)); when(passwordEncoder.matches("wrong", "old-hash")).thenReturn(false); assertThrows( InvalidCredentialsException.class, () -> userService.changePassword("admin@bilhej.se", "wrong", "newpassword123")); verify(userRepository, never()).save(any(User.class)); } @Test void shouldApplyEmailChangeWhenNewEmailAvailable() { User user = new User(); user.setEmail("old@example.com"); user.setPasswordHash("hash"); user.setRole("user"); when(userRepository.existsByEmail("new@example.com")).thenReturn(false); when(userRepository.save(user)).thenReturn(user); User result = userService.applyEmailChange(user, "new@example.com"); assertEquals("new@example.com", result.getEmail()); verify(userRepository).save(user); } @Test void shouldRejectApplyEmailChangeWhenNewEmailTaken() { User user = new User(); user.setEmail("old@example.com"); user.setPasswordHash("hash"); when(userRepository.existsByEmail("taken@example.com")).thenReturn(true); assertThrows( EmailAlreadyExistsException.class, () -> userService.applyEmailChange(user, "taken@example.com")); verify(userRepository, never()).save(any(User.class)); } }