diff --git a/src/api/SIGCM2.Application/Auth/Login/LoginCommandHandler.cs b/src/api/SIGCM2.Application/Auth/Login/LoginCommandHandler.cs index a7f12c8..b4d858c 100644 --- a/src/api/SIGCM2.Application/Auth/Login/LoginCommandHandler.cs +++ b/src/api/SIGCM2.Application/Auth/Login/LoginCommandHandler.cs @@ -1,4 +1,3 @@ -using System.Text.Json; using SIGCM2.Application.Abstractions; using SIGCM2.Application.Abstractions.Persistence; using SIGCM2.Application.Abstractions.Security; @@ -17,6 +16,7 @@ public sealed class LoginCommandHandler : ICommandHandler Handle(LoginCommand command) @@ -59,8 +61,10 @@ public sealed class LoginCommandHandler : ICommandHandler(usuario.PermisosJson) - ?? Array.Empty(); + // UDT-006: permisos vienen de RolPermiso, no de Usuario.PermisosJson + // Usuario.PermisosJson queda reservado para UDT-008 (overrides por usuario) + var permisoEntities = await _rolPermisoRepository.GetByRolCodigoAsync(usuario.Rol); + var permisos = permisoEntities.Select(p => p.Codigo).ToArray(); return new LoginResponseDto( AccessToken: accessToken, diff --git a/tests/SIGCM2.Application.Tests/Auth/Login/LoginCommandHandlerTests.cs b/tests/SIGCM2.Application.Tests/Auth/Login/LoginCommandHandlerTests.cs index c942368..7ceffe5 100644 --- a/tests/SIGCM2.Application.Tests/Auth/Login/LoginCommandHandlerTests.cs +++ b/tests/SIGCM2.Application.Tests/Auth/Login/LoginCommandHandlerTests.cs @@ -18,6 +18,7 @@ public class LoginCommandHandlerTests private readonly IRefreshTokenRepository _refreshRepo = Substitute.For(); private readonly IRefreshTokenGenerator _refreshGenerator = Substitute.For(); private readonly IClientContext _clientCtx = Substitute.For(); + private readonly IRolPermisoRepository _rolPermisoRepo = Substitute.For(); private readonly AuthOptions _authOptions = new() { AccessTokenMinutes = 60, RefreshTokenDays = 7 }; private readonly LoginCommandHandler _handler; @@ -28,16 +29,21 @@ public class LoginCommandHandlerTests _refreshGenerator.Generate().Returns("raw_refresh_token_value"); _refreshRepo.AddAsync(Arg.Any()).Returns(1); + // Default: repo devuelve lista vacía — tests que necesitan permisos la sobreescriben + _rolPermisoRepo.GetByRolCodigoAsync(Arg.Any(), Arg.Any()) + .Returns(new List().AsReadOnly()); + _handler = new LoginCommandHandler( _repository, _hasher, _jwtService, - _refreshRepo, _refreshGenerator, _clientCtx, _authOptions); + _refreshRepo, _refreshGenerator, _clientCtx, _authOptions, + _rolPermisoRepo); } // 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); + 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"); @@ -55,30 +61,93 @@ public class LoginCommandHandlerTests Assert.Equal("Admin Sys", result.Usuario.Nombre); Assert.Equal("admin", result.Usuario.Rol); Assert.NotNull(result.Usuario.Permisos); - Assert.Contains("*", result.Usuario.Permisos); + } + + // UDT-006 B-05: Permisos vienen desde IRolPermisoRepository, no desde PermisosJson + [Fact] + public async Task Handle_AdminLogin_PermisosFromRolPermisoRepository() + { + // Arrange + 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 adminPermisos = new List + { + MakePermiso(1, "administracion:usuarios:gestionar"), + MakePermiso(2, "administracion:roles:gestionar"), + MakePermiso(3, "administracion:permisos:ver"), + }; + _rolPermisoRepo.GetByRolCodigoAsync("admin", Arg.Any()) + .Returns(adminPermisos.AsReadOnly() as IReadOnlyList); + + // Act + var result = await _handler.Handle(new LoginCommand("admin", "@Diego550@")); + + // Assert — permisos vienen del repo, no de PermisosJson + Assert.Equal(3, result.Usuario.Permisos.Length); + Assert.Contains("administracion:usuarios:gestionar", result.Usuario.Permisos); + Assert.Contains("administracion:roles:gestionar", result.Usuario.Permisos); + Assert.Contains("administracion:permisos:ver", result.Usuario.Permisos); + // IRolPermisoRepository fue consultado con el rol del usuario + await _rolPermisoRepo.Received(1).GetByRolCodigoAsync("admin", Arg.Any()); + } + + // UDT-006 B-05-03: Rol sin permisos en RolPermiso → array vacío (nunca null) + [Fact] + public async Task Handle_RolSinPermisos_PermisosArrayVacio() + { + // Arrange + var usuario = new Usuario(5, "rep1", "$2a$12$hash", "Rep", "User", null, "reportes", "[]", true); + _repository.GetByUsernameAsync("rep1").Returns(usuario); + _hasher.Verify("pass", "$2a$12$hash").Returns(true); + _jwtService.GenerateAccessToken(usuario).Returns("jwt.rep"); + + // repo devuelve lista vacía para "reportes" + _rolPermisoRepo.GetByRolCodigoAsync("reportes", Arg.Any()) + .Returns(new List().AsReadOnly() as IReadOnlyList); + + // Act + var result = await _handler.Handle(new LoginCommand("rep1", "pass")); + + // Assert — empty array, NOT null + Assert.NotNull(result.Usuario.Permisos); + Assert.Empty(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); + var cajeroPermisos = new List + { + MakePermiso(10, "ventas:contado:crear"), + MakePermiso(11, "ventas:contado:cobrar"), + }; + var usuario = new Usuario(42, "cajero1", "$2a$12$hash3", "María", "González", null, "cajero", + "[]", true); _repository.GetByUsernameAsync("cajero1").Returns(usuario); _hasher.Verify("pass123", "$2a$12$hash3").Returns(true); _jwtService.GenerateAccessToken(usuario).Returns("jwt.cajero.token"); + _rolPermisoRepo.GetByRolCodigoAsync("cajero", Arg.Any()) + .Returns(cajeroPermisos.AsReadOnly() as IReadOnlyList); 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("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); + 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); + // Scenario: user does not exist → throws InvalidCredentialsException [Fact] public async Task Handle_UserNotFound_ThrowsInvalidCredentialsException()