From b79efc778aeed6300c29a8726905f38c4132fe2d Mon Sep 17 00:00:00 2001 From: dmolinari Date: Tue, 14 Apr 2026 13:28:15 -0300 Subject: [PATCH] test(app): extend LoginCommandHandler tests with refresh token persistence cases RED --- .../Auth/Login/LoginCommandHandlerTests.cs | 76 ++++++++++++++++++- 1 file changed, 75 insertions(+), 1 deletion(-) diff --git a/tests/SIGCM2.Application.Tests/Auth/Login/LoginCommandHandlerTests.cs b/tests/SIGCM2.Application.Tests/Auth/Login/LoginCommandHandlerTests.cs index 3458221..c942368 100644 --- a/tests/SIGCM2.Application.Tests/Auth/Login/LoginCommandHandlerTests.cs +++ b/tests/SIGCM2.Application.Tests/Auth/Login/LoginCommandHandlerTests.cs @@ -1,9 +1,12 @@ using NSubstitute; +using SIGCM2.Application.Abstractions; using SIGCM2.Application.Abstractions.Persistence; using SIGCM2.Application.Abstractions.Security; +using SIGCM2.Application.Auth; using SIGCM2.Application.Auth.Login; using SIGCM2.Domain.Entities; using SIGCM2.Domain.Exceptions; +using SIGCM2.Domain.Security; namespace SIGCM2.Application.Tests.Auth.Login; @@ -12,11 +15,22 @@ public class LoginCommandHandlerTests private readonly IUsuarioRepository _repository = Substitute.For(); private readonly IPasswordHasher _hasher = Substitute.For(); private readonly IJwtService _jwtService = Substitute.For(); + private readonly IRefreshTokenRepository _refreshRepo = Substitute.For(); + private readonly IRefreshTokenGenerator _refreshGenerator = Substitute.For(); + private readonly IClientContext _clientCtx = Substitute.For(); + private readonly AuthOptions _authOptions = new() { AccessTokenMinutes = 60, RefreshTokenDays = 7 }; private readonly LoginCommandHandler _handler; public LoginCommandHandlerTests() { - _handler = new LoginCommandHandler(_repository, _hasher, _jwtService); + _clientCtx.Ip.Returns("127.0.0.1"); + _clientCtx.UserAgent.Returns("TestAgent"); + _refreshGenerator.Generate().Returns("raw_refresh_token_value"); + _refreshRepo.AddAsync(Arg.Any()).Returns(1); + + _handler = new LoginCommandHandler( + _repository, _hasher, _jwtService, + _refreshRepo, _refreshGenerator, _clientCtx, _authOptions); } // Scenario: valid credentials → returns token response with usuario populated @@ -100,4 +114,64 @@ public class LoginCommandHandlerTests await Assert.ThrowsAsync(() => _handler.Handle(command)); } + + // T-034: Refresh token persistence on login + + [Fact] + public async Task Handle_PersistsHashedRefreshToken() + { + 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"); + + var command = new LoginCommand("admin", "@Diego550@"); + var result = await _handler.Handle(command); + + // Raw token returned to client + Assert.Equal("raw_refresh_token_value", result.RefreshToken); + + // Repository received a token with the hash of the raw value — not the raw itself + var expectedHash = TokenHasher.Sha256Base64Url("raw_refresh_token_value"); + await _refreshRepo.Received(1).AddAsync(Arg.Is(t => + t.TokenHash == expectedHash && + t.UsuarioId == 1)); + } + + [Fact] + public async Task Handle_GeneratesNewFamilyId() + { + var usuario = new Usuario(1, "admin", "$2a$12$hash", "Admin", "Sys", null, "admin", "[\"*\"]", true); + _repository.GetByUsernameAsync("admin").Returns(usuario); + _hasher.Verify(Arg.Any(), Arg.Any()).Returns(true); + _jwtService.GenerateAccessToken(Arg.Any()).Returns("jwt"); + + var capturedFamilies = new List(); + _refreshRepo.AddAsync(Arg.Do(t => capturedFamilies.Add(t.FamilyId))).Returns(1); + + await _handler.Handle(new LoginCommand("admin", "@Diego550@")); + await _handler.Handle(new LoginCommand("admin", "@Diego550@")); + + // Each login gets a unique FamilyId + Assert.Equal(2, capturedFamilies.Count); + Assert.NotEqual(capturedFamilies[0], capturedFamilies[1]); + } + + [Fact] + public async Task Handle_UsesConfiguredRefreshTokenDays() + { + var usuario = new Usuario(1, "admin", "$2a$12$hash", "Admin", "Sys", null, "admin", "[\"*\"]", true); + _repository.GetByUsernameAsync("admin").Returns(usuario); + _hasher.Verify(Arg.Any(), Arg.Any()).Returns(true); + _jwtService.GenerateAccessToken(Arg.Any()).Returns("jwt"); + + var before = DateTime.UtcNow; + await _handler.Handle(new LoginCommand("admin", "@Diego550@")); + var after = DateTime.UtcNow; + + // The token added to the repo must have ExpiresAt ~7 days from now + await _refreshRepo.Received(1).AddAsync(Arg.Is(t => + t.ExpiresAt >= before.AddDays(6).AddHours(23) && + t.ExpiresAt <= after.AddDays(7).AddSeconds(5))); + } }