using NSubstitute; using SIGCM2.Application.Abstractions.Persistence; using SIGCM2.Application.Abstractions.Security; using SIGCM2.Application.Audit; using SIGCM2.Application.Usuarios.Create; using SIGCM2.Domain.Entities; using SIGCM2.Domain.Exceptions; namespace SIGCM2.Application.Tests.Usuarios.Create; public class CreateUsuarioCommandHandlerTests { private readonly IUsuarioRepository _repository = Substitute.For(); private readonly IPasswordHasher _hasher = Substitute.For(); private readonly IAuditLogger _audit = Substitute.For(); private readonly CreateUsuarioCommandHandler _handler; private static CreateUsuarioCommand ValidCommand() => new( Username: "operador1", Password: "Secreto123", Nombre: "Juan", Apellido: "Pérez", Email: null, Rol: "vendedor"); public CreateUsuarioCommandHandlerTests() { _handler = new CreateUsuarioCommandHandler(_repository, _hasher, _audit); } // ── exists → throws ────────────────────────────────────────────────────── [Fact] public async Task Handle_UsernameAlreadyExists_ThrowsUsernameAlreadyExistsException() { _repository.ExistsByUsernameAsync("operador1", Arg.Any()) .Returns(true); await Assert.ThrowsAsync( () => _handler.Handle(ValidCommand())); } [Fact] public async Task Handle_UsernameAlreadyExists_DoesNotCallAddAsync() { _repository.ExistsByUsernameAsync(Arg.Any(), Arg.Any()) .Returns(true); try { await _handler.Handle(ValidCommand()); } catch (UsernameAlreadyExistsException) { } await _repository.DidNotReceive().AddAsync(Arg.Any(), Arg.Any()); } [Fact] public async Task Handle_UsernameAlreadyExists_ExceptionContainsUsername() { _repository.ExistsByUsernameAsync("operador1", Arg.Any()) .Returns(true); var ex = await Assert.ThrowsAsync( () => _handler.Handle(ValidCommand())); Assert.Equal("operador1", ex.Username); } // ── happy path ─────────────────────────────────────────────────────────── [Fact] public async Task Handle_HappyPath_HashesPasswordBeforePersisting() { _repository.ExistsByUsernameAsync(Arg.Any(), Arg.Any()) .Returns(false); _hasher.Hash("Secreto123").Returns("$2a$12$hashed"); _repository.AddAsync(Arg.Any(), Arg.Any()).Returns(42); await _handler.Handle(ValidCommand()); // AddAsync must be called with the hashed value, not the plain password await _repository.Received(1).AddAsync( Arg.Is(u => u.PasswordHash == "$2a$12$hashed"), Arg.Any()); } [Fact] public async Task Handle_HappyPath_NeverPersistsPlainPassword() { _repository.ExistsByUsernameAsync(Arg.Any(), Arg.Any()) .Returns(false); _hasher.Hash(Arg.Any()).Returns("$2a$12$hashed"); _repository.AddAsync(Arg.Any(), Arg.Any()).Returns(1); await _handler.Handle(ValidCommand()); await _repository.Received(1).AddAsync( Arg.Is(u => u.PasswordHash != "Secreto123"), Arg.Any()); } [Fact] public async Task Handle_HappyPath_CallsAddAsyncOnce() { _repository.ExistsByUsernameAsync(Arg.Any(), Arg.Any()) .Returns(false); _hasher.Hash(Arg.Any()).Returns("$2a$12$hashed"); _repository.AddAsync(Arg.Any(), Arg.Any()).Returns(7); await _handler.Handle(ValidCommand()); await _repository.Received(1).AddAsync(Arg.Any(), Arg.Any()); } [Fact] public async Task Handle_HappyPath_ReturnsDtoWithIdFromRepository() { _repository.ExistsByUsernameAsync(Arg.Any(), Arg.Any()) .Returns(false); _hasher.Hash(Arg.Any()).Returns("$2a$12$hashed"); _repository.AddAsync(Arg.Any(), Arg.Any()).Returns(42); var result = await _handler.Handle(ValidCommand()); Assert.Equal(42, result.Id); } [Fact] public async Task Handle_HappyPath_DtoContainsCorrectFields() { _repository.ExistsByUsernameAsync(Arg.Any(), Arg.Any()) .Returns(false); _hasher.Hash(Arg.Any()).Returns("$2a$12$hashed"); _repository.AddAsync(Arg.Any(), Arg.Any()).Returns(10); var cmd = new CreateUsuarioCommand("user1", "Pass1234", "Ana", "García", "ana@example.com", "admin"); var result = await _handler.Handle(cmd); Assert.Equal("user1", result.Username); Assert.Equal("Ana", result.Nombre); Assert.Equal("García", result.Apellido); Assert.Equal("ana@example.com", result.Email); Assert.Equal("admin", result.Rol); Assert.True(result.Activo); } [Fact] public async Task Handle_HappyPath_DtoDoesNotContainPasswordHash() { // UsuarioCreatedDto must not expose PasswordHash — compile-time check via reflection _repository.ExistsByUsernameAsync(Arg.Any(), Arg.Any()) .Returns(false); _hasher.Hash(Arg.Any()).Returns("$2a$12$secret"); _repository.AddAsync(Arg.Any(), Arg.Any()).Returns(1); var result = await _handler.Handle(ValidCommand()); var props = result.GetType().GetProperties().Select(p => p.Name); Assert.DoesNotContain("PasswordHash", props); } [Fact] public async Task Handle_HappyPath_NewUserIsActive() { _repository.ExistsByUsernameAsync(Arg.Any(), Arg.Any()) .Returns(false); _hasher.Hash(Arg.Any()).Returns("$2a$12$hashed"); _repository.AddAsync( Arg.Is(u => u.Activo && u.PermisosJson == "[]"), Arg.Any()).Returns(5); var result = await _handler.Handle(ValidCommand()); Assert.True(result.Activo); } }