Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.9.0" />
<PackageVersion Include="Microsoft.Testing.Extensions.CodeCoverage" Version="18.10.0" />
<PackageVersion Include="Microsoft.Testing.Extensions.TrxReport" Version="2.3.3" />
<PackageVersion Include="Moq" Version="4.20.72" />
<PackageVersion Include="Imposter" Version="0.1.9" />
<PackageVersion Include="Shouldly" Version="4.3.0" />
<PackageVersion Include="xunit.v3" Version="4.0.0" />
<PackageVersion Include="xunit.runner.visualstudio" Version="4.0.0" />
Expand Down Expand Up @@ -54,4 +54,4 @@
<PackageVersion Include="Selenium.WebDriver" Version="4.48.0" />
<PackageVersion Include="Reqnroll.NUnit" Version="3.3.4" />
</ItemGroup>
</Project>
</Project>
28 changes: 28 additions & 0 deletions SecurityService.UnitTests/Infrastructure/GeneratedImposters.cs
Original file line number Diff line number Diff line change
@@ -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<ApplicationUser>))]
[assembly: GenerateImposter(typeof(IPasswordHasher<ApplicationUser>))]
[assembly: GenerateImposter(typeof(ILogger<>))]
[assembly: GenerateImposter(typeof(IHttpContextAccessor))]
[assembly: GenerateImposter(typeof(IUserClaimsPrincipalFactory<ApplicationUser>))]
[assembly: GenerateImposter(typeof(IAuthenticationSchemeProvider))]
[assembly: GenerateImposter(typeof(IUserConfirmation<ApplicationUser>))]








220 changes: 203 additions & 17 deletions SecurityService.UnitTests/Infrastructure/IdentityMocks.cs

Large diffs are not rendered by default.

29 changes: 29 additions & 0 deletions SecurityService.UnitTests/Infrastructure/IdentityMocksTests.cs
Original file line number Diff line number Diff line change
@@ -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<InvalidOperationException>(() =>
userManager.FindByNameAsync("alice").Called(Count.Once()));
}
}
30 changes: 15 additions & 15 deletions SecurityService.UnitTests/Oidc/OidcEndpointTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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"));
Expand All @@ -48,19 +48,19 @@ public async Task UserInfoAsync_WithValidPrincipal_ReturnsJsonPayload()
};

var signInManager = IdentityMocks.CreateSignInManager(userManager);
var appManager = new Mock<OpenIddict.Abstractions.IOpenIddictApplicationManager>();
var authManager = new Mock<OpenIddict.Abstractions.IOpenIddictAuthorizationManager>();
var scopeManager = new Mock<OpenIddict.Abstractions.IOpenIddictScopeManager>();
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<SecurityServiceDbContext>();

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);
Expand Down Expand Up @@ -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"),
Expand All @@ -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);
Expand Down
39 changes: 20 additions & 19 deletions SecurityService.UnitTests/Pages/ChangePasswordPageModelTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -15,7 +15,7 @@ public class ChangePasswordPageModelTests
[Fact]
public void OnGet_WithReturnUrlAndClientIdQuery_PopulatesInput()
{
var mediator = new Mock<IMediator>();
var mediator = new IMediatorImposter();
var httpContext = new DefaultHttpContext();
httpContext.Request.QueryString = new QueryString("?clientId=test-client-id");

Expand All @@ -31,7 +31,7 @@ public void OnGet_WithReturnUrlAndClientIdQuery_PopulatesInput()
[Fact]
public async Task OnPost_WhenPasswordsDoNotMatch_ReturnsPageAndDoesNotCallMediator()
{
var mediator = new Mock<IMediator>(MockBehavior.Strict);
var mediator = new IMediatorImposter(ImposterMode.Explicit);
var model = CreateModel(mediator, new DefaultHttpContext());
model.Input = new SecurityService.Pages.Account.ChangePassword.IndexInputModel
{
Expand All @@ -47,16 +47,16 @@ public async Task OnPost_WhenPasswordsDoNotMatch_ReturnsPageAndDoesNotCallMediat

result.ShouldBeOfType<PageResult>();
model.ModelState[string.Empty]!.Errors.ShouldContain(error => error.ErrorMessage == "New Password does not match Confirm Password");
mediator.Verify(instance => instance.Send(It.IsAny<SecurityServiceCommands.ChangeUserPasswordCommand>(), It.IsAny<CancellationToken>()), Times.Never);
mediator.Send(Arg<IRequest<Result<ChangeUserPasswordResult>>>.Any(), Arg<CancellationToken>.Any()).Called(Count.Never());
}

[Fact]
public async Task OnPost_WhenMediatorFails_ReturnsPageWithGenericError()
{
var mediator = new Mock<IMediator>();
mediator.Setup(instance => instance.Send(
It.IsAny<SecurityServiceCommands.ChangeUserPasswordCommand>(),
It.IsAny<CancellationToken>()))
var mediator = new IMediatorImposter();
mediator.Send(
Arg<IRequest<Result<ChangeUserPasswordResult>>>.Any(),
Arg<CancellationToken>.Any())
.ReturnsAsync(Result.Failure("boom"));

var model = CreateModel(mediator, new DefaultHttpContext());
Expand All @@ -79,20 +79,21 @@ public async Task OnPost_WhenMediatorFails_ReturnsPageWithGenericError()
[Fact]
public async Task OnPost_WhenMediatorSucceeds_RedirectsToReturnedUriAndSendsExpectedCommand()
{
var mediator = new Mock<IMediator>();
var mediator = new IMediatorImposter();
SecurityServiceCommands.ChangeUserPasswordCommand? capturedCommand = null;
mediator.Setup(instance => instance.Send(
It.IsAny<SecurityServiceCommands.ChangeUserPasswordCommand>(),
It.IsAny<CancellationToken>()))
.Callback<IRequest<Result<ChangeUserPasswordResult>>, CancellationToken>((request, _) =>
{
capturedCommand = request.ShouldBeOfType<SecurityServiceCommands.ChangeUserPasswordCommand>();
})
mediator.Send(
Arg<IRequest<Result<ChangeUserPasswordResult>>>.Any(),
Arg<CancellationToken>.Any())
.ReturnsAsync(Result.Success(new ChangeUserPasswordResult
{
IsSuccessful = true,
RedirectUri = "http://localhost/app"
}));
}))
.Callback((request, _) =>
{
capturedCommand = request.ShouldBeOfType<SecurityServiceCommands.ChangeUserPasswordCommand>();
return default!;
});

var httpContext = new DefaultHttpContext();
httpContext.Request.QueryString = new QueryString("?clientId=query-client-id");
Expand Down Expand Up @@ -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<IMediator> 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
{
Expand Down
42 changes: 21 additions & 21 deletions SecurityService.UnitTests/Pages/ConfirmEmailPageModelTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<string>()))
userManager.AddPasswordAsync(user, Arg<string>.Any())
.ReturnsAsync(IdentityResult.Success);

var signInManager = IdentityMocks.CreateSignInManager(userManager);
Expand Down Expand Up @@ -59,7 +59,7 @@ public async Task OnGet_WithMissingQueryValues_ReturnsPageAndDoesNotCallMediator

result.ShouldBeOfType<PageResult>();
model.View.UserMessage.ShouldBe("The email confirmation link is invalid.");
userManager.Verify(instance => instance.FindByNameAsync(It.IsAny<string>()), Times.Never);
userManager.FindByNameAsync(Arg<string>.Any()).Called(Count.Never());
}

[Fact]
Expand All @@ -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);
Expand All @@ -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<ApplicationUser>()), Times.Never);
userManager.Verify(instance => instance.AddPasswordAsync(It.IsAny<ApplicationUser>(), It.IsAny<string>()), Times.Never);
userManager.ConfirmEmailAsync(user, "token-123").Called(Count.Once());
userManager.RemovePasswordAsync(Arg<ApplicationUser>.Any()).Called(Count.Never());
userManager.AddPasswordAsync(Arg<ApplicationUser>.Any(), Arg<string>.Any()).Called(Count.Never());
}

[Fact]
Expand All @@ -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<string>()))
userManager.AddPasswordAsync(user, Arg<string>.Any())
.ReturnsAsync(IdentityResult.Success);

var signInManager = IdentityMocks.CreateSignInManager(userManager);
Expand All @@ -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<string>()), Times.Once);
userManager.ConfirmEmailAsync(user, "token-123").Called(Count.Once());
userManager.RemovePasswordAsync(user).Called(Count.Once());
userManager.AddPasswordAsync(user, Arg<string>.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<ApplicationUser>> userManager,
Mock<SignInManager<ApplicationUser>> 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)
Expand Down
Loading
Loading