Add user avatar and profile feature, consolidate Auth and UserManagement features (#257)

This commit is contained in:
Aaron Po
2026-08-31 00:11:48 -04:00
committed by GitHub
parent ade2128a73
commit 39c5a89ca7
130 changed files with 1005 additions and 613 deletions

View File

@@ -0,0 +1,116 @@
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using Domain.Exceptions;
using Features.Auth.Commands.Authentication.ConfirmUser;
using Features.Auth.Dtos;
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<IUserEmailStore<ApplicationUser>> _emailStoreMock;
private readonly ConfirmUserHandler _handler;
private readonly Mock<ITokenService> _tokenServiceMock;
private readonly Mock<UserManager<ApplicationUser>> _userManagerMock;
public ConfirmUserHandlerTests()
{
_userManagerMock = UserManagerMockFactory.Create();
_emailStoreMock = new Mock<IUserEmailStore<ApplicationUser>>();
_tokenServiceMock = new Mock<ITokenService>();
_handler = new ConfirmUserHandler(
_userManagerMock.Object,
_emailStoreMock.Object,
_tokenServiceMock.Object
);
}
private static ValidatedToken MakeValidatedToken(Guid userId, string username)
{
List<Claim> claims = new()
{
new Claim(JwtRegisteredClaimNames.Sub, userId.ToString()),
new Claim(JwtRegisteredClaimNames.UniqueName, username),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
};
ClaimsPrincipal principal = new(new ClaimsIdentity(claims));
return new ValidatedToken(userId, username, principal);
}
[Fact]
public async Task Handle_WithValidConfirmationToken_ConfirmsUser()
{
Guid userId = Guid.NewGuid();
const string username = "testuser";
const string confirmationToken = "valid-confirmation-token";
ValidatedToken validatedToken = MakeValidatedToken(userId, username);
ApplicationUser user = new() { Id = userId, UserName = username };
_tokenServiceMock
.Setup(x => x.ValidateConfirmationTokenAsync(confirmationToken))
.ReturnsAsync(validatedToken);
_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),
CancellationToken.None
);
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]
public async Task Handle_WithInvalidConfirmationToken_ThrowsUnauthorizedException()
{
const string invalidToken = "invalid-confirmation-token";
_tokenServiceMock
.Setup(x => x.ValidateConfirmationTokenAsync(invalidToken))
.ThrowsAsync(new UnauthorizedException("Invalid confirmation token"));
Func<Task<ConfirmationPayload>> act = async () =>
await _handler.Handle(new ConfirmUserCommand(invalidToken), CancellationToken.None);
await act.Should().ThrowAsync<UnauthorizedException>();
}
[Fact]
public async Task Handle_WithNonExistentUser_ThrowsUnauthorizedException()
{
Guid userId = Guid.NewGuid();
const string username = "nonexistent";
const string confirmationToken = "valid-token-for-nonexistent-user";
_tokenServiceMock
.Setup(x => x.ValidateConfirmationTokenAsync(confirmationToken))
.ReturnsAsync(MakeValidatedToken(userId, username));
_userManagerMock
.Setup(x => x.FindByIdAsync(userId.ToString()))
.ReturnsAsync((ApplicationUser?)null);
Func<Task<ConfirmationPayload>> act = async () =>
await _handler.Handle(
new ConfirmUserCommand(confirmationToken),
CancellationToken.None
);
await act.Should()
.ThrowAsync<UnauthorizedException>()
.WithMessage("*User account not found*");
}
}

View File

@@ -0,0 +1,74 @@
using Domain.Exceptions;
using Features.Auth.Commands.Account.DeleteAccount;
using Features.Auth.Identity;
using Features.Auth.Tests.TestSupport;
using FluentAssertions;
using Microsoft.AspNetCore.Identity;
using Moq;
namespace Features.Auth.Tests.Commands;
public class DeleteAccountHandlerTests
{
private readonly DeleteAccountHandler _handler;
private readonly Mock<UserManager<ApplicationUser>> _userManagerMock;
public DeleteAccountHandlerTests()
{
_userManagerMock = UserManagerMockFactory.Create();
_handler = new DeleteAccountHandler(_userManagerMock.Object);
}
[Fact]
public async Task Handle_WithExistingUser_DeletesAccount()
{
Guid userId = Guid.NewGuid();
ApplicationUser user = new() { Id = userId };
_userManagerMock.Setup(x => x.FindByIdAsync(userId.ToString())).ReturnsAsync(user);
_userManagerMock.Setup(x => x.DeleteAsync(user)).ReturnsAsync(IdentityResult.Success);
await _handler.Handle(new DeleteAccountCommand(userId), CancellationToken.None);
_userManagerMock.Verify(x => x.DeleteAsync(user), Times.Once);
}
[Fact]
public async Task Handle_WithNonExistentUser_ThrowsNotFoundException()
{
Guid userId = Guid.NewGuid();
_userManagerMock
.Setup(x => x.FindByIdAsync(userId.ToString()))
.ReturnsAsync((ApplicationUser?)null);
Func<Task> act = async () =>
await _handler.Handle(new DeleteAccountCommand(userId), CancellationToken.None);
await act.Should().ThrowAsync<NotFoundException>();
}
[Fact]
public async Task Handle_WithDependentRecords_ThrowsConflictException()
{
Guid userId = Guid.NewGuid();
ApplicationUser user = new() { Id = userId };
_userManagerMock.Setup(x => x.FindByIdAsync(userId.ToString())).ReturnsAsync(user);
_userManagerMock
.Setup(x => x.DeleteAsync(user))
.ReturnsAsync(
IdentityResult.Failed(
new IdentityError
{
Code = "AccountHasDependentRecords",
Description = "still referenced",
}
)
);
Func<Task> act = async () =>
await _handler.Handle(new DeleteAccountCommand(userId), CancellationToken.None);
await act.Should().ThrowAsync<ConflictException>();
}
}

View File

@@ -0,0 +1,34 @@
using Features.Auth.Commands.Authentication.RefreshToken;
using Features.Auth.Dtos;
using Features.Auth.Services;
using FluentAssertions;
using Moq;
namespace Features.Auth.Tests.Commands;
public class RefreshTokenHandlerTests
{
[Fact]
public async Task Handle_MapsTokenServiceResult_ToLoginPayload()
{
Mock<ITokenService> tokenServiceMock = new();
RefreshTokenHandler handler = new(tokenServiceMock.Object);
Guid userId = Guid.NewGuid();
tokenServiceMock
.Setup(x => x.RefreshTokenAsync("old-refresh-token"))
.ReturnsAsync(
new RefreshTokenResult(userId, "testuser", "new-refresh-token", "new-access-token")
);
LoginPayload result = await handler.Handle(
new RefreshTokenCommand("old-refresh-token"),
CancellationToken.None
);
result.UserAccountId.Should().Be(userId);
result.Username.Should().Be("testuser");
result.RefreshToken.Should().Be("new-refresh-token");
result.AccessToken.Should().Be("new-access-token");
}
}

View File

@@ -0,0 +1,207 @@
using Domain.Exceptions;
using Features.Auth.Commands.Authentication.RegisterUser;
using Features.Auth.Dtos;
using Features.Auth.Identity;
using Features.Auth.Services;
using Features.Auth.Tests.TestSupport;
using FluentAssertions;
using MediatR;
using Microsoft.AspNetCore.Identity;
using Moq;
using Shared.Application.Emails;
namespace Features.Auth.Tests.Commands;
public class RegisterUserHandlerTests
{
private readonly RegisterUserHandler _handler;
private readonly Mock<IMediator> _mediatorMock;
private readonly Mock<ITokenService> _tokenServiceMock;
private readonly Mock<UserManager<ApplicationUser>> _userManagerMock;
public RegisterUserHandlerTests()
{
_userManagerMock = UserManagerMockFactory.Create();
_tokenServiceMock = new Mock<ITokenService>();
_mediatorMock = new Mock<IMediator>();
_handler = new RegisterUserHandler(
_userManagerMock.Object,
_tokenServiceMock.Object,
_mediatorMock.Object
);
}
private static RegisterUserCommand ValidCommand(
string username = "newuser",
string email = "john.doe@example.com"
)
{
return new RegisterUserCommand(
username,
"John",
"Doe",
email,
new DateTime(1990, 1, 1),
"SecurePassword123!"
);
}
[Fact]
public async Task Handle_WithValidData_CreatesUserAndReturnsPayload()
{
RegisterUserCommand command = ValidCommand();
Guid expectedUserId = Guid.NewGuid();
_userManagerMock
.Setup(x =>
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
)
)
.Callback<ApplicationUser, string>((u, _) => u.Id = expectedUserId)
.ReturnsAsync(IdentityResult.Success);
_tokenServiceMock
.Setup(x => x.GenerateAccessToken(It.IsAny<Guid>(), It.IsAny<string>()))
.Returns("access-token");
_tokenServiceMock
.Setup(x => x.GenerateRefreshToken(It.IsAny<Guid>(), It.IsAny<string>()))
.Returns("refresh-token");
_tokenServiceMock
.Setup(x => x.GenerateConfirmationToken(It.IsAny<Guid>(), It.IsAny<string>()))
.Returns("confirmation-token");
RegistrationPayload result = await _handler.Handle(command, CancellationToken.None);
result.Should().NotBeNull();
result.UserAccountId.Should().Be(expectedUserId);
result.Username.Should().Be(command.Username);
result.AccessToken.Should().Be("access-token");
result.RefreshToken.Should().Be("refresh-token");
result.ConfirmationEmailSent.Should().BeTrue();
_mediatorMock.Verify(
x => x.Send(It.IsAny<SendRegistrationEmailCommand>(), It.IsAny<CancellationToken>()),
Times.Once
);
}
[Fact]
public async Task Handle_WithExistingUsername_ThrowsConflictException()
{
RegisterUserCommand command = ValidCommand("existinguser");
_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);
await act.Should()
.ThrowAsync<ConflictException>()
.WithMessage("Username or email already exists");
}
[Fact]
public async Task Handle_WithExistingEmail_ThrowsConflictException()
{
RegisterUserCommand command = ValidCommand(email: "existing@example.com");
_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);
await act.Should()
.ThrowAsync<ConflictException>()
.WithMessage("Username or email already exists");
}
[Fact]
public async Task Handle_PasswordIsHashed_BeforeStoringInDatabase()
{
RegisterUserCommand command = ValidCommand();
_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>()))
.Returns("access-token");
_tokenServiceMock
.Setup(x => x.GenerateRefreshToken(It.IsAny<Guid>(), It.IsAny<string>()))
.Returns("refresh-token");
await _handler.Handle(command, CancellationToken.None);
// 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.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
);
}
[Fact]
public async Task Handle_SwallowsEmailFailure_AndReportsEmailNotSent()
{
RegisterUserCommand command = ValidCommand();
_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>()))
.Returns("access-token");
_tokenServiceMock
.Setup(x => x.GenerateRefreshToken(It.IsAny<Guid>(), It.IsAny<string>()))
.Returns("refresh-token");
_mediatorMock
.Setup(x =>
x.Send(It.IsAny<SendRegistrationEmailCommand>(), It.IsAny<CancellationToken>())
)
.ThrowsAsync(new Exception("smtp down"));
RegistrationPayload result = await _handler.Handle(command, CancellationToken.None);
result.ConfirmationEmailSent.Should().BeFalse();
result.AccessToken.Should().Be("access-token");
}
}

View File

@@ -0,0 +1,102 @@
using Features.Auth.Commands.Authentication.ResendConfirmationEmail;
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;
namespace Features.Auth.Tests.Commands;
public class ResendConfirmationEmailHandlerTests
{
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(
_userManagerMock.Object,
_tokenServiceMock.Object,
_mediatorMock.Object
);
}
[Fact]
public async Task Handle_SendsFreshConfirmationEmail_WhenUserExistsAndUnverified()
{
Guid userId = Guid.NewGuid();
ApplicationUser user = new()
{
Id = userId,
FirstName = "Aaron",
Email = "aaron@example.com",
};
_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);
_mediatorMock.Verify(
x =>
x.Send(
It.Is<SendResendConfirmationEmailCommand>(c =>
c.FirstName == "Aaron"
&& c.Email == "aaron@example.com"
&& c.ConfirmationToken == "fresh-token"
),
It.IsAny<CancellationToken>()
),
Times.Once
);
}
[Fact]
public async Task Handle_DoesNothing_WhenUserDoesNotExist()
{
Guid userId = Guid.NewGuid();
_userManagerMock
.Setup(x => x.FindByIdAsync(userId.ToString()))
.ReturnsAsync((ApplicationUser?)null);
await _handler.Handle(new ResendConfirmationEmailCommand(userId), CancellationToken.None);
_mediatorMock.Verify(
x =>
x.Send(
It.IsAny<SendResendConfirmationEmailCommand>(),
It.IsAny<CancellationToken>()
),
Times.Never
);
}
[Fact]
public async Task Handle_DoesNothing_WhenUserAlreadyVerified()
{
Guid userId = Guid.NewGuid();
ApplicationUser user = new() { Id = userId };
_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);
_mediatorMock.Verify(
x =>
x.Send(
It.IsAny<SendResendConfirmationEmailCommand>(),
It.IsAny<CancellationToken>()
),
Times.Never
);
}
}

View File

@@ -0,0 +1,96 @@
using Domain.Exceptions;
using Features.Auth.Commands.Account.UpdateEmail;
using Features.Auth.Dtos;
using Features.Auth.Identity;
using Features.Auth.Tests.TestSupport;
using FluentAssertions;
using Microsoft.AspNetCore.Identity;
using Moq;
namespace Features.Auth.Tests.Commands;
public class UpdateEmailHandlerTests
{
private readonly UpdateEmailHandler _handler;
private readonly Mock<UserManager<ApplicationUser>> _userManagerMock;
public UpdateEmailHandlerTests()
{
_userManagerMock = UserManagerMockFactory.Create();
_handler = new UpdateEmailHandler(_userManagerMock.Object);
}
[Fact]
public async Task Handle_WithValidRequest_UpdatesEmailAndResetsConfirmation()
{
Guid userId = Guid.NewGuid();
ApplicationUser user = new()
{
Id = userId,
Email = "old@example.com",
EmailConfirmed = true,
};
_userManagerMock.Setup(x => x.FindByIdAsync(userId.ToString())).ReturnsAsync(user);
_userManagerMock
.Setup(x => x.SetEmailAsync(user, "new@example.com"))
.Callback<ApplicationUser, string>(
(u, e) =>
{
u.Email = e;
u.EmailConfirmed = false;
}
)
.ReturnsAsync(IdentityResult.Success);
UpdateEmailPayload result = await _handler.Handle(
new UpdateEmailCommand(userId, "new@example.com"),
CancellationToken.None
);
result.UserAccountId.Should().Be(userId);
result.Email.Should().Be("new@example.com");
result.EmailConfirmed.Should().BeFalse();
}
[Fact]
public async Task Handle_WithNonExistentUser_ThrowsNotFoundException()
{
Guid userId = Guid.NewGuid();
_userManagerMock
.Setup(x => x.FindByIdAsync(userId.ToString()))
.ReturnsAsync((ApplicationUser?)null);
Func<Task<UpdateEmailPayload>> act = async () =>
await _handler.Handle(
new UpdateEmailCommand(userId, "new@example.com"),
CancellationToken.None
);
await act.Should().ThrowAsync<NotFoundException>();
}
[Fact]
public async Task Handle_WithDuplicateEmail_ThrowsConflictException()
{
Guid userId = Guid.NewGuid();
ApplicationUser user = new() { Id = userId, Email = "old@example.com" };
_userManagerMock.Setup(x => x.FindByIdAsync(userId.ToString())).ReturnsAsync(user);
_userManagerMock
.Setup(x => x.SetEmailAsync(user, "taken@example.com"))
.ReturnsAsync(
IdentityResult.Failed(
new IdentityError { Code = "DuplicateEmail", Description = "taken" }
)
);
Func<Task<UpdateEmailPayload>> act = async () =>
await _handler.Handle(
new UpdateEmailCommand(userId, "taken@example.com"),
CancellationToken.None
);
await act.Should().ThrowAsync<ConflictException>();
}
}

View File

@@ -0,0 +1,83 @@
using Domain.Exceptions;
using Features.Auth.Commands.Account.UpdatePassword;
using Features.Auth.Dtos;
using Features.Auth.Identity;
using Features.Auth.Tests.TestSupport;
using FluentAssertions;
using Microsoft.AspNetCore.Identity;
using Moq;
namespace Features.Auth.Tests.Commands;
public class UpdatePasswordHandlerTests
{
private readonly UpdatePasswordHandler _handler;
private readonly Mock<UserManager<ApplicationUser>> _userManagerMock;
public UpdatePasswordHandlerTests()
{
_userManagerMock = UserManagerMockFactory.Create();
_handler = new UpdatePasswordHandler(_userManagerMock.Object);
}
[Fact]
public async Task Handle_WithValidRequest_ChangesPassword()
{
Guid userId = Guid.NewGuid();
ApplicationUser user = new() { Id = userId };
_userManagerMock.Setup(x => x.FindByIdAsync(userId.ToString())).ReturnsAsync(user);
_userManagerMock
.Setup(x => x.ChangePasswordAsync(user, "OldPassword1!", "NewPassword1!"))
.ReturnsAsync(IdentityResult.Success);
UpdatePasswordPayload result = await _handler.Handle(
new UpdatePasswordCommand(userId, "OldPassword1!", "NewPassword1!"),
CancellationToken.None
);
result.UserAccountId.Should().Be(userId);
result.ChangedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(1));
}
[Fact]
public async Task Handle_WithNonExistentUser_ThrowsNotFoundException()
{
Guid userId = Guid.NewGuid();
_userManagerMock
.Setup(x => x.FindByIdAsync(userId.ToString()))
.ReturnsAsync((ApplicationUser?)null);
Func<Task<UpdatePasswordPayload>> act = async () =>
await _handler.Handle(
new UpdatePasswordCommand(userId, "OldPassword1!", "NewPassword1!"),
CancellationToken.None
);
await act.Should().ThrowAsync<NotFoundException>();
}
[Fact]
public async Task Handle_WithWrongCurrentPassword_ThrowsUnauthorizedException()
{
Guid userId = Guid.NewGuid();
ApplicationUser user = new() { Id = userId };
_userManagerMock.Setup(x => x.FindByIdAsync(userId.ToString())).ReturnsAsync(user);
_userManagerMock
.Setup(x => x.ChangePasswordAsync(user, "WrongPassword1!", "NewPassword1!"))
.ReturnsAsync(
IdentityResult.Failed(
new IdentityError { Code = "PasswordMismatch", Description = "mismatch" }
)
);
Func<Task<UpdatePasswordPayload>> act = async () =>
await _handler.Handle(
new UpdatePasswordCommand(userId, "WrongPassword1!", "NewPassword1!"),
CancellationToken.None
);
await act.Should().ThrowAsync<UnauthorizedException>();
}
}

View File

@@ -0,0 +1,67 @@
using Domain.Exceptions;
using Features.Auth.Commands.Profile.UpdateProfile;
using Features.Auth.Dtos;
using Features.Auth.Identity;
using Features.Auth.Tests.TestSupport;
using FluentAssertions;
using Microsoft.AspNetCore.Identity;
using Moq;
namespace Features.Auth.Tests.Commands;
public class UpdateProfileHandlerTests
{
private readonly UpdateProfileHandler _handler;
private readonly Mock<UserManager<ApplicationUser>> _userManagerMock;
public UpdateProfileHandlerTests()
{
_userManagerMock = UserManagerMockFactory.Create();
_handler = new UpdateProfileHandler(_userManagerMock.Object);
}
[Fact]
public async Task Handle_WithValidRequest_UpdatesProfileFields()
{
Guid userId = Guid.NewGuid();
ApplicationUser user = new()
{
Id = userId,
FirstName = "Old",
LastName = "Name",
DateOfBirth = new DateTime(1990, 1, 1),
};
DateTime newDob = new(1991, 2, 3);
_userManagerMock.Setup(x => x.FindByIdAsync(userId.ToString())).ReturnsAsync(user);
_userManagerMock.Setup(x => x.UpdateAsync(user)).ReturnsAsync(IdentityResult.Success);
UpdateProfilePayload result = await _handler.Handle(
new UpdateProfileCommand(userId, "New", "Person", newDob),
CancellationToken.None
);
result.UserAccountId.Should().Be(userId);
result.FirstName.Should().Be("New");
result.LastName.Should().Be("Person");
result.DateOfBirth.Should().Be(newDob);
_userManagerMock.Verify(x => x.UpdateAsync(user), Times.Once);
}
[Fact]
public async Task Handle_WithNonExistentUser_ThrowsNotFoundException()
{
Guid userId = Guid.NewGuid();
_userManagerMock
.Setup(x => x.FindByIdAsync(userId.ToString()))
.ReturnsAsync((ApplicationUser?)null);
Func<Task<UpdateProfilePayload>> act = async () =>
await _handler.Handle(
new UpdateProfileCommand(userId, "New", "Person", DateTime.Today),
CancellationToken.None
);
await act.Should().ThrowAsync<NotFoundException>();
}
}

View File

@@ -0,0 +1,84 @@
using Domain.Exceptions;
using Features.Auth.Commands.Account.UpdateUsername;
using Features.Auth.Dtos;
using Features.Auth.Identity;
using Features.Auth.Tests.TestSupport;
using FluentAssertions;
using Microsoft.AspNetCore.Identity;
using Moq;
namespace Features.Auth.Tests.Commands;
public class UpdateUsernameHandlerTests
{
private readonly UpdateUsernameHandler _handler;
private readonly Mock<UserManager<ApplicationUser>> _userManagerMock;
public UpdateUsernameHandlerTests()
{
_userManagerMock = UserManagerMockFactory.Create();
_handler = new UpdateUsernameHandler(_userManagerMock.Object);
}
[Fact]
public async Task Handle_WithValidRequest_UpdatesUsername()
{
Guid userId = Guid.NewGuid();
ApplicationUser user = new() { Id = userId, UserName = "old-name" };
_userManagerMock.Setup(x => x.FindByIdAsync(userId.ToString())).ReturnsAsync(user);
_userManagerMock
.Setup(x => x.SetUserNameAsync(user, "new-name"))
.Callback<ApplicationUser, string>((u, n) => u.UserName = n)
.ReturnsAsync(IdentityResult.Success);
UpdateUsernamePayload result = await _handler.Handle(
new UpdateUsernameCommand(userId, "new-name"),
CancellationToken.None
);
result.UserAccountId.Should().Be(userId);
result.Username.Should().Be("new-name");
}
[Fact]
public async Task Handle_WithNonExistentUser_ThrowsNotFoundException()
{
Guid userId = Guid.NewGuid();
_userManagerMock
.Setup(x => x.FindByIdAsync(userId.ToString()))
.ReturnsAsync((ApplicationUser?)null);
Func<Task<UpdateUsernamePayload>> act = async () =>
await _handler.Handle(
new UpdateUsernameCommand(userId, "new-name"),
CancellationToken.None
);
await act.Should().ThrowAsync<NotFoundException>();
}
[Fact]
public async Task Handle_WithDuplicateUsername_ThrowsConflictException()
{
Guid userId = Guid.NewGuid();
ApplicationUser user = new() { Id = userId, UserName = "old-name" };
_userManagerMock.Setup(x => x.FindByIdAsync(userId.ToString())).ReturnsAsync(user);
_userManagerMock
.Setup(x => x.SetUserNameAsync(user, "taken-name"))
.ReturnsAsync(
IdentityResult.Failed(
new IdentityError { Code = "DuplicateUserName", Description = "taken" }
)
);
Func<Task<UpdateUsernamePayload>> act = async () =>
await _handler.Handle(
new UpdateUsernameCommand(userId, "taken-name"),
CancellationToken.None
);
await act.Should().ThrowAsync<ConflictException>();
}
}

View File

@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<RootNamespace>Features.Auth.Tests</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
<PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="FluentAssertions" Version="7.2.0" />
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Features.Users\Features.Users.csproj" />
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Configuration\Infrastructure.Configuration.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,26 @@
using Domain.Entities;
using Features.Auth.Queries.GetAllUsers;
using Features.Auth.Repository;
using FluentAssertions;
using Moq;
namespace Features.Auth.Tests.Queries;
public class GetAllUsersHandlerTests
{
[Fact]
public async Task Handle_PassesLimitAndOffset_ToRepository()
{
Mock<IUserListRepository> repoMock = new();
GetAllUsersHandler handler = new(repoMock.Object);
repoMock.Setup(r => r.GetAllAsync(10, 5)).ReturnsAsync(Array.Empty<UserAccount>());
IEnumerable<UserAccount> result = await handler.Handle(
new GetAllUsersQuery(10, 5),
CancellationToken.None
);
result.Should().BeEmpty();
repoMock.Verify(r => r.GetAllAsync(10, 5), Times.Once);
}
}

View File

@@ -0,0 +1,45 @@
using Domain.Entities;
using Domain.Exceptions;
using Features.Auth.Queries.GetUserById;
using Features.Auth.Repository;
using FluentAssertions;
using Moq;
namespace Features.Auth.Tests.Queries;
public class GetUserByIdHandlerTests
{
private readonly GetUserByIdHandler _handler;
private readonly Mock<IUserListRepository> _repoMock = new();
public GetUserByIdHandlerTests()
{
_handler = new GetUserByIdHandler(_repoMock.Object);
}
[Fact]
public async Task Handle_ReturnsUser_WhenFound()
{
UserAccount user = new() { UserAccountId = Guid.NewGuid(), Username = "test" };
_repoMock.Setup(r => r.GetByIdAsync(user.UserAccountId)).ReturnsAsync(user);
UserAccount result = await _handler.Handle(
new GetUserByIdQuery(user.UserAccountId),
CancellationToken.None
);
result.Should().Be(user);
}
[Fact]
public async Task Handle_Throws_WhenNotFound()
{
Guid id = Guid.NewGuid();
_repoMock.Setup(r => r.GetByIdAsync(id)).ReturnsAsync((UserAccount?)null);
Func<Task<UserAccount>> act = async () =>
await _handler.Handle(new GetUserByIdQuery(id), CancellationToken.None);
await act.Should().ThrowAsync<NotFoundException>();
}
}

View File

@@ -0,0 +1,123 @@
using Domain.Exceptions;
using Features.Auth.Commands.Authentication.Login;
using Features.Auth.Dtos;
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.Queries;
public class LoginHandlerTests
{
private readonly LoginHandler _handler;
private readonly Mock<ITokenService> _tokenServiceMock;
private readonly Mock<UserManager<ApplicationUser>> _userManagerMock;
public LoginHandlerTests()
{
_userManagerMock = UserManagerMockFactory.Create();
_tokenServiceMock = new Mock<ITokenService>();
_handler = new LoginHandler(_userManagerMock.Object, _tokenServiceMock.Object);
}
[Fact]
public async Task Handle_WithValidData_ReturnsPayloadWithMatchingUsername()
{
const string username = "CogitoErgoSum";
Guid userAccountId = Guid.NewGuid();
ApplicationUser user = new()
{
Id = userAccountId,
UserName = username,
FirstName = "René",
LastName = "Descartes",
Email = "r.descartes@example.com",
DateOfBirth = new DateTime(1596, 03, 31),
};
_userManagerMock.Setup(x => x.FindByNameAsync(username)).ReturnsAsync(user);
_userManagerMock
.Setup(x => x.CheckPasswordAsync(user, It.IsAny<string>()))
.ReturnsAsync(true);
_tokenServiceMock
.Setup(x => x.GenerateAccessToken(It.IsAny<Guid>(), It.IsAny<string>()))
.Returns("access-token");
_tokenServiceMock
.Setup(x => x.GenerateRefreshToken(It.IsAny<Guid>(), It.IsAny<string>()))
.Returns("refresh-token");
LoginPayload result = await _handler.Handle(
new LoginCommand(username, "any-password"),
CancellationToken.None
);
result.Should().NotBeNull();
result.UserAccountId.Should().Be(userAccountId);
result.Username.Should().Be(username);
result.AccessToken.Should().Be("access-token");
result.RefreshToken.Should().Be("refresh-token");
}
[Fact]
public async Task Handle_WithUnregisteredUsername_ThrowsUnauthorizedException()
{
const string username = "de_beauvoir";
_userManagerMock
.Setup(x => x.FindByNameAsync(username))
.ReturnsAsync((ApplicationUser?)null);
Func<Task<LoginPayload>> act = async () =>
await _handler.Handle(new LoginCommand(username, "password"), CancellationToken.None);
await act.Should().ThrowAsync<UnauthorizedException>();
_userManagerMock.Verify(
x => x.CheckPasswordAsync(It.IsAny<ApplicationUser>(), It.IsAny<string>()),
Times.Never
);
}
[Fact]
public async Task Handle_WithNoActiveCredential_ThrowsUnauthorizedException()
{
const string username = "BRussell";
ApplicationUser user = new() { Id = Guid.NewGuid(), UserName = username };
_userManagerMock.Setup(x => x.FindByNameAsync(username)).ReturnsAsync(user);
_userManagerMock
.Setup(x => x.CheckPasswordAsync(user, It.IsAny<string>()))
.ReturnsAsync(false);
Func<Task<LoginPayload>> act = async () =>
await _handler.Handle(new LoginCommand(username, "password"), CancellationToken.None);
await act.Should()
.ThrowAsync<UnauthorizedException>()
.WithMessage("Invalid username or password.");
}
[Fact]
public async Task Handle_WithIncorrectPassword_ThrowsUnauthorizedException()
{
const string username = "RCarnap";
ApplicationUser user = new() { Id = Guid.NewGuid(), UserName = username };
_userManagerMock.Setup(x => x.FindByNameAsync(username)).ReturnsAsync(user);
_userManagerMock
.Setup(x => x.CheckPasswordAsync(user, It.IsAny<string>()))
.ReturnsAsync(false);
Func<Task<LoginPayload>> act = async () =>
await _handler.Handle(
new LoginCommand(username, "wrong-password"),
CancellationToken.None
);
await act.Should()
.ThrowAsync<UnauthorizedException>()
.WithMessage("Invalid username or password.");
}
}

View File

@@ -0,0 +1,172 @@
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using Domain.Exceptions;
using Features.Auth.Identity;
using Features.Auth.Services;
using Features.Auth.Tests.TestSupport;
using FluentAssertions;
using Infrastructure.Configuration;
using Infrastructure.Jwt;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Configuration;
using Moq;
namespace Features.Auth.Tests.Services;
public class TokenServiceRefreshTests
{
private readonly Mock<ITokenInfrastructure> _tokenInfraMock;
private readonly TokenService _tokenService;
private readonly Mock<UserManager<ApplicationUser>> _userManagerMock;
public TokenServiceRefreshTests()
{
_tokenInfraMock = new Mock<ITokenInfrastructure>();
_userManagerMock = UserManagerMockFactory.Create();
IConfiguration configuration = new ConfigurationBuilder()
.AddInMemoryCollection(
new Dictionary<string, string?>
{
[ConfigurationKeys.AccessTokenSecret] =
"test-access-secret-that-is-very-long-1234567890",
[ConfigurationKeys.RefreshTokenSecret] =
"test-refresh-secret-that-is-very-long-1234567890",
[ConfigurationKeys.ConfirmationTokenSecret] =
"test-confirmation-secret-that-is-very-long-1234567890",
}
)
.Build();
_tokenService = new TokenService(
_tokenInfraMock.Object,
_userManagerMock.Object,
configuration
);
}
[Fact]
public async Task RefreshTokenAsync_WithValidRefreshToken_ReturnsNewTokens()
{
// Arrange
Guid userId = Guid.NewGuid();
const string username = "testuser";
const string refreshToken = "valid-refresh-token";
List<Claim> claims = new()
{
new Claim(JwtRegisteredClaimNames.Sub, userId.ToString()),
new Claim(JwtRegisteredClaimNames.UniqueName, username),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
};
ClaimsIdentity claimsIdentity = new(claims);
ClaimsPrincipal principal = new(claimsIdentity);
ApplicationUser user = new() { Id = userId, UserName = username };
_tokenInfraMock
.Setup(x => x.ValidateJwtAsync(refreshToken, It.IsAny<string>()))
.ReturnsAsync(principal);
_tokenInfraMock
.Setup(x => x.GenerateJwt(userId, username, It.IsAny<DateTime>(), It.IsAny<string>()))
.Returns(
(Guid _, string _, DateTime _, string _) => $"generated-token-{Guid.NewGuid()}"
);
_userManagerMock.Setup(x => x.FindByIdAsync(userId.ToString())).ReturnsAsync(user);
// Act
RefreshTokenResult result = await _tokenService.RefreshTokenAsync(refreshToken);
// Assert
result.Should().NotBeNull();
result.UserId.Should().Be(userId);
result.Username.Should().Be(username);
result.AccessToken.Should().NotBeEmpty();
result.RefreshToken.Should().NotBeEmpty();
_userManagerMock.Verify(x => x.FindByIdAsync(userId.ToString()), 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
Guid userId = Guid.NewGuid();
const string username = "testuser";
const string refreshToken = "valid-refresh-token";
List<Claim> claims = new()
{
new Claim(JwtRegisteredClaimNames.Sub, userId.ToString()),
new Claim(JwtRegisteredClaimNames.UniqueName, username),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
};
ClaimsIdentity claimsIdentity = new(claims);
ClaimsPrincipal principal = new(claimsIdentity);
_tokenInfraMock
.Setup(x => x.ValidateJwtAsync(refreshToken, It.IsAny<string>()))
.ReturnsAsync(principal);
_userManagerMock
.Setup(x => x.FindByIdAsync(userId.ToString()))
.ReturnsAsync((ApplicationUser?)null);
// Act & Assert
await FluentActions
.Invoking(async () => await _tokenService.RefreshTokenAsync(refreshToken))
.Should()
.ThrowAsync<UnauthorizedException>()
.WithMessage("*User account not found*");
}
}

View File

@@ -0,0 +1,244 @@
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using Domain.Exceptions;
using Features.Auth.Services;
using Features.Auth.Tests.TestSupport;
using FluentAssertions;
using Infrastructure.Configuration;
using Infrastructure.Jwt;
using Microsoft.Extensions.Configuration;
using Moq;
namespace Features.Auth.Tests.Services;
public class TokenServiceValidationTests
{
private readonly Mock<ITokenInfrastructure> _tokenInfraMock;
private readonly TokenService _tokenService;
public TokenServiceValidationTests()
{
_tokenInfraMock = new Mock<ITokenInfrastructure>();
IConfiguration configuration = new ConfigurationBuilder()
.AddInMemoryCollection(
new Dictionary<string, string?>
{
[ConfigurationKeys.AccessTokenSecret] =
"test-access-secret-that-is-very-long-1234567890",
[ConfigurationKeys.RefreshTokenSecret] =
"test-refresh-secret-that-is-very-long-1234567890",
[ConfigurationKeys.ConfirmationTokenSecret] =
"test-confirmation-secret-that-is-very-long-1234567890",
}
)
.Build();
_tokenService = new TokenService(
_tokenInfraMock.Object,
UserManagerMockFactory.Create().Object,
configuration
);
}
[Fact]
public async Task ValidateRefreshTokenAsync_WithValidToken_ReturnsValidatedToken()
{
// Arrange
Guid userId = Guid.NewGuid();
const string username = "testuser";
const string token = "valid-refresh-token";
List<Claim> claims = new()
{
new Claim(JwtRegisteredClaimNames.Sub, userId.ToString()),
new Claim(JwtRegisteredClaimNames.UniqueName, username),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
};
ClaimsIdentity claimsIdentity = new(claims);
ClaimsPrincipal principal = new(claimsIdentity);
_tokenInfraMock
.Setup(x => x.ValidateJwtAsync(token, It.IsAny<string>()))
.ReturnsAsync(principal);
// Act
ValidatedToken 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
Guid userId = Guid.NewGuid();
const string username = "testuser";
const string token = "valid-confirmation-token";
List<Claim> claims = new()
{
new Claim(JwtRegisteredClaimNames.Sub, userId.ToString()),
new Claim(JwtRegisteredClaimNames.UniqueName, username),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
};
ClaimsIdentity claimsIdentity = new(claims);
ClaimsPrincipal principal = new(claimsIdentity);
_tokenInfraMock
.Setup(x => x.ValidateJwtAsync(token, It.IsAny<string>()))
.ReturnsAsync(principal);
// Act
ValidatedToken result = await _tokenService.ValidateConfirmationTokenAsync(token);
// Assert
result.Should().NotBeNull();
result.UserId.Should().Be(userId);
result.Username.Should().Be(username);
}
[Fact]
public async Task ValidateRefreshTokenAsync_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.ValidateRefreshTokenAsync(token))
.Should()
.ThrowAsync<UnauthorizedException>();
}
[Fact]
public async Task ValidateRefreshTokenAsync_WithMissingUserIdClaim_ThrowsUnauthorizedException()
{
// Arrange
const string username = "testuser";
const string token = "token-without-user-id";
// Claims without Sub (user ID)
List<Claim> claims = new()
{
new Claim(JwtRegisteredClaimNames.UniqueName, username),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
};
ClaimsIdentity claimsIdentity = new(claims);
ClaimsPrincipal principal = new(claimsIdentity);
_tokenInfraMock
.Setup(x => x.ValidateJwtAsync(token, It.IsAny<string>()))
.ReturnsAsync(principal);
// Act & Assert
await FluentActions
.Invoking(async () => await _tokenService.ValidateRefreshTokenAsync(token))
.Should()
.ThrowAsync<UnauthorizedException>()
.WithMessage("*missing required claims*");
}
[Fact]
public async Task ValidateRefreshTokenAsync_WithMissingUsernameClaim_ThrowsUnauthorizedException()
{
// Arrange
Guid userId = Guid.NewGuid();
const string token = "token-without-username";
// Claims without UniqueName (username)
List<Claim> claims = new()
{
new Claim(JwtRegisteredClaimNames.Sub, userId.ToString()),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
};
ClaimsIdentity claimsIdentity = new(claims);
ClaimsPrincipal principal = new(claimsIdentity);
_tokenInfraMock
.Setup(x => x.ValidateJwtAsync(token, It.IsAny<string>()))
.ReturnsAsync(principal);
// Act & Assert
await FluentActions
.Invoking(async () => await _tokenService.ValidateRefreshTokenAsync(token))
.Should()
.ThrowAsync<UnauthorizedException>()
.WithMessage("*missing required claims*");
}
[Fact]
public async Task ValidateRefreshTokenAsync_WithMalformedUserId_ThrowsUnauthorizedException()
{
// Arrange
const string username = "testuser";
const string token = "token-with-malformed-user-id";
// Claims with invalid GUID format
List<Claim> claims = new()
{
new Claim(JwtRegisteredClaimNames.Sub, "not-a-valid-guid"),
new Claim(JwtRegisteredClaimNames.UniqueName, username),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
};
ClaimsIdentity claimsIdentity = new(claims);
ClaimsPrincipal principal = new(claimsIdentity);
_tokenInfraMock
.Setup(x => x.ValidateJwtAsync(token, It.IsAny<string>()))
.ReturnsAsync(principal);
// Act & Assert
await FluentActions
.Invoking(async () => await _tokenService.ValidateRefreshTokenAsync(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>();
}
}

View File

@@ -0,0 +1,24 @@
using Features.Auth.Identity;
using Microsoft.AspNetCore.Identity;
using Moq;
namespace Features.Auth.Tests.TestSupport;
public static class UserManagerMockFactory
{
public static Mock<UserManager<ApplicationUser>> Create()
{
Mock<IUserStore<ApplicationUser>> storeMock = new();
return new Mock<UserManager<ApplicationUser>>(
storeMock.Object,
null!,
null!,
null!,
null!,
null!,
null!,
null!,
null!
);
}
}