using System.IdentityModel.Tokens.Jwt; using System.Security.Cryptography; using Microsoft.IdentityModel.Tokens; using SIGCM2.Domain.Entities; using SIGCM2.Infrastructure.Security; namespace SIGCM2.Application.Tests.Infrastructure; public class JwtServiceTests : IDisposable { private readonly RSA _rsa; private readonly JwtOptions _options; private readonly JwtService _jwtService; public JwtServiceTests() { // Generate a test RSA key pair inline (no files needed for unit tests) _rsa = RSA.Create(2048); _options = new JwtOptions { Issuer = "sigcm2.api", Audience = "sigcm2.web", AccessTokenMinutes = 60 }; _jwtService = new JwtService(_rsa, _options); } public void Dispose() => _rsa.Dispose(); // Scenario: generated token uses RS256 algorithm [Fact] public void GenerateAccessToken_UsesRS256Algorithm() { var usuario = MakeUsuario(); var token = _jwtService.GenerateAccessToken(usuario); var handler = new JwtSecurityTokenHandler(); var parsed = handler.ReadJwtToken(token); Assert.Equal("RS256", parsed.Header.Alg); } // Scenario: claims contain expected values [Fact] public void GenerateAccessToken_ContainsExpectedClaims() { var usuario = MakeUsuario(); var token = _jwtService.GenerateAccessToken(usuario); var handler = new JwtSecurityTokenHandler(); var parsed = handler.ReadJwtToken(token); Assert.Equal("1", parsed.Subject); // sub = user ID Assert.Equal("sigcm2.api", parsed.Issuer); // iss Assert.Contains("sigcm2.web", parsed.Audiences); // aud Assert.Contains(parsed.Claims, c => c.Type == "name" && c.Value == "admin"); Assert.Contains(parsed.Claims, c => c.Type == "rol" && c.Value == "admin"); } // Scenario: token is verifiable with the public key [Fact] public void GenerateAccessToken_IsVerifiableWithPublicKey() { var usuario = MakeUsuario(); var token = _jwtService.GenerateAccessToken(usuario); var publicKey = RSA.Create(); publicKey.ImportRSAPublicKey(_rsa.ExportRSAPublicKey(), out _); var validationParams = new TokenValidationParameters { ValidateIssuerSigningKey = true, IssuerSigningKey = new RsaSecurityKey(publicKey), ValidIssuer = "sigcm2.api", ValidAudience = "sigcm2.web", ValidateLifetime = true, ClockSkew = TimeSpan.Zero }; var handler = new JwtSecurityTokenHandler(); var principal = handler.ValidateToken(token, validationParams, out var validatedToken); Assert.NotNull(principal); Assert.IsType(validatedToken); } // Scenario: expiry is 60 minutes from now [Fact] public void GenerateAccessToken_ExpiryIs60MinutesFromNow() { var usuario = MakeUsuario(); var before = DateTime.UtcNow; var token = _jwtService.GenerateAccessToken(usuario); var after = DateTime.UtcNow; var handler = new JwtSecurityTokenHandler(); var parsed = handler.ReadJwtToken(token); var expectedMinExpiry = before.AddMinutes(59).AddSeconds(55); var expectedMaxExpiry = after.AddMinutes(60).AddSeconds(5); Assert.True(parsed.ValidTo >= expectedMinExpiry, $"exp {parsed.ValidTo} < expected min {expectedMinExpiry}"); Assert.True(parsed.ValidTo <= expectedMaxExpiry, $"exp {parsed.ValidTo} > expected max {expectedMaxExpiry}"); } // Triangulation: different user produces token with different sub claim [Fact] public void GenerateAccessToken_DifferentUser_DifferentSubClaim() { var user1 = MakeUsuario(id: 1, username: "admin"); var user2 = MakeUsuario(id: 2, username: "vendedor"); var token1 = _jwtService.GenerateAccessToken(user1); var token2 = _jwtService.GenerateAccessToken(user2); var handler = new JwtSecurityTokenHandler(); var parsed1 = handler.ReadJwtToken(token1); var parsed2 = handler.ReadJwtToken(token2); Assert.NotEqual(parsed1.Subject, parsed2.Subject); Assert.Equal("1", parsed1.Subject); Assert.Equal("2", parsed2.Subject); } private static Usuario MakeUsuario(int id = 1, string username = "admin") => new(id, username, "$2a$12$hash", "Administrador", "Sistema", null, "admin", "[\"*\"]", true); }