using Domain.Entities; using Domain.Exceptions; using Features.Auth.Dtos; using Features.Auth.Queries.Login; using Features.Auth.Repository; using Features.Auth.Services; using FluentAssertions; using Infrastructure.PasswordHashing; using Moq; namespace Features.Auth.Tests.Queries; public class LoginHandlerTests { private readonly Mock _authRepoMock; private readonly LoginHandler _handler; private readonly Mock _passwordInfraMock; private readonly Mock _tokenServiceMock; public LoginHandlerTests() { _authRepoMock = new Mock(); _passwordInfraMock = new Mock(); _tokenServiceMock = new Mock(); _handler = new LoginHandler(_authRepoMock.Object, _passwordInfraMock.Object, _tokenServiceMock.Object); } [Fact] public async Task Handle_WithValidData_ReturnsPayloadWithMatchingUsername() { const string username = "CogitoErgoSum"; Guid userAccountId = Guid.NewGuid(); UserAccount userAccount = new() { UserAccountId = userAccountId, Username = username, FirstName = "René", LastName = "Descartes", Email = "r.descartes@example.com", DateOfBirth = new DateTime(1596, 03, 31) }; UserCredential userCredential = new() { UserCredentialId = Guid.NewGuid(), UserAccountId = userAccountId, Hash = "some-hash", Expiry = DateTime.MaxValue }; _authRepoMock.Setup(x => x.GetUserByUsernameAsync(username)).ReturnsAsync(userAccount); _authRepoMock.Setup(x => x.GetActiveCredentialByUserAccountIdAsync(userAccountId)).ReturnsAsync(userCredential); _passwordInfraMock.Setup(x => x.Verify(It.IsAny(), It.IsAny())).Returns(true); _tokenServiceMock.Setup(x => x.GenerateAccessToken(It.IsAny())).Returns("access-token"); _tokenServiceMock.Setup(x => x.GenerateRefreshToken(It.IsAny())).Returns("refresh-token"); LoginPayload result = await _handler.Handle(new LoginQuery(username, "any-password"), CancellationToken.None); result.Should().NotBeNull(); result.UserAccountId.Should().Be(userAccountId); result.Username.Should().Be(username); result.AccessToken.Should().Be("access-token"); result.RefreshToken.Should().Be("refresh-token"); } [Fact] public async Task Handle_WithUnregisteredUsername_ThrowsUnauthorizedException() { const string username = "de_beauvoir"; _authRepoMock.Setup(x => x.GetUserByUsernameAsync(username)).ReturnsAsync((UserAccount?)null); Func> act = async () => await _handler.Handle(new LoginQuery(username, "password"), CancellationToken.None); await act.Should().ThrowAsync(); _authRepoMock.Verify(x => x.GetActiveCredentialByUserAccountIdAsync(It.IsAny()), Times.Never); } [Fact] public async Task Handle_WithNoActiveCredential_ThrowsUnauthorizedException() { const string username = "BRussell"; Guid userAccountId = Guid.NewGuid(); UserAccount userAccount = new() { UserAccountId = userAccountId, Username = username }; _authRepoMock.Setup(x => x.GetUserByUsernameAsync(username)).ReturnsAsync(userAccount); _authRepoMock.Setup(x => x.GetActiveCredentialByUserAccountIdAsync(userAccountId)) .ReturnsAsync((UserCredential?)null); Func> act = async () => await _handler.Handle(new LoginQuery(username, "password"), CancellationToken.None); await act.Should().ThrowAsync().WithMessage("Invalid username or password."); _passwordInfraMock.Verify(x => x.Verify(It.IsAny(), It.IsAny()), Times.Never); } [Fact] public async Task Handle_WithIncorrectPassword_ThrowsUnauthorizedException() { const string username = "RCarnap"; Guid userAccountId = Guid.NewGuid(); UserAccount userAccount = new() { UserAccountId = userAccountId, Username = username }; UserCredential userCredential = new() { UserCredentialId = Guid.NewGuid(), UserAccountId = userAccountId, Hash = "hashed-password" }; _authRepoMock.Setup(x => x.GetUserByUsernameAsync(username)).ReturnsAsync(userAccount); _authRepoMock.Setup(x => x.GetActiveCredentialByUserAccountIdAsync(userAccountId)).ReturnsAsync(userCredential); _passwordInfraMock.Setup(x => x.Verify(It.IsAny(), It.IsAny())).Returns(false); Func> act = async () => await _handler.Handle(new LoginQuery(username, "wrong-password"), CancellationToken.None); await act.Should().ThrowAsync().WithMessage("Invalid username or password."); } }