diff --git a/src/main/java/com/empress/usermanagementapi/controller/ForgotPasswordController.java b/src/main/java/com/empress/usermanagementapi/controller/ForgotPasswordController.java index 9a52a56..bfa9df6 100644 --- a/src/main/java/com/empress/usermanagementapi/controller/ForgotPasswordController.java +++ b/src/main/java/com/empress/usermanagementapi/controller/ForgotPasswordController.java @@ -3,6 +3,7 @@ import com.empress.usermanagementapi.entity.User; import com.empress.usermanagementapi.service.UserService; import com.empress.usermanagementapi.service.PasswordResetService; +import com.empress.usermanagementapi.util.LoggingUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Controller; @@ -15,6 +16,8 @@ public class ForgotPasswordController { private static final Logger log = LoggerFactory.getLogger(ForgotPasswordController.class); + private static final String RESET_REQUEST_MESSAGE = + "If an account with those details exists, a reset link has been sent."; private final UserService userService; private final PasswordResetService passwordResetService; @@ -46,24 +49,29 @@ public String handleForm(@RequestParam String username, return "forgot-password"; } - // Check username + email combo Optional opt = userService.findByUsernameAndEmail(trimmedUsername, trimmedEmail); if (opt.isEmpty()) { - // This is the part you were missing: explicit feedback - model.addAttribute("error", "No account found with that username and email."); + log.info("Password reset requested for non-matching account - username: {}, email: {}", + trimmedUsername, + LoggingUtil.maskEmail(trimmedEmail)); + model.addAttribute("success", RESET_REQUEST_MESSAGE); return "forgot-password"; } try { passwordResetService.createTokenAndSendResetEmail(trimmedEmail); + log.info("Password reset email sent - username: {}, email: {}", + trimmedUsername, + LoggingUtil.maskEmail(trimmedEmail)); } catch (Exception e) { - log.error("Failed to send password reset email to: {}", trimmedEmail, e); - model.addAttribute("error", "Failed to send password reset email. Please try again later."); - return "forgot-password"; + log.error("Failed to send password reset email - username: {}, email: {}", + trimmedUsername, + LoggingUtil.maskEmail(trimmedEmail), + e); } - model.addAttribute("success", "Password reset link has been sent to your email."); + model.addAttribute("success", RESET_REQUEST_MESSAGE); return "forgot-password"; } } diff --git a/src/test/java/com/empress/usermanagementapi/controller/ForgotPasswordControllerTest.java b/src/test/java/com/empress/usermanagementapi/controller/ForgotPasswordControllerTest.java new file mode 100644 index 0000000..cdba77f --- /dev/null +++ b/src/test/java/com/empress/usermanagementapi/controller/ForgotPasswordControllerTest.java @@ -0,0 +1,88 @@ +package com.empress.usermanagementapi.controller; + +import com.empress.usermanagementapi.entity.User; +import com.empress.usermanagementapi.service.PasswordResetService; +import com.empress.usermanagementapi.service.UserService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.ui.Model; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class ForgotPasswordControllerTest { + + private static final String GENERIC_MESSAGE = + "If an account with those details exists, a reset link has been sent."; + + @Mock + private UserService userService; + + @Mock + private PasswordResetService passwordResetService; + + @Mock + private Model model; + + private ForgotPasswordController controller; + + @BeforeEach + void setUp() { + controller = new ForgotPasswordController(userService, passwordResetService); + } + + @Test + void handleForm_WhenNoAccountMatches_ReturnsGenericSuccessWithoutSendingEmail() { + when(userService.findByUsernameAndEmail("missing", "missing@example.com")) + .thenReturn(Optional.empty()); + + String view = controller.handleForm(" missing ", " missing@example.com ", model); + + assertEquals("forgot-password", view); + verify(model).addAttribute("success", GENERIC_MESSAGE); + verify(model, never()).addAttribute(eq("error"), any()); + verify(passwordResetService, never()).createTokenAndSendResetEmail(anyString()); + } + + @Test + void handleForm_WhenAccountMatches_SendsEmailAndReturnsGenericSuccess() { + User user = new User(); + when(userService.findByUsernameAndEmail("user", "user@example.com")) + .thenReturn(Optional.of(user)); + + String view = controller.handleForm(" user ", " user@example.com ", model); + + assertEquals("forgot-password", view); + verify(passwordResetService).createTokenAndSendResetEmail("user@example.com"); + verify(model).addAttribute("success", GENERIC_MESSAGE); + verify(model, never()).addAttribute(eq("error"), any()); + } + + @Test + void handleForm_WhenEmailDeliveryFails_StillReturnsGenericSuccess() { + User user = new User(); + when(userService.findByUsernameAndEmail("user", "user@example.com")) + .thenReturn(Optional.of(user)); + doThrow(new RuntimeException("email delivery failed")) + .when(passwordResetService) + .createTokenAndSendResetEmail("user@example.com"); + + String view = controller.handleForm(" user ", " user@example.com ", model); + + assertEquals("forgot-password", view); + verify(model).addAttribute("success", GENERIC_MESSAGE); + verify(model, never()).addAttribute(eq("error"), any()); + } +}