using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using Domain.Entities; using Domain.Exceptions; using FluentAssertions; using Infrastructure.Jwt; using Infrastructure.Repository.Auth; using Moq; namespace Service.Auth.Tests; public class TokenServiceValidationTest { private readonly Mock _tokenInfraMock; private readonly Mock _authRepositoryMock; private readonly TokenService _tokenService; public TokenServiceValidationTest() { _tokenInfraMock = new Mock(); _authRepositoryMock = new Mock(); // Set environment variables for tokens Environment.SetEnvironmentVariable("ACCESS_TOKEN_SECRET", "test-access-secret-that-is-very-long-1234567890"); Environment.SetEnvironmentVariable("REFRESH_TOKEN_SECRET", "test-refresh-secret-that-is-very-long-1234567890"); Environment.SetEnvironmentVariable("CONFIRMATION_TOKEN_SECRET", "test-confirmation-secret-that-is-very-long-1234567890"); _tokenService = new TokenService( _tokenInfraMock.Object, _authRepositoryMock.Object ); } [Fact] public async Task ValidateAccessTokenAsync_WithValidToken_ReturnsValidatedToken() { // Arrange var userId = Guid.NewGuid(); const string username = "testuser"; const string token = "valid-access-token"; var claims = new List { new(JwtRegisteredClaimNames.Sub, userId.ToString()), new(JwtRegisteredClaimNames.UniqueName, username), new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), }; var claimsIdentity = new ClaimsIdentity(claims); var principal = new ClaimsPrincipal(claimsIdentity); _tokenInfraMock .Setup(x => x.ValidateJwtAsync(token, It.IsAny())) .ReturnsAsync(principal); // Act var result = await _tokenService.ValidateAccessTokenAsync(token); // Assert result.Should().NotBeNull(); result.UserId.Should().Be(userId); result.Username.Should().Be(username); result.Principal.Should().NotBeNull(); result.Principal.FindFirst(JwtRegisteredClaimNames.Sub)?.Value.Should().Be(userId.ToString()); } [Fact] public async Task ValidateRefreshTokenAsync_WithValidToken_ReturnsValidatedToken() { // Arrange var userId = Guid.NewGuid(); const string username = "testuser"; const string token = "valid-refresh-token"; var claims = new List { new(JwtRegisteredClaimNames.Sub, userId.ToString()), new(JwtRegisteredClaimNames.UniqueName, username), new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), }; var claimsIdentity = new ClaimsIdentity(claims); var principal = new ClaimsPrincipal(claimsIdentity); _tokenInfraMock .Setup(x => x.ValidateJwtAsync(token, It.IsAny())) .ReturnsAsync(principal); // Act var result = await _tokenService.ValidateRefreshTokenAsync(token); // Assert result.Should().NotBeNull(); result.UserId.Should().Be(userId); result.Username.Should().Be(username); } [Fact] public async Task ValidateConfirmationTokenAsync_WithValidToken_ReturnsValidatedToken() { // Arrange var userId = Guid.NewGuid(); const string username = "testuser"; const string token = "valid-confirmation-token"; var claims = new List { new(JwtRegisteredClaimNames.Sub, userId.ToString()), new(JwtRegisteredClaimNames.UniqueName, username), new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), }; var claimsIdentity = new ClaimsIdentity(claims); var principal = new ClaimsPrincipal(claimsIdentity); _tokenInfraMock .Setup(x => x.ValidateJwtAsync(token, It.IsAny())) .ReturnsAsync(principal); // Act var result = await _tokenService.ValidateConfirmationTokenAsync(token); // Assert result.Should().NotBeNull(); result.UserId.Should().Be(userId); result.Username.Should().Be(username); } [Fact] public async Task ValidateAccessTokenAsync_WithInvalidToken_ThrowsUnauthorizedException() { // Arrange const string token = "invalid-token"; _tokenInfraMock .Setup(x => x.ValidateJwtAsync(token, It.IsAny())) .ThrowsAsync(new UnauthorizedException("Invalid token")); // Act & Assert await FluentActions.Invoking(async () => await _tokenService.ValidateAccessTokenAsync(token) ).Should().ThrowAsync(); } [Fact] public async Task ValidateAccessTokenAsync_WithExpiredToken_ThrowsUnauthorizedException() { // Arrange const string token = "expired-token"; _tokenInfraMock .Setup(x => x.ValidateJwtAsync(token, It.IsAny())) .ThrowsAsync(new UnauthorizedException( "Token has expired" )); // Act & Assert await FluentActions.Invoking(async () => await _tokenService.ValidateAccessTokenAsync(token) ).Should().ThrowAsync(); } [Fact] public async Task ValidateAccessTokenAsync_WithMissingUserIdClaim_ThrowsUnauthorizedException() { // Arrange const string username = "testuser"; const string token = "token-without-user-id"; // Claims without Sub (user ID) var claims = new List { new(JwtRegisteredClaimNames.UniqueName, username), new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), }; var claimsIdentity = new ClaimsIdentity(claims); var principal = new ClaimsPrincipal(claimsIdentity); _tokenInfraMock .Setup(x => x.ValidateJwtAsync(token, It.IsAny())) .ReturnsAsync(principal); // Act & Assert await FluentActions.Invoking(async () => await _tokenService.ValidateAccessTokenAsync(token) ).Should().ThrowAsync() .WithMessage("*missing required claims*"); } [Fact] public async Task ValidateAccessTokenAsync_WithMissingUsernameClaim_ThrowsUnauthorizedException() { // Arrange var userId = Guid.NewGuid(); const string token = "token-without-username"; // Claims without UniqueName (username) var claims = new List { new(JwtRegisteredClaimNames.Sub, userId.ToString()), new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), }; var claimsIdentity = new ClaimsIdentity(claims); var principal = new ClaimsPrincipal(claimsIdentity); _tokenInfraMock .Setup(x => x.ValidateJwtAsync(token, It.IsAny())) .ReturnsAsync(principal); // Act & Assert await FluentActions.Invoking(async () => await _tokenService.ValidateAccessTokenAsync(token) ).Should().ThrowAsync() .WithMessage("*missing required claims*"); } [Fact] public async Task ValidateAccessTokenAsync_WithMalformedUserId_ThrowsUnauthorizedException() { // Arrange const string username = "testuser"; const string token = "token-with-malformed-user-id"; // Claims with invalid GUID format var claims = new List { new(JwtRegisteredClaimNames.Sub, "not-a-valid-guid"), new(JwtRegisteredClaimNames.UniqueName, username), new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), }; var claimsIdentity = new ClaimsIdentity(claims); var principal = new ClaimsPrincipal(claimsIdentity); _tokenInfraMock .Setup(x => x.ValidateJwtAsync(token, It.IsAny())) .ReturnsAsync(principal); // Act & Assert await FluentActions.Invoking(async () => await _tokenService.ValidateAccessTokenAsync(token) ).Should().ThrowAsync() .WithMessage("*malformed user ID*"); } [Fact] public async Task ValidateRefreshTokenAsync_WithInvalidToken_ThrowsUnauthorizedException() { // Arrange const string token = "invalid-refresh-token"; _tokenInfraMock .Setup(x => x.ValidateJwtAsync(token, It.IsAny())) .ThrowsAsync(new UnauthorizedException("Invalid token")); // Act & Assert await FluentActions.Invoking(async () => await _tokenService.ValidateRefreshTokenAsync(token) ).Should().ThrowAsync(); } [Fact] public async Task ValidateConfirmationTokenAsync_WithInvalidToken_ThrowsUnauthorizedException() { // Arrange const string token = "invalid-confirmation-token"; _tokenInfraMock .Setup(x => x.ValidateJwtAsync(token, It.IsAny())) .ThrowsAsync(new UnauthorizedException("Invalid token")); // Act & Assert await FluentActions.Invoking(async () => await _tokenService.ValidateConfirmationTokenAsync(token) ).Should().ThrowAsync(); } }