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