172 lines
6.5 KiB
C#
172 lines
6.5 KiB
C#
|
|
using NSubstitute;
|
||
|
|
using SIGCM2.Application.Abstractions.Persistence;
|
||
|
|
using SIGCM2.Application.Abstractions.Security;
|
||
|
|
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<IUsuarioRepository>();
|
||
|
|
private readonly IPasswordHasher _hasher = Substitute.For<IPasswordHasher>();
|
||
|
|
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);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── exists → throws ──────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public async Task Handle_UsernameAlreadyExists_ThrowsUsernameAlreadyExistsException()
|
||
|
|
{
|
||
|
|
_repository.ExistsByUsernameAsync("operador1", Arg.Any<CancellationToken>())
|
||
|
|
.Returns(true);
|
||
|
|
|
||
|
|
await Assert.ThrowsAsync<UsernameAlreadyExistsException>(
|
||
|
|
() => _handler.Handle(ValidCommand()));
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public async Task Handle_UsernameAlreadyExists_DoesNotCallAddAsync()
|
||
|
|
{
|
||
|
|
_repository.ExistsByUsernameAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
|
||
|
|
.Returns(true);
|
||
|
|
|
||
|
|
try { await _handler.Handle(ValidCommand()); } catch (UsernameAlreadyExistsException) { }
|
||
|
|
|
||
|
|
await _repository.DidNotReceive().AddAsync(Arg.Any<Usuario>(), Arg.Any<CancellationToken>());
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public async Task Handle_UsernameAlreadyExists_ExceptionContainsUsername()
|
||
|
|
{
|
||
|
|
_repository.ExistsByUsernameAsync("operador1", Arg.Any<CancellationToken>())
|
||
|
|
.Returns(true);
|
||
|
|
|
||
|
|
var ex = await Assert.ThrowsAsync<UsernameAlreadyExistsException>(
|
||
|
|
() => _handler.Handle(ValidCommand()));
|
||
|
|
|
||
|
|
Assert.Equal("operador1", ex.Username);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── happy path ───────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public async Task Handle_HappyPath_HashesPasswordBeforePersisting()
|
||
|
|
{
|
||
|
|
_repository.ExistsByUsernameAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
|
||
|
|
.Returns(false);
|
||
|
|
_hasher.Hash("Secreto123").Returns("$2a$12$hashed");
|
||
|
|
_repository.AddAsync(Arg.Any<Usuario>(), Arg.Any<CancellationToken>()).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<Usuario>(u => u.PasswordHash == "$2a$12$hashed"),
|
||
|
|
Arg.Any<CancellationToken>());
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public async Task Handle_HappyPath_NeverPersistsPlainPassword()
|
||
|
|
{
|
||
|
|
_repository.ExistsByUsernameAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
|
||
|
|
.Returns(false);
|
||
|
|
_hasher.Hash(Arg.Any<string>()).Returns("$2a$12$hashed");
|
||
|
|
_repository.AddAsync(Arg.Any<Usuario>(), Arg.Any<CancellationToken>()).Returns(1);
|
||
|
|
|
||
|
|
await _handler.Handle(ValidCommand());
|
||
|
|
|
||
|
|
await _repository.Received(1).AddAsync(
|
||
|
|
Arg.Is<Usuario>(u => u.PasswordHash != "Secreto123"),
|
||
|
|
Arg.Any<CancellationToken>());
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public async Task Handle_HappyPath_CallsAddAsyncOnce()
|
||
|
|
{
|
||
|
|
_repository.ExistsByUsernameAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
|
||
|
|
.Returns(false);
|
||
|
|
_hasher.Hash(Arg.Any<string>()).Returns("$2a$12$hashed");
|
||
|
|
_repository.AddAsync(Arg.Any<Usuario>(), Arg.Any<CancellationToken>()).Returns(7);
|
||
|
|
|
||
|
|
await _handler.Handle(ValidCommand());
|
||
|
|
|
||
|
|
await _repository.Received(1).AddAsync(Arg.Any<Usuario>(), Arg.Any<CancellationToken>());
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public async Task Handle_HappyPath_ReturnsDtoWithIdFromRepository()
|
||
|
|
{
|
||
|
|
_repository.ExistsByUsernameAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
|
||
|
|
.Returns(false);
|
||
|
|
_hasher.Hash(Arg.Any<string>()).Returns("$2a$12$hashed");
|
||
|
|
_repository.AddAsync(Arg.Any<Usuario>(), Arg.Any<CancellationToken>()).Returns(42);
|
||
|
|
|
||
|
|
var result = await _handler.Handle(ValidCommand());
|
||
|
|
|
||
|
|
Assert.Equal(42, result.Id);
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public async Task Handle_HappyPath_DtoContainsCorrectFields()
|
||
|
|
{
|
||
|
|
_repository.ExistsByUsernameAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
|
||
|
|
.Returns(false);
|
||
|
|
_hasher.Hash(Arg.Any<string>()).Returns("$2a$12$hashed");
|
||
|
|
_repository.AddAsync(Arg.Any<Usuario>(), Arg.Any<CancellationToken>()).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<string>(), Arg.Any<CancellationToken>())
|
||
|
|
.Returns(false);
|
||
|
|
_hasher.Hash(Arg.Any<string>()).Returns("$2a$12$secret");
|
||
|
|
_repository.AddAsync(Arg.Any<Usuario>(), Arg.Any<CancellationToken>()).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<string>(), Arg.Any<CancellationToken>())
|
||
|
|
.Returns(false);
|
||
|
|
_hasher.Hash(Arg.Any<string>()).Returns("$2a$12$hashed");
|
||
|
|
_repository.AddAsync(
|
||
|
|
Arg.Is<Usuario>(u => u.Activo && u.PermisosJson == "[]"),
|
||
|
|
Arg.Any<CancellationToken>()).Returns(5);
|
||
|
|
|
||
|
|
var result = await _handler.Handle(ValidCommand());
|
||
|
|
|
||
|
|
Assert.True(result.Activo);
|
||
|
|
}
|
||
|
|
}
|