mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-02-16 02:39:03 +00:00
Adding service layer testing (#151)
This commit is contained in:
@@ -3,7 +3,7 @@ using API.Core.Contracts.Common;
|
||||
using Domain.Entities;
|
||||
using Infrastructure.Jwt;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Service.Auth.Auth;
|
||||
using Service.Auth;
|
||||
|
||||
namespace API.Core.Controllers
|
||||
{
|
||||
|
||||
@@ -9,12 +9,12 @@ using Infrastructure.Repository.Sql;
|
||||
using Infrastructure.Repository.UserAccount;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Service.Auth.Auth;
|
||||
using Service.UserManagement.User;
|
||||
using API.Core.Contracts.Common;
|
||||
using Infrastructure.Email;
|
||||
using Infrastructure.Email.Templates;
|
||||
using Infrastructure.Email.Templates.Rendering;
|
||||
using Service.Auth;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
|
||||
@@ -20,6 +20,6 @@ FROM build AS final
|
||||
ARG BUILD_CONFIGURATION=Release
|
||||
WORKDIR /src
|
||||
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
|
||||
RUN mkdir -p /app/test-results
|
||||
RUN mkdir -p /app/test-results/api-specs
|
||||
WORKDIR /src/API/API.Specs
|
||||
ENTRYPOINT ["dotnet", "test", "API.Specs.csproj", "-c", "Release", "--logger", "trx;LogFileName=/app/test-results/test-results.trx"]
|
||||
ENTRYPOINT ["dotnet", "test", "API.Specs.csproj", "-c", "Release", "--logger", "trx;LogFileName=/app/test-results/api-specs/results.trx"]
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
<Project Path="Infrastructure\Infrastructure.Repository.Tests\Infrastructure.Repository.Tests.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Service/">
|
||||
<Project Path="Service/Service.Auth.Tests/Service.Auth.Tests.csproj" />
|
||||
<Project Path="Service/Service.UserManagement/Service.UserManagement.csproj" />
|
||||
<Project Path="Service\Service.Auth\Service.Auth.csproj" />
|
||||
</Folder>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
Include="Konscious.Security.Cryptography.Argon2"
|
||||
Version="1.3.1"
|
||||
/>
|
||||
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.1.3" />
|
||||
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.1.4" />
|
||||
<PackageReference Include="dbup" Version="5.0.41" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -11,6 +11,6 @@ WORKDIR "/src/Infrastructure/Infrastructure.Repository.Tests"
|
||||
RUN dotnet build "./Infrastructure.Repository.Tests.csproj" -c $BUILD_CONFIGURATION -o /app/build
|
||||
|
||||
FROM build AS final
|
||||
RUN mkdir -p /app/test-results
|
||||
RUN mkdir -p /app/test-results/repository-tests
|
||||
WORKDIR /src/Infrastructure/Infrastructure.Repository.Tests
|
||||
ENTRYPOINT ["dotnet", "test", "./Infrastructure.Repository.Tests.csproj", "-c", "Release", "--logger", "trx;LogFileName=/app/test-results/repository-tests.trx"]
|
||||
ENTRYPOINT ["dotnet", "test", "./Infrastructure.Repository.Tests.csproj", "-c", "Release", "--logger", "trx;LogFileName=/app/test-results/repository-tests/results.trx"]
|
||||
|
||||
@@ -6,28 +6,14 @@
|
||||
<IsPackable>false</IsPackable>
|
||||
<RootNamespace>Infrastructure.Repository.Tests</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.2" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
|
||||
<PackageReference Include="Moq" Version="4.20.72" />
|
||||
<PackageReference Include="xunit" Version="2.9.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
|
||||
<PackageReference Include="FluentAssertions" Version="6.9.0" />
|
||||
<PackageReference Include="DbMocker" Version="1.26.0" />
|
||||
<PackageReference
|
||||
Include="Microsoft.Extensions.Configuration"
|
||||
Version="9.0.0"
|
||||
/>
|
||||
<PackageReference
|
||||
Include="Microsoft.Extensions.Configuration.Abstractions"
|
||||
Version="9.0.0"
|
||||
/>
|
||||
<PackageReference
|
||||
Include="Microsoft.Extensions.Configuration.Binder"
|
||||
Version="9.0.0"
|
||||
/>
|
||||
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.1.3" />
|
||||
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.1.4" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -88,9 +88,7 @@ public class AuthRepository(ISqlConnectionFactory connectionFactory)
|
||||
AddParameter(command, "@UserAccountId", userAccountId);
|
||||
|
||||
await using var reader = await command.ExecuteReaderAsync();
|
||||
return await reader.ReadAsync()
|
||||
? MapToCredentialEntity(reader)
|
||||
: null;
|
||||
return await reader.ReadAsync() ? MapToCredentialEntity(reader) : null;
|
||||
}
|
||||
|
||||
public async Task RotateCredentialAsync(
|
||||
@@ -118,9 +116,7 @@ public class AuthRepository(ISqlConnectionFactory connectionFactory)
|
||||
{
|
||||
return new Domain.Entities.UserAccount
|
||||
{
|
||||
UserAccountId = reader.GetGuid(
|
||||
reader.GetOrdinal("UserAccountId")
|
||||
),
|
||||
UserAccountId = reader.GetGuid(reader.GetOrdinal("UserAccountId")),
|
||||
Username = reader.GetString(reader.GetOrdinal("Username")),
|
||||
FirstName = reader.GetString(reader.GetOrdinal("FirstName")),
|
||||
LastName = reader.GetString(reader.GetOrdinal("LastName")),
|
||||
@@ -129,9 +125,7 @@ public class AuthRepository(ISqlConnectionFactory connectionFactory)
|
||||
UpdatedAt = reader.IsDBNull(reader.GetOrdinal("UpdatedAt"))
|
||||
? null
|
||||
: reader.GetDateTime(reader.GetOrdinal("UpdatedAt")),
|
||||
DateOfBirth = reader.GetDateTime(
|
||||
reader.GetOrdinal("DateOfBirth")
|
||||
),
|
||||
DateOfBirth = reader.GetDateTime(reader.GetOrdinal("DateOfBirth")),
|
||||
Timer = reader.IsDBNull(reader.GetOrdinal("Timer"))
|
||||
? null
|
||||
: (byte[])reader["Timer"],
|
||||
@@ -148,9 +142,7 @@ public class AuthRepository(ISqlConnectionFactory connectionFactory)
|
||||
UserCredentialId = reader.GetGuid(
|
||||
reader.GetOrdinal("UserCredentialId")
|
||||
),
|
||||
UserAccountId = reader.GetGuid(
|
||||
reader.GetOrdinal("UserAccountId")
|
||||
),
|
||||
UserAccountId = reader.GetGuid(reader.GetOrdinal("UserAccountId")),
|
||||
Hash = reader.GetString(reader.GetOrdinal("Hash")),
|
||||
CreatedAt = reader.GetDateTime(reader.GetOrdinal("CreatedAt")),
|
||||
};
|
||||
|
||||
@@ -33,9 +33,7 @@ public interface IAuthRepository
|
||||
/// </summary>
|
||||
/// <param name="email">Email address to search for</param>
|
||||
/// <returns>UserAccount if found, null otherwise</returns>
|
||||
Task<Domain.Entities.UserAccount?> GetUserByEmailAsync(
|
||||
string email
|
||||
);
|
||||
Task<Domain.Entities.UserAccount?> GetUserByEmailAsync(string email);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a user account by username (typically used for login).
|
||||
@@ -43,9 +41,7 @@ public interface IAuthRepository
|
||||
/// </summary>
|
||||
/// <param name="username">Username to search for</param>
|
||||
/// <returns>UserAccount if found, null otherwise</returns>
|
||||
Task<Domain.Entities.UserAccount?> GetUserByUsernameAsync(
|
||||
string username
|
||||
);
|
||||
Task<Domain.Entities.UserAccount?> GetUserByUsernameAsync(string username);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the active (non-revoked) credential for a user account.
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<RootNamespace>Infrastructure.Repository</RootNamespace>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.1.3" />
|
||||
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.1.4" />
|
||||
<PackageReference
|
||||
Include="Microsoft.SqlServer.Types"
|
||||
Version="160.1000.6"
|
||||
@@ -14,7 +14,7 @@
|
||||
<PackageReference Include="System.Data.SqlClient" Version="4.9.0" />
|
||||
<PackageReference
|
||||
Include="Microsoft.Extensions.Configuration.Abstractions"
|
||||
Version="8.0.0"
|
||||
Version="9.0.0"
|
||||
/>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
||||
@@ -9,8 +9,6 @@ public interface IUserAccountRepository
|
||||
);
|
||||
Task UpdateAsync(Domain.Entities.UserAccount userAccount);
|
||||
Task DeleteAsync(Guid id);
|
||||
Task<Domain.Entities.UserAccount?> GetByUsernameAsync(
|
||||
string username
|
||||
);
|
||||
Task<Domain.Entities.UserAccount?> GetByUsernameAsync(string username);
|
||||
Task<Domain.Entities.UserAccount?> GetByEmailAsync(string email);
|
||||
}
|
||||
@@ -8,9 +8,7 @@ public class UserAccountRepository(ISqlConnectionFactory connectionFactory)
|
||||
: Repository<Domain.Entities.UserAccount>(connectionFactory),
|
||||
IUserAccountRepository
|
||||
{
|
||||
public async Task<Domain.Entities.UserAccount?> GetByIdAsync(
|
||||
Guid id
|
||||
)
|
||||
public async Task<Domain.Entities.UserAccount?> GetByIdAsync(Guid id)
|
||||
{
|
||||
await using var connection = await CreateConnection();
|
||||
await using var command = connection.CreateCommand();
|
||||
@@ -23,9 +21,10 @@ public class UserAccountRepository(ISqlConnectionFactory connectionFactory)
|
||||
return await reader.ReadAsync() ? MapToEntity(reader) : null;
|
||||
}
|
||||
|
||||
public async Task<
|
||||
IEnumerable<Domain.Entities.UserAccount>
|
||||
> GetAllAsync(int? limit, int? offset)
|
||||
public async Task<IEnumerable<Domain.Entities.UserAccount>> GetAllAsync(
|
||||
int? limit,
|
||||
int? offset
|
||||
)
|
||||
{
|
||||
await using var connection = await CreateConnection();
|
||||
await using var command = connection.CreateCommand();
|
||||
@@ -49,9 +48,7 @@ public class UserAccountRepository(ISqlConnectionFactory connectionFactory)
|
||||
return users;
|
||||
}
|
||||
|
||||
public async Task UpdateAsync(
|
||||
Domain.Entities.UserAccount userAccount
|
||||
)
|
||||
public async Task UpdateAsync(Domain.Entities.UserAccount userAccount)
|
||||
{
|
||||
await using var connection = await CreateConnection();
|
||||
await using var command = connection.CreateCommand();
|
||||
@@ -115,9 +112,7 @@ public class UserAccountRepository(ISqlConnectionFactory connectionFactory)
|
||||
{
|
||||
return new Domain.Entities.UserAccount
|
||||
{
|
||||
UserAccountId = reader.GetGuid(
|
||||
reader.GetOrdinal("UserAccountId")
|
||||
),
|
||||
UserAccountId = reader.GetGuid(reader.GetOrdinal("UserAccountId")),
|
||||
Username = reader.GetString(reader.GetOrdinal("Username")),
|
||||
FirstName = reader.GetString(reader.GetOrdinal("FirstName")),
|
||||
LastName = reader.GetString(reader.GetOrdinal("LastName")),
|
||||
@@ -126,9 +121,7 @@ public class UserAccountRepository(ISqlConnectionFactory connectionFactory)
|
||||
UpdatedAt = reader.IsDBNull(reader.GetOrdinal("UpdatedAt"))
|
||||
? null
|
||||
: reader.GetDateTime(reader.GetOrdinal("UpdatedAt")),
|
||||
DateOfBirth = reader.GetDateTime(
|
||||
reader.GetOrdinal("DateOfBirth")
|
||||
),
|
||||
DateOfBirth = reader.GetDateTime(reader.GetOrdinal("DateOfBirth")),
|
||||
Timer = reader.IsDBNull(reader.GetOrdinal("Timer"))
|
||||
? null
|
||||
: (byte[])reader["Timer"],
|
||||
|
||||
238
src/Core/Service/Service.Auth.Tests/LoginService.test.cs
Normal file
238
src/Core/Service/Service.Auth.Tests/LoginService.test.cs
Normal file
@@ -0,0 +1,238 @@
|
||||
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 LoginService _loginService;
|
||||
|
||||
public LoginServiceTest()
|
||||
{
|
||||
_authRepoMock = new Mock<IAuthRepository>();
|
||||
_passwordInfraMock = new Mock<IPasswordInfrastructure>();
|
||||
_loginService = new LoginService(
|
||||
_authRepoMock.Object,
|
||||
_passwordInfraMock.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);
|
||||
|
||||
// Act
|
||||
var result = await _loginService.LoginAsync(username, It.IsAny<string>());
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
result.UserAccountId.Should().Be(userAccountId);
|
||||
result.Username.Should().Be(username);
|
||||
|
||||
_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
|
||||
);
|
||||
}
|
||||
}
|
||||
501
src/Core/Service/Service.Auth.Tests/RegisterService.test.cs
Normal file
501
src/Core/Service/Service.Auth.Tests/RegisterService.test.cs
Normal file
@@ -0,0 +1,501 @@
|
||||
using Domain.Entities;
|
||||
using Domain.Exceptions;
|
||||
using FluentAssertions;
|
||||
using Infrastructure.Email;
|
||||
using Infrastructure.Email.Templates.Rendering;
|
||||
using Infrastructure.PasswordHashing;
|
||||
using Infrastructure.Repository.Auth;
|
||||
using Moq;
|
||||
|
||||
namespace Service.Auth.Tests;
|
||||
|
||||
public class RegisterServiceTest
|
||||
{
|
||||
private readonly Mock<IAuthRepository> _authRepoMock;
|
||||
private readonly Mock<IPasswordInfrastructure> _passwordInfraMock;
|
||||
private readonly Mock<IEmailProvider> _emailProviderMock;
|
||||
private readonly Mock<IEmailTemplateProvider> _emailTemplateProviderMock;
|
||||
private readonly RegisterService _registerService;
|
||||
|
||||
public RegisterServiceTest()
|
||||
{
|
||||
_authRepoMock = new Mock<IAuthRepository>();
|
||||
_passwordInfraMock = new Mock<IPasswordInfrastructure>();
|
||||
_emailProviderMock = new Mock<IEmailProvider>();
|
||||
_emailTemplateProviderMock = new Mock<IEmailTemplateProvider>();
|
||||
|
||||
_registerService = new RegisterService(
|
||||
_authRepoMock.Object,
|
||||
_passwordInfraMock.Object,
|
||||
_emailProviderMock.Object,
|
||||
_emailTemplateProviderMock.Object
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RegisterAsync_WithValidData_CreatesUserAndSendsEmail()
|
||||
{
|
||||
// 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();
|
||||
const string expectedEmailHtml = "<html><body>Welcome!</body></html>";
|
||||
|
||||
// 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: Email template rendering
|
||||
_emailTemplateProviderMock
|
||||
.Setup(x =>
|
||||
x.RenderUserRegisteredEmailAsync(
|
||||
userAccount.FirstName,
|
||||
It.IsAny<string>()
|
||||
)
|
||||
)
|
||||
.ReturnsAsync(expectedEmailHtml);
|
||||
|
||||
// Mock: Email sending
|
||||
_emailProviderMock
|
||||
.Setup(x =>
|
||||
x.SendAsync(
|
||||
userAccount.Email,
|
||||
"Welcome to The Biergarten App!",
|
||||
expectedEmailHtml,
|
||||
true
|
||||
)
|
||||
)
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
// Act
|
||||
var result = await _registerService.RegisterAsync(userAccount, password);
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
result.UserAccountId.Should().Be(expectedUserId);
|
||||
result.Username.Should().Be(userAccount.Username);
|
||||
result.Email.Should().Be(userAccount.Email);
|
||||
|
||||
// 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
|
||||
);
|
||||
_emailTemplateProviderMock.Verify(
|
||||
x =>
|
||||
x.RenderUserRegisteredEmailAsync(
|
||||
userAccount.FirstName,
|
||||
It.IsAny<string>()
|
||||
),
|
||||
Times.Once
|
||||
);
|
||||
_emailProviderMock.Verify(
|
||||
x =>
|
||||
x.SendAsync(
|
||||
userAccount.Email,
|
||||
"Welcome to The Biergarten App!",
|
||||
expectedEmailHtml,
|
||||
true
|
||||
),
|
||||
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
|
||||
);
|
||||
|
||||
// Verify email was never sent
|
||||
_emailProviderMock.Verify(
|
||||
x =>
|
||||
x.SendAsync(
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<bool>()
|
||||
),
|
||||
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() });
|
||||
|
||||
_emailTemplateProviderMock
|
||||
.Setup(x =>
|
||||
x.RenderUserRegisteredEmailAsync(
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<string>()
|
||||
)
|
||||
)
|
||||
.ReturnsAsync("<html></html>");
|
||||
|
||||
// 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
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RegisterAsync_EmailConfirmationLink_ContainsUserEmail()
|
||||
{
|
||||
// Arrange
|
||||
var userAccount = new UserAccount
|
||||
{
|
||||
Username = "testuser",
|
||||
FirstName = "Test",
|
||||
LastName = "User",
|
||||
Email = "test@example.com",
|
||||
DateOfBirth = new DateTime(1990, 1, 1),
|
||||
};
|
||||
var password = "Password123!";
|
||||
string? capturedConfirmationLink = null;
|
||||
|
||||
_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 = userAccount.Username,
|
||||
FirstName = userAccount.FirstName,
|
||||
LastName = userAccount.LastName,
|
||||
Email = userAccount.Email,
|
||||
DateOfBirth = userAccount.DateOfBirth,
|
||||
}
|
||||
);
|
||||
|
||||
_emailTemplateProviderMock
|
||||
.Setup(x =>
|
||||
x.RenderUserRegisteredEmailAsync(
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<string>()
|
||||
)
|
||||
)
|
||||
.Callback<string, string>(
|
||||
(_, link) => capturedConfirmationLink = link
|
||||
)
|
||||
.ReturnsAsync("<html></html>");
|
||||
|
||||
// Act
|
||||
await _registerService.RegisterAsync(userAccount, password);
|
||||
|
||||
// Assert
|
||||
capturedConfirmationLink.Should().NotBeNull();
|
||||
capturedConfirmationLink
|
||||
.Should()
|
||||
.Contain(Uri.EscapeDataString(userAccount.Email));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RegisterAsync_WhenEmailSendingFails_ExceptionPropagates()
|
||||
{
|
||||
// Arrange
|
||||
var userAccount = new UserAccount
|
||||
{
|
||||
Username = "testuser",
|
||||
FirstName = "Test",
|
||||
LastName = "User",
|
||||
Email = "test@example.com",
|
||||
DateOfBirth = new DateTime(1990, 1, 1),
|
||||
};
|
||||
var password = "Password123!";
|
||||
|
||||
_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(),
|
||||
Email = userAccount.Email,
|
||||
}
|
||||
);
|
||||
|
||||
_emailTemplateProviderMock
|
||||
.Setup(x =>
|
||||
x.RenderUserRegisteredEmailAsync(
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<string>()
|
||||
)
|
||||
)
|
||||
.ReturnsAsync("<html></html>");
|
||||
|
||||
_emailProviderMock
|
||||
.Setup(x =>
|
||||
x.SendAsync(
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<bool>()
|
||||
)
|
||||
)
|
||||
.ThrowsAsync(
|
||||
new InvalidOperationException("SMTP server unavailable")
|
||||
);
|
||||
|
||||
// Act
|
||||
var act = async () => await _registerService.RegisterAsync(userAccount, password);
|
||||
|
||||
// Assert
|
||||
await act.Should()
|
||||
.ThrowAsync<InvalidOperationException>()
|
||||
.WithMessage("SMTP server unavailable");
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -1,7 +1,6 @@
|
||||
using System.Threading.Tasks;
|
||||
using Domain.Entities;
|
||||
|
||||
namespace Service.Auth.Auth;
|
||||
namespace Service.Auth;
|
||||
|
||||
public interface ILoginService
|
||||
{
|
||||
@@ -1,7 +1,6 @@
|
||||
using System.Threading.Tasks;
|
||||
using Domain.Entities;
|
||||
|
||||
namespace Service.Auth.Auth;
|
||||
namespace Service.Auth;
|
||||
|
||||
public interface IRegisterService
|
||||
{
|
||||
@@ -1,10 +1,9 @@
|
||||
using System.Threading.Tasks;
|
||||
using Domain.Entities;
|
||||
using Domain.Exceptions;
|
||||
using Infrastructure.PasswordHashing;
|
||||
using Infrastructure.Repository.Auth;
|
||||
|
||||
namespace Service.Auth.Auth;
|
||||
namespace Service.Auth;
|
||||
|
||||
public class LoginService(
|
||||
IAuthRepository authRepo,
|
||||
@@ -1,13 +1,11 @@
|
||||
using System.Threading.Tasks;
|
||||
using Domain.Entities;
|
||||
using Domain.Exceptions;
|
||||
using Infrastructure.Email;
|
||||
using Infrastructure.Email.Templates;
|
||||
using Infrastructure.Email.Templates.Rendering;
|
||||
using Infrastructure.PasswordHashing;
|
||||
using Infrastructure.Repository.Auth;
|
||||
|
||||
namespace Service.Auth.Auth;
|
||||
namespace Service.Auth;
|
||||
|
||||
public class RegisterService(
|
||||
IAuthRepository authRepo,
|
||||
Reference in New Issue
Block a user