KoogleApp/test/GoodWood.Tests/Integration/AuthControllerTests.cs

371 lines
12 KiB
C#

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;
/// <summary>
/// Integration tests for AuthController.
/// Tests controller logic without full HTTP pipeline.
/// </summary>
public class AuthControllerTests
{
private readonly Mock<IDispatcher> _dispatcherMock;
private readonly Mock<IUserService> _userServiceMock;
private readonly Mock<IEmailService> _emailServiceMock;
private readonly AuthController _sut;
public AuthControllerTests()
{
_dispatcherMock = new Mock<IDispatcher>();
_userServiceMock = new Mock<IUserService>();
_emailServiceMock = new Mock<IEmailService>();
_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<LoginDto>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new UserDto { DisplayName = "Test User" });
// Act
var result = await _sut.Login(loginDto);
// Assert
result.Should().BeOfType<LocalRedirectResult>();
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<LoginDto>(), It.IsAny<CancellationToken>()))
.ReturnsAsync((UserDto?)null);
// Act
var result = await _sut.Login(loginDto);
// Assert
result.Should().BeOfType<LocalRedirectResult>();
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<LoginDto>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new UserDto { DisplayName = "Test User" });
// Act
var result = await _sut.Login(loginDto);
// Assert
result.Should().BeOfType<LocalRedirectResult>();
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<object>()), 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<LocalRedirectResult>();
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<LocalRedirectResult>();
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<RegisterUserDto>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(IdentityResult.Success);
// Act
var result = await _sut.Register(registerDto);
// Assert
result.Should().BeOfType<LocalRedirectResult>();
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<RegisterUserDto>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(IdentityResult.Failed(new IdentityError { Code = "PasswordTooWeak" }));
// Act
var result = await _sut.Register(registerDto);
// Assert
result.Should().BeOfType<LocalRedirectResult>();
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<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync((string?)null);
// Act
var result = await _sut.ForgotPassword("nonexistent@example.com");
// Assert - Should show success even for non-existent emails
result.Should().BeOfType<LocalRedirectResult>();
var redirectResult = (LocalRedirectResult)result;
redirectResult.Url.Should().Contain("success=true");
}
[Fact]
public async Task ForgotPassword_CallsRequestPasswordResetAsync()
{
// Arrange
_userServiceMock.Setup(s => s.RequestPasswordResetAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync("reset-token");
_emailServiceMock.Setup(s => s.SendPasswordResetEmailAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
// Act
await _sut.ForgotPassword("test@example.com");
// Assert
_userServiceMock.Verify(s => s.RequestPasswordResetAsync("test@example.com", It.IsAny<CancellationToken>()), Times.Once);
}
[Fact]
public async Task ForgotPassword_SendsEmail_WhenTokenGenerated()
{
// Arrange
_userServiceMock.Setup(s => s.RequestPasswordResetAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync("reset-token");
_emailServiceMock.Setup(s => s.SendPasswordResetEmailAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
// Act
await _sut.ForgotPassword("test@example.com");
// Assert
_emailServiceMock.Verify(s => s.SendPasswordResetEmailAsync(
"test@example.com",
It.Is<string>(url => url.Contains("reset-password") && url.Contains("token=")),
It.IsAny<CancellationToken>()), Times.Once);
}
[Fact]
public async Task ForgotPassword_DoesNotSendEmail_WhenNoToken()
{
// Arrange - No user found, no token generated
_userServiceMock.Setup(s => s.RequestPasswordResetAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
.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<string>(),
It.IsAny<string>(),
It.IsAny<CancellationToken>()), 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<LocalRedirectResult>();
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<ResetPasswordDto>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(IdentityResult.Success);
// Act
var result = await _sut.ResetPassword(resetDto);
// Assert
result.Should().BeOfType<LocalRedirectResult>();
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<ResetPasswordDto>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(IdentityResult.Failed(new IdentityError { Code = "InvalidToken" }));
// Act
var result = await _sut.ResetPassword(resetDto);
// Assert
result.Should().BeOfType<LocalRedirectResult>();
var redirectResult = (LocalRedirectResult)result;
redirectResult.Url.Should().Contain("error=InvalidToken");
}
#endregion
}