mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-07-16 17:47:22 +00:00
Migrate Auth and Emails to vertical slices (Features.Auth, Features.Emails)
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.
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
using Domain.Entities;
|
||||
using Domain.Exceptions;
|
||||
using FluentAssertions;
|
||||
using Features.Auth.Commands.ConfirmUser;
|
||||
using Features.Auth.Repository;
|
||||
using Features.Auth.Services;
|
||||
using Moq;
|
||||
|
||||
namespace Features.Auth.Tests.Commands;
|
||||
|
||||
public class ConfirmUserHandlerTests
|
||||
{
|
||||
private readonly Mock<IAuthRepository> _authRepositoryMock;
|
||||
private readonly Mock<ITokenService> _tokenServiceMock;
|
||||
private readonly ConfirmUserHandler _handler;
|
||||
|
||||
public ConfirmUserHandlerTests()
|
||||
{
|
||||
_authRepositoryMock = new Mock<IAuthRepository>();
|
||||
_tokenServiceMock = new Mock<ITokenService>();
|
||||
_handler = new ConfirmUserHandler(_authRepositoryMock.Object, _tokenServiceMock.Object);
|
||||
}
|
||||
|
||||
private static ValidatedToken MakeValidatedToken(Guid userId, string username)
|
||||
{
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
new(JwtRegisteredClaimNames.Sub, userId.ToString()),
|
||||
new(JwtRegisteredClaimNames.UniqueName, username),
|
||||
new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
|
||||
};
|
||||
var principal = new ClaimsPrincipal(new ClaimsIdentity(claims));
|
||||
return new ValidatedToken(userId, username, principal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_WithValidConfirmationToken_ConfirmsUser()
|
||||
{
|
||||
var userId = Guid.NewGuid();
|
||||
const string username = "testuser";
|
||||
const string confirmationToken = "valid-confirmation-token";
|
||||
|
||||
var validatedToken = MakeValidatedToken(userId, username);
|
||||
var userAccount = new UserAccount { UserAccountId = userId, Username = username };
|
||||
|
||||
_tokenServiceMock.Setup(x => x.ValidateConfirmationTokenAsync(confirmationToken)).ReturnsAsync(validatedToken);
|
||||
_authRepositoryMock.Setup(x => x.ConfirmUserAccountAsync(userId)).ReturnsAsync(userAccount);
|
||||
|
||||
var result = await _handler.Handle(new ConfirmUserCommand(confirmationToken), CancellationToken.None);
|
||||
|
||||
result.Should().NotBeNull();
|
||||
result.UserAccountId.Should().Be(userId);
|
||||
result.ConfirmedDate.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_WithInvalidConfirmationToken_ThrowsUnauthorizedException()
|
||||
{
|
||||
const string invalidToken = "invalid-confirmation-token";
|
||||
_tokenServiceMock
|
||||
.Setup(x => x.ValidateConfirmationTokenAsync(invalidToken))
|
||||
.ThrowsAsync(new UnauthorizedException("Invalid confirmation token"));
|
||||
|
||||
var act = async () => await _handler.Handle(new ConfirmUserCommand(invalidToken), CancellationToken.None);
|
||||
|
||||
await act.Should().ThrowAsync<UnauthorizedException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_WithNonExistentUser_ThrowsUnauthorizedException()
|
||||
{
|
||||
var userId = Guid.NewGuid();
|
||||
const string username = "nonexistent";
|
||||
const string confirmationToken = "valid-token-for-nonexistent-user";
|
||||
|
||||
_tokenServiceMock
|
||||
.Setup(x => x.ValidateConfirmationTokenAsync(confirmationToken))
|
||||
.ReturnsAsync(MakeValidatedToken(userId, username));
|
||||
_authRepositoryMock.Setup(x => x.ConfirmUserAccountAsync(userId)).ReturnsAsync((UserAccount?)null);
|
||||
|
||||
var act = async () => await _handler.Handle(new ConfirmUserCommand(confirmationToken), CancellationToken.None);
|
||||
|
||||
await act.Should().ThrowAsync<UnauthorizedException>().WithMessage("*User account not found*");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using Domain.Entities;
|
||||
using FluentAssertions;
|
||||
using Features.Auth.Commands.RefreshToken;
|
||||
using Features.Auth.Services;
|
||||
using Moq;
|
||||
|
||||
namespace Features.Auth.Tests.Commands;
|
||||
|
||||
public class RefreshTokenHandlerTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task Handle_MapsTokenServiceResult_ToLoginPayload()
|
||||
{
|
||||
var tokenServiceMock = new Mock<ITokenService>();
|
||||
var handler = new RefreshTokenHandler(tokenServiceMock.Object);
|
||||
var userId = Guid.NewGuid();
|
||||
var user = new UserAccount { UserAccountId = userId, Username = "testuser" };
|
||||
|
||||
tokenServiceMock
|
||||
.Setup(x => x.RefreshTokenAsync("old-refresh-token"))
|
||||
.ReturnsAsync(new RefreshTokenResult(user, "new-refresh-token", "new-access-token"));
|
||||
|
||||
var result = await handler.Handle(new RefreshTokenCommand("old-refresh-token"), CancellationToken.None);
|
||||
|
||||
result.UserAccountId.Should().Be(userId);
|
||||
result.Username.Should().Be("testuser");
|
||||
result.RefreshToken.Should().Be("new-refresh-token");
|
||||
result.AccessToken.Should().Be("new-access-token");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
using Domain.Entities;
|
||||
using Domain.Exceptions;
|
||||
using FluentAssertions;
|
||||
using Features.Auth.Commands.RegisterUser;
|
||||
using Features.Auth.Repository;
|
||||
using Features.Auth.Services;
|
||||
using Infrastructure.PasswordHashing;
|
||||
using MediatR;
|
||||
using Moq;
|
||||
using Shared.Application.Emails;
|
||||
|
||||
namespace Features.Auth.Tests.Commands;
|
||||
|
||||
public class RegisterUserHandlerTests
|
||||
{
|
||||
private readonly Mock<IAuthRepository> _authRepoMock;
|
||||
private readonly Mock<IPasswordInfrastructure> _passwordInfraMock;
|
||||
private readonly Mock<ITokenService> _tokenServiceMock;
|
||||
private readonly Mock<IMediator> _mediatorMock;
|
||||
private readonly RegisterUserHandler _handler;
|
||||
|
||||
public RegisterUserHandlerTests()
|
||||
{
|
||||
_authRepoMock = new Mock<IAuthRepository>();
|
||||
_passwordInfraMock = new Mock<IPasswordInfrastructure>();
|
||||
_tokenServiceMock = new Mock<ITokenService>();
|
||||
_mediatorMock = new Mock<IMediator>();
|
||||
|
||||
_handler = new RegisterUserHandler(
|
||||
_authRepoMock.Object,
|
||||
_passwordInfraMock.Object,
|
||||
_tokenServiceMock.Object,
|
||||
_mediatorMock.Object
|
||||
);
|
||||
}
|
||||
|
||||
private static RegisterUserCommand ValidCommand(string username = "newuser", string email = "john.doe@example.com") =>
|
||||
new(username, "John", "Doe", email, new DateTime(1990, 1, 1), "SecurePassword123!");
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_WithValidData_CreatesUserAndReturnsPayload()
|
||||
{
|
||||
var command = ValidCommand();
|
||||
const string hashedPassword = "hashed_password_value";
|
||||
var expectedUserId = Guid.NewGuid();
|
||||
|
||||
_authRepoMock.Setup(x => x.GetUserByUsernameAsync(command.Username)).ReturnsAsync((UserAccount?)null);
|
||||
_authRepoMock.Setup(x => x.GetUserByEmailAsync(command.Email)).ReturnsAsync((UserAccount?)null);
|
||||
_passwordInfraMock.Setup(x => x.Hash(command.Password)).Returns(hashedPassword);
|
||||
|
||||
_authRepoMock
|
||||
.Setup(x => x.RegisterUserAsync(command.Username, command.FirstName, command.LastName, command.Email, command.DateOfBirth, hashedPassword))
|
||||
.ReturnsAsync(new UserAccount
|
||||
{
|
||||
UserAccountId = expectedUserId,
|
||||
Username = command.Username,
|
||||
FirstName = command.FirstName,
|
||||
LastName = command.LastName,
|
||||
Email = command.Email,
|
||||
DateOfBirth = command.DateOfBirth,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
});
|
||||
|
||||
_tokenServiceMock.Setup(x => x.GenerateAccessToken(It.IsAny<UserAccount>())).Returns("access-token");
|
||||
_tokenServiceMock.Setup(x => x.GenerateRefreshToken(It.IsAny<UserAccount>())).Returns("refresh-token");
|
||||
_tokenServiceMock.Setup(x => x.GenerateConfirmationToken(It.IsAny<UserAccount>())).Returns("confirmation-token");
|
||||
|
||||
var result = await _handler.Handle(command, CancellationToken.None);
|
||||
|
||||
result.Should().NotBeNull();
|
||||
result.UserAccountId.Should().Be(expectedUserId);
|
||||
result.Username.Should().Be(command.Username);
|
||||
result.AccessToken.Should().Be("access-token");
|
||||
result.RefreshToken.Should().Be("refresh-token");
|
||||
result.ConfirmationEmailSent.Should().BeTrue();
|
||||
|
||||
_mediatorMock.Verify(
|
||||
x => x.Send(It.IsAny<SendRegistrationEmailCommand>(), It.IsAny<CancellationToken>()),
|
||||
Times.Once
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_WithExistingUsername_ThrowsConflictException()
|
||||
{
|
||||
var command = ValidCommand(username: "existinguser");
|
||||
var existingUser = new UserAccount { UserAccountId = Guid.NewGuid(), Username = "existinguser" };
|
||||
|
||||
_authRepoMock.Setup(x => x.GetUserByUsernameAsync(command.Username)).ReturnsAsync(existingUser);
|
||||
_authRepoMock.Setup(x => x.GetUserByEmailAsync(command.Email)).ReturnsAsync((UserAccount?)null);
|
||||
|
||||
var act = async () => await _handler.Handle(command, CancellationToken.None);
|
||||
|
||||
await act.Should().ThrowAsync<ConflictException>().WithMessage("Username or email already exists");
|
||||
|
||||
_authRepoMock.Verify(
|
||||
x => x.RegisterUserAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<DateTime>(), It.IsAny<string>()),
|
||||
Times.Never
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_WithExistingEmail_ThrowsConflictException()
|
||||
{
|
||||
var command = ValidCommand(email: "existing@example.com");
|
||||
var existingUser = new UserAccount { UserAccountId = Guid.NewGuid(), Email = "existing@example.com" };
|
||||
|
||||
_authRepoMock.Setup(x => x.GetUserByUsernameAsync(command.Username)).ReturnsAsync((UserAccount?)null);
|
||||
_authRepoMock.Setup(x => x.GetUserByEmailAsync(command.Email)).ReturnsAsync(existingUser);
|
||||
|
||||
var act = async () => await _handler.Handle(command, CancellationToken.None);
|
||||
|
||||
await act.Should().ThrowAsync<ConflictException>().WithMessage("Username or email already exists");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_PasswordIsHashed_BeforeStoringInDatabase()
|
||||
{
|
||||
var command = ValidCommand();
|
||||
const string hashedPassword = "hashed_secure_password";
|
||||
|
||||
_authRepoMock.Setup(x => x.GetUserByUsernameAsync(It.IsAny<string>())).ReturnsAsync((UserAccount?)null);
|
||||
_authRepoMock.Setup(x => x.GetUserByEmailAsync(It.IsAny<string>())).ReturnsAsync((UserAccount?)null);
|
||||
_passwordInfraMock.Setup(x => x.Hash(command.Password)).Returns(hashedPassword);
|
||||
|
||||
_authRepoMock
|
||||
.Setup(x => x.RegisterUserAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<DateTime>(), hashedPassword))
|
||||
.ReturnsAsync(new UserAccount { UserAccountId = Guid.NewGuid() });
|
||||
|
||||
_tokenServiceMock.Setup(x => x.GenerateAccessToken(It.IsAny<UserAccount>())).Returns("access-token");
|
||||
_tokenServiceMock.Setup(x => x.GenerateRefreshToken(It.IsAny<UserAccount>())).Returns("refresh-token");
|
||||
|
||||
await _handler.Handle(command, CancellationToken.None);
|
||||
|
||||
_passwordInfraMock.Verify(x => x.Hash(command.Password), Times.Once);
|
||||
_authRepoMock.Verify(
|
||||
x => x.RegisterUserAsync(command.Username, command.FirstName, command.LastName, command.Email, command.DateOfBirth, hashedPassword),
|
||||
Times.Once
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_SwallowsEmailFailure_AndReportsEmailNotSent()
|
||||
{
|
||||
var command = ValidCommand();
|
||||
|
||||
_authRepoMock.Setup(x => x.GetUserByUsernameAsync(It.IsAny<string>())).ReturnsAsync((UserAccount?)null);
|
||||
_authRepoMock.Setup(x => x.GetUserByEmailAsync(It.IsAny<string>())).ReturnsAsync((UserAccount?)null);
|
||||
_passwordInfraMock.Setup(x => x.Hash(It.IsAny<string>())).Returns("hashed");
|
||||
_authRepoMock
|
||||
.Setup(x => x.RegisterUserAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<DateTime>(), It.IsAny<string>()))
|
||||
.ReturnsAsync(new UserAccount { UserAccountId = Guid.NewGuid(), Username = command.Username, Email = command.Email });
|
||||
|
||||
_tokenServiceMock.Setup(x => x.GenerateAccessToken(It.IsAny<UserAccount>())).Returns("access-token");
|
||||
_tokenServiceMock.Setup(x => x.GenerateRefreshToken(It.IsAny<UserAccount>())).Returns("refresh-token");
|
||||
_mediatorMock
|
||||
.Setup(x => x.Send(It.IsAny<SendRegistrationEmailCommand>(), It.IsAny<CancellationToken>()))
|
||||
.ThrowsAsync(new Exception("smtp down"));
|
||||
|
||||
var result = await _handler.Handle(command, CancellationToken.None);
|
||||
|
||||
result.ConfirmationEmailSent.Should().BeFalse();
|
||||
result.AccessToken.Should().Be("access-token");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
using Domain.Entities;
|
||||
using Features.Auth.Commands.ResendConfirmationEmail;
|
||||
using Features.Auth.Repository;
|
||||
using Features.Auth.Services;
|
||||
using MediatR;
|
||||
using Moq;
|
||||
using Shared.Application.Emails;
|
||||
|
||||
namespace Features.Auth.Tests.Commands;
|
||||
|
||||
public class ResendConfirmationEmailHandlerTests
|
||||
{
|
||||
private readonly Mock<IAuthRepository> _authRepositoryMock = new();
|
||||
private readonly Mock<ITokenService> _tokenServiceMock = new();
|
||||
private readonly Mock<IMediator> _mediatorMock = new();
|
||||
private readonly ResendConfirmationEmailHandler _handler;
|
||||
|
||||
public ResendConfirmationEmailHandlerTests()
|
||||
{
|
||||
_handler = new ResendConfirmationEmailHandler(_authRepositoryMock.Object, _tokenServiceMock.Object, _mediatorMock.Object);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_SendsFreshConfirmationEmail_WhenUserExistsAndUnverified()
|
||||
{
|
||||
var userId = Guid.NewGuid();
|
||||
var user = new UserAccount { UserAccountId = userId, FirstName = "Aaron", Email = "aaron@example.com" };
|
||||
|
||||
_authRepositoryMock.Setup(x => x.GetUserByIdAsync(userId)).ReturnsAsync(user);
|
||||
_authRepositoryMock.Setup(x => x.IsUserVerifiedAsync(userId)).ReturnsAsync(false);
|
||||
_tokenServiceMock.Setup(x => x.GenerateConfirmationToken(user)).Returns("fresh-token");
|
||||
|
||||
await _handler.Handle(new ResendConfirmationEmailCommand(userId), CancellationToken.None);
|
||||
|
||||
_mediatorMock.Verify(
|
||||
x => x.Send(It.Is<SendResendConfirmationEmailCommand>(c =>
|
||||
c.FirstName == "Aaron" && c.Email == "aaron@example.com" && c.ConfirmationToken == "fresh-token"
|
||||
), It.IsAny<CancellationToken>()),
|
||||
Times.Once
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_DoesNothing_WhenUserDoesNotExist()
|
||||
{
|
||||
var userId = Guid.NewGuid();
|
||||
_authRepositoryMock.Setup(x => x.GetUserByIdAsync(userId)).ReturnsAsync((UserAccount?)null);
|
||||
|
||||
await _handler.Handle(new ResendConfirmationEmailCommand(userId), CancellationToken.None);
|
||||
|
||||
_mediatorMock.Verify(
|
||||
x => x.Send(It.IsAny<SendResendConfirmationEmailCommand>(), It.IsAny<CancellationToken>()),
|
||||
Times.Never
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_DoesNothing_WhenUserAlreadyVerified()
|
||||
{
|
||||
var userId = Guid.NewGuid();
|
||||
var user = new UserAccount { UserAccountId = userId };
|
||||
|
||||
_authRepositoryMock.Setup(x => x.GetUserByIdAsync(userId)).ReturnsAsync(user);
|
||||
_authRepositoryMock.Setup(x => x.IsUserVerifiedAsync(userId)).ReturnsAsync(true);
|
||||
|
||||
await _handler.Handle(new ResendConfirmationEmailCommand(userId), CancellationToken.None);
|
||||
|
||||
_mediatorMock.Verify(
|
||||
x => x.Send(It.IsAny<SendResendConfirmationEmailCommand>(), It.IsAny<CancellationToken>()),
|
||||
Times.Never
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
<RootNamespace>Features.Auth.Tests</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
|
||||
<PackageReference Include="xunit" Version="2.9.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
|
||||
<PackageReference Include="Moq" Version="4.20.72" />
|
||||
<PackageReference Include="FluentAssertions" Version="6.9.0" />
|
||||
<PackageReference Include="DbMocker" Version="1.26.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Features.Auth\Features.Auth.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,110 @@
|
||||
using Domain.Entities;
|
||||
using Domain.Exceptions;
|
||||
using FluentAssertions;
|
||||
using Features.Auth.Queries.Login;
|
||||
using Features.Auth.Repository;
|
||||
using Features.Auth.Services;
|
||||
using Infrastructure.PasswordHashing;
|
||||
using Moq;
|
||||
|
||||
namespace Features.Auth.Tests.Queries;
|
||||
|
||||
public class LoginHandlerTests
|
||||
{
|
||||
private readonly Mock<IAuthRepository> _authRepoMock;
|
||||
private readonly Mock<IPasswordInfrastructure> _passwordInfraMock;
|
||||
private readonly Mock<ITokenService> _tokenServiceMock;
|
||||
private readonly LoginHandler _handler;
|
||||
|
||||
public LoginHandlerTests()
|
||||
{
|
||||
_authRepoMock = new Mock<IAuthRepository>();
|
||||
_passwordInfraMock = new Mock<IPasswordInfrastructure>();
|
||||
_tokenServiceMock = new Mock<ITokenService>();
|
||||
_handler = new LoginHandler(_authRepoMock.Object, _passwordInfraMock.Object, _tokenServiceMock.Object);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_WithValidData_ReturnsPayloadWithMatchingUsername()
|
||||
{
|
||||
const string username = "CogitoErgoSum";
|
||||
var userAccountId = Guid.NewGuid();
|
||||
|
||||
var userAccount = new UserAccount
|
||||
{
|
||||
UserAccountId = userAccountId,
|
||||
Username = username,
|
||||
FirstName = "René",
|
||||
LastName = "Descartes",
|
||||
Email = "r.descartes@example.com",
|
||||
DateOfBirth = new DateTime(1596, 03, 31),
|
||||
};
|
||||
|
||||
var userCredential = new UserCredential
|
||||
{
|
||||
UserCredentialId = Guid.NewGuid(),
|
||||
UserAccountId = userAccountId,
|
||||
Hash = "some-hash",
|
||||
Expiry = DateTime.MaxValue,
|
||||
};
|
||||
|
||||
_authRepoMock.Setup(x => x.GetUserByUsernameAsync(username)).ReturnsAsync(userAccount);
|
||||
_authRepoMock.Setup(x => x.GetActiveCredentialByUserAccountIdAsync(userAccountId)).ReturnsAsync(userCredential);
|
||||
_passwordInfraMock.Setup(x => x.Verify(It.IsAny<string>(), It.IsAny<string>())).Returns(true);
|
||||
_tokenServiceMock.Setup(x => x.GenerateAccessToken(It.IsAny<UserAccount>())).Returns("access-token");
|
||||
_tokenServiceMock.Setup(x => x.GenerateRefreshToken(It.IsAny<UserAccount>())).Returns("refresh-token");
|
||||
|
||||
var result = await _handler.Handle(new LoginQuery(username, "any-password"), CancellationToken.None);
|
||||
|
||||
result.Should().NotBeNull();
|
||||
result.UserAccountId.Should().Be(userAccountId);
|
||||
result.Username.Should().Be(username);
|
||||
result.AccessToken.Should().Be("access-token");
|
||||
result.RefreshToken.Should().Be("refresh-token");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_WithUnregisteredUsername_ThrowsUnauthorizedException()
|
||||
{
|
||||
const string username = "de_beauvoir";
|
||||
_authRepoMock.Setup(x => x.GetUserByUsernameAsync(username)).ReturnsAsync((UserAccount?)null);
|
||||
|
||||
var act = async () => await _handler.Handle(new LoginQuery(username, "password"), CancellationToken.None);
|
||||
|
||||
await act.Should().ThrowAsync<UnauthorizedException>();
|
||||
_authRepoMock.Verify(x => x.GetActiveCredentialByUserAccountIdAsync(It.IsAny<Guid>()), Times.Never);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_WithNoActiveCredential_ThrowsUnauthorizedException()
|
||||
{
|
||||
const string username = "BRussell";
|
||||
var userAccountId = Guid.NewGuid();
|
||||
var userAccount = new UserAccount { UserAccountId = userAccountId, Username = username };
|
||||
|
||||
_authRepoMock.Setup(x => x.GetUserByUsernameAsync(username)).ReturnsAsync(userAccount);
|
||||
_authRepoMock.Setup(x => x.GetActiveCredentialByUserAccountIdAsync(userAccountId)).ReturnsAsync((UserCredential?)null);
|
||||
|
||||
var act = async () => await _handler.Handle(new LoginQuery(username, "password"), CancellationToken.None);
|
||||
|
||||
await act.Should().ThrowAsync<UnauthorizedException>().WithMessage("Invalid username or password.");
|
||||
_passwordInfraMock.Verify(x => x.Verify(It.IsAny<string>(), It.IsAny<string>()), Times.Never);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_WithIncorrectPassword_ThrowsUnauthorizedException()
|
||||
{
|
||||
const string username = "RCarnap";
|
||||
var userAccountId = Guid.NewGuid();
|
||||
var userAccount = new UserAccount { UserAccountId = userAccountId, Username = username };
|
||||
var userCredential = new UserCredential { UserCredentialId = Guid.NewGuid(), UserAccountId = userAccountId, Hash = "hashed-password" };
|
||||
|
||||
_authRepoMock.Setup(x => x.GetUserByUsernameAsync(username)).ReturnsAsync(userAccount);
|
||||
_authRepoMock.Setup(x => x.GetActiveCredentialByUserAccountIdAsync(userAccountId)).ReturnsAsync(userCredential);
|
||||
_passwordInfraMock.Setup(x => x.Verify(It.IsAny<string>(), It.IsAny<string>())).Returns(false);
|
||||
|
||||
var act = async () => await _handler.Handle(new LoginQuery(username, "wrong-password"), CancellationToken.None);
|
||||
|
||||
await act.Should().ThrowAsync<UnauthorizedException>().WithMessage("Invalid username or password.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
using Apps72.Dev.Data.DbMocker;
|
||||
using FluentAssertions;
|
||||
using Features.Auth.Repository;
|
||||
|
||||
namespace Features.Auth.Tests.Repository;
|
||||
|
||||
public class AuthRepositoryTests
|
||||
{
|
||||
private static AuthRepository CreateRepo(MockDbConnection conn) =>
|
||||
new(new TestConnectionFactory(conn));
|
||||
|
||||
[Fact]
|
||||
public async Task RegisterUserAsync_CreatesUserWithCredential_ReturnsUserAccount()
|
||||
{
|
||||
var expectedUserId = Guid.NewGuid();
|
||||
var conn = new MockDbConnection();
|
||||
|
||||
conn.Mocks.When(cmd => cmd.CommandText == "USP_RegisterUser")
|
||||
.ReturnsScalar(expectedUserId);
|
||||
|
||||
// Mock the subsequent read for the newly created user by id
|
||||
conn.Mocks.When(cmd => cmd.CommandText == "usp_GetUserAccountById")
|
||||
.ReturnsTable(
|
||||
MockTable
|
||||
.WithColumns(
|
||||
("UserAccountId", typeof(Guid)),
|
||||
("Username", typeof(string)),
|
||||
("FirstName", typeof(string)),
|
||||
("LastName", typeof(string)),
|
||||
("Email", typeof(string)),
|
||||
("CreatedAt", typeof(DateTime)),
|
||||
("UpdatedAt", typeof(DateTime?)),
|
||||
("DateOfBirth", typeof(DateTime)),
|
||||
("Timer", typeof(byte[]))
|
||||
)
|
||||
.AddRow(
|
||||
expectedUserId,
|
||||
"testuser",
|
||||
"Test",
|
||||
"User",
|
||||
"test@example.com",
|
||||
DateTime.UtcNow,
|
||||
null,
|
||||
new DateTime(1990, 1, 1),
|
||||
null
|
||||
)
|
||||
);
|
||||
|
||||
var repo = CreateRepo(conn);
|
||||
var result = await repo.RegisterUserAsync(
|
||||
username: "testuser",
|
||||
firstName: "Test",
|
||||
lastName: "User",
|
||||
email: "test@example.com",
|
||||
dateOfBirth: new DateTime(1990, 1, 1),
|
||||
passwordHash: "hashedpassword123"
|
||||
);
|
||||
|
||||
result.Should().NotBeNull();
|
||||
result.UserAccountId.Should().Be(expectedUserId);
|
||||
result.Username.Should().Be("testuser");
|
||||
result.FirstName.Should().Be("Test");
|
||||
result.LastName.Should().Be("User");
|
||||
result.Email.Should().Be("test@example.com");
|
||||
result.DateOfBirth.Should().Be(new DateTime(1990, 1, 1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetUserByEmailAsync_ReturnsUser_WhenExists()
|
||||
{
|
||||
var userId = Guid.NewGuid();
|
||||
var conn = new MockDbConnection();
|
||||
|
||||
conn.Mocks.When(cmd => cmd.CommandText == "usp_GetUserAccountByEmail")
|
||||
.ReturnsTable(
|
||||
MockTable
|
||||
.WithColumns(
|
||||
("UserAccountId", typeof(Guid)),
|
||||
("Username", typeof(string)),
|
||||
("FirstName", typeof(string)),
|
||||
("LastName", typeof(string)),
|
||||
("Email", typeof(string)),
|
||||
("CreatedAt", typeof(DateTime)),
|
||||
("UpdatedAt", typeof(DateTime?)),
|
||||
("DateOfBirth", typeof(DateTime)),
|
||||
("Timer", typeof(byte[]))
|
||||
)
|
||||
.AddRow(
|
||||
userId,
|
||||
"emailuser",
|
||||
"Email",
|
||||
"User",
|
||||
"emailuser@example.com",
|
||||
DateTime.UtcNow,
|
||||
null,
|
||||
new DateTime(1990, 5, 15),
|
||||
null
|
||||
)
|
||||
);
|
||||
|
||||
var repo = CreateRepo(conn);
|
||||
var result = await repo.GetUserByEmailAsync("emailuser@example.com");
|
||||
|
||||
result.Should().NotBeNull();
|
||||
result!.UserAccountId.Should().Be(userId);
|
||||
result.Username.Should().Be("emailuser");
|
||||
result.Email.Should().Be("emailuser@example.com");
|
||||
result.FirstName.Should().Be("Email");
|
||||
result.LastName.Should().Be("User");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetUserByEmailAsync_ReturnsNull_WhenNotExists()
|
||||
{
|
||||
var conn = new MockDbConnection();
|
||||
|
||||
conn.Mocks.When(cmd => cmd.CommandText == "usp_GetUserAccountByEmail")
|
||||
.ReturnsTable(MockTable.Empty());
|
||||
|
||||
var repo = CreateRepo(conn);
|
||||
var result = await repo.GetUserByEmailAsync("nonexistent@example.com");
|
||||
|
||||
result.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetUserByUsernameAsync_ReturnsUser_WhenExists()
|
||||
{
|
||||
var userId = Guid.NewGuid();
|
||||
var conn = new MockDbConnection();
|
||||
|
||||
conn.Mocks.When(cmd =>
|
||||
cmd.CommandText == "usp_GetUserAccountByUsername"
|
||||
)
|
||||
.ReturnsTable(
|
||||
MockTable
|
||||
.WithColumns(
|
||||
("UserAccountId", typeof(Guid)),
|
||||
("Username", typeof(string)),
|
||||
("FirstName", typeof(string)),
|
||||
("LastName", typeof(string)),
|
||||
("Email", typeof(string)),
|
||||
("CreatedAt", typeof(DateTime)),
|
||||
("UpdatedAt", typeof(DateTime?)),
|
||||
("DateOfBirth", typeof(DateTime)),
|
||||
("Timer", typeof(byte[]))
|
||||
)
|
||||
.AddRow(
|
||||
userId,
|
||||
"usernameuser",
|
||||
"Username",
|
||||
"User",
|
||||
"username@example.com",
|
||||
DateTime.UtcNow,
|
||||
null,
|
||||
new DateTime(1985, 8, 20),
|
||||
null
|
||||
)
|
||||
);
|
||||
|
||||
var repo = CreateRepo(conn);
|
||||
var result = await repo.GetUserByUsernameAsync("usernameuser");
|
||||
|
||||
result.Should().NotBeNull();
|
||||
result!.UserAccountId.Should().Be(userId);
|
||||
result.Username.Should().Be("usernameuser");
|
||||
result.Email.Should().Be("username@example.com");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetUserByUsernameAsync_ReturnsNull_WhenNotExists()
|
||||
{
|
||||
var conn = new MockDbConnection();
|
||||
|
||||
conn.Mocks.When(cmd =>
|
||||
cmd.CommandText == "usp_GetUserAccountByUsername"
|
||||
)
|
||||
.ReturnsTable(MockTable.Empty());
|
||||
|
||||
var repo = CreateRepo(conn);
|
||||
var result = await repo.GetUserByUsernameAsync("nonexistent");
|
||||
|
||||
result.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetActiveCredentialByUserAccountIdAsync_ReturnsCredential_WhenExists()
|
||||
{
|
||||
var userId = Guid.NewGuid();
|
||||
var credentialId = Guid.NewGuid();
|
||||
var conn = new MockDbConnection();
|
||||
|
||||
conn.Mocks.When(cmd =>
|
||||
cmd.CommandText == "USP_GetActiveUserCredentialByUserAccountId"
|
||||
)
|
||||
.ReturnsTable(
|
||||
MockTable
|
||||
.WithColumns(
|
||||
("UserCredentialId", typeof(Guid)),
|
||||
("UserAccountId", typeof(Guid)),
|
||||
("Hash", typeof(string)),
|
||||
("CreatedAt", typeof(DateTime)),
|
||||
("Timer", typeof(byte[]))
|
||||
)
|
||||
.AddRow(
|
||||
credentialId,
|
||||
userId,
|
||||
"hashed_password_value",
|
||||
DateTime.UtcNow,
|
||||
null
|
||||
)
|
||||
);
|
||||
|
||||
var repo = CreateRepo(conn);
|
||||
var result = await repo.GetActiveCredentialByUserAccountIdAsync(userId);
|
||||
|
||||
result.Should().NotBeNull();
|
||||
result!.UserCredentialId.Should().Be(credentialId);
|
||||
result.UserAccountId.Should().Be(userId);
|
||||
result.Hash.Should().Be("hashed_password_value");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetActiveCredentialByUserAccountIdAsync_ReturnsNull_WhenNotExists()
|
||||
{
|
||||
var userId = Guid.NewGuid();
|
||||
var conn = new MockDbConnection();
|
||||
|
||||
conn.Mocks.When(cmd =>
|
||||
cmd.CommandText == "USP_GetActiveUserCredentialByUserAccountId"
|
||||
)
|
||||
.ReturnsTable(MockTable.Empty());
|
||||
|
||||
var repo = CreateRepo(conn);
|
||||
var result = await repo.GetActiveCredentialByUserAccountIdAsync(userId);
|
||||
|
||||
result.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RotateCredentialAsync_ExecutesSuccessfully()
|
||||
{
|
||||
var userId = Guid.NewGuid();
|
||||
var newPasswordHash = "new_hashed_password";
|
||||
var conn = new MockDbConnection();
|
||||
|
||||
conn.Mocks.When(cmd => cmd.CommandText == "USP_RotateUserCredential")
|
||||
.ReturnsScalar(1);
|
||||
|
||||
var repo = CreateRepo(conn);
|
||||
|
||||
// Should not throw
|
||||
var act = async () =>
|
||||
await repo.RotateCredentialAsync(userId, newPasswordHash);
|
||||
await act.Should().NotThrowAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using System.Data.Common;
|
||||
using Infrastructure.Sql;
|
||||
|
||||
namespace Features.Auth.Tests.Repository;
|
||||
|
||||
internal class TestConnectionFactory(DbConnection conn) : ISqlConnectionFactory
|
||||
{
|
||||
private readonly DbConnection _conn = conn;
|
||||
|
||||
public DbConnection CreateConnection() => _conn;
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
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*");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
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 TokenServiceValidationTests
|
||||
{
|
||||
private readonly Mock<ITokenInfrastructure> _tokenInfraMock;
|
||||
private readonly Mock<IAuthRepository> _authRepositoryMock;
|
||||
private readonly TokenService _tokenService;
|
||||
|
||||
public TokenServiceValidationTests()
|
||||
{
|
||||
_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 ValidateAccessTokenAsync_WithValidToken_ReturnsValidatedToken()
|
||||
{
|
||||
// Arrange
|
||||
var userId = Guid.NewGuid();
|
||||
const string username = "testuser";
|
||||
const string token = "valid-access-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(token, It.IsAny<string>()))
|
||||
.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<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(token, It.IsAny<string>()))
|
||||
.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<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(token, It.IsAny<string>()))
|
||||
.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<string>()))
|
||||
.ThrowsAsync(new UnauthorizedException("Invalid token"));
|
||||
|
||||
// Act & Assert
|
||||
await FluentActions.Invoking(async () =>
|
||||
await _tokenService.ValidateAccessTokenAsync(token)
|
||||
).Should().ThrowAsync<UnauthorizedException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ValidateAccessTokenAsync_WithExpiredToken_ThrowsUnauthorizedException()
|
||||
{
|
||||
// Arrange
|
||||
const string token = "expired-token";
|
||||
|
||||
_tokenInfraMock
|
||||
.Setup(x => x.ValidateJwtAsync(token, It.IsAny<string>()))
|
||||
.ThrowsAsync(new UnauthorizedException(
|
||||
"Token has expired"
|
||||
));
|
||||
|
||||
// Act & Assert
|
||||
await FluentActions.Invoking(async () =>
|
||||
await _tokenService.ValidateAccessTokenAsync(token)
|
||||
).Should().ThrowAsync<UnauthorizedException>();
|
||||
}
|
||||
|
||||
[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<Claim>
|
||||
{
|
||||
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<string>()))
|
||||
.ReturnsAsync(principal);
|
||||
|
||||
// Act & Assert
|
||||
await FluentActions.Invoking(async () =>
|
||||
await _tokenService.ValidateAccessTokenAsync(token)
|
||||
).Should().ThrowAsync<UnauthorizedException>()
|
||||
.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<Claim>
|
||||
{
|
||||
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<string>()))
|
||||
.ReturnsAsync(principal);
|
||||
|
||||
// Act & Assert
|
||||
await FluentActions.Invoking(async () =>
|
||||
await _tokenService.ValidateAccessTokenAsync(token)
|
||||
).Should().ThrowAsync<UnauthorizedException>()
|
||||
.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<Claim>
|
||||
{
|
||||
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<string>()))
|
||||
.ReturnsAsync(principal);
|
||||
|
||||
// Act & Assert
|
||||
await FluentActions.Invoking(async () =>
|
||||
await _tokenService.ValidateAccessTokenAsync(token)
|
||||
).Should().ThrowAsync<UnauthorizedException>()
|
||||
.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<string>()))
|
||||
.ThrowsAsync(new UnauthorizedException("Invalid token"));
|
||||
|
||||
// Act & Assert
|
||||
await FluentActions.Invoking(async () =>
|
||||
await _tokenService.ValidateRefreshTokenAsync(token)
|
||||
).Should().ThrowAsync<UnauthorizedException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ValidateConfirmationTokenAsync_WithInvalidToken_ThrowsUnauthorizedException()
|
||||
{
|
||||
// Arrange
|
||||
const string token = "invalid-confirmation-token";
|
||||
|
||||
_tokenInfraMock
|
||||
.Setup(x => x.ValidateJwtAsync(token, It.IsAny<string>()))
|
||||
.ThrowsAsync(new UnauthorizedException("Invalid token"));
|
||||
|
||||
// Act & Assert
|
||||
await FluentActions.Invoking(async () =>
|
||||
await _tokenService.ValidateConfirmationTokenAsync(token)
|
||||
).Should().ThrowAsync<UnauthorizedException>();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user