using NSubstitute; using SIGCM2.Application.Abstractions.Persistence; using SIGCM2.Application.Abstractions.Security; using SIGCM2.Application.Auth.Login; using SIGCM2.Domain.Entities; using SIGCM2.Domain.Exceptions; namespace SIGCM2.Application.Tests.Auth.Login; public class LoginCommandHandlerTests { private readonly IUsuarioRepository _repository = Substitute.For(); private readonly IPasswordHasher _hasher = Substitute.For(); private readonly IJwtService _jwtService = Substitute.For(); private readonly LoginCommandHandler _handler; public LoginCommandHandlerTests() { _handler = new LoginCommandHandler(_repository, _hasher, _jwtService); } // Scenario: valid credentials → returns token response with usuario populated [Fact] public async Task Handle_ValidCredentials_ReturnsTokenResponse() { var usuario = new Usuario(1, "admin", "$2a$12$hash", "Admin", "Sys", null, "admin", "[\"*\"]", true); _repository.GetByUsernameAsync("admin").Returns(usuario); _hasher.Verify("@Diego550@", "$2a$12$hash").Returns(true); _jwtService.GenerateAccessToken(usuario).Returns("jwt.token.here"); var command = new LoginCommand("admin", "@Diego550@"); var result = await _handler.Handle(command); Assert.Equal("jwt.token.here", result.AccessToken); Assert.False(string.IsNullOrWhiteSpace(result.RefreshToken)); Assert.Equal(3600, result.ExpiresIn); // Contract: Usuario must be populated Assert.NotNull(result.Usuario); Assert.Equal(1, result.Usuario.Id); Assert.Equal("Admin Sys", result.Usuario.Nombre); Assert.Equal("admin", result.Usuario.Rol); Assert.NotNull(result.Usuario.Permisos); Assert.Contains("*", result.Usuario.Permisos); } // Triangulation: Usuario object maps id/nombre/rol/permisos from authenticated user [Fact] public async Task Handle_ValidCredentials_UsuarioMatchesAuthenticatedUser() { var usuario = new Usuario(42, "cajero1", "$2a$12$hash3", "María", "González", null, "Cajero", "[\"ventas:contado:create\",\"ventas:contado:read\"]", true); _repository.GetByUsernameAsync("cajero1").Returns(usuario); _hasher.Verify("pass123", "$2a$12$hash3").Returns(true); _jwtService.GenerateAccessToken(usuario).Returns("jwt.cajero.token"); var command = new LoginCommand("cajero1", "pass123"); var result = await _handler.Handle(command); Assert.Equal(42, result.Usuario.Id); Assert.Equal("María González", result.Usuario.Nombre); Assert.Equal("Cajero", result.Usuario.Rol); Assert.Equal(2, result.Usuario.Permisos.Length); Assert.Contains("ventas:contado:create", result.Usuario.Permisos); Assert.Contains("ventas:contado:read", result.Usuario.Permisos); } // Scenario: user does not exist → throws InvalidCredentialsException [Fact] public async Task Handle_UserNotFound_ThrowsInvalidCredentialsException() { _repository.GetByUsernameAsync("noexiste").Returns((Usuario?)null); var command = new LoginCommand("noexiste", "anything"); await Assert.ThrowsAsync(() => _handler.Handle(command)); } // Scenario: user is inactive → throws InvalidCredentialsException [Fact] public async Task Handle_InactiveUser_ThrowsInvalidCredentialsException() { var inactive = new Usuario(2, "operador", "$2a$12$hash2", "Juan", "Pérez", null, "vendedor", "[]", false); _repository.GetByUsernameAsync("operador").Returns(inactive); var command = new LoginCommand("operador", "correctpassword"); await Assert.ThrowsAsync(() => _handler.Handle(command)); } // Scenario: wrong password → throws InvalidCredentialsException [Fact] public async Task Handle_WrongPassword_ThrowsInvalidCredentialsException() { var usuario = new Usuario(1, "admin", "$2a$12$hash", "Admin", "Sys", null, "admin", "[\"*\"]", true); _repository.GetByUsernameAsync("admin").Returns(usuario); _hasher.Verify("WrongPass1", "$2a$12$hash").Returns(false); var command = new LoginCommand("admin", "WrongPass1"); await Assert.ThrowsAsync(() => _handler.Handle(command)); } }