Remove repository tests, usages of registration dto used in repo

This commit is contained in:
Aaron Po
2026-08-20 20:55:47 -04:00
parent 1855041910
commit e60683a415
29 changed files with 341 additions and 1351 deletions

View File

@@ -1,10 +1,10 @@
using Features.Auth.Dtos;
using MediatR;
namespace Features.Auth.Queries.Login;
namespace Features.Auth.Commands.Login;
/// <summary>
/// Authenticates a user using their username and password and issues new tokens. Bound directly
/// from the request body of <c>POST /api/auth/login</c>.
/// </summary>
public record LoginQuery(string Username, string Password) : IRequest<LoginPayload>;
public record LoginCommand(string Username, string Password) : IRequest<LoginPayload>;

View File

@@ -6,22 +6,22 @@ using Features.Auth.Services;
using Infrastructure.PasswordHashing;
using MediatR;
namespace Features.Auth.Queries.Login;
namespace Features.Auth.Commands.Login;
/// <summary>
/// Handles <see cref="LoginQuery" /> by verifying credentials and issuing access/refresh tokens.
/// Handles <see cref="LoginCommand" /> by verifying credentials and issuing access/refresh tokens.
/// </summary>
public class LoginHandler(
IAuthRepository authRepo,
IPasswordInfrastructure passwordInfrastructure,
ITokenService tokenService
) : IRequestHandler<LoginQuery, LoginPayload>
) : IRequestHandler<LoginCommand, LoginPayload>
{
/// <exception cref="UnauthorizedException">
/// Thrown when the username does not match any account, the account has no active credential,
/// or the supplied password does not match the stored hash.
/// </exception>
public async Task<LoginPayload> Handle(LoginQuery request, CancellationToken cancellationToken)
public async Task<LoginPayload> Handle(LoginCommand request, CancellationToken cancellationToken)
{
UserAccount user =
await authRepo.GetUserByUsernameAsync(request.Username)

View File

@@ -1,9 +1,9 @@
using FluentValidation;
namespace Features.Auth.Queries.Login;
namespace Features.Auth.Commands.Login;
/// <summary>Validates that <see cref="LoginQuery.Username" /> and <see cref="LoginQuery.Password" /> are present.</summary>
public class LoginValidator : AbstractValidator<LoginQuery>
/// <summary>Validates that <see cref="LoginCommand.Username" /> and <see cref="LoginCommand.Password" /> are present.</summary>
public class LoginValidator : AbstractValidator<LoginCommand>
{
public LoginValidator()
{

View File

@@ -34,14 +34,18 @@ public class RegisterUserHandler(
string hashed = passwordInfrastructure.Hash(request.Password);
UserAccount createdUser = await authRepo.RegisterUserAsync(
new UserRegistrationDto(
request.Username,
request.FirstName,
request.LastName,
request.Email,
request.DateOfBirth,
hashed
)
new UserAccount
{
Username = request.Username,
FirstName = request.FirstName,
LastName = request.LastName,
DateOfBirth = request.DateOfBirth,
Email = request.Email,
UserCredential = new UserCredential
{
Hash = hashed,
}
}
);
string accessToken = tokenService.GenerateAccessToken(createdUser);

View File

@@ -1,9 +1,9 @@
using Features.Auth.Commands.ConfirmUser;
using Features.Auth.Commands.Login;
using Features.Auth.Commands.RefreshToken;
using Features.Auth.Commands.RegisterUser;
using Features.Auth.Commands.ResendConfirmationEmail;
using Features.Auth.Dtos;
using Features.Auth.Queries.Login;
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
@@ -51,9 +51,9 @@ public class AuthController(IMediator mediator) : ControllerBase
/// </remarks>
[AllowAnonymous]
[HttpPost("login")]
public async Task<ActionResult<ResponseBody<LoginPayload>>> Login([FromBody] LoginQuery query)
public async Task<ActionResult<ResponseBody<LoginPayload>>> Login([FromBody] LoginCommand command)
{
LoginPayload payload = await mediator.Send(query);
LoginPayload payload = await mediator.Send(command);
return Ok(
new ResponseBody<LoginPayload>
{

View File

@@ -1,12 +0,0 @@
namespace Features.Auth.Dtos;
/// <summary>Data required to persist a new user account and its initial credential.</summary>
/// <param name="PasswordHash">The already-hashed password; never a plaintext value.</param>
public record UserRegistrationDto(
string Username,
string FirstName,
string LastName,
string Email,
DateTime DateOfBirth,
string PasswordHash
);

View File

@@ -20,4 +20,8 @@
<ProjectReference Include="..\..\Shared\Shared.Contracts\Shared.Contracts.csproj" />
<ProjectReference Include="..\..\Shared\Shared.Application\Shared.Application.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Queries\" />
</ItemGroup>
</Project>

View File

@@ -16,17 +16,14 @@ public class AuthRepository(ISqlConnectionFactory connectionFactory)
: Repository<UserAccount>(connectionFactory),
IAuthRepository
{
private const string SelectColumns =
"UserAccountID, Username, FirstName, LastName, Email, CreatedAt, UpdatedAt, DateOfBirth, RowVersion";
/// <inheritdoc />
/// <remarks>The account and credential are inserted within a single database transaction.</remarks>
public async Task<UserAccount> RegisterUserAsync(UserRegistrationDto userRegistrationDto)
public async Task<UserAccount> RegisterUserAsync(UserAccount ua)
{
await using DbConnection connection = await CreateConnection();
await using DbTransaction transaction = await connection.BeginTransactionAsync();
var (username, firstName, lastName, email, dateOfBirth, passwordHash) = userRegistrationDto;
Guid userAccountId;
try
@@ -38,15 +35,18 @@ public class AuthRepository(ISqlConnectionFactory connectionFactory)
OUTPUT INSERTED.UserAccountID
VALUES (@Username, @FirstName, @LastName, @DateOfBirth, @Email);
""",
new { username, firstName, lastName, dateOfBirth, email },
new { ua.Username, ua.FirstName, ua.LastName, ua.DateOfBirth, ua.Email},
transaction
)
);
int credentialRows = await connection.ExecuteAsync(
new CommandDefinition(
"INSERT INTO dbo.UserCredential (UserAccountId, Hash) VALUES (@UserAccountId, @Hash);",
new { UserAccountId = userAccountId, Hash = passwordHash },
"""
INSERT INTO dbo.UserCredential (UserAccountId, Hash)
VALUES (@UserAccountId, @Hash);
""",
new { UserAccountId = userAccountId, Hash = ua.UserCredential?.Hash },
transaction
)
);
@@ -62,6 +62,7 @@ public class AuthRepository(ISqlConnectionFactory connectionFactory)
throw;
}
// perform round trip to get new User entity
return await GetUserByIdAsync(userAccountId)
?? throw new Exception("Failed to retrieve newly registered user.");
}
@@ -71,7 +72,11 @@ public class AuthRepository(ISqlConnectionFactory connectionFactory)
{
await using DbConnection connection = await CreateConnection();
return await connection.QueryFirstOrDefaultAsync<UserAccount>(
$"SELECT {SelectColumns} FROM dbo.UserAccount WHERE Email = @Email",
"""
SELECT UserAccountID, Username, FirstName, LastName, Email, CreatedAt, UpdatedAt, DateOfBirth, RowVersion
FROM dbo.UserAccount
WHERE Email = @Email
""",
new { Email = email }
);
}
@@ -81,7 +86,11 @@ public class AuthRepository(ISqlConnectionFactory connectionFactory)
{
await using DbConnection connection = await CreateConnection();
return await connection.QueryFirstOrDefaultAsync<UserAccount>(
$"SELECT {SelectColumns} FROM dbo.UserAccount WHERE Username = @Username",
"""
SELECT UserAccountID, Username, FirstName, LastName, Email, CreatedAt, UpdatedAt, DateOfBirth, RowVersion
FROM dbo.UserAccount
WHERE Username = @Username
""",
new { Username = username }
);
}
@@ -111,7 +120,11 @@ public class AuthRepository(ISqlConnectionFactory connectionFactory)
bool exists =
await connection.ExecuteScalarAsync<int?>(
new CommandDefinition(
"SELECT 1 FROM dbo.UserAccount WHERE UserAccountID = @UserAccountId",
"""
SELECT 1
FROM dbo.UserAccount
WHERE UserAccountID = @UserAccountId
""",
new { UserAccountId = userAccountId },
transaction
)
@@ -122,7 +135,11 @@ public class AuthRepository(ISqlConnectionFactory connectionFactory)
await connection.ExecuteAsync(
new CommandDefinition(
"UPDATE dbo.UserCredential SET IsRevoked = 1, RevokedAt = GETDATE() WHERE UserAccountId = @UserAccountId",
"""
UPDATE dbo.UserCredential
SET IsRevoked = 1, RevokedAt = GETDATE()
WHERE UserAccountId = @UserAccountId
""",
new { UserAccountId = userAccountId },
transaction
)
@@ -130,7 +147,10 @@ public class AuthRepository(ISqlConnectionFactory connectionFactory)
await connection.ExecuteAsync(
new CommandDefinition(
"INSERT INTO dbo.UserCredential (UserAccountId, Hash) VALUES (@UserAccountId, @Hash)",
"""
INSERT INTO dbo.UserCredential (UserAccountId, Hash)
VALUES (@UserAccountId, @Hash)
""",
new { UserAccountId = userAccountId, Hash = newPasswordHash },
transaction
)
@@ -150,7 +170,11 @@ public class AuthRepository(ISqlConnectionFactory connectionFactory)
{
await using DbConnection connection = await CreateConnection();
return await connection.QueryFirstOrDefaultAsync<UserAccount>(
$"SELECT {SelectColumns} FROM dbo.UserAccount WHERE UserAccountID = @UserAccountId",
"""
SELECT UserAccountID, Username, FirstName, LastName, Email, CreatedAt, UpdatedAt, DateOfBirth, RowVersion
FROM dbo.UserAccount
WHERE UserAccountID = @UserAccountId
""",
new { UserAccountId = userAccountId }
);
}
@@ -179,7 +203,10 @@ public class AuthRepository(ISqlConnectionFactory connectionFactory)
try
{
await connection.ExecuteAsync(
"INSERT INTO dbo.UserVerification (UserAccountId, VerificationDateTime) VALUES (@UserAccountId, GETDATE())",
"""
INSERT INTO dbo.UserVerification (UserAccountId, VerificationDateTime)
VALUES (@UserAccountId, GETDATE())
""",
new { UserAccountId = userAccountId }
);
}
@@ -197,7 +224,11 @@ public class AuthRepository(ISqlConnectionFactory connectionFactory)
{
await using DbConnection connection = await CreateConnection();
int? result = await connection.ExecuteScalarAsync<int?>(
"SELECT TOP 1 1 FROM dbo.UserVerification WHERE UserAccountID = @UserAccountId",
"""
SELECT TOP 1 1
FROM dbo.UserVerification
WHERE UserAccountID = @UserAccountId
""",
new { UserAccountId = userAccountId }
);
return result.HasValue;

View File

@@ -13,7 +13,7 @@ public interface IAuthRepository
/// returns the created account.
/// </summary>
/// <exception cref="Exception">Thrown when the newly registered user cannot be retrieved after registration.</exception>
Task<UserAccount> RegisterUserAsync(UserRegistrationDto userRegistrationDto);
Task<UserAccount> RegisterUserAsync(UserAccount ua);
/// <summary>Retrieves a user account by email, or <see langword="null" /> if none exists.</summary>
Task<UserAccount?> GetUserByEmailAsync(string email);