371 lines
12 KiB
C#
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
|
|
}
|