Migrate handlers to UserManager<>

This commit is contained in:
Aaron Po
2026-08-22 01:31:49 -04:00
parent 6b8e8d6208
commit ceadf125d8
9 changed files with 205 additions and 299 deletions

View File

@@ -1,27 +1,34 @@
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using Domain.Entities;
using Domain.Exceptions;
using Features.Auth.Commands.ConfirmUser;
using Features.Auth.Dtos;
using Features.Auth.Repository;
using Features.Auth.Identity;
using Features.Auth.Services;
using Features.Auth.Tests.TestSupport;
using FluentAssertions;
using Microsoft.AspNetCore.Identity;
using Moq;
namespace Features.Auth.Tests.Commands;
public class ConfirmUserHandlerTests
{
private readonly Mock<IAuthRepository> _authRepositoryMock;
private readonly Mock<IUserEmailStore<ApplicationUser>> _emailStoreMock;
private readonly ConfirmUserHandler _handler;
private readonly Mock<ITokenService> _tokenServiceMock;
private readonly Mock<UserManager<ApplicationUser>> _userManagerMock;
public ConfirmUserHandlerTests()
{
_authRepositoryMock = new Mock<IAuthRepository>();
_userManagerMock = UserManagerMockFactory.Create();
_emailStoreMock = new Mock<IUserEmailStore<ApplicationUser>>();
_tokenServiceMock = new Mock<ITokenService>();
_handler = new ConfirmUserHandler(_authRepositoryMock.Object, _tokenServiceMock.Object);
_handler = new ConfirmUserHandler(
_userManagerMock.Object,
_emailStoreMock.Object,
_tokenServiceMock.Object
);
}
private static ValidatedToken MakeValidatedToken(Guid userId, string username)
@@ -44,12 +51,13 @@ public class ConfirmUserHandlerTests
const string confirmationToken = "valid-confirmation-token";
ValidatedToken validatedToken = MakeValidatedToken(userId, username);
UserAccount userAccount = new() { UserAccountId = userId, Username = username };
ApplicationUser user = new() { Id = userId, UserName = username };
_tokenServiceMock
.Setup(x => x.ValidateConfirmationTokenAsync(confirmationToken))
.ReturnsAsync(validatedToken);
_authRepositoryMock.Setup(x => x.ConfirmUserAccountAsync(userId)).ReturnsAsync(userAccount);
_userManagerMock.Setup(x => x.FindByIdAsync(userId.ToString())).ReturnsAsync(user);
_userManagerMock.Setup(x => x.UpdateAsync(user)).ReturnsAsync(IdentityResult.Success);
ConfirmationPayload result = await _handler.Handle(
new ConfirmUserCommand(confirmationToken),
@@ -59,6 +67,12 @@ public class ConfirmUserHandlerTests
result.Should().NotBeNull();
result.UserAccountId.Should().Be(userId);
result.ConfirmedDate.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(1));
_emailStoreMock.Verify(
x => x.SetEmailConfirmedAsync(user, true, It.IsAny<CancellationToken>()),
Times.Once
);
_userManagerMock.Verify(x => x.UpdateAsync(user), Times.Once);
}
[Fact]
@@ -85,9 +99,9 @@ public class ConfirmUserHandlerTests
_tokenServiceMock
.Setup(x => x.ValidateConfirmationTokenAsync(confirmationToken))
.ReturnsAsync(MakeValidatedToken(userId, username));
_authRepositoryMock
.Setup(x => x.ConfirmUserAccountAsync(userId))
.ReturnsAsync((UserAccount?)null);
_userManagerMock
.Setup(x => x.FindByIdAsync(userId.ToString()))
.ReturnsAsync((ApplicationUser?)null);
Func<Task<ConfirmationPayload>> act = async () =>
await _handler.Handle(

View File

@@ -1,12 +1,12 @@
using Domain.Entities;
using Domain.Exceptions;
using Features.Auth.Commands.RegisterUser;
using Features.Auth.Dtos;
using Features.Auth.Repository;
using Features.Auth.Identity;
using Features.Auth.Services;
using Features.Auth.Tests.TestSupport;
using FluentAssertions;
using Infrastructure.PasswordHashing;
using MediatR;
using Microsoft.AspNetCore.Identity;
using Moq;
using Shared.Application.Emails;
@@ -14,22 +14,19 @@ namespace Features.Auth.Tests.Commands;
public class RegisterUserHandlerTests
{
private readonly Mock<IAuthRepository> _authRepoMock;
private readonly RegisterUserHandler _handler;
private readonly Mock<IMediator> _mediatorMock;
private readonly Mock<IPasswordInfrastructure> _passwordInfraMock;
private readonly Mock<ITokenService> _tokenServiceMock;
private readonly Mock<UserManager<ApplicationUser>> _userManagerMock;
public RegisterUserHandlerTests()
{
_authRepoMock = new Mock<IAuthRepository>();
_passwordInfraMock = new Mock<IPasswordInfrastructure>();
_userManagerMock = UserManagerMockFactory.Create();
_tokenServiceMock = new Mock<ITokenService>();
_mediatorMock = new Mock<IMediator>();
_handler = new RegisterUserHandler(
_authRepoMock.Object,
_passwordInfraMock.Object,
_userManagerMock.Object,
_tokenServiceMock.Object,
_mediatorMock.Object
);
@@ -54,42 +51,23 @@ public class RegisterUserHandlerTests
public async Task Handle_WithValidData_CreatesUserAndReturnsPayload()
{
RegisterUserCommand command = ValidCommand();
const string hashedPassword = "hashed_password_value";
Guid 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
_userManagerMock
.Setup(x =>
x.RegisterUserAsync(
It.Is<UserAccount>(ua =>
ua.FirstName == command.FirstName
&& ua.LastName == command.LastName
&& ua.Username == command.Username
&& ua.Email == command.Email
&& ua.DateOfBirth == command.DateOfBirth
&& ua.UserCredential!.Hash == hashedPassword
)
x.CreateAsync(
It.Is<ApplicationUser>(u =>
u.FirstName == command.FirstName
&& u.LastName == command.LastName
&& u.UserName == command.Username
&& u.Email == command.Email
&& u.DateOfBirth == command.DateOfBirth
),
command.Password
)
)
.ReturnsAsync(
new UserAccount
{
UserAccountId = expectedUserId,
Username = command.Username,
FirstName = command.FirstName,
LastName = command.LastName,
Email = command.Email,
DateOfBirth = command.DateOfBirth,
CreatedAt = DateTime.UtcNow,
}
);
.Callback<ApplicationUser, string>((u, _) => u.Id = expectedUserId)
.ReturnsAsync(IdentityResult.Success);
_tokenServiceMock
.Setup(x => x.GenerateAccessToken(It.IsAny<Guid>(), It.IsAny<string>()))
@@ -120,18 +98,14 @@ public class RegisterUserHandlerTests
public async Task Handle_WithExistingUsername_ThrowsConflictException()
{
RegisterUserCommand command = ValidCommand("existinguser");
UserAccount existingUser = new()
{
UserAccountId = Guid.NewGuid(),
Username = "existinguser",
};
_authRepoMock
.Setup(x => x.GetUserByUsernameAsync(command.Username))
.ReturnsAsync(existingUser);
_authRepoMock
.Setup(x => x.GetUserByEmailAsync(command.Email))
.ReturnsAsync((UserAccount?)null);
_userManagerMock
.Setup(x => x.CreateAsync(It.IsAny<ApplicationUser>(), command.Password))
.ReturnsAsync(
IdentityResult.Failed(
new IdentityError { Code = "DuplicateUserName", Description = "Username taken." }
)
);
Func<Task<RegistrationPayload>> act = async () =>
await _handler.Handle(command, CancellationToken.None);
@@ -139,27 +113,20 @@ public class RegisterUserHandlerTests
await act.Should()
.ThrowAsync<ConflictException>()
.WithMessage("Username or email already exists");
_authRepoMock.Verify(
x => x.RegisterUserAsync(It.IsAny<UserAccount>()),
Times.Never
);
}
[Fact]
public async Task Handle_WithExistingEmail_ThrowsConflictException()
{
RegisterUserCommand command = ValidCommand(email: "existing@example.com");
UserAccount existingUser = new()
{
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);
_userManagerMock
.Setup(x => x.CreateAsync(It.IsAny<ApplicationUser>(), command.Password))
.ReturnsAsync(
IdentityResult.Failed(
new IdentityError { Code = "DuplicateEmail", Description = "Email taken." }
)
);
Func<Task<RegistrationPayload>> act = async () =>
await _handler.Handle(command, CancellationToken.None);
@@ -173,23 +140,11 @@ public class RegisterUserHandlerTests
public async Task Handle_PasswordIsHashed_BeforeStoringInDatabase()
{
RegisterUserCommand 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.Is<UserAccount>(ua => ua.UserCredential!.Hash == hashedPassword)
)
)
.ReturnsAsync(new UserAccount { UserAccountId = Guid.NewGuid() });
_userManagerMock
.Setup(x => x.CreateAsync(It.IsAny<ApplicationUser>(), command.Password))
.Callback<ApplicationUser, string>((u, _) => u.Id = Guid.NewGuid())
.ReturnsAsync(IdentityResult.Success);
_tokenServiceMock
.Setup(x => x.GenerateAccessToken(It.IsAny<Guid>(), It.IsAny<string>()))
@@ -200,18 +155,19 @@ public class RegisterUserHandlerTests
await _handler.Handle(command, CancellationToken.None);
_passwordInfraMock.Verify(x => x.Hash(command.Password), Times.Once);
_authRepoMock.Verify(
// UserManager.CreateAsync receives the plaintext password and hashes it internally via
// IPasswordHasher<ApplicationUser> -- the handler never sees or stores a hash directly.
_userManagerMock.Verify(
x =>
x.RegisterUserAsync(
It.Is<UserAccount>(ua =>
ua.FirstName == command.FirstName
&& ua.LastName == command.LastName
&& ua.Username == command.Username
&& ua.DateOfBirth == command.DateOfBirth
&& ua.Email == command.Email
&& ua.UserCredential!.Hash == hashedPassword
)
x.CreateAsync(
It.Is<ApplicationUser>(u =>
u.FirstName == command.FirstName
&& u.LastName == command.LastName
&& u.UserName == command.Username
&& u.DateOfBirth == command.DateOfBirth
&& u.Email == command.Email
),
command.Password
),
Times.Once
);
@@ -222,23 +178,10 @@ public class RegisterUserHandlerTests
{
RegisterUserCommand 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<UserAccount>()))
.ReturnsAsync(
new UserAccount
{
UserAccountId = Guid.NewGuid(),
Username = command.Username,
Email = command.Email,
}
);
_userManagerMock
.Setup(x => x.CreateAsync(It.IsAny<ApplicationUser>(), command.Password))
.Callback<ApplicationUser, string>((u, _) => u.Id = Guid.NewGuid())
.ReturnsAsync(IdentityResult.Success);
_tokenServiceMock
.Setup(x => x.GenerateAccessToken(It.IsAny<Guid>(), It.IsAny<string>()))

View File

@@ -1,8 +1,9 @@
using Domain.Entities;
using Features.Auth.Commands.ResendConfirmationEmail;
using Features.Auth.Repository;
using Features.Auth.Identity;
using Features.Auth.Services;
using Features.Auth.Tests.TestSupport;
using MediatR;
using Microsoft.AspNetCore.Identity;
using Moq;
using Shared.Application.Emails;
@@ -10,15 +11,15 @@ namespace Features.Auth.Tests.Commands;
public class ResendConfirmationEmailHandlerTests
{
private readonly Mock<IAuthRepository> _authRepositoryMock = new();
private readonly ResendConfirmationEmailHandler _handler;
private readonly Mock<IMediator> _mediatorMock = new();
private readonly Mock<ITokenService> _tokenServiceMock = new();
private readonly Mock<UserManager<ApplicationUser>> _userManagerMock = UserManagerMockFactory.Create();
public ResendConfirmationEmailHandlerTests()
{
_handler = new ResendConfirmationEmailHandler(
_authRepositoryMock.Object,
_userManagerMock.Object,
_tokenServiceMock.Object,
_mediatorMock.Object
);
@@ -28,16 +29,18 @@ public class ResendConfirmationEmailHandlerTests
public async Task Handle_SendsFreshConfirmationEmail_WhenUserExistsAndUnverified()
{
Guid userId = Guid.NewGuid();
UserAccount user = new()
ApplicationUser user = new()
{
UserAccountId = userId,
Id = 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.UserAccountId, user.Username)).Returns("fresh-token");
_userManagerMock.Setup(x => x.FindByIdAsync(userId.ToString())).ReturnsAsync(user);
_userManagerMock.Setup(x => x.IsEmailConfirmedAsync(user)).ReturnsAsync(false);
_tokenServiceMock
.Setup(x => x.GenerateConfirmationToken(user.Id, user.UserName))
.Returns("fresh-token");
await _handler.Handle(new ResendConfirmationEmailCommand(userId), CancellationToken.None);
@@ -59,7 +62,9 @@ public class ResendConfirmationEmailHandlerTests
public async Task Handle_DoesNothing_WhenUserDoesNotExist()
{
Guid userId = Guid.NewGuid();
_authRepositoryMock.Setup(x => x.GetUserByIdAsync(userId)).ReturnsAsync((UserAccount?)null);
_userManagerMock
.Setup(x => x.FindByIdAsync(userId.ToString()))
.ReturnsAsync((ApplicationUser?)null);
await _handler.Handle(new ResendConfirmationEmailCommand(userId), CancellationToken.None);
@@ -77,10 +82,10 @@ public class ResendConfirmationEmailHandlerTests
public async Task Handle_DoesNothing_WhenUserAlreadyVerified()
{
Guid userId = Guid.NewGuid();
UserAccount user = new() { UserAccountId = userId };
ApplicationUser user = new() { Id = userId };
_authRepositoryMock.Setup(x => x.GetUserByIdAsync(userId)).ReturnsAsync(user);
_authRepositoryMock.Setup(x => x.IsUserVerifiedAsync(userId)).ReturnsAsync(true);
_userManagerMock.Setup(x => x.FindByIdAsync(userId.ToString())).ReturnsAsync(user);
_userManagerMock.Setup(x => x.IsEmailConfirmedAsync(user)).ReturnsAsync(true);
await _handler.Handle(new ResendConfirmationEmailCommand(userId), CancellationToken.None);