test(udt-001): backend unit and integration tests (30 tests)
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
using SIGCM2.Infrastructure.Security;
|
||||
|
||||
namespace SIGCM2.Application.Tests.Infrastructure;
|
||||
|
||||
public class BcryptPasswordHasherTests
|
||||
{
|
||||
private readonly BcryptPasswordHasher _hasher = new();
|
||||
|
||||
// The seed hash for '@Diego550@' generated at cost 12
|
||||
private const string SeedHash = "$2a$12$rmq6tlSAQ8WXhR2CwLCSeuwCJKz/.8Eab95UQCUNfwe4dokeOqMcW";
|
||||
|
||||
// Scenario: correct password verifies against seed hash
|
||||
[Fact]
|
||||
public void Verify_CorrectPassword_ReturnsTrue()
|
||||
{
|
||||
var result = _hasher.Verify("@Diego550@", SeedHash);
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
// Triangulation: wrong password does not verify
|
||||
[Fact]
|
||||
public void Verify_WrongPassword_ReturnsFalse()
|
||||
{
|
||||
var result = _hasher.Verify("WrongPass1", SeedHash);
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
// Hash + Verify round-trip: hash a new password and verify it
|
||||
[Fact]
|
||||
public void Hash_ThenVerify_ReturnsTrue()
|
||||
{
|
||||
var plain = "TestPassword123!";
|
||||
var hash = _hasher.Hash(plain);
|
||||
|
||||
Assert.StartsWith("$2a$", hash); // BCrypt format
|
||||
Assert.True(_hasher.Verify(plain, hash));
|
||||
}
|
||||
|
||||
// Triangulation: verification of different password against generated hash fails
|
||||
[Fact]
|
||||
public void Hash_ThenVerifyWrong_ReturnsFalse()
|
||||
{
|
||||
var hash = _hasher.Hash("OriginalPassword1!");
|
||||
Assert.False(_hasher.Verify("DifferentPassword1!", hash));
|
||||
}
|
||||
}
|
||||
127
tests/SIGCM2.Application.Tests/Infrastructure/JwtServiceTests.cs
Normal file
127
tests/SIGCM2.Application.Tests/Infrastructure/JwtServiceTests.cs
Normal file
@@ -0,0 +1,127 @@
|
||||
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<JwtSecurityToken>(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);
|
||||
}
|
||||
Reference in New Issue
Block a user