using Fluxor; using GoodWood.Application.DTOs; using GoodWood.Application.Interfaces; using GoodWood.Domain.Interfaces; using GoodWood.Web.Controllers; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; namespace GoodWood.Tests.Integration; /// /// Integration tests for AuthController. /// Tests controller logic without full HTTP pipeline. /// public class AuthControllerTests { private readonly Mock _dispatcherMock; private readonly Mock _userServiceMock; private readonly Mock _emailServiceMock; private readonly AuthController _sut; public AuthControllerTests() { _dispatcherMock = new Mock(); _userServiceMock = new Mock(); _emailServiceMock = new Mock(); _sut = new AuthController(_dispatcherMock.Object, _userServiceMock.Object, _emailServiceMock.Object); // Setup controller context with request info for password reset URL building var httpContext = new DefaultHttpContext(); httpContext.Request.Scheme = "https"; httpContext.Request.Host = new HostString("localhost"); _sut.ControllerContext = new ControllerContext { HttpContext = httpContext }; } #region Login Tests [Fact] public async Task Login_ReturnsRedirectToHome_WhenCredentialsValid() { // Arrange var loginDto = new LoginDto { Email = "test@example.com", Password = "password123", ReturnUrl = "/" }; _userServiceMock.Setup(s => s.LoginAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(new UserDto { DisplayName = "Test User" }); // Act var result = await _sut.Login(loginDto); // Assert result.Should().BeOfType(); var redirectResult = (LocalRedirectResult)result; redirectResult.Url.Should().Be("/"); } [Fact] public async Task Login_ReturnsRedirectToLoginWithError_WhenCredentialsInvalid() { // Arrange var loginDto = new LoginDto { Email = "invalid@example.com", Password = "wrongpassword" }; _userServiceMock.Setup(s => s.LoginAsync(It.IsAny(), It.IsAny())) .ReturnsAsync((UserDto?)null); // Act var result = await _sut.Login(loginDto); // Assert result.Should().BeOfType(); var redirectResult = (LocalRedirectResult)result; redirectResult.Url.Should().Contain("error=true"); } [Fact] public async Task Login_UsesDefaultReturnUrl_WhenNotProvided() { // Arrange var loginDto = new LoginDto { Email = "test@example.com", Password = "password123", ReturnUrl = null }; _userServiceMock.Setup(s => s.LoginAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(new UserDto { DisplayName = "Test User" }); // Act var result = await _sut.Login(loginDto); // Assert result.Should().BeOfType(); var redirectResult = (LocalRedirectResult)result; redirectResult.Url.Should().Be("/"); } #endregion #region Logout Tests [Fact] public async Task Logout_CallsSignOutAsync() { // Arrange _userServiceMock.Setup(s => s.SignOutAsync()) .Returns(Task.CompletedTask); // Act await _sut.Logout(); // Assert _userServiceMock.Verify(s => s.SignOutAsync(), Times.Once); } [Fact] public async Task Logout_DispatchesLogoutCompleteAction() { // Arrange _userServiceMock.Setup(s => s.SignOutAsync()) .Returns(Task.CompletedTask); // Act await _sut.Logout(); // Assert _dispatcherMock.Verify(d => d.Dispatch(It.IsAny()), Times.Once); } [Fact] public async Task Logout_RedirectsToLoginPage_ByDefault() { // Arrange _userServiceMock.Setup(s => s.SignOutAsync()) .Returns(Task.CompletedTask); // Act var result = await _sut.Logout(); // Assert result.Should().BeOfType(); var redirectResult = (LocalRedirectResult)result; redirectResult.Url.Should().Contain("/account/login"); } [Fact] public async Task Logout_RedirectsToCustomUrl_WhenProvided() { // Arrange _userServiceMock.Setup(s => s.SignOutAsync()) .Returns(Task.CompletedTask); // Act var result = await _sut.Logout("/custom-page"); // Assert result.Should().BeOfType(); var redirectResult = (LocalRedirectResult)result; redirectResult.Url.Should().Be("/custom-page"); } #endregion #region Register Tests [Fact] public async Task Register_ReturnsRedirectToLoginWithSuccess_WhenValid() { // Arrange var registerDto = new RegisterUserDto { Email = "newuser@example.com", Password = "Password123!", DisplayName = "New User" }; _userServiceMock.Setup(s => s.RegisterUserAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(IdentityResult.Success); // Act var result = await _sut.Register(registerDto); // Assert result.Should().BeOfType(); var redirectResult = (LocalRedirectResult)result; redirectResult.Url.Should().Contain("registered=true"); } [Fact] public async Task Register_ReturnsRedirectWithErrors_WhenFailed() { // Arrange var registerDto = new RegisterUserDto { Email = "newuser@example.com", Password = "weak", DisplayName = "New User" }; _userServiceMock.Setup(s => s.RegisterUserAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(IdentityResult.Failed(new IdentityError { Code = "PasswordTooWeak" })); // Act var result = await _sut.Register(registerDto); // Assert result.Should().BeOfType(); var redirectResult = (LocalRedirectResult)result; redirectResult.Url.Should().Contain("error="); } #endregion #region ForgotPassword Tests [Fact] public async Task ForgotPassword_AlwaysReturnsSuccess() { // Arrange - Email enumeration protection: always return success _userServiceMock.Setup(s => s.RequestPasswordResetAsync(It.IsAny(), It.IsAny())) .ReturnsAsync((string?)null); // Act var result = await _sut.ForgotPassword("nonexistent@example.com"); // Assert - Should show success even for non-existent emails result.Should().BeOfType(); var redirectResult = (LocalRedirectResult)result; redirectResult.Url.Should().Contain("success=true"); } [Fact] public async Task ForgotPassword_CallsRequestPasswordResetAsync() { // Arrange _userServiceMock.Setup(s => s.RequestPasswordResetAsync(It.IsAny(), It.IsAny())) .ReturnsAsync("reset-token"); _emailServiceMock.Setup(s => s.SendPasswordResetEmailAsync(It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(true); // Act await _sut.ForgotPassword("test@example.com"); // Assert _userServiceMock.Verify(s => s.RequestPasswordResetAsync("test@example.com", It.IsAny()), Times.Once); } [Fact] public async Task ForgotPassword_SendsEmail_WhenTokenGenerated() { // Arrange _userServiceMock.Setup(s => s.RequestPasswordResetAsync(It.IsAny(), It.IsAny())) .ReturnsAsync("reset-token"); _emailServiceMock.Setup(s => s.SendPasswordResetEmailAsync(It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(true); // Act await _sut.ForgotPassword("test@example.com"); // Assert _emailServiceMock.Verify(s => s.SendPasswordResetEmailAsync( "test@example.com", It.Is(url => url.Contains("reset-password") && url.Contains("token=")), It.IsAny()), Times.Once); } [Fact] public async Task ForgotPassword_DoesNotSendEmail_WhenNoToken() { // Arrange - No user found, no token generated _userServiceMock.Setup(s => s.RequestPasswordResetAsync(It.IsAny(), It.IsAny())) .ReturnsAsync((string?)null); // Act await _sut.ForgotPassword("nonexistent@example.com"); // Assert - Email should not be sent for non-existent users _emailServiceMock.Verify(s => s.SendPasswordResetEmailAsync( It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } #endregion #region ResetPassword Tests [Fact] public async Task ResetPassword_ReturnsError_WhenPasswordsMismatch() { // Arrange var resetDto = new ResetPasswordFormDto { Email = "test@example.com", Token = "valid-token", NewPassword = "NewPassword123!", ConfirmPassword = "DifferentPassword!" }; // Act var result = await _sut.ResetPassword(resetDto); // Assert result.Should().BeOfType(); var redirectResult = (LocalRedirectResult)result; redirectResult.Url.Should().Contain("error=PasswordMismatch"); } [Fact] public async Task ResetPassword_ReturnsSuccess_WhenValid() { // Arrange var resetDto = new ResetPasswordFormDto { Email = "test@example.com", Token = "valid-token", NewPassword = "NewPassword123!", ConfirmPassword = "NewPassword123!" }; _userServiceMock.Setup(s => s.ResetPasswordAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(IdentityResult.Success); // Act var result = await _sut.ResetPassword(resetDto); // Assert result.Should().BeOfType(); var redirectResult = (LocalRedirectResult)result; redirectResult.Url.Should().Contain("success=true"); } [Fact] public async Task ResetPassword_ReturnsErrors_WhenResetFails() { // Arrange var resetDto = new ResetPasswordFormDto { Email = "test@example.com", Token = "invalid-token", NewPassword = "NewPassword123!", ConfirmPassword = "NewPassword123!" }; _userServiceMock.Setup(s => s.ResetPasswordAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(IdentityResult.Failed(new IdentityError { Code = "InvalidToken" })); // Act var result = await _sut.ResetPassword(resetDto); // Assert result.Should().BeOfType(); var redirectResult = (LocalRedirectResult)result; redirectResult.Url.Should().Contain("error=InvalidToken"); } #endregion }