mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-07-16 09:37:23 +00:00
Auth and Emails land together since Auth's registration/resend flows need Emails to exist first. Service.Auth's four services (Register, Login, Confirmation, Token) collapse into MediatR commands/queries in Features.Auth; TokenService stays as a slice-internal service since multiple handlers call it. Auth no longer references Emails directly — it sends SendRegistrationEmailCommand/SendResendConfirmationEmailCommand (defined in Shared.Application) which Features.Emails handles, so neither slice has a project reference on the other. Service.Emails' IEmailService becomes Features.Emails' IEmailDispatcher, simplified to take (firstName, email, token) instead of a full UserAccount since that's all it ever used. API.Specs' TestApiFactory/MockEmailService are updated to swap in the relocated interface. Also deletes Infrastructure.Repository now that Breweries, UserManagement, and Auth have all moved their repos out of it, and replaces the two now-dead docker-compose test services (repository.tests, service.auth.tests, both pointing at deleted Dockerfiles) with a single unit.tests service that runs every Features.*.Tests project via Dockerfile.tests.
164 lines
5.5 KiB
C#
164 lines
5.5 KiB
C#
using System.IdentityModel.Tokens.Jwt;
|
|
using System.Security.Claims;
|
|
using Domain.Entities;
|
|
using Domain.Exceptions;
|
|
using FluentAssertions;
|
|
using Features.Auth.Repository;
|
|
using Features.Auth.Services;
|
|
using Infrastructure.Jwt;
|
|
using Moq;
|
|
|
|
namespace Features.Auth.Tests.Services;
|
|
|
|
public class TokenServiceRefreshTests
|
|
{
|
|
private readonly Mock<ITokenInfrastructure> _tokenInfraMock;
|
|
private readonly Mock<IAuthRepository> _authRepositoryMock;
|
|
private readonly TokenService _tokenService;
|
|
|
|
public TokenServiceRefreshTests()
|
|
{
|
|
_tokenInfraMock = new Mock<ITokenInfrastructure>();
|
|
_authRepositoryMock = new Mock<IAuthRepository>();
|
|
|
|
// 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 RefreshTokenAsync_WithValidRefreshToken_ReturnsNewTokens()
|
|
{
|
|
// Arrange
|
|
var userId = Guid.NewGuid();
|
|
const string username = "testuser";
|
|
const string refreshToken = "valid-refresh-token";
|
|
|
|
var claims = new List<Claim>
|
|
{
|
|
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);
|
|
|
|
var userAccount = new UserAccount
|
|
{
|
|
UserAccountId = userId,
|
|
Username = username,
|
|
FirstName = "Test",
|
|
LastName = "User",
|
|
Email = "test@example.com",
|
|
DateOfBirth = new DateTime(1990, 1, 1),
|
|
};
|
|
|
|
// Mock the validation of refresh token
|
|
_tokenInfraMock
|
|
.Setup(x => x.ValidateJwtAsync(refreshToken, It.IsAny<string>()))
|
|
.ReturnsAsync(principal);
|
|
|
|
// Mock the generation of new tokens
|
|
_tokenInfraMock
|
|
.Setup(x => x.GenerateJwt(userId, username, It.IsAny<DateTime>(), It.IsAny<string>()))
|
|
.Returns((Guid _, string _, DateTime _, string _) => $"generated-token-{Guid.NewGuid()}");
|
|
|
|
_authRepositoryMock
|
|
.Setup(x => x.GetUserByIdAsync(userId))
|
|
.ReturnsAsync(userAccount);
|
|
|
|
// Act
|
|
var result = await _tokenService.RefreshTokenAsync(refreshToken);
|
|
|
|
// Assert
|
|
result.Should().NotBeNull();
|
|
result.UserAccount.UserAccountId.Should().Be(userId);
|
|
result.UserAccount.Username.Should().Be(username);
|
|
result.AccessToken.Should().NotBeEmpty();
|
|
result.RefreshToken.Should().NotBeEmpty();
|
|
|
|
_authRepositoryMock.Verify(
|
|
x => x.GetUserByIdAsync(userId),
|
|
Times.Once
|
|
);
|
|
|
|
// Verify tokens were generated (called twice - once for access, once for refresh)
|
|
_tokenInfraMock.Verify(
|
|
x => x.GenerateJwt(It.IsAny<Guid>(), It.IsAny<string>(), It.IsAny<DateTime>(), It.IsAny<string>()),
|
|
Times.Exactly(2)
|
|
);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task RefreshTokenAsync_WithInvalidRefreshToken_ThrowsUnauthorizedException()
|
|
{
|
|
// Arrange
|
|
const string invalidToken = "invalid-refresh-token";
|
|
|
|
_tokenInfraMock
|
|
.Setup(x => x.ValidateJwtAsync(invalidToken, It.IsAny<string>()))
|
|
.ThrowsAsync(new UnauthorizedException("Invalid refresh token"));
|
|
|
|
// Act & Assert
|
|
await FluentActions.Invoking(async () =>
|
|
await _tokenService.RefreshTokenAsync(invalidToken)
|
|
).Should().ThrowAsync<UnauthorizedException>();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task RefreshTokenAsync_WithExpiredRefreshToken_ThrowsUnauthorizedException()
|
|
{
|
|
// Arrange
|
|
const string expiredToken = "expired-refresh-token";
|
|
|
|
_tokenInfraMock
|
|
.Setup(x => x.ValidateJwtAsync(expiredToken, It.IsAny<string>()))
|
|
.ThrowsAsync(new UnauthorizedException("Refresh token has expired"));
|
|
|
|
// Act & Assert
|
|
await FluentActions.Invoking(async () =>
|
|
await _tokenService.RefreshTokenAsync(expiredToken)
|
|
).Should().ThrowAsync<UnauthorizedException>();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task RefreshTokenAsync_WithNonExistentUser_ThrowsUnauthorizedException()
|
|
{
|
|
// Arrange
|
|
var userId = Guid.NewGuid();
|
|
const string username = "testuser";
|
|
const string refreshToken = "valid-refresh-token";
|
|
|
|
var claims = new List<Claim>
|
|
{
|
|
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(refreshToken, It.IsAny<string>()))
|
|
.ReturnsAsync(principal);
|
|
|
|
_authRepositoryMock
|
|
.Setup(x => x.GetUserByIdAsync(userId))
|
|
.ReturnsAsync((UserAccount?)null);
|
|
|
|
// Act & Assert
|
|
await FluentActions.Invoking(async () =>
|
|
await _tokenService.RefreshTokenAsync(refreshToken)
|
|
).Should().ThrowAsync<UnauthorizedException>()
|
|
.WithMessage("*User account not found*");
|
|
}
|
|
}
|