diff --git a/Directory.Packages.props b/Directory.Packages.props index b4706f78..84e750f6 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -18,7 +18,7 @@ - + @@ -54,4 +54,4 @@ - \ No newline at end of file + diff --git a/SecurityService.UnitTests/Infrastructure/GeneratedImposters.cs b/SecurityService.UnitTests/Infrastructure/GeneratedImposters.cs new file mode 100644 index 00000000..19382375 --- /dev/null +++ b/SecurityService.UnitTests/Infrastructure/GeneratedImposters.cs @@ -0,0 +1,28 @@ +using Imposter.Abstractions; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Logging; +using MediatR; +using OpenIddict.Abstractions; +using SecurityService.Database.Entities; + +[assembly: GenerateImposter(typeof(IMediator))] +[assembly: GenerateImposter(typeof(IOpenIddictApplicationManager))] +[assembly: GenerateImposter(typeof(IOpenIddictAuthorizationManager))] +[assembly: GenerateImposter(typeof(IOpenIddictScopeManager))] +[assembly: GenerateImposter(typeof(IUserPasswordStore))] +[assembly: GenerateImposter(typeof(IPasswordHasher))] +[assembly: GenerateImposter(typeof(ILogger<>))] +[assembly: GenerateImposter(typeof(IHttpContextAccessor))] +[assembly: GenerateImposter(typeof(IUserClaimsPrincipalFactory))] +[assembly: GenerateImposter(typeof(IAuthenticationSchemeProvider))] +[assembly: GenerateImposter(typeof(IUserConfirmation))] + + + + + + + + diff --git a/SecurityService.UnitTests/Infrastructure/IdentityMocks.cs b/SecurityService.UnitTests/Infrastructure/IdentityMocks.cs index 812080a0..94117541 100644 --- a/SecurityService.UnitTests/Infrastructure/IdentityMocks.cs +++ b/SecurityService.UnitTests/Infrastructure/IdentityMocks.cs @@ -1,3 +1,4 @@ +using System.Security.Claims; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; @@ -7,52 +8,237 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.Extensions.DependencyInjection; -using Moq; +using Imposter.Abstractions; using SecurityService.Database.Entities; namespace SecurityService.UnitTests.Infrastructure; internal static class IdentityMocks { - public static Mock> CreateUserManager() + public static UserManagerDouble CreateUserManager() { - var store = new Mock>(); + var store = new IUserPasswordStoreImposter(); var options = Options.Create(new IdentityOptions()); - var passwordHasher = new Mock>(); + var passwordHasher = new IPasswordHasherImposter(); var userValidators = Array.Empty>(); var passwordValidators = Array.Empty>(); var keyNormalizer = new UpperInvariantLookupNormalizer(); var errors = new IdentityErrorDescriber(); var services = new ServiceCollection().BuildServiceProvider(); - var logger = new Mock>>(); + var logger = new ILoggerImposter>(); - return new Mock>( - store.Object, + return new UserManagerDouble(new TestUserManager( + store.Instance(), options, - passwordHasher.Object, + passwordHasher.Instance(), userValidators, passwordValidators, keyNormalizer, errors, services, - logger.Object); + logger.Instance())); } - public static Mock> CreateSignInManager(Mock>? userManager = null) + public static SignInManagerDouble CreateSignInManager(UserManagerDouble? userManager = null) { userManager ??= CreateUserManager(); - var contextAccessor = new Mock(); - contextAccessor.Setup(accessor => accessor.HttpContext).Returns(new DefaultHttpContext()); - var claimsFactory = new Mock>(); + var contextAccessor = new IHttpContextAccessorImposter(); + contextAccessor.HttpContext.Getter().Returns(new DefaultHttpContext()); + var claimsFactory = new IUserClaimsPrincipalFactoryImposter(); var options = Options.Create(new IdentityOptions()); - var logger = new Mock>>(); - var schemes = new Mock(); - var confirmation = new Mock>(); + var logger = new ILoggerImposter>(); + var schemes = new IAuthenticationSchemeProviderImposter(); + var confirmation = new IUserConfirmationImposter(); - return new Mock>(userManager.Object, contextAccessor.Object, claimsFactory.Object, options, logger.Object, schemes.Object, confirmation.Object); + return new SignInManagerDouble(new SignInManager(userManager.Instance(), contextAccessor.Instance(), claimsFactory.Instance(), options, logger.Instance(), schemes.Instance(), confirmation.Instance())); } } +internal sealed class UserManagerDouble +{ + private readonly List> _findByNameSetups = []; + private readonly List _findByNameCalls = []; + private readonly List> _findByIdSetups = []; + private readonly List _findByIdCalls = []; + private readonly List>> _getRolesSetups = []; + private readonly List _getRolesCalls = []; + private readonly List>> _getClaimsSetups = []; + private readonly List _getClaimsCalls = []; + private readonly List> _getUserSetups = []; + private readonly List _getUserCalls = []; + private readonly List> _confirmEmailSetups = []; + private readonly List<(ApplicationUser User, string Token)> _confirmEmailCalls = []; + private readonly List> _removePasswordSetups = []; + private readonly List _removePasswordCalls = []; + private readonly List> _addPasswordSetups = []; + private readonly List<(ApplicationUser User, string Password)> _addPasswordCalls = []; + private readonly List> _generateTokenSetups = []; + private readonly List _generateTokenCalls = []; + private readonly List> _resetPasswordSetups = []; + private readonly List<(ApplicationUser User, string Token, string Password)> _resetPasswordCalls = []; + private readonly TestUserManager _instance; + + public UserManagerDouble(TestUserManager instance) + { + _instance = instance; + _instance.FindByName = userName => Resolve(_findByNameSetups, _findByNameCalls, userName); + _instance.FindById = userId => Resolve(_findByIdSetups, _findByIdCalls, userId); + _instance.GetRoles = user => Resolve(_getRolesSetups, _getRolesCalls, user) ?? []; + _instance.GetClaims = user => Resolve(_getClaimsSetups, _getClaimsCalls, user) ?? []; + _instance.GetUser = principal => Resolve(_getUserSetups, _getUserCalls, principal); + _instance.ConfirmEmail = request => Resolve(_confirmEmailSetups, _confirmEmailCalls, request) ?? IdentityResult.Success; + _instance.RemovePassword = user => Resolve(_removePasswordSetups, _removePasswordCalls, user) ?? IdentityResult.Success; + _instance.AddPassword = request => Resolve(_addPasswordSetups, _addPasswordCalls, request) ?? IdentityResult.Success; + _instance.GenerateToken = user => Resolve(_generateTokenSetups, _generateTokenCalls, user) ?? string.Empty; + _instance.ResetPassword = request => Resolve(_resetPasswordSetups, _resetPasswordCalls, request) ?? IdentityResult.Success; + } + + public UserManager Instance() => _instance; + + public Call FindByNameAsync(string userName) => Add(_findByNameSetups, _findByNameCalls, actual => userName.Equals(actual, StringComparison.Ordinal)); + public Call FindByNameAsync(Arg _) => Add(_findByNameSetups, _findByNameCalls, Any()); + public Call FindByIdAsync(string userId) => Add(_findByIdSetups, _findByIdCalls, actual => userId.Equals(actual, StringComparison.Ordinal)); + public Call FindByIdAsync(Arg _) => Add(_findByIdSetups, _findByIdCalls, Any()); + public Call> GetRolesAsync(ApplicationUser user) => Add(_getRolesSetups, _getRolesCalls, actual => user.Equals(actual)); + public Call> GetRolesAsync(Arg _) => Add(_getRolesSetups, _getRolesCalls, Any()); + public Call> GetClaimsAsync(ApplicationUser user) => Add(_getClaimsSetups, _getClaimsCalls, actual => user.Equals(actual)); + public Call> GetClaimsAsync(Arg _) => Add(_getClaimsSetups, _getClaimsCalls, Any()); + public Call GetUserAsync(ClaimsPrincipal principal) => Add(_getUserSetups, _getUserCalls, actual => principal.Equals(actual)); + public Call GetUserAsync(Arg _) => Add(_getUserSetups, _getUserCalls, Any()); + public Call<(ApplicationUser User, string Token), IdentityResult> ConfirmEmailAsync(ApplicationUser user, string token) => Add(_confirmEmailSetups, _confirmEmailCalls, request => user.Equals(request.User) && token.Equals(request.Token, StringComparison.Ordinal)); + public Call<(ApplicationUser User, string Token), IdentityResult> ConfirmEmailAsync(ApplicationUser user, Arg _) => Add(_confirmEmailSetups, _confirmEmailCalls, request => user.Equals(request.User)); + public Call<(ApplicationUser User, string Token), IdentityResult> ConfirmEmailAsync(Arg _, Arg __) => Add(_confirmEmailSetups, _confirmEmailCalls, Any<(ApplicationUser User, string Token)>()); + public Call RemovePasswordAsync(ApplicationUser user) => Add(_removePasswordSetups, _removePasswordCalls, actual => user.Equals(actual)); + public Call RemovePasswordAsync(Arg _) => Add(_removePasswordSetups, _removePasswordCalls, Any()); + public Call<(ApplicationUser User, string Password), IdentityResult> AddPasswordAsync(ApplicationUser user, string password) => Add(_addPasswordSetups, _addPasswordCalls, request => user.Equals(request.User) && password.Equals(request.Password, StringComparison.Ordinal)); + public Call<(ApplicationUser User, string Password), IdentityResult> AddPasswordAsync(ApplicationUser user, Arg _) => Add(_addPasswordSetups, _addPasswordCalls, request => user.Equals(request.User)); + public Call<(ApplicationUser User, string Password), IdentityResult> AddPasswordAsync(Arg _, Arg __) => Add(_addPasswordSetups, _addPasswordCalls, Any<(ApplicationUser User, string Password)>()); + public Call GeneratePasswordResetTokenAsync(ApplicationUser user) => Add(_generateTokenSetups, _generateTokenCalls, actual => user.Equals(actual)); + public Call GeneratePasswordResetTokenAsync(Arg _) => Add(_generateTokenSetups, _generateTokenCalls, Any()); + public Call<(ApplicationUser User, string Token, string Password), IdentityResult> ResetPasswordAsync(ApplicationUser user, string token, string password) => Add(_resetPasswordSetups, _resetPasswordCalls, request => user.Equals(request.User) && token.Equals(request.Token, StringComparison.Ordinal) && password.Equals(request.Password, StringComparison.Ordinal)); + public Call<(ApplicationUser User, string Token, string Password), IdentityResult> ResetPasswordAsync(Arg _, Arg __, Arg ___) => Add(_resetPasswordSetups, _resetPasswordCalls, Any<(ApplicationUser User, string Token, string Password)>()); + + private static Func Any() => _ => true; + + private static Call Add(ICollection> setups, IReadOnlyList calls, Func matcher) + { + var setup = new CallSetup(matcher); + setups.Add(setup); + return new Call(setup, calls, matcher); + } + + private static TResult? Resolve(IReadOnlyList> setups, ICollection calls, TArgument argument) + { + calls.Add(argument); + for (var index = setups.Count - 1; index >= 0; index--) + { + if (setups[index].TryInvoke(argument, out var result)) + return result; + } + + return default; + } +} + +internal sealed class SignInManagerDouble +{ + private readonly SignInManager _instance; + + public SignInManagerDouble(SignInManager instance) => _instance = instance; + + public SignInManager Instance() => _instance; +} + +internal sealed class CallSetup +{ + private readonly Func _matcher; + private TResult? _result; + + public CallSetup(Func matcher) => _matcher = matcher; + + public TResult? Result + { + get => _result; + set => _result = value; + } + + public bool TryInvoke(TArgument argument, out TResult? result) + { + if (!_matcher(argument)) + { + result = default; + return false; + } + + result = _result; + return true; + } +} + +internal sealed class Call +{ + private readonly CallSetup _setup; + private readonly IReadOnlyList _calls; + private readonly Func _matcher; + + public Call(CallSetup setup, IReadOnlyList calls, Func matcher) + { + _setup = setup; + _calls = calls; + _matcher = matcher; + } + + public Call ReturnsAsync(TResult value) + { + _setup.Result = value; + return this; + } + + public void Called(Count expected) + { + var actual = _calls.Count(_matcher); + if (!expected.Matches(actual)) + throw new InvalidOperationException($"Expected {expected} calls but received {actual}."); + } +} + +internal sealed class TestUserManager : UserManager +{ + public TestUserManager( + IUserStore store, + IOptions optionsAccessor, + IPasswordHasher passwordHasher, + IEnumerable> userValidators, + IEnumerable> passwordValidators, + ILookupNormalizer keyNormalizer, + IdentityErrorDescriber errors, + IServiceProvider services, + ILogger> logger) + : base(store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger) { } + + public Func FindByName { get; set; } = _ => null; + public Func FindById { get; set; } = _ => null; + public Func GetUser { get; set; } = _ => null; + public Func> GetRoles { get; set; } = _ => []; + public Func> GetClaims { get; set; } = _ => []; + public Func<(ApplicationUser User, string Token), IdentityResult> ConfirmEmail { get; set; } = _ => IdentityResult.Success; + public Func RemovePassword { get; set; } = _ => IdentityResult.Success; + public Func<(ApplicationUser User, string Password), IdentityResult> AddPassword { get; set; } = _ => IdentityResult.Success; + public Func GenerateToken { get; set; } = _ => string.Empty; + public Func<(ApplicationUser User, string Token, string Password), IdentityResult> ResetPassword { get; set; } = _ => IdentityResult.Success; + + public override Task FindByNameAsync(string userName) => Task.FromResult(FindByName(userName)); + public override Task FindByIdAsync(string userId) => Task.FromResult(FindById(userId)); + public override Task> GetRolesAsync(ApplicationUser user) => Task.FromResult(GetRoles(user)); + public override Task> GetClaimsAsync(ApplicationUser user) => Task.FromResult(GetClaims(user)); + public override Task GetUserAsync(ClaimsPrincipal principal) => Task.FromResult(GetUser(principal)); + public override Task ConfirmEmailAsync(ApplicationUser user, string token) => Task.FromResult(ConfirmEmail((user, token))); + public override Task RemovePasswordAsync(ApplicationUser user) => Task.FromResult(RemovePassword(user)); + public override Task AddPasswordAsync(ApplicationUser user, string password) => Task.FromResult(AddPassword((user, password))); + public override Task GeneratePasswordResetTokenAsync(ApplicationUser user) => Task.FromResult(GenerateToken(user)); + public override Task ResetPasswordAsync(ApplicationUser user, string token, string newPassword) => Task.FromResult(ResetPassword((user, token, newPassword))); +} + //internal sealed class FakeEmailService : IEmailService //{ // public List Messages { get; } = []; diff --git a/SecurityService.UnitTests/Infrastructure/IdentityMocksTests.cs b/SecurityService.UnitTests/Infrastructure/IdentityMocksTests.cs new file mode 100644 index 00000000..8bcee62b --- /dev/null +++ b/SecurityService.UnitTests/Infrastructure/IdentityMocksTests.cs @@ -0,0 +1,29 @@ +using Imposter.Abstractions; +using SecurityService.Database.Entities; +using Shouldly; + +namespace SecurityService.UnitTests.Infrastructure; + +public class IdentityMocksTests +{ + [Fact] + public async Task UserManagerDouble_UsesConfiguredArgumentMatchers() + { + var user = new ApplicationUser { UserName = "alice" }; + var userManager = IdentityMocks.CreateUserManager(); + + userManager.FindByNameAsync("alice").ReturnsAsync(user); + + (await userManager.Instance().FindByNameAsync("bob")).ShouldBeNull(); + (await userManager.Instance().FindByNameAsync("alice")).ShouldBeSameAs(user); + } + + [Fact] + public void UserManagerDouble_CalledFailsWhenCountDoesNotMatch() + { + var userManager = IdentityMocks.CreateUserManager(); + + Should.Throw(() => + userManager.FindByNameAsync("alice").Called(Count.Once())); + } +} diff --git a/SecurityService.UnitTests/Oidc/OidcEndpointTests.cs b/SecurityService.UnitTests/Oidc/OidcEndpointTests.cs index aee7b456..65f98f75 100644 --- a/SecurityService.UnitTests/Oidc/OidcEndpointTests.cs +++ b/SecurityService.UnitTests/Oidc/OidcEndpointTests.cs @@ -3,7 +3,7 @@ using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; -using Moq; +using Imposter.Abstractions; using OpenIddict.Validation.AspNetCore; using SecurityService.BusinessLogic.Oidc; using SecurityService.Database; @@ -29,9 +29,9 @@ public async Task UserInfoAsync_WithValidPrincipal_ReturnsJsonPayload() GivenName = "Alice", FamilyName = "Example" }; - userManager.Setup(manager => manager.FindByIdAsync("user-1")).ReturnsAsync(user); - userManager.Setup(manager => manager.GetRolesAsync(user)).ReturnsAsync(["Admin"]); - userManager.Setup(manager => manager.GetClaimsAsync(user)).ReturnsAsync([]); + userManager.FindByIdAsync("user-1").ReturnsAsync(user); + userManager.GetRolesAsync(user).ReturnsAsync(["Admin"]); + userManager.GetClaimsAsync(user).ReturnsAsync([]); var identity = new ClaimsIdentity("Test"); identity.AddClaim(new Claim(OpenIddict.Abstractions.OpenIddictConstants.Claims.Subject, "user-1")); @@ -48,19 +48,19 @@ public async Task UserInfoAsync_WithValidPrincipal_ReturnsJsonPayload() }; var signInManager = IdentityMocks.CreateSignInManager(userManager); - var appManager = new Mock(); - var authManager = new Mock(); - var scopeManager = new Mock(); + var appManager = new IOpenIddictApplicationManagerImposter(); + var authManager = new IOpenIddictAuthorizationManagerImposter(); + var scopeManager = new IOpenIddictScopeManagerImposter(); using var provider = TestServiceProviderFactory.Create(nameof(this.UserInfoAsync_WithValidPrincipal_ReturnsJsonPayload)); using var scope = provider.CreateScope(); var dbContext = scope.ServiceProvider.GetRequiredService(); var handler = new OidcRequestHandler( - userManager.Object, - signInManager.Object, - appManager.Object, - authManager.Object, - scopeManager.Object, + userManager.Instance(), + signInManager.Instance(), + appManager.Instance(), + authManager.Instance(), + scopeManager.Instance(), dbContext); var result = await handler.Handle(new OidcCommands.UserInfoCommand(context), CancellationToken.None); @@ -121,8 +121,8 @@ public async Task CreatePrincipal_WhenUserClaimsDuplicateBuiltInClaims_DoesNotDu FamilyName = "User" }; - userManager.Setup(manager => manager.GetRolesAsync(user)).ReturnsAsync(["Merchant"]); - userManager.Setup(manager => manager.GetClaimsAsync(user)).ReturnsAsync( + userManager.GetRolesAsync(user).ReturnsAsync(["Merchant"]); + userManager.GetClaimsAsync(user).ReturnsAsync( [ new Claim(OpenIddictConstants.Claims.Email, "merchantuser@stagingmerchant1.co.uk"), new Claim(OpenIddictConstants.Claims.GivenName, "Staging Merchant 1"), @@ -132,7 +132,7 @@ public async Task CreatePrincipal_WhenUserClaimsDuplicateBuiltInClaims_DoesNotDu var principal = await OidcHelpers.CreatePrincipal( user, - userManager.Object, + userManager.Instance(), [OpenIddictConstants.Scopes.Email, OpenIddictConstants.Scopes.Profile, OpenIddictConstants.Scopes.Roles], ["transactionProcessor"], authorizationId: null); diff --git a/SecurityService.UnitTests/Pages/ChangePasswordPageModelTests.cs b/SecurityService.UnitTests/Pages/ChangePasswordPageModelTests.cs index 5a7a9d32..219924ce 100644 --- a/SecurityService.UnitTests/Pages/ChangePasswordPageModelTests.cs +++ b/SecurityService.UnitTests/Pages/ChangePasswordPageModelTests.cs @@ -2,7 +2,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; -using Moq; +using Imposter.Abstractions; using SecurityService.BusinessLogic.Requests; using SecurityService.Models; using Shouldly; @@ -15,7 +15,7 @@ public class ChangePasswordPageModelTests [Fact] public void OnGet_WithReturnUrlAndClientIdQuery_PopulatesInput() { - var mediator = new Mock(); + var mediator = new IMediatorImposter(); var httpContext = new DefaultHttpContext(); httpContext.Request.QueryString = new QueryString("?clientId=test-client-id"); @@ -31,7 +31,7 @@ public void OnGet_WithReturnUrlAndClientIdQuery_PopulatesInput() [Fact] public async Task OnPost_WhenPasswordsDoNotMatch_ReturnsPageAndDoesNotCallMediator() { - var mediator = new Mock(MockBehavior.Strict); + var mediator = new IMediatorImposter(ImposterMode.Explicit); var model = CreateModel(mediator, new DefaultHttpContext()); model.Input = new SecurityService.Pages.Account.ChangePassword.IndexInputModel { @@ -47,16 +47,16 @@ public async Task OnPost_WhenPasswordsDoNotMatch_ReturnsPageAndDoesNotCallMediat result.ShouldBeOfType(); model.ModelState[string.Empty]!.Errors.ShouldContain(error => error.ErrorMessage == "New Password does not match Confirm Password"); - mediator.Verify(instance => instance.Send(It.IsAny(), It.IsAny()), Times.Never); + mediator.Send(Arg>>.Any(), Arg.Any()).Called(Count.Never()); } [Fact] public async Task OnPost_WhenMediatorFails_ReturnsPageWithGenericError() { - var mediator = new Mock(); - mediator.Setup(instance => instance.Send( - It.IsAny(), - It.IsAny())) + var mediator = new IMediatorImposter(); + mediator.Send( + Arg>>.Any(), + Arg.Any()) .ReturnsAsync(Result.Failure("boom")); var model = CreateModel(mediator, new DefaultHttpContext()); @@ -79,20 +79,21 @@ public async Task OnPost_WhenMediatorFails_ReturnsPageWithGenericError() [Fact] public async Task OnPost_WhenMediatorSucceeds_RedirectsToReturnedUriAndSendsExpectedCommand() { - var mediator = new Mock(); + var mediator = new IMediatorImposter(); SecurityServiceCommands.ChangeUserPasswordCommand? capturedCommand = null; - mediator.Setup(instance => instance.Send( - It.IsAny(), - It.IsAny())) - .Callback>, CancellationToken>((request, _) => - { - capturedCommand = request.ShouldBeOfType(); - }) + mediator.Send( + Arg>>.Any(), + Arg.Any()) .ReturnsAsync(Result.Success(new ChangeUserPasswordResult { IsSuccessful = true, RedirectUri = "http://localhost/app" - })); + })) + .Callback((request, _) => + { + capturedCommand = request.ShouldBeOfType(); + return default!; + }); var httpContext = new DefaultHttpContext(); httpContext.Request.QueryString = new QueryString("?clientId=query-client-id"); @@ -122,9 +123,9 @@ public async Task OnPost_WhenMediatorSucceeds_RedirectsToReturnedUriAndSendsExpe capturedCommand.ClientId.ShouldBe("query-client-id"); } - private static SecurityService.Pages.Account.ChangePassword.IndexModel CreateModel(Mock mediator, HttpContext httpContext) + private static SecurityService.Pages.Account.ChangePassword.IndexModel CreateModel(IMediatorImposter mediator, HttpContext httpContext) { - return new SecurityService.Pages.Account.ChangePassword.IndexModel(mediator.Object) + return new SecurityService.Pages.Account.ChangePassword.IndexModel(mediator.Instance()) { PageContext = new Microsoft.AspNetCore.Mvc.RazorPages.PageContext { diff --git a/SecurityService.UnitTests/Pages/ConfirmEmailPageModelTests.cs b/SecurityService.UnitTests/Pages/ConfirmEmailPageModelTests.cs index 0fca008c..4ee9862f 100644 --- a/SecurityService.UnitTests/Pages/ConfirmEmailPageModelTests.cs +++ b/SecurityService.UnitTests/Pages/ConfirmEmailPageModelTests.cs @@ -4,7 +4,7 @@ using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.Extensions.DependencyInjection; -using Moq; +using Imposter.Abstractions; using SecurityService.BusinessLogic; using SecurityService.UnitTests.Infrastructure; using Shouldly; @@ -23,13 +23,13 @@ public async Task OnGet_WithQueryValues_PopulatesInput() Email = "alice@example.com" }; var userManager = IdentityMocks.CreateUserManager(); - userManager.Setup(instance => instance.FindByNameAsync("alice")) + userManager.FindByNameAsync("alice") .ReturnsAsync(user); - userManager.Setup(instance => instance.ConfirmEmailAsync(user, "token-123")) + userManager.ConfirmEmailAsync(user, "token-123") .ReturnsAsync(IdentityResult.Success); - userManager.Setup(instance => instance.RemovePasswordAsync(user)) + userManager.RemovePasswordAsync(user) .ReturnsAsync(IdentityResult.Success); - userManager.Setup(instance => instance.AddPasswordAsync(user, It.IsAny())) + userManager.AddPasswordAsync(user, Arg.Any()) .ReturnsAsync(IdentityResult.Success); var signInManager = IdentityMocks.CreateSignInManager(userManager); @@ -59,7 +59,7 @@ public async Task OnGet_WithMissingQueryValues_ReturnsPageAndDoesNotCallMediator result.ShouldBeOfType(); model.View.UserMessage.ShouldBe("The email confirmation link is invalid."); - userManager.Verify(instance => instance.FindByNameAsync(It.IsAny()), Times.Never); + userManager.FindByNameAsync(Arg.Any()).Called(Count.Never()); } [Fact] @@ -71,9 +71,9 @@ public async Task OnGet_WhenConfirmationFails_ReturnsPageWithFailureMessage() Email = "alice@example.com" }; var userManager = IdentityMocks.CreateUserManager(); - userManager.Setup(instance => instance.FindByNameAsync("alice")) + userManager.FindByNameAsync("alice") .ReturnsAsync(user); - userManager.Setup(instance => instance.ConfirmEmailAsync(user, "token-123")) + userManager.ConfirmEmailAsync(user, "token-123") .ReturnsAsync(IdentityResult.Failed(new IdentityError { Description = "failed" })); var signInManager = IdentityMocks.CreateSignInManager(userManager); @@ -89,9 +89,9 @@ public async Task OnGet_WhenConfirmationFails_ReturnsPageWithFailureMessage() model.View.UserMessage.ShouldBe("Failed confirming user email address for username alice"); model.Input.Username.ShouldBe("alice"); model.Input.Token.ShouldBe("token-123"); - userManager.Verify(instance => instance.ConfirmEmailAsync(user, "token-123"), Times.Once); - userManager.Verify(instance => instance.RemovePasswordAsync(It.IsAny()), Times.Never); - userManager.Verify(instance => instance.AddPasswordAsync(It.IsAny(), It.IsAny()), Times.Never); + userManager.ConfirmEmailAsync(user, "token-123").Called(Count.Once()); + userManager.RemovePasswordAsync(Arg.Any()).Called(Count.Never()); + userManager.AddPasswordAsync(Arg.Any(), Arg.Any()).Called(Count.Never()); } [Fact] @@ -103,13 +103,13 @@ public async Task OnGet_WhenConfirmationSucceeds_ReturnsPageAndSendsExpectedComm Email = "alice@example.com" }; var userManager = IdentityMocks.CreateUserManager(); - userManager.Setup(instance => instance.FindByNameAsync("alice")) + userManager.FindByNameAsync("alice") .ReturnsAsync(user); - userManager.Setup(instance => instance.ConfirmEmailAsync(user, "token-123")) + userManager.ConfirmEmailAsync(user, "token-123") .ReturnsAsync(IdentityResult.Success); - userManager.Setup(instance => instance.RemovePasswordAsync(user)) + userManager.RemovePasswordAsync(user) .ReturnsAsync(IdentityResult.Success); - userManager.Setup(instance => instance.AddPasswordAsync(user, It.IsAny())) + userManager.AddPasswordAsync(user, Arg.Any()) .ReturnsAsync(IdentityResult.Success); var signInManager = IdentityMocks.CreateSignInManager(userManager); @@ -126,19 +126,19 @@ public async Task OnGet_WhenConfirmationSucceeds_ReturnsPageAndSendsExpectedComm model.Input.Username.ShouldBe("alice"); model.Input.Token.ShouldBe("token-123"); model.View.UserMessage.ShouldBe("Thanks for confirming your email address, you should receive a welcome email soon."); - userManager.Verify(instance => instance.ConfirmEmailAsync(user, "token-123"), Times.Once); - userManager.Verify(instance => instance.RemovePasswordAsync(user), Times.Once); - userManager.Verify(instance => instance.AddPasswordAsync(user, It.IsAny()), Times.Once); + userManager.ConfirmEmailAsync(user, "token-123").Called(Count.Once()); + userManager.RemovePasswordAsync(user).Called(Count.Once()); + userManager.AddPasswordAsync(user, Arg.Any()).Called(Count.Once()); messagingClient.LastEmailRequest.ShouldNotBeNull(); messagingClient.LastEmailRequest.Subject.ShouldBe("Welcome to Transaction Processing"); messagingClient.LastEmailRequest.ToAddresses.ShouldContain("alice@example.com"); } private static ServiceProvider CreateProvider(string databaseName, - Mock> userManager, - Mock> signInManager) + UserManagerDouble userManager, + SignInManagerDouble signInManager) { - return TestServiceProviderFactory.Create(databaseName, userManager.Object, signInManager.Object); + return TestServiceProviderFactory.Create(databaseName, userManager.Instance(), signInManager.Instance()); } private static SecurityService.Pages.Account.ConfirmEmail.IndexModel CreateModel(ServiceProvider provider, HttpContext httpContext) diff --git a/SecurityService.UnitTests/Pages/ConsentPageModelTests.cs b/SecurityService.UnitTests/Pages/ConsentPageModelTests.cs index a6ddfcd7..eabcf839 100644 --- a/SecurityService.UnitTests/Pages/ConsentPageModelTests.cs +++ b/SecurityService.UnitTests/Pages/ConsentPageModelTests.cs @@ -2,7 +2,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; -using Moq; +using Imposter.Abstractions; using SecurityService.BusinessLogic.Oidc; using Shouldly; using SimpleResults; @@ -14,8 +14,8 @@ public class ConsentPageModelTests [Fact] public async Task OnGetAsync_WhenHandlerReturnsLocalRedirect_ReturnsLocalRedirectResult() { - var mediator = new Mock(); - mediator.Setup(m => m.Send(It.IsAny(), It.IsAny())) + var mediator = new IMediatorImposter(); + mediator.Send(Arg>>.Any(), Arg.Any()) .ReturnsAsync(Result.Success(new ConsentGetLocalRedirectResult("/return"))); var model = CreateModel(mediator, new DefaultHttpContext()); @@ -32,8 +32,8 @@ public async Task OnGetAsync_WhenHandlerReturnsPage_SetsPropertiesAndReturnsPage { var identityScopes = new[] { new ScopeDisplayItem("openid", "OpenID", null, true, false) }; var apiScopes = new[] { new ScopeDisplayItem("api", "API", null, false, false) }; - var mediator = new Mock(); - mediator.Setup(m => m.Send(It.IsAny(), It.IsAny())) + var mediator = new IMediatorImposter(); + mediator.Send(Arg>>.Any(), Arg.Any()) .ReturnsAsync(Result.Success(new ConsentGetPageResult("My App", identityScopes, apiScopes))); var model = CreateModel(mediator, new DefaultHttpContext()); @@ -51,13 +51,14 @@ public async Task OnGetAsync_SendsQueryWithCorrectReturnUrl() { OidcCommands.ConsentGetQuery? capturedQuery = null; - var mediator = new Mock(); - mediator.Setup(m => m.Send(It.IsAny(), It.IsAny())) - .Callback>, CancellationToken>((req, _) => + var mediator = new IMediatorImposter(); + mediator.Send(Arg>>.Any(), Arg.Any()) + .ReturnsAsync(Result.Success(new ConsentGetPageResult(string.Empty, [], []))) + .Callback((req, _) => { capturedQuery = req.ShouldBeOfType(); - }) - .ReturnsAsync(Result.Success(new ConsentGetPageResult(string.Empty, [], []))); + return default!; + }); var model = CreateModel(mediator, new DefaultHttpContext()); await model.OnGetAsync("/my-return", CancellationToken.None); @@ -69,8 +70,8 @@ public async Task OnGetAsync_SendsQueryWithCorrectReturnUrl() [Fact] public async Task OnPostAsync_WhenHandlerReturnsRedirect_ReturnsRedirectResult() { - var mediator = new Mock(); - mediator.Setup(m => m.Send(It.IsAny(), It.IsAny())) + var mediator = new IMediatorImposter(); + mediator.Send(Arg>>.Any(), Arg.Any()) .ReturnsAsync(Result.Success(new ConsentPostRedirectResult("/return?consent=denied"))); var model = CreateModel(mediator, new DefaultHttpContext()); @@ -90,8 +91,8 @@ public async Task OnPostAsync_WhenHandlerReturnsRedirect_ReturnsRedirectResult() [Fact] public async Task OnPostAsync_WhenHandlerReturnsPageWithError_AddsModelErrorAndReturnsPage() { - var mediator = new Mock(); - mediator.Setup(m => m.Send(It.IsAny(), It.IsAny())) + var mediator = new IMediatorImposter(); + mediator.Send(Arg>>.Any(), Arg.Any()) .ReturnsAsync(Result.Success(new ConsentPostPageResult("Select at least one scope to continue."))); var model = CreateModel(mediator, new DefaultHttpContext()); @@ -113,13 +114,14 @@ public async Task OnPostAsync_SendsCommandWithCorrectValues() { OidcCommands.ConsentPostCommand? capturedCommand = null; - var mediator = new Mock(); - mediator.Setup(m => m.Send(It.IsAny(), It.IsAny())) - .Callback>, CancellationToken>((req, _) => + var mediator = new IMediatorImposter(); + mediator.Send(Arg>>.Any(), Arg.Any()) + .ReturnsAsync(Result.Success(new ConsentPostRedirectResult("/return?consent=accepted"))) + .Callback((req, _) => { capturedCommand = req.ShouldBeOfType(); - }) - .ReturnsAsync(Result.Success(new ConsentPostRedirectResult("/return?consent=accepted"))); + return default!; + }); var model = CreateModel(mediator, new DefaultHttpContext()); model.Input = new SecurityService.Pages.Consent.IndexModel.InputModel @@ -138,9 +140,9 @@ public async Task OnPostAsync_SendsCommandWithCorrectValues() capturedCommand.SelectedScopes.ShouldContain("profile"); } - private static SecurityService.Pages.Consent.IndexModel CreateModel(Mock mediator, HttpContext httpContext) + private static SecurityService.Pages.Consent.IndexModel CreateModel(IMediatorImposter mediator, HttpContext httpContext) { - return new SecurityService.Pages.Consent.IndexModel(mediator.Object) + return new SecurityService.Pages.Consent.IndexModel(mediator.Instance()) { PageContext = new PageContext { diff --git a/SecurityService.UnitTests/Pages/ForgotPasswordPageModelTests.cs b/SecurityService.UnitTests/Pages/ForgotPasswordPageModelTests.cs index 3814e633..1353aad6 100644 --- a/SecurityService.UnitTests/Pages/ForgotPasswordPageModelTests.cs +++ b/SecurityService.UnitTests/Pages/ForgotPasswordPageModelTests.cs @@ -5,7 +5,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.Extensions.DependencyInjection; -using Moq; +using Imposter.Abstractions; using SecurityService.BusinessLogic; using SecurityService.UnitTests.Infrastructure; using Shouldly; @@ -51,7 +51,7 @@ public async Task OnPost_WhenCancelButtonSelected_RedirectsToLoginAndDoesNotCall var redirect = result.ShouldBeOfType(); redirect.Url.ShouldBe("Login/Index"); - userManager.Verify(instance => instance.FindByNameAsync(It.IsAny()), Times.Never); + userManager.FindByNameAsync(Arg.Any()).Called(Count.Never()); } [Fact] @@ -63,9 +63,9 @@ public async Task OnPost_WhenMediatorPathSucceeds_ReturnsPageAndSendsResetEmail( Email = "alice@example.com" }; var userManager = IdentityMocks.CreateUserManager(); - userManager.Setup(instance => instance.FindByNameAsync("alice")) + userManager.FindByNameAsync("alice") .ReturnsAsync(user); - userManager.Setup(instance => instance.GeneratePasswordResetTokenAsync(user)) + userManager.GeneratePasswordResetTokenAsync(user) .ReturnsAsync("token+/="); var signInManager = IdentityMocks.CreateSignInManager(userManager); @@ -82,8 +82,8 @@ public async Task OnPost_WhenMediatorPathSucceeds_ReturnsPageAndSendsResetEmail( result.ShouldBeOfType(); model.View.UserMessage.ShouldBe("Password Reset sent, please check your registered email for further instructions."); - userManager.Verify(instance => instance.FindByNameAsync("alice"), Times.Once); - userManager.Verify(instance => instance.GeneratePasswordResetTokenAsync(user), Times.Once); + userManager.FindByNameAsync("alice").Called(Count.Once()); + userManager.GeneratePasswordResetTokenAsync(user).Called(Count.Once()); messagingClient.LastEmailRequest.ShouldNotBeNull(); messagingClient.LastEmailRequest.Subject.ShouldBe("Password Reset Requested"); messagingClient.LastEmailRequest.ToAddresses.ShouldContain("alice@example.com"); @@ -94,7 +94,7 @@ public async Task OnPost_WhenMediatorPathSucceeds_ReturnsPageAndSendsResetEmail( public async Task OnPost_WhenUserIsMissing_ReturnsPageWithGenericMessageAndDoesNotSendEmail() { var userManager = IdentityMocks.CreateUserManager(); - userManager.Setup(instance => instance.FindByNameAsync("alice")) + userManager.FindByNameAsync("alice") .ReturnsAsync((ApplicationUser?)null); var signInManager = IdentityMocks.CreateSignInManager(userManager); @@ -111,16 +111,16 @@ public async Task OnPost_WhenUserIsMissing_ReturnsPageWithGenericMessageAndDoesN result.ShouldBeOfType(); model.View.UserMessage.ShouldBe("Password Reset sent, please check your registered email for further instructions."); - userManager.Verify(instance => instance.FindByNameAsync("alice"), Times.Once); - userManager.Verify(instance => instance.GeneratePasswordResetTokenAsync(It.IsAny()), Times.Never); + userManager.FindByNameAsync("alice").Called(Count.Once()); + userManager.GeneratePasswordResetTokenAsync(Arg.Any()).Called(Count.Never()); messagingClient.LastEmailRequest.ShouldBeNull(); } private static ServiceProvider CreateProvider(string databaseName, - Mock> userManager, - Mock> signInManager) + UserManagerDouble userManager, + SignInManagerDouble signInManager) { - return TestServiceProviderFactory.Create(databaseName, userManager.Object, signInManager.Object); + return TestServiceProviderFactory.Create(databaseName, userManager.Instance(), signInManager.Instance()); } private static SecurityService.Pages.Account.ForgotPassword.IndexModel CreateModel(ServiceProvider provider, HttpContext httpContext) diff --git a/SecurityService.UnitTests/Pages/GrantsPageModelTests.cs b/SecurityService.UnitTests/Pages/GrantsPageModelTests.cs index cc04d3fa..d20cc4da 100644 --- a/SecurityService.UnitTests/Pages/GrantsPageModelTests.cs +++ b/SecurityService.UnitTests/Pages/GrantsPageModelTests.cs @@ -3,7 +3,7 @@ using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; -using Moq; +using Imposter.Abstractions; using SecurityService.BusinessLogic.Requests; using SecurityService.Database.Entities; using SecurityService.Models; @@ -19,10 +19,10 @@ public class GrantsPageModelTests public async Task OnGetAsync_WhenUserNotFound_RedirectsToLogin() { var userManager = IdentityMocks.CreateUserManager(); - userManager.Setup(m => m.GetUserAsync(It.IsAny())) + userManager.GetUserAsync(Arg.Any()) .ReturnsAsync((ApplicationUser?)null); - var mediator = new Mock(); + var mediator = new IMediatorImposter(); var model = CreateModel(userManager, mediator, new DefaultHttpContext()); var result = await model.OnGetAsync(CancellationToken.None); @@ -35,7 +35,7 @@ public async Task OnGetAsync_WhenUserFound_QueriesGrantsAndReturnsPage() { var user = new ApplicationUser { Id = "user-1" }; var userManager = IdentityMocks.CreateUserManager(); - userManager.Setup(m => m.GetUserAsync(It.IsAny())) + userManager.GetUserAsync(Arg.Any()) .ReturnsAsync(user); var grants = new List @@ -43,8 +43,8 @@ public async Task OnGetAsync_WhenUserFound_QueriesGrantsAndReturnsPage() new GrantDetails("auth-1", "client-1", "Client One", new[] { "openid" }, DateTimeOffset.UtcNow) }; - var mediator = new Mock(); - mediator.Setup(m => m.Send(It.Is(q => q.UserId == "user-1"), It.IsAny())) + var mediator = new IMediatorImposter(); + mediator.Send(Arg>>>.Is(q => q is SecurityServiceQueries.GetUserGrantsQuery query && query.UserId == "user-1"), Arg.Any()) .ReturnsAsync(Result.Success(grants)); var model = CreateModel(userManager, mediator, new DefaultHttpContext()); @@ -53,23 +53,23 @@ public async Task OnGetAsync_WhenUserFound_QueriesGrantsAndReturnsPage() result.ShouldBeOfType(); model.Grants.ShouldHaveSingleItem(); - mediator.Verify(m => m.Send(It.Is(q => q.UserId == "user-1"), It.IsAny()), Times.Once); + mediator.Send(Arg>>>.Is(q => q is SecurityServiceQueries.GetUserGrantsQuery query && query.UserId == "user-1"), Arg.Any()).Called(Count.Once()); } [Fact] public async Task OnPostRevokeAsync_WhenUserNotFound_RedirectsToLogin() { var userManager = IdentityMocks.CreateUserManager(); - userManager.Setup(m => m.GetUserAsync(It.IsAny())) + userManager.GetUserAsync(Arg.Any()) .ReturnsAsync((ApplicationUser?)null); - var mediator = new Mock(MockBehavior.Strict); + var mediator = new IMediatorImposter(ImposterMode.Explicit); var model = CreateModel(userManager, mediator, new DefaultHttpContext()); var result = await model.OnPostRevokeAsync("auth-1", CancellationToken.None); result.ShouldBeOfType(); - mediator.Verify(m => m.Send(It.IsAny(), It.IsAny()), Times.Never); + mediator.Send(Arg.Any(), Arg.Any()).Called(Count.Never()); } [Fact] @@ -77,11 +77,11 @@ public async Task OnPostRevokeAsync_WhenRevokeSucceeds_RedirectsToPage() { var user = new ApplicationUser { Id = "user-1" }; var userManager = IdentityMocks.CreateUserManager(); - userManager.Setup(m => m.GetUserAsync(It.IsAny())) + userManager.GetUserAsync(Arg.Any()) .ReturnsAsync(user); - var mediator = new Mock(); - mediator.Setup(m => m.Send(It.IsAny(), It.IsAny())) + var mediator = new IMediatorImposter(); + mediator.Send(Arg>.Any(), Arg.Any()) .ReturnsAsync(Result.Success()); var model = CreateModel(userManager, mediator, new DefaultHttpContext()); @@ -96,13 +96,13 @@ public async Task OnPostRevokeAsync_WhenRevokeFails_ReturnsPageWithStatusMessage { var user = new ApplicationUser { Id = "user-1" }; var userManager = IdentityMocks.CreateUserManager(); - userManager.Setup(m => m.GetUserAsync(It.IsAny())) + userManager.GetUserAsync(Arg.Any()) .ReturnsAsync(user); - var mediator = new Mock(); - mediator.Setup(m => m.Send(It.IsAny(), It.IsAny())) + var mediator = new IMediatorImposter(); + mediator.Send(Arg>.Any(), Arg.Any()) .ReturnsAsync(Result.Failure("The authorization could not be revoked.")); - mediator.Setup(m => m.Send(It.IsAny(), It.IsAny())) + mediator.Send(Arg>>>.Any(), Arg.Any()) .ReturnsAsync(Result.Success(new List())); var model = CreateModel(userManager, mediator, new DefaultHttpContext()); @@ -114,11 +114,11 @@ public async Task OnPostRevokeAsync_WhenRevokeFails_ReturnsPageWithStatusMessage } private static SecurityService.Pages.Account.Grants.IndexModel CreateModel( - Mock> userManager, - Mock mediator, + UserManagerDouble userManager, + IMediatorImposter mediator, HttpContext httpContext) { - return new SecurityService.Pages.Account.Grants.IndexModel(userManager.Object, mediator.Object) + return new SecurityService.Pages.Account.Grants.IndexModel(userManager.Instance(), mediator.Instance()) { PageContext = new PageContext { diff --git a/SecurityService.UnitTests/Pages/LoginPageModelTests.cs b/SecurityService.UnitTests/Pages/LoginPageModelTests.cs index 4eccd309..371e48c0 100644 --- a/SecurityService.UnitTests/Pages/LoginPageModelTests.cs +++ b/SecurityService.UnitTests/Pages/LoginPageModelTests.cs @@ -1,7 +1,7 @@ using MediatR; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Moq; +using Imposter.Abstractions; using SecurityService.BusinessLogic.Requests; using SecurityService.Models; using SecurityService.UnitTests.Infrastructure; @@ -15,8 +15,8 @@ public class LoginPageModelTests [Fact] public async Task OnPost_WhenForgotPasswordSelected_RedirectsToForgotPasswordWithReturnUrlAndClientId() { - var mediator = new Mock(); - mediator.Setup(m => m.Send(It.IsAny(), It.IsAny())) + var mediator = new IMediatorImposter(); + mediator.Send(Arg>>>.Any(), Arg.Any()) .ReturnsAsync(Result.Success(new List())); var model = CreateModel(mediator, new DefaultHttpContext()); @@ -34,12 +34,12 @@ public async Task OnPost_WhenForgotPasswordSelected_RedirectsToForgotPasswordWit redirect.RouteValues.ShouldNotBeNull(); redirect.RouteValues["returnUrl"].ShouldBe("/connect/authorize?client_id=test-client-id"); redirect.RouteValues["clientId"].ShouldBe("test-client-id"); - mediator.Verify(m => m.Send(It.IsAny(), It.IsAny()), Times.Never); + mediator.Send(Arg.Any(), Arg.Any()).Called(Count.Never()); } - private static SecurityService.Pages.Account.Login.IndexModel CreateModel(Mock mediator, HttpContext httpContext) + private static SecurityService.Pages.Account.Login.IndexModel CreateModel(IMediatorImposter mediator, HttpContext httpContext) { - return new SecurityService.Pages.Account.Login.IndexModel(mediator.Object) + return new SecurityService.Pages.Account.Login.IndexModel(mediator.Instance()) { PageContext = new Microsoft.AspNetCore.Mvc.RazorPages.PageContext { @@ -48,4 +48,3 @@ private static SecurityService.Pages.Account.Login.IndexModel CreateModel(Mock(); - userManager.Verify(instance => instance.FindByNameAsync(It.IsAny()), Times.Never); + userManager.FindByNameAsync(Arg.Any()).Called(Count.Never()); } [Fact] @@ -76,14 +76,14 @@ public async Task OnPost_WhenPasswordsDoNotMatch_ReturnsPageAndDoesNotCallMediat result.ShouldBeOfType(); model.ModelState[string.Empty]!.Errors.ShouldContain(error => error.ErrorMessage == "Password does not match Confirm Password"); - userManager.Verify(instance => instance.FindByNameAsync(It.IsAny()), Times.Never); + userManager.FindByNameAsync(Arg.Any()).Called(Count.Never()); } [Fact] public async Task OnPost_WhenMediatorPathFails_ReturnsPageWithFailureMessage() { var userManager = IdentityMocks.CreateUserManager(); - userManager.Setup(instance => instance.FindByNameAsync("alice")) + userManager.FindByNameAsync("alice") .ReturnsAsync((ApplicationUser?)null); var signInManager = IdentityMocks.CreateSignInManager(userManager); @@ -102,8 +102,8 @@ public async Task OnPost_WhenMediatorPathFails_ReturnsPageWithFailureMessage() result.ShouldBeOfType(); model.View.UserMessage.ShouldContain("Failed processing password reset for username alice"); - userManager.Verify(instance => instance.FindByNameAsync("alice"), Times.Once); - userManager.Verify(instance => instance.ResetPasswordAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + userManager.FindByNameAsync("alice").Called(Count.Once()); + userManager.ResetPasswordAsync(Arg.Any(), Arg.Any(), Arg.Any()).Called(Count.Never()); } [Fact] @@ -115,9 +115,9 @@ public async Task OnPost_WhenMediatorPathSucceeds_RedirectsToClientUri() Email = "alice@example.com" }; var userManager = IdentityMocks.CreateUserManager(); - userManager.Setup(instance => instance.FindByNameAsync("alice")) + userManager.FindByNameAsync("alice") .ReturnsAsync(user); - userManager.Setup(instance => instance.ResetPasswordAsync(user, "token-123", "NewPassword1!")) + userManager.ResetPasswordAsync(user, "token-123", "NewPassword1!") .ReturnsAsync(IdentityResult.Success); var signInManager = IdentityMocks.CreateSignInManager(userManager); @@ -138,8 +138,8 @@ public async Task OnPost_WhenMediatorPathSucceeds_RedirectsToClientUri() var redirect = result.ShouldBeOfType(); redirect.Url.ShouldBe("http://localhost/app"); - userManager.Verify(instance => instance.FindByNameAsync("alice"), Times.Once); - userManager.Verify(instance => instance.ResetPasswordAsync(user, "token-123", "NewPassword1!"), Times.Once); + userManager.FindByNameAsync("alice").Called(Count.Once()); + userManager.ResetPasswordAsync(user, "token-123", "NewPassword1!").Called(Count.Once()); } private static async Task SeedClientAsync(ServiceProvider provider, string clientId, string clientUri) @@ -158,10 +158,10 @@ private static async Task SeedClientAsync(ServiceProvider provider, string clien } private static ServiceProvider CreateProvider(string databaseName, - Mock> userManager, - Mock> signInManager) + UserManagerDouble userManager, + SignInManagerDouble signInManager) { - return TestServiceProviderFactory.Create(databaseName, userManager.Object, signInManager.Object); + return TestServiceProviderFactory.Create(databaseName, userManager.Instance(), signInManager.Instance()); } private static SecurityService.Pages.Account.ResetPassword.IndexModel CreateModel(ServiceProvider provider, HttpContext httpContext) diff --git a/SecurityService.UnitTests/Pages/VerifyPageModelTests.cs b/SecurityService.UnitTests/Pages/VerifyPageModelTests.cs index 49adb115..1015db24 100644 --- a/SecurityService.UnitTests/Pages/VerifyPageModelTests.cs +++ b/SecurityService.UnitTests/Pages/VerifyPageModelTests.cs @@ -4,7 +4,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; -using Moq; +using Imposter.Abstractions; using SecurityService.BusinessLogic.Oidc; using Shouldly; using SimpleResults; @@ -16,8 +16,8 @@ public class VerifyPageModelTests [Fact] public async Task OnGetAsync_WhenHandlerReturnsRedirect_ReturnsRedirectResult() { - var mediator = new Mock(); - mediator.Setup(m => m.Send(It.IsAny(), It.IsAny())) + var mediator = new IMediatorImposter(); + mediator.Send(Arg>>.Any(), Arg.Any()) .ReturnsAsync(Result.Success(new VerifyGetRedirectResult("/Account/Login?returnUrl=%2Fconnect%2Fverify"))); var model = CreateModel(mediator, new DefaultHttpContext()); @@ -31,8 +31,8 @@ public async Task OnGetAsync_WhenHandlerReturnsRedirect_ReturnsRedirectResult() [Fact] public async Task OnGetAsync_WhenHandlerReturnsPageWithStatusMessage_SetsStatusMessageAndReturnsPage() { - var mediator = new Mock(); - mediator.Setup(m => m.Send(It.IsAny(), It.IsAny())) + var mediator = new IMediatorImposter(); + mediator.Send(Arg>>.Any(), Arg.Any()) .ReturnsAsync(Result.Success(new VerifyGetPageResult("The specified user code is invalid.", null))); var model = CreateModel(mediator, new DefaultHttpContext()); @@ -55,8 +55,8 @@ [new ScopeDisplayItem("openid", "OpenID", null, true, false)], [], "ABCD-1234"); - var mediator = new Mock(); - mediator.Setup(m => m.Send(It.IsAny(), It.IsAny())) + var mediator = new IMediatorImposter(); + mediator.Send(Arg>>.Any(), Arg.Any()) .ReturnsAsync(Result.Success(new VerifyGetPageResult(string.Empty, displayData))); var model = CreateModel(mediator, new DefaultHttpContext()); @@ -77,13 +77,14 @@ public async Task OnGetAsync_SendsQueryWithCorrectHttpContext() var httpContext = new DefaultHttpContext(); OidcCommands.VerifyGetQuery? capturedQuery = null; - var mediator = new Mock(); - mediator.Setup(m => m.Send(It.IsAny(), It.IsAny())) - .Callback>, CancellationToken>((req, _) => + var mediator = new IMediatorImposter(); + mediator.Send(Arg>>.Any(), Arg.Any()) + .ReturnsAsync(Result.Success(new VerifyGetPageResult(string.Empty, null))) + .Callback((req, _) => { capturedQuery = req.ShouldBeOfType(); - }) - .ReturnsAsync(Result.Success(new VerifyGetPageResult(string.Empty, null))); + return default!; + }); var model = CreateModel(mediator, httpContext); await model.OnGetAsync(CancellationToken.None); @@ -95,8 +96,8 @@ public async Task OnGetAsync_SendsQueryWithCorrectHttpContext() [Fact] public async Task OnPostAsync_WhenHandlerReturnsRedirect_ReturnsRedirectResult() { - var mediator = new Mock(); - mediator.Setup(m => m.Send(It.IsAny(), It.IsAny())) + var mediator = new IMediatorImposter(); + mediator.Send(Arg>>.Any(), Arg.Any()) .ReturnsAsync(Result.Success(new VerifyPostRedirectResult("/connect/verify?user_code=ABCD-1234"))); var model = CreateModel(mediator, new DefaultHttpContext()); @@ -115,8 +116,8 @@ public async Task OnPostAsync_WhenHandlerReturnsRedirect_ReturnsRedirectResult() [Fact] public async Task OnPostAsync_WhenHandlerReturnsForbid_ReturnsForbidResult() { - var mediator = new Mock(); - mediator.Setup(m => m.Send(It.IsAny(), It.IsAny())) + var mediator = new IMediatorImposter(); + mediator.Send(Arg>>.Any(), Arg.Any()) .ReturnsAsync(Result.Success(new VerifyPostForbidResult("OpenIddict.Server.AspNetCore"))); var model = CreateModel(mediator, new DefaultHttpContext()); @@ -133,8 +134,8 @@ public async Task OnPostAsync_WhenHandlerReturnsSignIn_ReturnsSignInResult() var principal = new ClaimsPrincipal(new ClaimsIdentity()); var properties = new AuthenticationProperties { RedirectUri = "/" }; - var mediator = new Mock(); - mediator.Setup(m => m.Send(It.IsAny(), It.IsAny())) + var mediator = new IMediatorImposter(); + mediator.Send(Arg>>.Any(), Arg.Any()) .ReturnsAsync(Result.Success(new VerifyPostSignInResult(principal, properties, "OpenIddict.Server.AspNetCore"))); var model = CreateModel(mediator, new DefaultHttpContext()); @@ -150,8 +151,8 @@ public async Task OnPostAsync_WhenHandlerReturnsSignIn_ReturnsSignInResult() [Fact] public async Task OnPostAsync_WhenHandlerReturnsPageWithError_AddsModelErrorAndReturnsPage() { - var mediator = new Mock(); - mediator.Setup(m => m.Send(It.IsAny(), It.IsAny())) + var mediator = new IMediatorImposter(); + mediator.Send(Arg>>.Any(), Arg.Any()) .ReturnsAsync(Result.Success(new VerifyPostPageResult("Enter the user code shown on the device.", null))); var model = CreateModel(mediator, new DefaultHttpContext()); @@ -168,13 +169,14 @@ public async Task OnPostAsync_SendsCommandWithCorrectActionAndUserCode() { OidcCommands.VerifyPostCommand? capturedCommand = null; - var mediator = new Mock(); - mediator.Setup(m => m.Send(It.IsAny(), It.IsAny())) - .Callback>, CancellationToken>((req, _) => + var mediator = new IMediatorImposter(); + mediator.Send(Arg>>.Any(), Arg.Any()) + .ReturnsAsync(Result.Success(new VerifyPostRedirectResult("/connect/verify?user_code=ABCD-1234"))) + .Callback((req, _) => { capturedCommand = req.ShouldBeOfType(); - }) - .ReturnsAsync(Result.Success(new VerifyPostRedirectResult("/connect/verify?user_code=ABCD-1234"))); + return default!; + }); var model = CreateModel(mediator, new DefaultHttpContext()); model.Input = new SecurityService.Pages.Connect.VerifyModel.InputModel @@ -190,9 +192,9 @@ public async Task OnPostAsync_SendsCommandWithCorrectActionAndUserCode() capturedCommand.UserCode.ShouldBe("ABCD-1234"); } - private static SecurityService.Pages.Connect.VerifyModel CreateModel(Mock mediator, HttpContext httpContext) + private static SecurityService.Pages.Connect.VerifyModel CreateModel(IMediatorImposter mediator, HttpContext httpContext) { - return new SecurityService.Pages.Connect.VerifyModel(mediator.Object) + return new SecurityService.Pages.Connect.VerifyModel(mediator.Instance()) { PageContext = new PageContext { diff --git a/SecurityService.UnitTests/RequestHandlers/ConsentRequestHandlerTests.cs b/SecurityService.UnitTests/RequestHandlers/ConsentRequestHandlerTests.cs index 7dcc7a1a..93d1a436 100644 --- a/SecurityService.UnitTests/RequestHandlers/ConsentRequestHandlerTests.cs +++ b/SecurityService.UnitTests/RequestHandlers/ConsentRequestHandlerTests.cs @@ -1,6 +1,6 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; -using Moq; +using Imposter.Abstractions; using OpenIddict.Abstractions; using SecurityService.BusinessLogic.Oidc; using SecurityService.BusinessLogic.RequestHandlers; @@ -15,12 +15,12 @@ public class ConsentRequestHandlerTests [Fact] public async Task ConsentGetQuery_WhenNoOpenIddictServerRequest_ReturnsLocalRedirect() { - var appManager = new Mock(); + var appManager = new IOpenIddictApplicationManagerImposter(); using var serviceProvider = TestServiceProviderFactory.Create(nameof(ConsentGetQuery_WhenNoOpenIddictServerRequest_ReturnsLocalRedirect)); using var scope = serviceProvider.CreateScope(); var dbContext = scope.ServiceProvider.GetRequiredService(); - var handler = new ConsentRequestHandler(appManager.Object, dbContext); + var handler = new ConsentRequestHandler(appManager.Instance(), dbContext); var context = new DefaultHttpContext(); var result = await handler.Handle(new OidcCommands.ConsentGetQuery(context, "/return"), CancellationToken.None); @@ -33,12 +33,12 @@ public async Task ConsentGetQuery_WhenNoOpenIddictServerRequest_ReturnsLocalRedi [Fact] public async Task ConsentPostCommand_WhenDenyButton_ReturnsRedirectWithDenied() { - var appManager = new Mock(); + var appManager = new IOpenIddictApplicationManagerImposter(); using var serviceProvider = TestServiceProviderFactory.Create(nameof(ConsentPostCommand_WhenDenyButton_ReturnsRedirectWithDenied)); using var scope = serviceProvider.CreateScope(); var dbContext = scope.ServiceProvider.GetRequiredService(); - var handler = new ConsentRequestHandler(appManager.Object, dbContext); + var handler = new ConsentRequestHandler(appManager.Instance(), dbContext); var result = await handler.Handle( new OidcCommands.ConsentPostCommand("/return", "deny", Array.Empty()), @@ -52,12 +52,12 @@ public async Task ConsentPostCommand_WhenDenyButton_ReturnsRedirectWithDenied() [Fact] public async Task ConsentPostCommand_WhenNoScopesSelected_ReturnsPageWithError() { - var appManager = new Mock(); + var appManager = new IOpenIddictApplicationManagerImposter(); using var serviceProvider = TestServiceProviderFactory.Create(nameof(ConsentPostCommand_WhenNoScopesSelected_ReturnsPageWithError)); using var scope = serviceProvider.CreateScope(); var dbContext = scope.ServiceProvider.GetRequiredService(); - var handler = new ConsentRequestHandler(appManager.Object, dbContext); + var handler = new ConsentRequestHandler(appManager.Instance(), dbContext); var result = await handler.Handle( new OidcCommands.ConsentPostCommand("/return", "accept", Array.Empty()), @@ -71,12 +71,12 @@ public async Task ConsentPostCommand_WhenNoScopesSelected_ReturnsPageWithError() [Fact] public async Task ConsentPostCommand_WhenScopesSelected_ReturnsRedirectWithAccepted() { - var appManager = new Mock(); + var appManager = new IOpenIddictApplicationManagerImposter(); using var serviceProvider = TestServiceProviderFactory.Create(nameof(ConsentPostCommand_WhenScopesSelected_ReturnsRedirectWithAccepted)); using var scope = serviceProvider.CreateScope(); var dbContext = scope.ServiceProvider.GetRequiredService(); - var handler = new ConsentRequestHandler(appManager.Object, dbContext); + var handler = new ConsentRequestHandler(appManager.Instance(), dbContext); var result = await handler.Handle( new OidcCommands.ConsentPostCommand("/return", "accept", ["openid", "profile"]), diff --git a/SecurityService.UnitTests/RequestHandlers/UserRequestHandlerTests.cs b/SecurityService.UnitTests/RequestHandlers/UserRequestHandlerTests.cs index 4b989c89..f2fe619a 100644 --- a/SecurityService.UnitTests/RequestHandlers/UserRequestHandlerTests.cs +++ b/SecurityService.UnitTests/RequestHandlers/UserRequestHandlerTests.cs @@ -2,7 +2,7 @@ using MediatR; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.DependencyInjection; -using Moq; +using Imposter.Abstractions; using SecurityService.BusinessLogic; using SecurityService.BusinessLogic.Requests; using SecurityService.Database.DbContexts; @@ -102,31 +102,31 @@ public async Task ConfirmUserEmailAddress_UsesResolvedMediatorAndConfirmsUser() Email = "alice@example.com" }; var userManager = IdentityMocks.CreateUserManager(); - userManager.Setup(instance => instance.FindByNameAsync("alice")) + userManager.FindByNameAsync("alice") .ReturnsAsync(user); - userManager.Setup(instance => instance.ConfirmEmailAsync(user, "token-123")) + userManager.ConfirmEmailAsync(user, "token-123") .ReturnsAsync(IdentityResult.Success); var signInManager = IdentityMocks.CreateSignInManager(userManager); - using var provider = TestServiceProviderFactory.Create(nameof(this.ConfirmUserEmailAddress_UsesResolvedMediatorAndConfirmsUser), userManager.Object, signInManager.Object); + using var provider = TestServiceProviderFactory.Create(nameof(this.ConfirmUserEmailAddress_UsesResolvedMediatorAndConfirmsUser), userManager.Instance(), signInManager.Instance()); var mediator = provider.GetRequiredService(); var result = await mediator.Send(new SecurityServiceCommands.ConfirmUserEmailAddressCommand("alice", "token-123")); result.IsSuccess.ShouldBeTrue(); - userManager.Verify(instance => instance.FindByNameAsync("alice"), Times.Once); - userManager.Verify(instance => instance.ConfirmEmailAsync(user, "token-123"), Times.Once); + userManager.FindByNameAsync("alice").Called(Count.Once()); + userManager.ConfirmEmailAsync(user, "token-123").Called(Count.Once()); } [Fact] public async Task ConfirmUserEmailAddress_WhenUserMissing_ReturnsNotFound() { var userManager = IdentityMocks.CreateUserManager(); - userManager.Setup(instance => instance.FindByNameAsync("alice")) + userManager.FindByNameAsync("alice") .ReturnsAsync((ApplicationUser?)null); var signInManager = IdentityMocks.CreateSignInManager(userManager); - using var provider = TestServiceProviderFactory.Create(nameof(this.ConfirmUserEmailAddress_WhenUserMissing_ReturnsNotFound), userManager.Object, signInManager.Object); + using var provider = TestServiceProviderFactory.Create(nameof(this.ConfirmUserEmailAddress_WhenUserMissing_ReturnsNotFound), userManager.Instance(), signInManager.Instance()); var mediator = provider.GetRequiredService(); var result = await mediator.Send(new SecurityServiceCommands.ConfirmUserEmailAddressCommand("alice", "token-123")); @@ -144,13 +144,13 @@ public async Task ConfirmUserEmailAddress_WhenConfirmationFails_ReturnsFailure() Email = "alice@example.com" }; var userManager = IdentityMocks.CreateUserManager(); - userManager.Setup(instance => instance.FindByNameAsync("alice")) + userManager.FindByNameAsync("alice") .ReturnsAsync(user); - userManager.Setup(instance => instance.ConfirmEmailAsync(user, "token-123")) + userManager.ConfirmEmailAsync(user, "token-123") .ReturnsAsync(IdentityResult.Failed(new IdentityError { Description = "bad token" })); var signInManager = IdentityMocks.CreateSignInManager(userManager); - using var provider = TestServiceProviderFactory.Create(nameof(this.ConfirmUserEmailAddress_WhenConfirmationFails_ReturnsFailure), userManager.Object, signInManager.Object); + using var provider = TestServiceProviderFactory.Create(nameof(this.ConfirmUserEmailAddress_WhenConfirmationFails_ReturnsFailure), userManager.Instance(), signInManager.Instance()); var mediator = provider.GetRequiredService(); var result = await mediator.Send(new SecurityServiceCommands.ConfirmUserEmailAddressCommand("alice", "token-123")); @@ -168,24 +168,24 @@ public async Task SendWelcomeEmail_UsesResolvedMediatorAndSendsEmail() Email = "alice@example.com" }; var userManager = IdentityMocks.CreateUserManager(); - userManager.Setup(instance => instance.FindByNameAsync("alice")) + userManager.FindByNameAsync("alice") .ReturnsAsync(user); - userManager.Setup(instance => instance.RemovePasswordAsync(user)) + userManager.RemovePasswordAsync(user) .ReturnsAsync(IdentityResult.Success); - userManager.Setup(instance => instance.AddPasswordAsync(user, It.IsAny())) + userManager.AddPasswordAsync(user, Arg.Any()) .ReturnsAsync(IdentityResult.Success); var signInManager = IdentityMocks.CreateSignInManager(userManager); - using var provider = TestServiceProviderFactory.Create(nameof(this.SendWelcomeEmail_UsesResolvedMediatorAndSendsEmail), userManager.Object, signInManager.Object); + using var provider = TestServiceProviderFactory.Create(nameof(this.SendWelcomeEmail_UsesResolvedMediatorAndSendsEmail), userManager.Instance(), signInManager.Instance()); var mediator = provider.GetRequiredService(); var result = await mediator.Send(new SecurityServiceCommands.SendWelcomeEmailCommand("alice")); var messagingClient = provider.GetRequiredService().ShouldBeOfType(); result.IsSuccess.ShouldBeTrue(); - userManager.Verify(instance => instance.FindByNameAsync("alice"), Times.Once); - userManager.Verify(instance => instance.RemovePasswordAsync(user), Times.Once); - userManager.Verify(instance => instance.AddPasswordAsync(user, It.IsAny()), Times.Once); + userManager.FindByNameAsync("alice").Called(Count.Once()); + userManager.RemovePasswordAsync(user).Called(Count.Once()); + userManager.AddPasswordAsync(user, Arg.Any()).Called(Count.Once()); messagingClient.LastEmailRequest.ShouldNotBeNull(); messagingClient.LastEmailRequest.Subject.ShouldBe("Welcome to Transaction Processing"); messagingClient.LastEmailRequest.ToAddresses.ShouldContain("alice@example.com"); @@ -200,20 +200,20 @@ public async Task SendWelcomeEmail_WhenRemovePasswordFails_ReturnsFailure() Email = "alice@example.com" }; var userManager = IdentityMocks.CreateUserManager(); - userManager.Setup(instance => instance.FindByNameAsync("alice")) + userManager.FindByNameAsync("alice") .ReturnsAsync(user); - userManager.Setup(instance => instance.RemovePasswordAsync(user)) + userManager.RemovePasswordAsync(user) .ReturnsAsync(IdentityResult.Failed(new IdentityError { Description = "cannot remove" })); var signInManager = IdentityMocks.CreateSignInManager(userManager); - using var provider = TestServiceProviderFactory.Create(nameof(this.SendWelcomeEmail_WhenRemovePasswordFails_ReturnsFailure), userManager.Object, signInManager.Object); + using var provider = TestServiceProviderFactory.Create(nameof(this.SendWelcomeEmail_WhenRemovePasswordFails_ReturnsFailure), userManager.Instance(), signInManager.Instance()); var mediator = provider.GetRequiredService(); var result = await mediator.Send(new SecurityServiceCommands.SendWelcomeEmailCommand("alice")); result.IsFailed.ShouldBeTrue(); result.Status.ShouldBe(SimpleResults.ResultStatus.Failure); - userManager.Verify(instance => instance.AddPasswordAsync(It.IsAny(), It.IsAny()), Times.Never); + userManager.AddPasswordAsync(Arg.Any(), Arg.Any()).Called(Count.Never()); } [Fact] @@ -225,15 +225,15 @@ public async Task SendWelcomeEmail_WhenAddPasswordFails_ReturnsFailure() Email = "alice@example.com" }; var userManager = IdentityMocks.CreateUserManager(); - userManager.Setup(instance => instance.FindByNameAsync("alice")) + userManager.FindByNameAsync("alice") .ReturnsAsync(user); - userManager.Setup(instance => instance.RemovePasswordAsync(user)) + userManager.RemovePasswordAsync(user) .ReturnsAsync(IdentityResult.Success); - userManager.Setup(instance => instance.AddPasswordAsync(user, It.IsAny())) + userManager.AddPasswordAsync(user, Arg.Any()) .ReturnsAsync(IdentityResult.Failed(new IdentityError { Description = "cannot add" })); var signInManager = IdentityMocks.CreateSignInManager(userManager); - using var provider = TestServiceProviderFactory.Create(nameof(this.SendWelcomeEmail_WhenAddPasswordFails_ReturnsFailure), userManager.Object, signInManager.Object); + using var provider = TestServiceProviderFactory.Create(nameof(this.SendWelcomeEmail_WhenAddPasswordFails_ReturnsFailure), userManager.Instance(), signInManager.Instance()); var mediator = provider.GetRequiredService(); var result = await mediator.Send(new SecurityServiceCommands.SendWelcomeEmailCommand("alice")); @@ -251,21 +251,21 @@ public async Task ProcessPasswordResetRequest_UsesResolvedMediatorAndSendsEmail( Email = "alice@example.com" }; var userManager = IdentityMocks.CreateUserManager(); - userManager.Setup(instance => instance.FindByNameAsync("alice")) + userManager.FindByNameAsync("alice") .ReturnsAsync(user); - userManager.Setup(instance => instance.GeneratePasswordResetTokenAsync(user)) + userManager.GeneratePasswordResetTokenAsync(user) .ReturnsAsync("token+/="); var signInManager = IdentityMocks.CreateSignInManager(userManager); - using var provider = TestServiceProviderFactory.Create(nameof(this.ProcessPasswordResetRequest_UsesResolvedMediatorAndSendsEmail), userManager.Object, signInManager.Object); + using var provider = TestServiceProviderFactory.Create(nameof(this.ProcessPasswordResetRequest_UsesResolvedMediatorAndSendsEmail), userManager.Instance(), signInManager.Instance()); var mediator = provider.GetRequiredService(); var result = await mediator.Send(new SecurityServiceCommands.ProcessPasswordResetRequestCommand("alice", "alice", "test-client-id")); var messagingClient = provider.GetRequiredService().ShouldBeOfType(); result.IsSuccess.ShouldBeTrue(); - userManager.Verify(instance => instance.FindByNameAsync("alice"), Times.Once); - userManager.Verify(instance => instance.GeneratePasswordResetTokenAsync(user), Times.Once); + userManager.FindByNameAsync("alice").Called(Count.Once()); + userManager.GeneratePasswordResetTokenAsync(user).Called(Count.Once()); messagingClient.LastEmailRequest.ShouldNotBeNull(); messagingClient.LastEmailRequest.Subject.ShouldBe("Password Reset Requested"); messagingClient.LastEmailRequest.ToAddresses.ShouldContain("alice@example.com"); @@ -276,18 +276,18 @@ public async Task ProcessPasswordResetRequest_UsesResolvedMediatorAndSendsEmail( public async Task ProcessPasswordResetRequest_WhenUserMissing_ReturnsSuccessAndDoesNotSendEmail() { var userManager = IdentityMocks.CreateUserManager(); - userManager.Setup(instance => instance.FindByNameAsync("alice")) + userManager.FindByNameAsync("alice") .ReturnsAsync((ApplicationUser?)null); var signInManager = IdentityMocks.CreateSignInManager(userManager); - using var provider = TestServiceProviderFactory.Create(nameof(this.ProcessPasswordResetRequest_WhenUserMissing_ReturnsSuccessAndDoesNotSendEmail), userManager.Object, signInManager.Object); + using var provider = TestServiceProviderFactory.Create(nameof(this.ProcessPasswordResetRequest_WhenUserMissing_ReturnsSuccessAndDoesNotSendEmail), userManager.Instance(), signInManager.Instance()); var mediator = provider.GetRequiredService(); var messagingClient = provider.GetRequiredService().ShouldBeOfType(); var result = await mediator.Send(new SecurityServiceCommands.ProcessPasswordResetRequestCommand("alice", "alice", "test-client-id")); result.IsSuccess.ShouldBeTrue(); - userManager.Verify(instance => instance.GeneratePasswordResetTokenAsync(It.IsAny()), Times.Never); + userManager.GeneratePasswordResetTokenAsync(Arg.Any()).Called(Count.Never()); messagingClient.LastEmailRequest.ShouldBeNull(); } @@ -300,13 +300,13 @@ public async Task ProcessPasswordResetConfirmation_UsesResolvedMediatorAndReturn Email = "alice@example.com" }; var userManager = IdentityMocks.CreateUserManager(); - userManager.Setup(instance => instance.FindByNameAsync("alice")) + userManager.FindByNameAsync("alice") .ReturnsAsync(user); - userManager.Setup(instance => instance.ResetPasswordAsync(user, "token-123", "NewPassword1!")) + userManager.ResetPasswordAsync(user, "token-123", "NewPassword1!") .ReturnsAsync(IdentityResult.Success); var signInManager = IdentityMocks.CreateSignInManager(userManager); - using var provider = TestServiceProviderFactory.Create(nameof(this.ProcessPasswordResetConfirmation_UsesResolvedMediatorAndReturnsClientUri), userManager.Object, signInManager.Object); + using var provider = TestServiceProviderFactory.Create(nameof(this.ProcessPasswordResetConfirmation_UsesResolvedMediatorAndReturnsClientUri), userManager.Instance(), signInManager.Instance()); await SeedClientAsync(provider, "test-client-id", "http://localhost/app"); var mediator = provider.GetRequiredService(); @@ -314,26 +314,26 @@ public async Task ProcessPasswordResetConfirmation_UsesResolvedMediatorAndReturn result.IsSuccess.ShouldBeTrue(); result.Data.ShouldBe("http://localhost/app"); - userManager.Verify(instance => instance.FindByNameAsync("alice"), Times.Once); - userManager.Verify(instance => instance.ResetPasswordAsync(user, "token-123", "NewPassword1!"), Times.Once); + userManager.FindByNameAsync("alice").Called(Count.Once()); + userManager.ResetPasswordAsync(user, "token-123", "NewPassword1!").Called(Count.Once()); } [Fact] public async Task ProcessPasswordResetConfirmation_WhenUserMissing_ReturnsNotFound() { var userManager = IdentityMocks.CreateUserManager(); - userManager.Setup(instance => instance.FindByNameAsync("alice")) + userManager.FindByNameAsync("alice") .ReturnsAsync((ApplicationUser?)null); var signInManager = IdentityMocks.CreateSignInManager(userManager); - using var provider = TestServiceProviderFactory.Create(nameof(this.ProcessPasswordResetConfirmation_WhenUserMissing_ReturnsNotFound), userManager.Object, signInManager.Object); + using var provider = TestServiceProviderFactory.Create(nameof(this.ProcessPasswordResetConfirmation_WhenUserMissing_ReturnsNotFound), userManager.Instance(), signInManager.Instance()); var mediator = provider.GetRequiredService(); var result = await mediator.Send(new SecurityServiceCommands.ProcessPasswordResetConfirmationCommand("alice", "token-123", "NewPassword1!", "test-client-id")); result.IsFailed.ShouldBeTrue(); result.Status.ShouldBe(SimpleResults.ResultStatus.NotFound); - userManager.Verify(instance => instance.ResetPasswordAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + userManager.ResetPasswordAsync(Arg.Any(), Arg.Any(), Arg.Any()).Called(Count.Never()); } [Fact] @@ -345,13 +345,13 @@ public async Task ProcessPasswordResetConfirmation_WhenResetFails_ReturnsFailure Email = "alice@example.com" }; var userManager = IdentityMocks.CreateUserManager(); - userManager.Setup(instance => instance.FindByNameAsync("alice")) + userManager.FindByNameAsync("alice") .ReturnsAsync(user); - userManager.Setup(instance => instance.ResetPasswordAsync(user, "token-123", "NewPassword1!")) + userManager.ResetPasswordAsync(user, "token-123", "NewPassword1!") .ReturnsAsync(IdentityResult.Failed(new IdentityError { Description = "bad token" })); var signInManager = IdentityMocks.CreateSignInManager(userManager); - using var provider = TestServiceProviderFactory.Create(nameof(this.ProcessPasswordResetConfirmation_WhenResetFails_ReturnsFailure), userManager.Object, signInManager.Object); + using var provider = TestServiceProviderFactory.Create(nameof(this.ProcessPasswordResetConfirmation_WhenResetFails_ReturnsFailure), userManager.Instance(), signInManager.Instance()); var mediator = provider.GetRequiredService(); var result = await mediator.Send(new SecurityServiceCommands.ProcessPasswordResetConfirmationCommand("alice", "token-123", "NewPassword1!", "test-client-id")); @@ -369,13 +369,13 @@ public async Task ProcessPasswordResetConfirmation_WhenClientMissing_ReturnsInva Email = "alice@example.com" }; var userManager = IdentityMocks.CreateUserManager(); - userManager.Setup(instance => instance.FindByNameAsync("alice")) + userManager.FindByNameAsync("alice") .ReturnsAsync(user); - userManager.Setup(instance => instance.ResetPasswordAsync(user, "token-123", "NewPassword1!")) + userManager.ResetPasswordAsync(user, "token-123", "NewPassword1!") .ReturnsAsync(IdentityResult.Success); var signInManager = IdentityMocks.CreateSignInManager(userManager); - using var provider = TestServiceProviderFactory.Create(nameof(this.ProcessPasswordResetConfirmation_WhenClientMissing_ReturnsInvalid), userManager.Object, signInManager.Object); + using var provider = TestServiceProviderFactory.Create(nameof(this.ProcessPasswordResetConfirmation_WhenClientMissing_ReturnsInvalid), userManager.Instance(), signInManager.Instance()); var mediator = provider.GetRequiredService(); var result = await mediator.Send(new SecurityServiceCommands.ProcessPasswordResetConfirmationCommand("alice", "token-123", "NewPassword1!", "missing-client-id")); diff --git a/SecurityService.UnitTests/RequestHandlers/VerifyRequestHandlerTests.cs b/SecurityService.UnitTests/RequestHandlers/VerifyRequestHandlerTests.cs index 60d54b15..8fcc89e8 100644 --- a/SecurityService.UnitTests/RequestHandlers/VerifyRequestHandlerTests.cs +++ b/SecurityService.UnitTests/RequestHandlers/VerifyRequestHandlerTests.cs @@ -3,7 +3,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.DependencyInjection; -using Moq; +using Imposter.Abstractions; using OpenIddict.Server.AspNetCore; using SecurityService.BusinessLogic.Oidc; using SecurityService.BusinessLogic.RequestHandlers; @@ -20,13 +20,13 @@ public class VerifyRequestHandlerTests public async Task VerifyGetQuery_WhenAuthFails_ReturnsPageResultWithNoData() { var userManager = IdentityMocks.CreateUserManager(); - var scopeManager = new Mock(); - var appManager = new Mock(); + var scopeManager = new IOpenIddictScopeManagerImposter(); + var appManager = new IOpenIddictApplicationManagerImposter(); using var serviceProvider = TestServiceProviderFactory.Create(nameof(this.VerifyGetQuery_WhenAuthFails_ReturnsPageResultWithNoData)); using var scope = serviceProvider.CreateScope(); var dbContext = scope.ServiceProvider.GetRequiredService(); - var handler = new VerifyRequestHandler(appManager.Object, scopeManager.Object, dbContext, userManager.Object); + var handler = new VerifyRequestHandler(appManager.Instance(), scopeManager.Instance(), dbContext, userManager.Instance()); var context = CreateHttpContext(AuthenticateResult.Fail("no ticket"), isAuthenticated: false, userCode: string.Empty); var result = await handler.Handle(new OidcCommands.VerifyGetQuery(context), CancellationToken.None); @@ -41,13 +41,13 @@ public async Task VerifyGetQuery_WhenAuthFails_ReturnsPageResultWithNoData() public async Task VerifyGetQuery_WhenAuthFailsWithUserCode_ReturnsPageWithInvalidCodeMessage() { var userManager = IdentityMocks.CreateUserManager(); - var scopeManager = new Mock(); - var appManager = new Mock(); + var scopeManager = new IOpenIddictScopeManagerImposter(); + var appManager = new IOpenIddictApplicationManagerImposter(); using var serviceProvider = TestServiceProviderFactory.Create(nameof(this.VerifyGetQuery_WhenAuthFailsWithUserCode_ReturnsPageWithInvalidCodeMessage)); using var scope = serviceProvider.CreateScope(); var dbContext = scope.ServiceProvider.GetRequiredService(); - var handler = new VerifyRequestHandler(appManager.Object, scopeManager.Object, dbContext, userManager.Object); + var handler = new VerifyRequestHandler(appManager.Instance(), scopeManager.Instance(), dbContext, userManager.Instance()); var context = CreateHttpContext(AuthenticateResult.Fail("no ticket"), isAuthenticated: false, userCode: "ABCD-1234"); var result = await handler.Handle(new OidcCommands.VerifyGetQuery(context), CancellationToken.None); @@ -62,14 +62,14 @@ public async Task VerifyGetQuery_WhenAuthFailsWithUserCode_ReturnsPageWithInvali public async Task VerifyGetQuery_WhenAuthSucceedsButUserNotAuthenticated_ReturnsRedirectToLogin() { var userManager = IdentityMocks.CreateUserManager(); - var scopeManager = new Mock(); - var appManager = new Mock(); + var scopeManager = new IOpenIddictScopeManagerImposter(); + var appManager = new IOpenIddictApplicationManagerImposter(); using var serviceProvider = TestServiceProviderFactory.Create(nameof(this.VerifyGetQuery_WhenAuthSucceedsButUserNotAuthenticated_ReturnsRedirectToLogin)); using var scope = serviceProvider.CreateScope(); var dbContext = scope.ServiceProvider.GetRequiredService(); var authTicket = CreateAuthTicket("client-1"); - var handler = new VerifyRequestHandler(appManager.Object, scopeManager.Object, dbContext, userManager.Object); + var handler = new VerifyRequestHandler(appManager.Instance(), scopeManager.Instance(), dbContext, userManager.Instance()); var context = CreateHttpContext(AuthenticateResult.Success(authTicket), isAuthenticated: false, userCode: string.Empty); var result = await handler.Handle(new OidcCommands.VerifyGetQuery(context), CancellationToken.None); @@ -83,13 +83,13 @@ public async Task VerifyGetQuery_WhenAuthSucceedsButUserNotAuthenticated_Returns public async Task VerifyPostCommand_WithLookupAction_WhenEmptyUserCode_ReturnsPageWithError() { var userManager = IdentityMocks.CreateUserManager(); - var scopeManager = new Mock(); - var appManager = new Mock(); + var scopeManager = new IOpenIddictScopeManagerImposter(); + var appManager = new IOpenIddictApplicationManagerImposter(); using var serviceProvider = TestServiceProviderFactory.Create(nameof(this.VerifyPostCommand_WithLookupAction_WhenEmptyUserCode_ReturnsPageWithError)); using var scope = serviceProvider.CreateScope(); var dbContext = scope.ServiceProvider.GetRequiredService(); - var handler = new VerifyRequestHandler(appManager.Object, scopeManager.Object, dbContext, userManager.Object); + var handler = new VerifyRequestHandler(appManager.Instance(), scopeManager.Instance(), dbContext, userManager.Instance()); var context = CreateHttpContext(AuthenticateResult.Fail("irrelevant"), isAuthenticated: false, userCode: string.Empty); var result = await handler.Handle( @@ -105,13 +105,13 @@ public async Task VerifyPostCommand_WithLookupAction_WhenEmptyUserCode_ReturnsPa public async Task VerifyPostCommand_WithLookupAction_WhenValidUserCode_ReturnsRedirect() { var userManager = IdentityMocks.CreateUserManager(); - var scopeManager = new Mock(); - var appManager = new Mock(); + var scopeManager = new IOpenIddictScopeManagerImposter(); + var appManager = new IOpenIddictApplicationManagerImposter(); using var serviceProvider = TestServiceProviderFactory.Create(nameof(this.VerifyPostCommand_WithLookupAction_WhenValidUserCode_ReturnsRedirect)); using var scope = serviceProvider.CreateScope(); var dbContext = scope.ServiceProvider.GetRequiredService(); - var handler = new VerifyRequestHandler(appManager.Object, scopeManager.Object, dbContext, userManager.Object); + var handler = new VerifyRequestHandler(appManager.Instance(), scopeManager.Instance(), dbContext, userManager.Instance()); var context = CreateHttpContext(AuthenticateResult.Fail("irrelevant"), isAuthenticated: false, userCode: string.Empty); var result = await handler.Handle( @@ -127,13 +127,13 @@ public async Task VerifyPostCommand_WithLookupAction_WhenValidUserCode_ReturnsRe public async Task VerifyPostCommand_WhenAuthFails_ReturnsPageWithInvalidCodeError() { var userManager = IdentityMocks.CreateUserManager(); - var scopeManager = new Mock(); - var appManager = new Mock(); + var scopeManager = new IOpenIddictScopeManagerImposter(); + var appManager = new IOpenIddictApplicationManagerImposter(); using var serviceProvider = TestServiceProviderFactory.Create(nameof(this.VerifyPostCommand_WhenAuthFails_ReturnsPageWithInvalidCodeError)); using var scope = serviceProvider.CreateScope(); var dbContext = scope.ServiceProvider.GetRequiredService(); - var handler = new VerifyRequestHandler(appManager.Object, scopeManager.Object, dbContext, userManager.Object); + var handler = new VerifyRequestHandler(appManager.Instance(), scopeManager.Instance(), dbContext, userManager.Instance()); var context = CreateHttpContext(AuthenticateResult.Fail("no ticket"), isAuthenticated: false, userCode: string.Empty); var result = await handler.Handle( @@ -149,14 +149,14 @@ public async Task VerifyPostCommand_WhenAuthFails_ReturnsPageWithInvalidCodeErro public async Task VerifyPostCommand_WhenAuthSucceedsButUserNotAuthenticated_ReturnsRedirectToLogin() { var userManager = IdentityMocks.CreateUserManager(); - var scopeManager = new Mock(); - var appManager = new Mock(); + var scopeManager = new IOpenIddictScopeManagerImposter(); + var appManager = new IOpenIddictApplicationManagerImposter(); using var serviceProvider = TestServiceProviderFactory.Create(nameof(this.VerifyPostCommand_WhenAuthSucceedsButUserNotAuthenticated_ReturnsRedirectToLogin)); using var scope = serviceProvider.CreateScope(); var dbContext = scope.ServiceProvider.GetRequiredService(); var authTicket = CreateAuthTicket("client-1"); - var handler = new VerifyRequestHandler(appManager.Object, scopeManager.Object, dbContext, userManager.Object); + var handler = new VerifyRequestHandler(appManager.Instance(), scopeManager.Instance(), dbContext, userManager.Instance()); var context = CreateHttpContext(AuthenticateResult.Success(authTicket), isAuthenticated: false, userCode: string.Empty); var result = await handler.Handle( @@ -172,14 +172,14 @@ public async Task VerifyPostCommand_WhenAuthSucceedsButUserNotAuthenticated_Retu public async Task VerifyPostCommand_WithDenyAction_WhenAuthenticatedUser_ReturnsForbid() { var userManager = IdentityMocks.CreateUserManager(); - var scopeManager = new Mock(); - var appManager = new Mock(); + var scopeManager = new IOpenIddictScopeManagerImposter(); + var appManager = new IOpenIddictApplicationManagerImposter(); using var serviceProvider = TestServiceProviderFactory.Create(nameof(this.VerifyPostCommand_WithDenyAction_WhenAuthenticatedUser_ReturnsForbid)); using var scope = serviceProvider.CreateScope(); var dbContext = scope.ServiceProvider.GetRequiredService(); var authTicket = CreateAuthTicket("client-1"); - var handler = new VerifyRequestHandler(appManager.Object, scopeManager.Object, dbContext, userManager.Object); + var handler = new VerifyRequestHandler(appManager.Instance(), scopeManager.Instance(), dbContext, userManager.Instance()); var context = CreateHttpContext(AuthenticateResult.Success(authTicket), isAuthenticated: true, userCode: string.Empty); var result = await handler.Handle( @@ -195,15 +195,15 @@ public async Task VerifyPostCommand_WithDenyAction_WhenAuthenticatedUser_Returns public async Task VerifyPostCommand_WhenAuthenticatedUserNotFoundInStore_ReturnsRedirectToLogin() { var userManager = IdentityMocks.CreateUserManager(); - userManager.Setup(m => m.GetUserAsync(It.IsAny())).ReturnsAsync((ApplicationUser?)null); - var scopeManager = new Mock(); - var appManager = new Mock(); + userManager.GetUserAsync(Arg.Any()).ReturnsAsync((ApplicationUser?)null); + var scopeManager = new IOpenIddictScopeManagerImposter(); + var appManager = new IOpenIddictApplicationManagerImposter(); using var serviceProvider = TestServiceProviderFactory.Create(nameof(this.VerifyPostCommand_WhenAuthenticatedUserNotFoundInStore_ReturnsRedirectToLogin)); using var scope = serviceProvider.CreateScope(); var dbContext = scope.ServiceProvider.GetRequiredService(); var authTicket = CreateAuthTicket("client-1"); - var handler = new VerifyRequestHandler(appManager.Object, scopeManager.Object, dbContext, userManager.Object); + var handler = new VerifyRequestHandler(appManager.Instance(), scopeManager.Instance(), dbContext, userManager.Instance()); var context = CreateHttpContext(AuthenticateResult.Success(authTicket), isAuthenticated: true, userCode: string.Empty); var result = await handler.Handle( @@ -274,4 +274,3 @@ public Task SignOutAsync(HttpContext context, string? scheme, AuthenticationProp Task.CompletedTask; } } - diff --git a/SecurityService.UnitTests/SecurityService.UnitTests.csproj b/SecurityService.UnitTests/SecurityService.UnitTests.csproj index 3a84388c..b939b018 100644 --- a/SecurityService.UnitTests/SecurityService.UnitTests.csproj +++ b/SecurityService.UnitTests/SecurityService.UnitTests.csproj @@ -12,7 +12,7 @@ - +