feat(auth): extend LoginResponse with username + mustChangePassword + ultimoLogin [UDT-008]
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user