mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-06-01 01:54:00 +00:00
Move dotnet api into new directory
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
using Domain.Entities;
|
||||
using Domain.Exceptions;
|
||||
using FluentAssertions;
|
||||
using Infrastructure.Repository.Auth;
|
||||
using Moq;
|
||||
using Service.Emails;
|
||||
|
||||
namespace Service.Auth.Tests;
|
||||
|
||||
public class ConfirmationServiceTest
|
||||
{
|
||||
private readonly Mock<IAuthRepository> _authRepositoryMock;
|
||||
private readonly Mock<ITokenService> _tokenServiceMock;
|
||||
private readonly Mock<IEmailService> _emailServiceMock;
|
||||
private readonly ConfirmationService _confirmationService;
|
||||
|
||||
public ConfirmationServiceTest()
|
||||
{
|
||||
_authRepositoryMock = new Mock<IAuthRepository>();
|
||||
_tokenServiceMock = new Mock<ITokenService>();
|
||||
_emailServiceMock = new Mock<IEmailService>();
|
||||
|
||||
_confirmationService = new ConfirmationService(
|
||||
_authRepositoryMock.Object,
|
||||
_tokenServiceMock.Object,
|
||||
_emailServiceMock.Object
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ConfirmUserAsync_WithValidConfirmationToken_ConfirmsUser()
|
||||
{
|
||||
// Arrange
|
||||
var userId = Guid.NewGuid();
|
||||
const string username = "testuser";
|
||||
const string confirmationToken = "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);
|
||||
|
||||
var validatedToken = new ValidatedToken(userId, username, principal);
|
||||
var userAccount = new UserAccount
|
||||
{
|
||||
UserAccountId = userId,
|
||||
Username = username,
|
||||
FirstName = "Test",
|
||||
LastName = "User",
|
||||
Email = "test@example.com",
|
||||
DateOfBirth = new DateTime(1990, 1, 1),
|
||||
};
|
||||
|
||||
_tokenServiceMock
|
||||
.Setup(x => x.ValidateConfirmationTokenAsync(confirmationToken))
|
||||
.ReturnsAsync(validatedToken);
|
||||
|
||||
_authRepositoryMock
|
||||
.Setup(x => x.ConfirmUserAccountAsync(userId))
|
||||
.ReturnsAsync(userAccount);
|
||||
|
||||
// Act
|
||||
var result =
|
||||
await _confirmationService.ConfirmUserAsync(confirmationToken);
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
result.UserId.Should().Be(userId);
|
||||
result.ConfirmedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(1));
|
||||
|
||||
_tokenServiceMock.Verify(
|
||||
x => x.ValidateConfirmationTokenAsync(confirmationToken),
|
||||
Times.Once
|
||||
);
|
||||
|
||||
_authRepositoryMock.Verify(
|
||||
x => x.ConfirmUserAccountAsync(userId),
|
||||
Times.Once
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ConfirmUserAsync_WithInvalidConfirmationToken_ThrowsUnauthorizedException()
|
||||
{
|
||||
// Arrange
|
||||
const string invalidToken = "invalid-confirmation-token";
|
||||
|
||||
_tokenServiceMock
|
||||
.Setup(x => x.ValidateConfirmationTokenAsync(invalidToken))
|
||||
.ThrowsAsync(new UnauthorizedException(
|
||||
"Invalid confirmation token"
|
||||
));
|
||||
|
||||
// Act & Assert
|
||||
await FluentActions.Invoking(async () =>
|
||||
await _confirmationService.ConfirmUserAsync(invalidToken)
|
||||
).Should().ThrowAsync<UnauthorizedException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ConfirmUserAsync_WithExpiredConfirmationToken_ThrowsUnauthorizedException()
|
||||
{
|
||||
// Arrange
|
||||
const string expiredToken = "expired-confirmation-token";
|
||||
|
||||
_tokenServiceMock
|
||||
.Setup(x => x.ValidateConfirmationTokenAsync(expiredToken))
|
||||
.ThrowsAsync(new UnauthorizedException(
|
||||
"Confirmation token has expired"
|
||||
));
|
||||
|
||||
// Act & Assert
|
||||
await FluentActions.Invoking(async () =>
|
||||
await _confirmationService.ConfirmUserAsync(expiredToken)
|
||||
).Should().ThrowAsync<UnauthorizedException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ConfirmUserAsync_WithNonExistentUser_ThrowsUnauthorizedException()
|
||||
{
|
||||
// Arrange
|
||||
var userId = Guid.NewGuid();
|
||||
const string username = "nonexistent";
|
||||
const string confirmationToken = "valid-token-for-nonexistent-user";
|
||||
|
||||
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 validatedToken = new ValidatedToken(userId, username, principal);
|
||||
|
||||
_tokenServiceMock
|
||||
.Setup(x => x.ValidateConfirmationTokenAsync(confirmationToken))
|
||||
.ReturnsAsync(validatedToken);
|
||||
|
||||
_authRepositoryMock
|
||||
.Setup(x => x.ConfirmUserAccountAsync(userId))
|
||||
.ReturnsAsync((UserAccount?)null);
|
||||
|
||||
// Act & Assert
|
||||
await FluentActions.Invoking(async () =>
|
||||
await _confirmationService.ConfirmUserAsync(confirmationToken)
|
||||
).Should().ThrowAsync<UnauthorizedException>()
|
||||
.WithMessage("*User account not found*");
|
||||
}
|
||||
}
|
||||
21
web/backend/Service/Service.Auth.Tests/Dockerfile
Normal file
21
web/backend/Service/Service.Auth.Tests/Dockerfile
Normal file
@@ -0,0 +1,21 @@
|
||||
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
||||
ARG BUILD_CONFIGURATION=Release
|
||||
WORKDIR /src
|
||||
COPY ["Domain/Domain.Entities/Domain.Entities.csproj", "Domain.Entities/"]
|
||||
COPY ["Domain/Domain.Exceptions/Domain.Exceptions.csproj", "Domain.Exceptions/"]
|
||||
COPY ["Infrastructure/Infrastructure.Email/Infrastructure.Email.csproj", "Infrastructure/Infrastructure.Email/"]
|
||||
COPY ["Infrastructure/Infrastructure.Email.Templates/Infrastructure.Email.Templates.csproj", "Infrastructure/Infrastructure.Email.Templates/"]
|
||||
COPY ["Infrastructure/Infrastructure.Jwt/Infrastructure.Jwt.csproj", "Infrastructure/Infrastructure.Jwt/"]
|
||||
COPY ["Infrastructure/Infrastructure.Repository/Infrastructure.Repository.csproj", "Infrastructure/Infrastructure.Repository/"]
|
||||
COPY ["Infrastructure/Infrastructure.PasswordHashing/Infrastructure.PasswordHashing.csproj", "Infrastructure/Infrastructure.PasswordHashing/"]
|
||||
COPY ["Service/Service.Auth/Service.Auth.csproj", "Service/Service.Auth/"]
|
||||
COPY ["Service/Service.Auth.Tests/Service.Auth.Tests.csproj", "Service/Service.Auth.Tests/"]
|
||||
RUN dotnet restore "Service/Service.Auth.Tests/Service.Auth.Tests.csproj"
|
||||
COPY . .
|
||||
WORKDIR "/src/Service/Service.Auth.Tests"
|
||||
RUN dotnet build "./Service.Auth.Tests.csproj" -c $BUILD_CONFIGURATION -o /app/build
|
||||
|
||||
FROM build AS final
|
||||
RUN mkdir -p /app/test-results/service-auth-tests
|
||||
WORKDIR /src/Service/Service.Auth.Tests
|
||||
ENTRYPOINT ["dotnet", "test", "./Service.Auth.Tests.csproj", "-c", "Release", "--logger", "trx;LogFileName=/app/test-results/service-auth-tests/results.trx"]
|
||||
254
web/backend/Service/Service.Auth.Tests/LoginService.test.cs
Normal file
254
web/backend/Service/Service.Auth.Tests/LoginService.test.cs
Normal file
@@ -0,0 +1,254 @@
|
||||
using Domain.Entities;
|
||||
using Domain.Exceptions;
|
||||
using FluentAssertions;
|
||||
using Infrastructure.PasswordHashing;
|
||||
using Infrastructure.Repository.Auth;
|
||||
using Moq;
|
||||
|
||||
namespace Service.Auth.Tests;
|
||||
|
||||
public class LoginServiceTest
|
||||
{
|
||||
private readonly Mock<IAuthRepository> _authRepoMock;
|
||||
private readonly Mock<IPasswordInfrastructure> _passwordInfraMock;
|
||||
private readonly Mock<ITokenService> _tokenServiceMock;
|
||||
private readonly LoginService _loginService;
|
||||
|
||||
public LoginServiceTest()
|
||||
{
|
||||
_authRepoMock = new Mock<IAuthRepository>();
|
||||
_passwordInfraMock = new Mock<IPasswordInfrastructure>();
|
||||
_tokenServiceMock = new Mock<ITokenService>();
|
||||
_loginService = new LoginService(
|
||||
_authRepoMock.Object,
|
||||
_passwordInfraMock.Object,
|
||||
_tokenServiceMock.Object
|
||||
);
|
||||
}
|
||||
|
||||
// Happy path: login returns the user account with the same username -- successful login
|
||||
[Fact]
|
||||
public async Task LoginAsync_WithValidData_ReturnsUserAccountWithMatchingUsername()
|
||||
{
|
||||
// Arrange
|
||||
const string username = "CogitoErgoSum";
|
||||
var userAccountId = Guid.NewGuid();
|
||||
|
||||
UserAccount userAccount = new()
|
||||
{
|
||||
UserAccountId = userAccountId,
|
||||
Username = username,
|
||||
FirstName = "René",
|
||||
LastName = "Descartes",
|
||||
Email = "r.descartes@example.com",
|
||||
DateOfBirth = new DateTime(1596, 03, 31),
|
||||
};
|
||||
|
||||
UserCredential userCredential = new()
|
||||
{
|
||||
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");
|
||||
|
||||
// Act
|
||||
var result = await _loginService.LoginAsync(
|
||||
username,
|
||||
It.IsAny<string>()
|
||||
);
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
result.UserAccount.UserAccountId.Should().Be(userAccountId);
|
||||
result.UserAccount.Username.Should().Be(username);
|
||||
result.AccessToken.Should().Be("access-token");
|
||||
result.RefreshToken.Should().Be("refresh-token");
|
||||
|
||||
_authRepoMock.Verify(
|
||||
x => x.GetActiveCredentialByUserAccountIdAsync(userAccountId),
|
||||
Times.Once
|
||||
);
|
||||
|
||||
_authRepoMock.Verify(
|
||||
x => x.GetUserByUsernameAsync(username),
|
||||
Times.Once
|
||||
);
|
||||
|
||||
_passwordInfraMock.Verify(
|
||||
x => x.Verify(It.IsAny<string>(), It.IsAny<string>()),
|
||||
Times.Once
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LoginAsync_WithUnregisteredUsername_ThrowsUnauthorizedException()
|
||||
{
|
||||
// Arrange
|
||||
const string username = "de_beauvoir";
|
||||
|
||||
_authRepoMock
|
||||
.Setup(x => x.GetUserByUsernameAsync(username))
|
||||
.ReturnsAsync((UserAccount?)null);
|
||||
|
||||
// Act
|
||||
var act = async () =>
|
||||
await _loginService.LoginAsync(username, It.IsAny<string>());
|
||||
|
||||
// Assert
|
||||
await act.Should().ThrowAsync<UnauthorizedException>();
|
||||
|
||||
_authRepoMock.Verify(
|
||||
x => x.GetUserByUsernameAsync(username),
|
||||
Times.Once
|
||||
);
|
||||
|
||||
_authRepoMock.Verify(
|
||||
x => x.GetActiveCredentialByUserAccountIdAsync(It.IsAny<Guid>()),
|
||||
Times.Never
|
||||
);
|
||||
|
||||
_passwordInfraMock.Verify(
|
||||
x => x.Verify(It.IsAny<string>(), It.IsAny<string>()),
|
||||
Times.Never
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LoginAsync_WithNoActiveCredential_ThrowsUnauthorizedException()
|
||||
{
|
||||
// Arrange
|
||||
const string username = "BRussell";
|
||||
var userAccountId = Guid.NewGuid();
|
||||
|
||||
UserAccount userAccount = new()
|
||||
{
|
||||
UserAccountId = userAccountId,
|
||||
Username = username,
|
||||
FirstName = "Bertrand",
|
||||
LastName = "Russell",
|
||||
Email = "b.russell@example.co.uk",
|
||||
DateOfBirth = new DateTime(1872, 05, 18),
|
||||
};
|
||||
|
||||
_authRepoMock
|
||||
.Setup(x => x.GetUserByUsernameAsync(username))
|
||||
.ReturnsAsync(userAccount);
|
||||
|
||||
_authRepoMock
|
||||
.Setup(x =>
|
||||
x.GetActiveCredentialByUserAccountIdAsync(userAccountId)
|
||||
)
|
||||
.ReturnsAsync((UserCredential?)null);
|
||||
|
||||
// Act
|
||||
var act = async () =>
|
||||
await _loginService.LoginAsync(username, It.IsAny<string>());
|
||||
|
||||
// Assert
|
||||
await act.Should()
|
||||
.ThrowAsync<UnauthorizedException>()
|
||||
.WithMessage("Invalid username or password.");
|
||||
|
||||
_authRepoMock.Verify(
|
||||
x => x.GetUserByUsernameAsync(username),
|
||||
Times.Once
|
||||
);
|
||||
|
||||
_authRepoMock.Verify(
|
||||
x => x.GetActiveCredentialByUserAccountIdAsync(userAccountId),
|
||||
Times.Once
|
||||
);
|
||||
|
||||
_passwordInfraMock.Verify(
|
||||
x => x.Verify(It.IsAny<string>(), It.IsAny<string>()),
|
||||
Times.Never
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LoginAsync_WithIncorrectPassword_ThrowsUnauthorizedException()
|
||||
{
|
||||
// Arrange
|
||||
const string username = "RCarnap";
|
||||
var userAccountId = Guid.NewGuid();
|
||||
|
||||
UserAccount userAccount = new()
|
||||
{
|
||||
UserAccountId = userAccountId,
|
||||
Username = username,
|
||||
FirstName = "Rudolf",
|
||||
LastName = "Carnap",
|
||||
Email = "r.carnap@example.de",
|
||||
DateOfBirth = new DateTime(1891, 05, 18),
|
||||
};
|
||||
|
||||
UserCredential userCredential = new()
|
||||
{
|
||||
UserCredentialId = Guid.NewGuid(),
|
||||
UserAccountId = userAccountId,
|
||||
Hash = "hashed-password",
|
||||
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(false);
|
||||
|
||||
// Act
|
||||
var act = async () =>
|
||||
await _loginService.LoginAsync(username, It.IsAny<string>());
|
||||
|
||||
// Assert
|
||||
await act.Should()
|
||||
.ThrowAsync<UnauthorizedException>()
|
||||
.WithMessage("Invalid username or password.");
|
||||
|
||||
_authRepoMock.Verify(
|
||||
x => x.GetUserByUsernameAsync(username),
|
||||
Times.Once
|
||||
);
|
||||
|
||||
_authRepoMock.Verify(
|
||||
x => x.GetActiveCredentialByUserAccountIdAsync(userAccountId),
|
||||
Times.Once
|
||||
);
|
||||
|
||||
_passwordInfraMock.Verify(
|
||||
x => x.Verify(It.IsAny<string>(), It.IsAny<string>()),
|
||||
Times.Once
|
||||
);
|
||||
}
|
||||
}
|
||||
322
web/backend/Service/Service.Auth.Tests/RegisterService.test.cs
Normal file
322
web/backend/Service/Service.Auth.Tests/RegisterService.test.cs
Normal file
@@ -0,0 +1,322 @@
|
||||
using Domain.Entities;
|
||||
using Domain.Exceptions;
|
||||
using FluentAssertions;
|
||||
using Infrastructure.PasswordHashing;
|
||||
using Infrastructure.Repository.Auth;
|
||||
using Moq;
|
||||
using Service.Emails;
|
||||
|
||||
namespace Service.Auth.Tests;
|
||||
|
||||
public class RegisterServiceTest
|
||||
{
|
||||
private readonly Mock<IAuthRepository> _authRepoMock;
|
||||
private readonly Mock<IPasswordInfrastructure> _passwordInfraMock;
|
||||
private readonly Mock<ITokenService> _tokenServiceMock;
|
||||
private readonly Mock<IEmailService> _emailServiceMock; // todo handle email related test cases here
|
||||
private readonly RegisterService _registerService;
|
||||
|
||||
public RegisterServiceTest()
|
||||
{
|
||||
_authRepoMock = new Mock<IAuthRepository>();
|
||||
_passwordInfraMock = new Mock<IPasswordInfrastructure>();
|
||||
_tokenServiceMock = new Mock<ITokenService>();
|
||||
_emailServiceMock = new Mock<IEmailService>();
|
||||
|
||||
_registerService = new RegisterService(
|
||||
_authRepoMock.Object,
|
||||
_passwordInfraMock.Object,
|
||||
_tokenServiceMock.Object,
|
||||
_emailServiceMock.Object
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RegisterAsync_WithValidData_CreatesUserAndReturnsAuthServiceReturn()
|
||||
{
|
||||
// Arrange
|
||||
var userAccount = new UserAccount
|
||||
{
|
||||
Username = "newuser",
|
||||
FirstName = "John",
|
||||
LastName = "Doe",
|
||||
Email = "john.doe@example.com",
|
||||
DateOfBirth = new DateTime(1990, 1, 1),
|
||||
};
|
||||
|
||||
const string password = "SecurePassword123!";
|
||||
const string hashedPassword = "hashed_password_value";
|
||||
var expectedUserId = Guid.NewGuid();
|
||||
|
||||
// Mock: No existing user
|
||||
_authRepoMock
|
||||
.Setup(x => x.GetUserByUsernameAsync(userAccount.Username))
|
||||
.ReturnsAsync((UserAccount?)null);
|
||||
|
||||
_authRepoMock
|
||||
.Setup(x => x.GetUserByEmailAsync(userAccount.Email))
|
||||
.ReturnsAsync((UserAccount?)null);
|
||||
|
||||
// Mock: Password hashing
|
||||
_passwordInfraMock
|
||||
.Setup(x => x.Hash(password))
|
||||
.Returns(hashedPassword);
|
||||
|
||||
// Mock: User registration
|
||||
_authRepoMock
|
||||
.Setup(x =>
|
||||
x.RegisterUserAsync(
|
||||
userAccount.Username,
|
||||
userAccount.FirstName,
|
||||
userAccount.LastName,
|
||||
userAccount.Email,
|
||||
userAccount.DateOfBirth,
|
||||
hashedPassword
|
||||
)
|
||||
)
|
||||
.ReturnsAsync(
|
||||
new UserAccount
|
||||
{
|
||||
UserAccountId = expectedUserId,
|
||||
Username = userAccount.Username,
|
||||
FirstName = userAccount.FirstName,
|
||||
LastName = userAccount.LastName,
|
||||
Email = userAccount.Email,
|
||||
DateOfBirth = userAccount.DateOfBirth,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
}
|
||||
);
|
||||
|
||||
// Mock: Token generation
|
||||
_tokenServiceMock
|
||||
.Setup(x => x.GenerateAccessToken(It.IsAny<UserAccount>()))
|
||||
.Returns("access-token");
|
||||
|
||||
_tokenServiceMock
|
||||
.Setup(x => x.GenerateRefreshToken(It.IsAny<UserAccount>()))
|
||||
.Returns("refresh-token");
|
||||
|
||||
// Act
|
||||
var result = await _registerService.RegisterAsync(
|
||||
userAccount,
|
||||
password
|
||||
);
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
result.UserAccount.UserAccountId.Should().Be(expectedUserId);
|
||||
result.UserAccount.Username.Should().Be(userAccount.Username);
|
||||
result.UserAccount.Email.Should().Be(userAccount.Email);
|
||||
result.AccessToken.Should().Be("access-token");
|
||||
result.RefreshToken.Should().Be("refresh-token");
|
||||
|
||||
// Verify all mocks were called as expected
|
||||
_authRepoMock.Verify(
|
||||
x => x.GetUserByUsernameAsync(userAccount.Username),
|
||||
Times.Once
|
||||
);
|
||||
_authRepoMock.Verify(
|
||||
x => x.GetUserByEmailAsync(userAccount.Email),
|
||||
Times.Once
|
||||
);
|
||||
_passwordInfraMock.Verify(x => x.Hash(password), Times.Once);
|
||||
_authRepoMock.Verify(
|
||||
x =>
|
||||
x.RegisterUserAsync(
|
||||
userAccount.Username,
|
||||
userAccount.FirstName,
|
||||
userAccount.LastName,
|
||||
userAccount.Email,
|
||||
userAccount.DateOfBirth,
|
||||
hashedPassword
|
||||
),
|
||||
Times.Once
|
||||
);
|
||||
_emailServiceMock.Verify(
|
||||
x =>
|
||||
x.SendRegistrationEmailAsync(
|
||||
It.IsAny<UserAccount>(),
|
||||
It.IsAny<string>()
|
||||
),
|
||||
Times.Once
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RegisterAsync_WithExistingUsername_ThrowsConflictException()
|
||||
{
|
||||
// Arrange
|
||||
var userAccount = new UserAccount
|
||||
{
|
||||
Username = "existinguser",
|
||||
FirstName = "Jane",
|
||||
LastName = "Smith",
|
||||
Email = "jane.smith@example.com",
|
||||
DateOfBirth = new DateTime(1995, 5, 15),
|
||||
};
|
||||
var password = "Password123!";
|
||||
|
||||
var existingUser = new UserAccount
|
||||
{
|
||||
UserAccountId = Guid.NewGuid(),
|
||||
Username = "existinguser",
|
||||
FirstName = "Existing",
|
||||
LastName = "User",
|
||||
Email = "existing@example.com",
|
||||
DateOfBirth = new DateTime(1990, 1, 1),
|
||||
};
|
||||
|
||||
_authRepoMock
|
||||
.Setup(x => x.GetUserByUsernameAsync(userAccount.Username))
|
||||
.ReturnsAsync(existingUser);
|
||||
|
||||
_authRepoMock
|
||||
.Setup(x => x.GetUserByEmailAsync(userAccount.Email))
|
||||
.ReturnsAsync((UserAccount?)null);
|
||||
|
||||
// Act
|
||||
var act = async () =>
|
||||
await _registerService.RegisterAsync(userAccount, password);
|
||||
|
||||
// Assert
|
||||
await act.Should()
|
||||
.ThrowAsync<ConflictException>()
|
||||
.WithMessage("Username or email already exists");
|
||||
|
||||
// Verify that registration was never called
|
||||
_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 RegisterAsync_WithExistingEmail_ThrowsConflictException()
|
||||
{
|
||||
// Arrange
|
||||
var userAccount = new UserAccount
|
||||
{
|
||||
Username = "newuser",
|
||||
FirstName = "Jane",
|
||||
LastName = "Smith",
|
||||
Email = "existing@example.com",
|
||||
DateOfBirth = new DateTime(1995, 5, 15),
|
||||
};
|
||||
var password = "Password123!";
|
||||
|
||||
var existingUser = new UserAccount
|
||||
{
|
||||
UserAccountId = Guid.NewGuid(),
|
||||
Username = "otheruser",
|
||||
FirstName = "Existing",
|
||||
LastName = "User",
|
||||
Email = "existing@example.com",
|
||||
DateOfBirth = new DateTime(1990, 1, 1),
|
||||
};
|
||||
|
||||
_authRepoMock
|
||||
.Setup(x => x.GetUserByUsernameAsync(userAccount.Username))
|
||||
.ReturnsAsync((UserAccount?)null);
|
||||
|
||||
_authRepoMock
|
||||
.Setup(x => x.GetUserByEmailAsync(userAccount.Email))
|
||||
.ReturnsAsync(existingUser);
|
||||
|
||||
// Act
|
||||
var act = async () =>
|
||||
await _registerService.RegisterAsync(userAccount, password);
|
||||
|
||||
// Assert
|
||||
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 RegisterAsync_PasswordIsHashed_BeforeStoringInDatabase()
|
||||
{
|
||||
// Arrange
|
||||
var userAccount = new UserAccount
|
||||
{
|
||||
Username = "secureuser",
|
||||
FirstName = "Secure",
|
||||
LastName = "User",
|
||||
Email = "secure@example.com",
|
||||
DateOfBirth = new DateTime(1990, 1, 1),
|
||||
};
|
||||
var plainPassword = "PlainPassword123!";
|
||||
var 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(plainPassword))
|
||||
.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");
|
||||
|
||||
// Act
|
||||
await _registerService.RegisterAsync(userAccount, plainPassword);
|
||||
|
||||
// Assert
|
||||
_passwordInfraMock.Verify(x => x.Hash(plainPassword), Times.Once);
|
||||
_authRepoMock.Verify(
|
||||
x =>
|
||||
x.RegisterUserAsync(
|
||||
userAccount.Username,
|
||||
userAccount.FirstName,
|
||||
userAccount.LastName,
|
||||
userAccount.Email,
|
||||
userAccount.DateOfBirth,
|
||||
hashedPassword
|
||||
), // Verify hashed password is used
|
||||
Times.Once
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
<RootNamespace>Service.Auth.Tests</RootNamespace>
|
||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||
</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" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Infrastructure\Infrastructure.PasswordHashing\Infrastructure.PasswordHashing.csproj" />
|
||||
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Repository\Infrastructure.Repository.csproj" />
|
||||
<ProjectReference Include="..\Service.Auth\Service.Auth.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,162 @@
|
||||
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 TokenServiceRefreshTest
|
||||
{
|
||||
private readonly Mock<ITokenInfrastructure> _tokenInfraMock;
|
||||
private readonly Mock<IAuthRepository> _authRepositoryMock;
|
||||
private readonly TokenService _tokenService;
|
||||
|
||||
public TokenServiceRefreshTest()
|
||||
{
|
||||
_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,282 @@
|
||||
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<ITokenInfrastructure> _tokenInfraMock;
|
||||
private readonly Mock<IAuthRepository> _authRepositoryMock;
|
||||
private readonly TokenService _tokenService;
|
||||
|
||||
public TokenServiceValidationTest()
|
||||
{
|
||||
_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