diff --git a/src/test/java/com/safetypin/authentication/controller/AuthenticationControllerTest.java b/src/test/java/com/safetypin/authentication/controller/AuthenticationControllerTest.java index 2ca283a60476089b22b0e939a42796f897c6197d..2a562f1b3c075d93a3b71ba8c2ac1acd8ca9587a 100644 --- a/src/test/java/com/safetypin/authentication/controller/AuthenticationControllerTest.java +++ b/src/test/java/com/safetypin/authentication/controller/AuthenticationControllerTest.java @@ -170,20 +170,40 @@ class AuthenticationControllerTest { .andExpect(jsonPath("$.message").value("Password reset OTP has been sent to your email")); } + @Test + void testForgotPassword_IllegalArgumentException() throws Exception { + PasswordResetRequest request = new PasswordResetRequest(); + request.setEmail("social-login@example.com"); + + // Mock the service to throw IllegalArgumentException + String errorMessage = "Password reset is only available for email-registered users."; + Mockito.doThrow(new IllegalArgumentException(errorMessage)) + .when(authenticationService).forgotPassword("social-login@example.com"); + + mockMvc.perform(post("/api/auth/forgot-password") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.message").value(errorMessage)) + .andExpect(jsonPath("$.data").doesNotExist()); + } + @Test void testVerifyResetOTP_Success() throws Exception { VerifyResetOTPRequest request = new VerifyResetOTPRequest(); request.setEmail("email@example.com"); request.setOtp("123456"); - Mockito.when(authenticationService.verifyPasswordResetOTP("email@example.com", "123456")).thenReturn(true); + Mockito.when(authenticationService.verifyPasswordResetOTP("email@example.com", "123456")).thenReturn("reset-token-123"); mockMvc.perform(post("/api/auth/verify-reset-otp") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))) .andExpect(status().isOk()) .andExpect(jsonPath("$.success").value(true)) - .andExpect(jsonPath("$.message").value("OTP verified successfully. You can now reset your password.")); + .andExpect(jsonPath("$.message").value("OTP verified successfully. Reset token valid for 3 minutes.")) + .andExpect(jsonPath("$.data.resetToken").value("reset-token-123")); } @Test @@ -192,7 +212,7 @@ class AuthenticationControllerTest { request.setEmail("email@example.com"); request.setOtp("123456"); - Mockito.when(authenticationService.verifyPasswordResetOTP("email@example.com", "123456")).thenReturn(false); + Mockito.when(authenticationService.verifyPasswordResetOTP("email@example.com", "123456")).thenReturn(null); mockMvc.perform(post("/api/auth/verify-reset-otp") .contentType(MediaType.APPLICATION_JSON) @@ -202,13 +222,35 @@ class AuthenticationControllerTest { .andExpect(jsonPath("$.message").value("Invalid OTP")); } + @Test + void testVerifyResetOTP_IllegalArgumentException() throws Exception { + VerifyResetOTPRequest request = new VerifyResetOTPRequest(); + request.setEmail("social-login@example.com"); + request.setOtp("123456"); + + // Mock the service to throw IllegalArgumentException + String errorMessage = "Password reset is only available for email-registered users."; + Mockito.when(authenticationService.verifyPasswordResetOTP("social-login@example.com", "123456")) + .thenThrow(new IllegalArgumentException(errorMessage)); + + mockMvc.perform(post("/api/auth/verify-reset-otp") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.message").value(errorMessage)) + .andExpect(jsonPath("$.data").doesNotExist()); + } + @Test void testResetPassword_Success() throws Exception { PasswordResetWithOTPRequest request = new PasswordResetWithOTPRequest(); request.setEmail("email@example.com"); request.setNewPassword("newPassword123"); + request.setResetToken("valid-reset-token"); - Mockito.doNothing().when(authenticationService).resetPassword("email@example.com", "newPassword123"); + Mockito.doNothing().when(authenticationService).resetPassword( + "email@example.com", "newPassword123", "valid-reset-token"); mockMvc.perform(post("/api/auth/reset-password") .contentType(MediaType.APPLICATION_JSON) @@ -219,20 +261,21 @@ class AuthenticationControllerTest { } @Test - void testResetPassword_NotVerified() throws Exception { + void testResetPassword_InvalidToken() throws Exception { PasswordResetWithOTPRequest request = new PasswordResetWithOTPRequest(); request.setEmail("email@example.com"); request.setNewPassword("newPassword123"); + request.setResetToken("invalid-token"); - Mockito.doThrow(new InvalidCredentialsException("You must verify your OTP before resetting password.")) - .when(authenticationService).resetPassword("email@example.com", "newPassword123"); + Mockito.doThrow(new InvalidCredentialsException("Invalid or expired reset token. Please request a new OTP.")) + .when(authenticationService).resetPassword("email@example.com", "newPassword123", "invalid-token"); mockMvc.perform(post("/api/auth/reset-password") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))) .andExpect(status().isBadRequest()) .andExpect(jsonPath("$.success").value(false)) - .andExpect(jsonPath("$.message").value("You must verify your OTP before resetting password.")); + .andExpect(jsonPath("$.message").value("Invalid or expired reset token. Please request a new OTP.")); } @Test diff --git a/src/test/java/com/safetypin/authentication/service/AuthenticationServiceTest.java b/src/test/java/com/safetypin/authentication/service/AuthenticationServiceTest.java index 3109ede04bdaf96df8b0e766f11501ce49e4b855..c56dd4ea023ad3e305558d938b3b811bfece777c 100644 --- a/src/test/java/com/safetypin/authentication/service/AuthenticationServiceTest.java +++ b/src/test/java/com/safetypin/authentication/service/AuthenticationServiceTest.java @@ -274,9 +274,11 @@ class AuthenticationServiceTest { when(userService.findByEmail("test@example.com")).thenReturn(Optional.of(user)); when(otpService.verifyOTP("test@example.com", "123456")).thenReturn(true); + when(otpService.generateResetToken("test@example.com")).thenReturn("reset-token-123"); - boolean result = authenticationService.verifyPasswordResetOTP("test@example.com", "123456"); - assertTrue(result); + String resetToken = authenticationService.verifyPasswordResetOTP("test@example.com", "123456"); + assertNotNull(resetToken); + assertEquals("reset-token-123", resetToken); } @Test @@ -288,8 +290,8 @@ class AuthenticationServiceTest { when(userService.findByEmail("test@example.com")).thenReturn(Optional.of(user)); when(otpService.verifyOTP("test@example.com", "123456")).thenReturn(false); - boolean result = authenticationService.verifyPasswordResetOTP("test@example.com", "123456"); - assertFalse(result); + String resetToken = authenticationService.verifyPasswordResetOTP("test@example.com", "123456"); + assertNull(resetToken); } @Test @@ -329,44 +331,56 @@ class AuthenticationServiceTest { when(userService.findByEmail("social@example.com")).thenReturn(Optional.of(user)); Exception exception = assertThrows(IllegalArgumentException.class, () -> - authenticationService.resetPassword("social@example.com", "newPassword") + authenticationService.resetPassword("social@example.com", "newPassword", "reset-token") ); assertTrue(exception.getMessage().contains("Password reset is only available for email-registered users")); - verify(otpService, never()).isVerifiedForPasswordReset(anyString()); verify(userService, never()).save(any(User.class)); } - @Test - void testResetPassword_WithoutOTP_Success() { + void testResetPassword_Success() { User user = new User(); user.setEmail("test@example.com"); user.setPassword("oldEncodedPassword"); user.setProvider("EMAIL"); when(userService.findByEmail("test@example.com")).thenReturn(Optional.of(user)); - when(otpService.isVerifiedForPasswordReset("test@example.com")).thenReturn(true); + when(otpService.verifyResetToken("valid-token", "test@example.com")).thenReturn(true); when(passwordEncoder.encode("newPassword")).thenReturn("newEncodedPassword"); - authenticationService.resetPassword("test@example.com", "newPassword"); + authenticationService.resetPassword("test@example.com", "newPassword", "valid-token"); verify(userService).save(user); assertEquals("newEncodedPassword", user.getPassword()); - verify(otpService).clearPasswordResetVerification("test@example.com"); } @Test - void testResetPassword_WithoutOTP_NotVerified() { + void testResetPassword_InvalidToken() { + User user = new User(); + user.setEmail("test@example.com"); + user.setProvider("EMAIL"); + + when(userService.findByEmail("test@example.com")).thenReturn(Optional.of(user)); + when(otpService.verifyResetToken("invalid-token", "test@example.com")).thenReturn(false); + + assertThrows(InvalidCredentialsException.class, () -> + authenticationService.resetPassword("test@example.com", "newPassword", "invalid-token") + ); + + verify(userService, never()).save(any(User.class)); + } + + @Test + void testResetPassword_NullToken() { User user = new User(); user.setEmail("test@example.com"); user.setProvider("EMAIL"); when(userService.findByEmail("test@example.com")).thenReturn(Optional.of(user)); - when(otpService.isVerifiedForPasswordReset("test@example.com")).thenReturn(false); assertThrows(InvalidCredentialsException.class, () -> - authenticationService.resetPassword("test@example.com", "newPassword") + authenticationService.resetPassword("test@example.com", "newPassword", null) ); verify(userService, never()).save(any(User.class)); diff --git a/src/test/java/com/safetypin/authentication/service/OTPServiceTest.java b/src/test/java/com/safetypin/authentication/service/OTPServiceTest.java index 7d13f88c43e84545464b85d4a71f6246f8b539a0..24e6bbcbd8048749550d4cee17107372ef06aef8 100644 --- a/src/test/java/com/safetypin/authentication/service/OTPServiceTest.java +++ b/src/test/java/com/safetypin/authentication/service/OTPServiceTest.java @@ -8,13 +8,14 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import java.lang.reflect.Constructor; +import java.lang.reflect.Field; import java.time.LocalDateTime; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.anyString; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) @@ -213,53 +214,79 @@ class OTPServiceTest { } @Test - void testIsVerifiedForPasswordResetWhenEmailNotVerified() { - // Test when email is not in the verifiedResetEmails map - boolean result = otpService.isVerifiedForPasswordReset("notverified@example.com"); - assertFalse(result, "Should return false for non-verified email"); + void testGenerateResetToken() { + String email = "test@example.com"; + String resetToken = otpService.generateResetToken(email); + + assertNotNull(resetToken, "Generated reset token should not be null"); + assertTrue(resetToken.length() > 0, "Reset token should not be empty"); } @Test - void testIsVerifiedForPasswordResetWhenVerificationExpired() throws Exception { - String email = "expired@example.com"; - - // Use reflection to access and modify the private verifiedResetEmails map - java.lang.reflect.Field verifiedResetEmailsField = OTPService.class.getDeclaredField("verifiedResetEmails"); - verifiedResetEmailsField.setAccessible(true); + void testVerifyResetToken_Success() throws Exception { + String email = "test@example.com"; + String resetToken = otpService.generateResetToken(email); + + boolean isValid = otpService.verifyResetToken(resetToken, email); + assertTrue(isValid, "Reset token should be valid"); + + // Verify that the token has been removed after verification + Field resetTokenStorageField = OTPService.class.getDeclaredField("resetTokenStorage"); + resetTokenStorageField.setAccessible(true); @SuppressWarnings("unchecked") - ConcurrentHashMap<String, LocalDateTime> verifiedResetEmails = - (ConcurrentHashMap<String, LocalDateTime>) verifiedResetEmailsField.get(otpService); - - // Add an expired verification (more than 5 minutes old) - verifiedResetEmails.put(email, LocalDateTime.now().minusMinutes(6)); - - // Verify that the method returns false for expired verification - boolean result = otpService.isVerifiedForPasswordReset(email); - assertFalse(result, "Should return false for expired verification"); - - // Verify that the expired entry was removed - assertFalse(verifiedResetEmails.containsKey(email), "Expired entry should be removed from the map"); + ConcurrentHashMap<String, Object> resetTokenStorage = + (ConcurrentHashMap<String, Object>) resetTokenStorageField.get(otpService); + + assertFalse(resetTokenStorage.containsKey(resetToken), "Token should be removed after verification"); } @Test - void testIsVerifiedForPasswordResetSuccess() throws Exception { - String email = "valid@example.com"; - - // Use reflection to access and modify the private verifiedResetEmails map - java.lang.reflect.Field verifiedResetEmailsField = OTPService.class.getDeclaredField("verifiedResetEmails"); - verifiedResetEmailsField.setAccessible(true); + void testVerifyResetToken_WrongEmail() { + String email = "test@example.com"; + String resetToken = otpService.generateResetToken(email); + + boolean isValid = otpService.verifyResetToken(resetToken, "wrong@example.com"); + assertFalse(isValid, "Reset token should not be valid for different email"); + } + + @Test + void testVerifyResetToken_Expired() throws Exception { + String email = "test@example.com"; + String resetToken = otpService.generateResetToken(email); + + // Access the private resetTokenStorage field via reflection + Field resetTokenStorageField = OTPService.class.getDeclaredField("resetTokenStorage"); + resetTokenStorageField.setAccessible(true); @SuppressWarnings("unchecked") - ConcurrentHashMap<String, LocalDateTime> verifiedResetEmails = - (ConcurrentHashMap<String, LocalDateTime>) verifiedResetEmailsField.get(otpService); - - // Add a valid verification time (just now) - verifiedResetEmails.put(email, LocalDateTime.now()); + ConcurrentHashMap<String, Object> resetTokenStorage = + (ConcurrentHashMap<String, Object>) resetTokenStorageField.get(otpService); + + // Get the ResetTokenDetails class + Object tokenDetails = resetTokenStorage.get(resetToken); + assertNotNull(tokenDetails, "Token details should exist"); + + // Find the class and constructor of ResetTokenDetails + Class<?> resetTokenDetailsClass = tokenDetails.getClass(); + Constructor<?> constructor = resetTokenDetailsClass.getDeclaredConstructor(String.class, LocalDateTime.class); + constructor.setAccessible(true); - // Verify that the method returns true for valid verification - boolean result = otpService.isVerifiedForPasswordReset(email); - assertTrue(result, "Should return true for valid verification"); + // Create a new ResetTokenDetails instance with an expired time (4 minutes ago) + Object expiredTokenDetails = constructor.newInstance(email, LocalDateTime.now().minusMinutes(4)); - // Verify that the entry remains in the map - assertTrue(verifiedResetEmails.containsKey(email), "Valid entry should remain in the map"); + // Replace the original token details with the expired one + resetTokenStorage.put(resetToken, expiredTokenDetails); + + // Verify that the token is now expired + boolean isValid = otpService.verifyResetToken(resetToken, email); + assertFalse(isValid, "Reset token should be expired"); + } + + @Test + void testVerifyResetToken_NotFound() { + String nonExistentToken = "non-existent-token"; + String email = "test@example.com"; + + boolean isValid = otpService.verifyResetToken(nonExistentToken, email); + assertFalse(isValid, "Verification should fail for non-existent token"); } } \ No newline at end of file