feat(application): LoginCommandHandler usa PermisoResolver para permisos efectivos [UDT-009]

This commit is contained in:
2026-04-15 21:29:33 -03:00
parent 86310de286
commit fb07a1139a
2 changed files with 136 additions and 4 deletions

View File

@@ -150,6 +150,135 @@ public class LoginCommandHandlerTests
Assert.Contains("ventas:contado:cobrar", result.Usuario.Permisos);
}
// ── UDT-009: PermisoResolver integration in LoginCommandHandler ─────────────
// L-01: Admin sin overrides → permisos = exactamente los del rol
[Fact]
public async Task Handle_AdminNoOverrides_PermisosEqualRolPermisos()
{
// Arrange
var usuario = new Usuario(1, "admin", "$2a$12$hash", "Admin", "Sys", null, "admin",
"""{"grant":[],"deny":[]}""", true);
_repository.GetByUsernameAsync("admin").Returns(usuario);
_hasher.Verify("pass", "$2a$12$hash").Returns(true);
_jwtService.GenerateAccessToken(usuario).Returns("jwt");
var adminPermisos = Enumerable.Range(1, 21)
.Select(i => MakePermiso(i, $"perm:mod{i}:accion{i}"))
.ToList();
_rolPermisoRepo.GetByRolCodigoAsync("admin", Arg.Any<CancellationToken>())
.Returns(adminPermisos.AsReadOnly() as IReadOnlyList<Permiso>);
// Act
var result = await _handler.Handle(new LoginCommand("admin", "pass"));
// Assert
Assert.Equal(21, result.Usuario.Permisos.Length);
}
// L-02: Cajero + grant nuevo permiso → result contiene permiso del grant
[Fact]
public async Task Handle_CajeroWithGrant_PermisosContainGrantedPermiso()
{
// Arrange
var usuario = new Usuario(2, "cajero1", "$2a$12$hash", "C", "A", null, "cajero",
"""{"grant":["textos:editar"],"deny":[]}""", true);
_repository.GetByUsernameAsync("cajero1").Returns(usuario);
_hasher.Verify("pass", "$2a$12$hash").Returns(true);
_jwtService.GenerateAccessToken(usuario).Returns("jwt");
var cajeroPermisos = new List<Permiso>
{
MakePermiso(10, "ventas:contado:crear"),
MakePermiso(11, "ventas:contado:modificar"),
MakePermiso(12, "ventas:contado:cobrar"),
MakePermiso(13, "ventas:contado:facturar"),
};
_rolPermisoRepo.GetByRolCodigoAsync("cajero", Arg.Any<CancellationToken>())
.Returns(cajeroPermisos.AsReadOnly() as IReadOnlyList<Permiso>);
// Act
var result = await _handler.Handle(new LoginCommand("cajero1", "pass"));
// Assert
Assert.Equal(5, result.Usuario.Permisos.Length);
Assert.Contains("textos:editar", result.Usuario.Permisos);
Assert.Contains("ventas:contado:crear", result.Usuario.Permisos);
}
// L-03: Cajero + deny uno del rol → result NO contiene el permiso denegado
[Fact]
public async Task Handle_CajeroWithDeny_PermisosExcludeDeniedPermiso()
{
// Arrange
var usuario = new Usuario(3, "cajero2", "$2a$12$hash", "C", "B", null, "cajero",
"""{"grant":[],"deny":["ventas:contado:cobrar"]}""", true);
_repository.GetByUsernameAsync("cajero2").Returns(usuario);
_hasher.Verify("pass", "$2a$12$hash").Returns(true);
_jwtService.GenerateAccessToken(usuario).Returns("jwt");
var cajeroPermisos = new List<Permiso>
{
MakePermiso(10, "ventas:contado:crear"),
MakePermiso(11, "ventas:contado:modificar"),
MakePermiso(12, "ventas:contado:cobrar"),
MakePermiso(13, "ventas:contado:facturar"),
};
_rolPermisoRepo.GetByRolCodigoAsync("cajero", Arg.Any<CancellationToken>())
.Returns(cajeroPermisos.AsReadOnly() as IReadOnlyList<Permiso>);
// Act
var result = await _handler.Handle(new LoginCommand("cajero2", "pass"));
// Assert — ventas:contado:cobrar was denied
Assert.Equal(3, result.Usuario.Permisos.Length);
Assert.DoesNotContain("ventas:contado:cobrar", result.Usuario.Permisos);
Assert.Contains("ventas:contado:crear", result.Usuario.Permisos);
}
// L-04: DTO always returns Permisos as string[] — not grant/deny shape
[Fact]
public async Task Handle_AlwaysReturnsPermisosAsStringArray_NotGrantDenyShape()
{
var usuario = new Usuario(1, "admin", "$2a$12$hash", "A", "B", null, "admin",
"""{"grant":["extra:perm"],"deny":[]}""", true);
_repository.GetByUsernameAsync("admin").Returns(usuario);
_hasher.Verify("pass", "$2a$12$hash").Returns(true);
_jwtService.GenerateAccessToken(usuario).Returns("jwt");
_rolPermisoRepo.GetByRolCodigoAsync("admin", Arg.Any<CancellationToken>())
.Returns(new List<Permiso>().AsReadOnly() as IReadOnlyList<Permiso>);
var result = await _handler.Handle(new LoginCommand("admin", "pass"));
// Must be string[] — no grant/deny wrapping
Assert.IsType<string[]>(result.Usuario.Permisos);
}
// L-05: Legacy PermisosJson "[]" → treated as Empty → permisos = only rol
[Fact]
public async Task Handle_LegacyPermisosJson_EmptyArray_TreatedAsEmpty()
{
var usuario = new Usuario(1, "cajero1", "$2a$12$hash", "C", "A", null, "cajero",
"[]", true);
_repository.GetByUsernameAsync("cajero1").Returns(usuario);
_hasher.Verify("pass", "$2a$12$hash").Returns(true);
_jwtService.GenerateAccessToken(usuario).Returns("jwt");
var cajeroPermisos = new List<Permiso>
{
MakePermiso(10, "ventas:contado:crear"),
MakePermiso(11, "ventas:contado:cobrar"),
};
_rolPermisoRepo.GetByRolCodigoAsync("cajero", Arg.Any<CancellationToken>())
.Returns(cajeroPermisos.AsReadOnly() as IReadOnlyList<Permiso>);
var result = await _handler.Handle(new LoginCommand("cajero1", "pass"));
Assert.Equal(2, result.Usuario.Permisos.Length);
Assert.Contains("ventas:contado:crear", result.Usuario.Permisos);
Assert.Contains("ventas:contado:cobrar", result.Usuario.Permisos);
}
// Helper: construir Permiso via ForRead para tests
private static Permiso MakePermiso(int id, string codigo) =>
Permiso.ForRead(id, codigo, codigo, null, codigo.Split(':')[0], true, DateTime.UtcNow);