feat(auth): extend LoginResponse with username + mustChangePassword + ultimoLogin [UDT-008]

This commit is contained in:
2026-04-15 17:39:48 -03:00
parent d1f7b3805b
commit 9dcd63543e
9 changed files with 312 additions and 9 deletions

View File

@@ -1,3 +1,4 @@
using Microsoft.Extensions.Logging;
using NSubstitute;
using SIGCM2.Application.Abstractions;
using SIGCM2.Application.Abstractions.Persistence;
@@ -19,6 +20,7 @@ public class LoginCommandHandlerTests
private readonly IRefreshTokenGenerator _refreshGenerator = Substitute.For<IRefreshTokenGenerator>();
private readonly IClientContext _clientCtx = Substitute.For<IClientContext>();
private readonly IRolPermisoRepository _rolPermisoRepo = Substitute.For<IRolPermisoRepository>();
private readonly ILogger<LoginCommandHandler> _logger = Substitute.For<ILogger<LoginCommandHandler>>();
private readonly AuthOptions _authOptions = new() { AccessTokenMinutes = 60, RefreshTokenDays = 7 };
private readonly LoginCommandHandler _handler;
@@ -29,6 +31,10 @@ public class LoginCommandHandlerTests
_refreshGenerator.Generate().Returns("raw_refresh_token_value");
_refreshRepo.AddAsync(Arg.Any<RefreshToken>()).Returns(1);
// Default: UpdateUltimoLoginAsync succeeds silently
_repository.UpdateUltimoLoginAsync(Arg.Any<int>(), Arg.Any<DateTime>(), Arg.Any<CancellationToken>())
.Returns(Task.CompletedTask);
// Default: repo devuelve lista vacía — tests que necesitan permisos la sobreescriben
_rolPermisoRepo.GetByRolCodigoAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
.Returns(new List<Permiso>().AsReadOnly());
@@ -36,7 +42,7 @@ public class LoginCommandHandlerTests
_handler = new LoginCommandHandler(
_repository, _hasher, _jwtService,
_refreshRepo, _refreshGenerator, _clientCtx, _authOptions,
_rolPermisoRepo);
_rolPermisoRepo, _logger);
}
// Scenario: valid credentials → returns token response with usuario populated
@@ -243,4 +249,78 @@ public class LoginCommandHandlerTests
t.ExpiresAt >= before.AddDays(6).AddHours(23) &&
t.ExpiresAt <= after.AddDays(7).AddSeconds(5)));
}
// ── UDT-008: username + mustChangePassword + UltimoLogin ─────────────────
[Fact]
public async Task Handle_PopulatesUsername_InUsuarioDto()
{
var usuario = new Usuario(1, "jperez", "$2a$12$hash", "Juan", "Pérez", null, "cajero", "[]", true);
_repository.GetByUsernameAsync("jperez").Returns(usuario);
_hasher.Verify(Arg.Any<string>(), Arg.Any<string>()).Returns(true);
_jwtService.GenerateAccessToken(Arg.Any<Usuario>()).Returns("jwt");
var result = await _handler.Handle(new LoginCommand("jperez", "pass"));
Assert.Equal("jperez", result.Usuario.Username);
}
[Fact]
public async Task Handle_PopulatesMustChangePassword_False_WhenZero()
{
var usuario = new Usuario(1, "jperez", "$2a$12$hash", "Juan", "Pérez", null, "cajero", "[]", true,
mustChangePassword: false);
_repository.GetByUsernameAsync("jperez").Returns(usuario);
_hasher.Verify(Arg.Any<string>(), Arg.Any<string>()).Returns(true);
_jwtService.GenerateAccessToken(Arg.Any<Usuario>()).Returns("jwt");
var result = await _handler.Handle(new LoginCommand("jperez", "pass"));
Assert.False(result.Usuario.MustChangePassword);
}
[Fact]
public async Task Handle_PopulatesMustChangePassword_True_WhenSet()
{
var usuario = new Usuario(1, "jperez", "$2a$12$hash", "Juan", "Pérez", null, "cajero", "[]", true,
mustChangePassword: true);
_repository.GetByUsernameAsync("jperez").Returns(usuario);
_hasher.Verify(Arg.Any<string>(), Arg.Any<string>()).Returns(true);
_jwtService.GenerateAccessToken(Arg.Any<Usuario>()).Returns("jwt");
var result = await _handler.Handle(new LoginCommand("jperez", "pass"));
Assert.True(result.Usuario.MustChangePassword);
}
[Fact]
public async Task Handle_CallsUpdateUltimoLoginAsync_AfterSuccessfulAuth()
{
var usuario = new Usuario(1, "jperez", "$2a$12$hash", "Juan", "Pérez", null, "cajero", "[]", true);
_repository.GetByUsernameAsync("jperez").Returns(usuario);
_hasher.Verify(Arg.Any<string>(), Arg.Any<string>()).Returns(true);
_jwtService.GenerateAccessToken(Arg.Any<Usuario>()).Returns("jwt");
await _handler.Handle(new LoginCommand("jperez", "pass"));
await _repository.Received(1).UpdateUltimoLoginAsync(1, Arg.Any<DateTime>(), Arg.Any<CancellationToken>());
}
[Fact]
public async Task Handle_Succeeds_EvenIf_UpdateUltimoLogin_Throws()
{
var usuario = new Usuario(1, "jperez", "$2a$12$hash", "Juan", "Pérez", null, "cajero", "[]", true);
_repository.GetByUsernameAsync("jperez").Returns(usuario);
_hasher.Verify(Arg.Any<string>(), Arg.Any<string>()).Returns(true);
_jwtService.GenerateAccessToken(Arg.Any<Usuario>()).Returns("jwt");
// Simulate DB hiccup on UltimoLogin update
_repository.UpdateUltimoLoginAsync(Arg.Any<int>(), Arg.Any<DateTime>(), Arg.Any<CancellationToken>())
.Returns(Task.FromException(new Exception("DB timeout")));
// Login must still succeed
var result = await _handler.Handle(new LoginCommand("jperez", "pass"));
Assert.NotNull(result);
Assert.NotNull(result.AccessToken);
}
}