1
0
mirror of https://github.com/bitwarden/server.git synced 2025-06-15 07:20:49 -05:00

Chore: document SutProvider and clean up UserServiceTests (#5879)

* UserServiceTests - use builder pattern for SutProvider to reduce boilerplate
* SutProvider - add xmldoc
This commit is contained in:
Thomas Rittson 2025-06-12 19:21:05 +10:00 committed by GitHub
parent 463dc1232d
commit 64b288035c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 169 additions and 166 deletions

View File

@ -12,7 +12,6 @@ using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services; using Bit.Core.AdminConsole.Services;
using Bit.Core.Auth.Enums; using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Models; using Bit.Core.Auth.Models;
using Bit.Core.Auth.Models.Business.Tokenables;
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
using Bit.Core.Billing.Constants; using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Models; using Bit.Core.Billing.Models;
@ -29,12 +28,9 @@ using Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.Platform.Push; using Bit.Core.Platform.Push;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Settings; using Bit.Core.Settings;
using Bit.Core.Tokens;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Bit.Core.Vault.Repositories;
using Fido2NetLib; using Fido2NetLib;
using Fido2NetLib.Objects; using Fido2NetLib.Objects;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -44,12 +40,11 @@ using JsonSerializer = System.Text.Json.JsonSerializer;
namespace Bit.Core.Services; namespace Bit.Core.Services;
public class UserService : UserManager<User>, IUserService, IDisposable public class UserService : UserManager<User>, IUserService
{ {
private const string PremiumPlanId = "premium-annually"; private const string PremiumPlanId = "premium-annually";
private readonly IUserRepository _userRepository; private readonly IUserRepository _userRepository;
private readonly ICipherRepository _cipherRepository;
private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly IOrganizationRepository _organizationRepository; private readonly IOrganizationRepository _organizationRepository;
private readonly IOrganizationDomainRepository _organizationDomainRepository; private readonly IOrganizationDomainRepository _organizationDomainRepository;
@ -65,17 +60,14 @@ public class UserService : UserManager<User>, IUserService, IDisposable
private readonly IPaymentService _paymentService; private readonly IPaymentService _paymentService;
private readonly IPolicyRepository _policyRepository; private readonly IPolicyRepository _policyRepository;
private readonly IPolicyService _policyService; private readonly IPolicyService _policyService;
private readonly IDataProtector _organizationServiceDataProtector;
private readonly IFido2 _fido2; private readonly IFido2 _fido2;
private readonly ICurrentContext _currentContext; private readonly ICurrentContext _currentContext;
private readonly IGlobalSettings _globalSettings; private readonly IGlobalSettings _globalSettings;
private readonly IAcceptOrgUserCommand _acceptOrgUserCommand; private readonly IAcceptOrgUserCommand _acceptOrgUserCommand;
private readonly IProviderUserRepository _providerUserRepository; private readonly IProviderUserRepository _providerUserRepository;
private readonly IStripeSyncService _stripeSyncService; private readonly IStripeSyncService _stripeSyncService;
private readonly IDataProtectorTokenFactory<OrgUserInviteTokenable> _orgUserInviteTokenDataFactory;
private readonly IFeatureService _featureService; private readonly IFeatureService _featureService;
private readonly IPremiumUserBillingService _premiumUserBillingService; private readonly IPremiumUserBillingService _premiumUserBillingService;
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
private readonly IRevokeNonCompliantOrganizationUserCommand _revokeNonCompliantOrganizationUserCommand; private readonly IRevokeNonCompliantOrganizationUserCommand _revokeNonCompliantOrganizationUserCommand;
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery; private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
private readonly IDistributedCache _distributedCache; private readonly IDistributedCache _distributedCache;
@ -83,7 +75,6 @@ public class UserService : UserManager<User>, IUserService, IDisposable
public UserService( public UserService(
IUserRepository userRepository, IUserRepository userRepository,
ICipherRepository cipherRepository,
IOrganizationUserRepository organizationUserRepository, IOrganizationUserRepository organizationUserRepository,
IOrganizationRepository organizationRepository, IOrganizationRepository organizationRepository,
IOrganizationDomainRepository organizationDomainRepository, IOrganizationDomainRepository organizationDomainRepository,
@ -101,7 +92,6 @@ public class UserService : UserManager<User>, IUserService, IDisposable
ILicensingService licenseService, ILicensingService licenseService,
IEventService eventService, IEventService eventService,
IApplicationCacheService applicationCacheService, IApplicationCacheService applicationCacheService,
IDataProtectionProvider dataProtectionProvider,
IPaymentService paymentService, IPaymentService paymentService,
IPolicyRepository policyRepository, IPolicyRepository policyRepository,
IPolicyService policyService, IPolicyService policyService,
@ -111,10 +101,8 @@ public class UserService : UserManager<User>, IUserService, IDisposable
IAcceptOrgUserCommand acceptOrgUserCommand, IAcceptOrgUserCommand acceptOrgUserCommand,
IProviderUserRepository providerUserRepository, IProviderUserRepository providerUserRepository,
IStripeSyncService stripeSyncService, IStripeSyncService stripeSyncService,
IDataProtectorTokenFactory<OrgUserInviteTokenable> orgUserInviteTokenDataFactory,
IFeatureService featureService, IFeatureService featureService,
IPremiumUserBillingService premiumUserBillingService, IPremiumUserBillingService premiumUserBillingService,
IRemoveOrganizationUserCommand removeOrganizationUserCommand,
IRevokeNonCompliantOrganizationUserCommand revokeNonCompliantOrganizationUserCommand, IRevokeNonCompliantOrganizationUserCommand revokeNonCompliantOrganizationUserCommand,
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery, ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
IDistributedCache distributedCache, IDistributedCache distributedCache,
@ -131,7 +119,6 @@ public class UserService : UserManager<User>, IUserService, IDisposable
logger) logger)
{ {
_userRepository = userRepository; _userRepository = userRepository;
_cipherRepository = cipherRepository;
_organizationUserRepository = organizationUserRepository; _organizationUserRepository = organizationUserRepository;
_organizationRepository = organizationRepository; _organizationRepository = organizationRepository;
_organizationDomainRepository = organizationDomainRepository; _organizationDomainRepository = organizationDomainRepository;
@ -147,18 +134,14 @@ public class UserService : UserManager<User>, IUserService, IDisposable
_paymentService = paymentService; _paymentService = paymentService;
_policyRepository = policyRepository; _policyRepository = policyRepository;
_policyService = policyService; _policyService = policyService;
_organizationServiceDataProtector = dataProtectionProvider.CreateProtector(
"OrganizationServiceDataProtector");
_fido2 = fido2; _fido2 = fido2;
_currentContext = currentContext; _currentContext = currentContext;
_globalSettings = globalSettings; _globalSettings = globalSettings;
_acceptOrgUserCommand = acceptOrgUserCommand; _acceptOrgUserCommand = acceptOrgUserCommand;
_providerUserRepository = providerUserRepository; _providerUserRepository = providerUserRepository;
_stripeSyncService = stripeSyncService; _stripeSyncService = stripeSyncService;
_orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory;
_featureService = featureService; _featureService = featureService;
_premiumUserBillingService = premiumUserBillingService; _premiumUserBillingService = premiumUserBillingService;
_removeOrganizationUserCommand = removeOrganizationUserCommand;
_revokeNonCompliantOrganizationUserCommand = revokeNonCompliantOrganizationUserCommand; _revokeNonCompliantOrganizationUserCommand = revokeNonCompliantOrganizationUserCommand;
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery; _twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
_distributedCache = distributedCache; _distributedCache = distributedCache;

View File

@ -4,8 +4,19 @@ using AutoFixture.Kernel;
namespace Bit.Test.Common.AutoFixture; namespace Bit.Test.Common.AutoFixture;
/// <summary>
/// A utility class that encapsulates a system under test (sut) and its dependencies.
/// By default, all dependencies are initialized as mocks using the NSubstitute library.
/// SutProvider provides an interface for accessing these dependencies in the arrange and assert stages of your tests.
/// </summary>
/// <typeparam name="TSut">The concrete implementation of the class being tested.</typeparam>
public class SutProvider<TSut> : ISutProvider public class SutProvider<TSut> : ISutProvider
{ {
/// <summary>
/// A record of the configured dependencies (constructor parameters). The outer Dictionary is keyed by the dependency's
/// type, and the inner dictionary is keyed by the parameter name (optionally used to disambiguate parameters with the same type).
/// The inner dictionary value is the dependency.
/// </summary>
private Dictionary<Type, Dictionary<string, object>> _dependencies; private Dictionary<Type, Dictionary<string, object>> _dependencies;
private readonly IFixture _fixture; private readonly IFixture _fixture;
private readonly ConstructorParameterRelay<TSut> _constructorParameterRelay; private readonly ConstructorParameterRelay<TSut> _constructorParameterRelay;
@ -23,9 +34,21 @@ public class SutProvider<TSut> : ISutProvider
_fixture.Customizations.Add(_constructorParameterRelay); _fixture.Customizations.Add(_constructorParameterRelay);
} }
/// <summary>
/// Registers a dependency to be injected when the sut is created. You must call <see cref="Create"/> after
/// this method to (re)create the sut with the dependency.
/// </summary>
/// <param name="dependency">The dependency to register.</param>
/// <param name="parameterName">An optional parameter name to disambiguate the dependency if there are multiple of the same type. You generally don't need this.</param>
/// <typeparam name="T">The type to register the dependency under - usually an interface. This should match the type expected by the sut's constructor.</typeparam>
/// <returns></returns>
public SutProvider<TSut> SetDependency<T>(T dependency, string parameterName = "") public SutProvider<TSut> SetDependency<T>(T dependency, string parameterName = "")
=> SetDependency(typeof(T), dependency, parameterName); => SetDependency(typeof(T), dependency, parameterName);
public SutProvider<TSut> SetDependency(Type dependencyType, object dependency, string parameterName = "")
/// <summary>
/// An overload for <see cref="SetDependency{T}"/> which takes a runtime <see cref="Type"/> object rather than a compile-time type.
/// </summary>
private SutProvider<TSut> SetDependency(Type dependencyType, object dependency, string parameterName = "")
{ {
if (_dependencies.TryGetValue(dependencyType, out var dependencyForType)) if (_dependencies.TryGetValue(dependencyType, out var dependencyForType))
{ {
@ -39,45 +62,69 @@ public class SutProvider<TSut> : ISutProvider
return this; return this;
} }
/// <summary>
/// Gets a dependency of the sut. Can only be called after the dependency has been set, either explicitly with
/// <see cref="SetDependency{T}"/> or automatically with <see cref="Create"/>.
/// As dependencies are initialized with NSubstitute mocks by default, this is often used to retrieve those mocks in order to
/// configure them during the arrange stage, or check received calls in the assert stage.
/// </summary>
/// <param name="parameterName">An optional parameter name to disambiguate the dependency if there are multiple of the same type. You generally don't need this.</param>
/// <typeparam name="T">The type of the dependency you want to get - usually an interface.</typeparam>
/// <returns>The dependency.</returns>
public T GetDependency<T>(string parameterName = "") => (T)GetDependency(typeof(T), parameterName); public T GetDependency<T>(string parameterName = "") => (T)GetDependency(typeof(T), parameterName);
public object GetDependency(Type dependencyType, string parameterName = "")
/// <summary>
/// An overload for <see cref="GetDependency{T}"/> which takes a runtime <see cref="Type"/> object rather than a compile-time type.
/// </summary>
private object GetDependency(Type dependencyType, string parameterName = "")
{ {
if (DependencyIsSet(dependencyType, parameterName)) if (DependencyIsSet(dependencyType, parameterName))
{ {
return _dependencies[dependencyType][parameterName]; return _dependencies[dependencyType][parameterName];
} }
else if (_dependencies.TryGetValue(dependencyType, out var knownDependencies))
if (_dependencies.TryGetValue(dependencyType, out var knownDependencies))
{ {
if (knownDependencies.Values.Count == 1) if (knownDependencies.Values.Count == 1)
{ {
return knownDependencies.Values.Single(); return knownDependencies.Values.Single();
} }
else
{
throw new ArgumentException(string.Concat($"Dependency of type {dependencyType.Name} and name ", throw new ArgumentException(string.Concat($"Dependency of type {dependencyType.Name} and name ",
$"{parameterName} does not exist. Available dependency names are: ", $"{parameterName} does not exist. Available dependency names are: ",
string.Join(", ", knownDependencies.Keys))); string.Join(", ", knownDependencies.Keys)));
} }
}
else
{
throw new ArgumentException($"Dependency of type {dependencyType.Name} and name {parameterName} has not been set."); throw new ArgumentException($"Dependency of type {dependencyType.Name} and name {parameterName} has not been set.");
} }
}
/// <summary>
/// Clear all the dependencies and the sut. This reverts the SutProvider back to a fully uninitialized state.
/// </summary>
public void Reset() public void Reset()
{ {
_dependencies = new Dictionary<Type, Dictionary<string, object>>(); _dependencies = new Dictionary<Type, Dictionary<string, object>>();
Sut = default; Sut = default;
} }
/// <summary>
/// Recreate a new sut with all new dependencies. This will reset all dependencies, including mocked return values
/// and any dependencies set with <see cref="SetDependency{T}"/>.
/// </summary>
public void Recreate() public void Recreate()
{ {
_dependencies = new Dictionary<Type, Dictionary<string, object>>(); _dependencies = new Dictionary<Type, Dictionary<string, object>>();
Sut = _fixture.Create<TSut>(); Sut = _fixture.Create<TSut>();
} }
/// <inheritdoc cref="Create()"/>>
ISutProvider ISutProvider.Create() => Create(); ISutProvider ISutProvider.Create() => Create();
/// <summary>
/// Creates the sut, injecting any dependencies configured via <see cref="SetDependency{T}"/> and falling back to
/// NSubstitute mocks for any dependencies that have not been explicitly configured.
/// </summary>
/// <returns></returns>
public SutProvider<TSut> Create() public SutProvider<TSut> Create()
{ {
Sut = _fixture.Create<TSut>(); Sut = _fixture.Create<TSut>();
@ -89,6 +136,19 @@ public class SutProvider<TSut> : ISutProvider
private object GetDefault(Type type) => type.IsValueType ? Activator.CreateInstance(type) : null; private object GetDefault(Type type) => type.IsValueType ? Activator.CreateInstance(type) : null;
/// <summary>
/// A specimen builder which tells Autofixture to use the dependency registered in <see cref="SutProvider{T}"/>
/// when creating test data. If no matching dependency exists in <see cref="SutProvider{TSut}"/>, it creates
/// an NSubstitute mock and registers it using <see cref="SutProvider{TSut}.SetDependency{T}"/>
/// so it can be retrieved later.
/// This is the link between <see cref="SutProvider{T}"/> and Autofixture.
/// </summary>
/// <remarks>
/// Autofixture knows how to create sample data of simple types (such as an int or string) but not more complex classes.
/// We create our own <see cref="ISpecimenBuilder"/> and register it with the <see cref="Fixture"/> in
/// <see cref="SutProvider{TSut}"/> to provide that instruction.
/// </remarks>
/// <typeparam name="T">The type of the sut.</typeparam>
private class ConstructorParameterRelay<T> : ISpecimenBuilder private class ConstructorParameterRelay<T> : ISpecimenBuilder
{ {
private readonly SutProvider<T> _sutProvider; private readonly SutProvider<T> _sutProvider;
@ -102,6 +162,7 @@ public class SutProvider<TSut> : ISutProvider
public object Create(object request, ISpecimenContext context) public object Create(object request, ISpecimenContext context)
{ {
// Basic checks to filter out irrelevant requests from Autofixture
if (context == null) if (context == null)
{ {
throw new ArgumentNullException(nameof(context)); throw new ArgumentNullException(nameof(context));
@ -116,16 +177,22 @@ public class SutProvider<TSut> : ISutProvider
return new NoSpecimen(); return new NoSpecimen();
} }
// Use the dependency set under this parameter name, if any
if (_sutProvider.DependencyIsSet(parameterInfo.ParameterType, parameterInfo.Name)) if (_sutProvider.DependencyIsSet(parameterInfo.ParameterType, parameterInfo.Name))
{ {
return _sutProvider.GetDependency(parameterInfo.ParameterType, parameterInfo.Name); return _sutProvider.GetDependency(parameterInfo.ParameterType, parameterInfo.Name);
} }
// Return default type if set
else if (_sutProvider.DependencyIsSet(parameterInfo.ParameterType, "")) // Use the default dependency set for this type, if any (i.e. no parameter name has been specified)
if (_sutProvider.DependencyIsSet(parameterInfo.ParameterType, ""))
{ {
return _sutProvider.GetDependency(parameterInfo.ParameterType, ""); return _sutProvider.GetDependency(parameterInfo.ParameterType, "");
} }
// Fallback: pass the request down the chain. This lets another fixture customization populate the value.
// If you haven't added any customizations, this should be an NSubstitute mock.
// It is registered with SetDependency so you can retrieve it later.
// This is the equivalent of _fixture.Create<parameterInfo.ParameterType>, but no overload for // This is the equivalent of _fixture.Create<parameterInfo.ParameterType>, but no overload for
// Create(Type type) exists. // Create(Type type) exists.
var dependency = new SpecimenContext(_fixture).Resolve(new SeededRequest(parameterInfo.ParameterType, var dependency = new SpecimenContext(_fixture).Resolve(new SeededRequest(parameterInfo.ParameterType,

View File

@ -288,7 +288,7 @@ public class SavePolicyCommandTests
{ {
return new SutProvider<SavePolicyCommand>() return new SutProvider<SavePolicyCommand>()
.WithFakeTimeProvider() .WithFakeTimeProvider()
.SetDependency(typeof(IEnumerable<IPolicyValidator>), policyValidators ?? []) .SetDependency(policyValidators ?? [])
.Create(); .Create();
} }

View File

@ -7,13 +7,10 @@ using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies; using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services; using Bit.Core.AdminConsole.Services;
using Bit.Core.Auth.Enums; using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Models; using Bit.Core.Auth.Models;
using Bit.Core.Auth.Models.Business.Tokenables;
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
using Bit.Core.Billing.Services;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
@ -21,22 +18,15 @@ using Bit.Core.Exceptions;
using Bit.Core.Models.Business; using Bit.Core.Models.Business;
using Bit.Core.Models.Data.Organizations; using Bit.Core.Models.Data.Organizations;
using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.Platform.Push;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Settings; using Bit.Core.Settings;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Bit.Core.Vault.Repositories;
using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.AutoFixture.Attributes;
using Bit.Test.Common.Fakes;
using Bit.Test.Common.Helpers; using Bit.Test.Common.Helpers;
using Fido2NetLib;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using NSubstitute; using NSubstitute;
using Xunit; using Xunit;
@ -179,9 +169,12 @@ public class UserServiceTests
[Theory] [Theory]
[BitAutoData(DeviceType.UnknownBrowser, "Unknown Browser")] [BitAutoData(DeviceType.UnknownBrowser, "Unknown Browser")]
[BitAutoData(DeviceType.Android, "Android")] [BitAutoData(DeviceType.Android, "Android")]
public async Task SendNewDeviceVerificationEmailAsync_DeviceMatches(DeviceType deviceType, string deviceTypeName, SutProvider<UserService> sutProvider, User user) public async Task SendNewDeviceVerificationEmailAsync_DeviceMatches(DeviceType deviceType, string deviceTypeName,
User user)
{ {
SetupFakeTokenProvider(sutProvider, user); var sutProvider = new SutProvider<UserService>()
.CreateWithUserServiceCustomizations(user);
var context = sutProvider.GetDependency<ICurrentContext>(); var context = sutProvider.GetDependency<ICurrentContext>();
context.DeviceType = deviceType; context.DeviceType = deviceType;
context.IpAddress = "1.1.1.1"; context.IpAddress = "1.1.1.1";
@ -194,9 +187,11 @@ public class UserServiceTests
} }
[Theory, BitAutoData] [Theory, BitAutoData]
public async Task SendNewDeviceVerificationEmailAsync_NullDeviceTypeShouldSendUnkownBrowserType(SutProvider<UserService> sutProvider, User user) public async Task SendNewDeviceVerificationEmailAsync_NullDeviceTypeShouldSendUnkownBrowserType(User user)
{ {
SetupFakeTokenProvider(sutProvider, user); var sutProvider = new SutProvider<UserService>()
.CreateWithUserServiceCustomizations(user);
var context = sutProvider.GetDependency<ICurrentContext>(); var context = sutProvider.GetDependency<ICurrentContext>();
context.DeviceType = null; context.DeviceType = null;
context.IpAddress = "1.1.1.1"; context.IpAddress = "1.1.1.1";
@ -266,76 +261,28 @@ public class UserServiceTests
[BitAutoData(true, "bad_test_password", false, ShouldCheck.Password | ShouldCheck.OTP)] [BitAutoData(true, "bad_test_password", false, ShouldCheck.Password | ShouldCheck.OTP)]
public async Task VerifySecretAsync_Works( public async Task VerifySecretAsync_Works(
bool shouldHavePassword, string secret, bool expectedIsVerified, ShouldCheck shouldCheck, // inline theory data bool shouldHavePassword, string secret, bool expectedIsVerified, ShouldCheck shouldCheck, // inline theory data
SutProvider<UserService> sutProvider, User user) // AutoFixture injected data User user) // AutoFixture injected data
{ {
// Arrange // Arrange
var tokenProvider = SetupFakeTokenProvider(sutProvider, user);
SetupUserAndDevice(user, shouldHavePassword); SetupUserAndDevice(user, shouldHavePassword);
var sutProvider = new SutProvider<UserService>()
.CreateWithUserServiceCustomizations(user);
// Setup the fake password verification // Setup the fake password verification
var substitutedUserPasswordStore = Substitute.For<IUserPasswordStore<User>>(); sutProvider.GetDependency<IUserPasswordStore<User>>()
substitutedUserPasswordStore
.GetPasswordHashAsync(user, Arg.Any<CancellationToken>()) .GetPasswordHashAsync(user, Arg.Any<CancellationToken>())
.Returns((ci) => .Returns(Task.FromResult("hashed_test_password"));
{
return Task.FromResult("hashed_test_password");
});
sutProvider.SetDependency<IUserStore<User>>(substitutedUserPasswordStore, "store"); sutProvider.GetDependency<IPasswordHasher<User>>()
sutProvider.GetDependency<IPasswordHasher<User>>("passwordHasher")
.VerifyHashedPassword(user, "hashed_test_password", "test_password") .VerifyHashedPassword(user, "hashed_test_password", "test_password")
.Returns((ci) => .Returns(PasswordVerificationResult.Success);
{
return PasswordVerificationResult.Success;
});
// HACK: SutProvider is being weird about not injecting the IPasswordHasher that I configured var actualIsVerified = await sutProvider.Sut.VerifySecretAsync(user, secret);
var sut = new UserService(
sutProvider.GetDependency<IUserRepository>(),
sutProvider.GetDependency<ICipherRepository>(),
sutProvider.GetDependency<IOrganizationUserRepository>(),
sutProvider.GetDependency<IOrganizationRepository>(),
sutProvider.GetDependency<IOrganizationDomainRepository>(),
sutProvider.GetDependency<IMailService>(),
sutProvider.GetDependency<IPushNotificationService>(),
sutProvider.GetDependency<IUserStore<User>>(),
sutProvider.GetDependency<IOptions<IdentityOptions>>(),
sutProvider.GetDependency<IPasswordHasher<User>>(),
sutProvider.GetDependency<IEnumerable<IUserValidator<User>>>(),
sutProvider.GetDependency<IEnumerable<IPasswordValidator<User>>>(),
sutProvider.GetDependency<ILookupNormalizer>(),
sutProvider.GetDependency<IdentityErrorDescriber>(),
sutProvider.GetDependency<IServiceProvider>(),
sutProvider.GetDependency<ILogger<UserManager<User>>>(),
sutProvider.GetDependency<ILicensingService>(),
sutProvider.GetDependency<IEventService>(),
sutProvider.GetDependency<IApplicationCacheService>(),
sutProvider.GetDependency<IDataProtectionProvider>(),
sutProvider.GetDependency<IPaymentService>(),
sutProvider.GetDependency<IPolicyRepository>(),
sutProvider.GetDependency<IPolicyService>(),
sutProvider.GetDependency<IFido2>(),
sutProvider.GetDependency<ICurrentContext>(),
sutProvider.GetDependency<IGlobalSettings>(),
sutProvider.GetDependency<IAcceptOrgUserCommand>(),
sutProvider.GetDependency<IProviderUserRepository>(),
sutProvider.GetDependency<IStripeSyncService>(),
new FakeDataProtectorTokenFactory<OrgUserInviteTokenable>(),
sutProvider.GetDependency<IFeatureService>(),
sutProvider.GetDependency<IPremiumUserBillingService>(),
sutProvider.GetDependency<IRemoveOrganizationUserCommand>(),
sutProvider.GetDependency<IRevokeNonCompliantOrganizationUserCommand>(),
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>(),
sutProvider.GetDependency<IDistributedCache>(),
sutProvider.GetDependency<IPolicyRequirementQuery>()
);
var actualIsVerified = await sut.VerifySecretAsync(user, secret);
Assert.Equal(expectedIsVerified, actualIsVerified); Assert.Equal(expectedIsVerified, actualIsVerified);
await tokenProvider await sutProvider.GetDependency<IUserTwoFactorTokenProvider<User>>()
.Received(shouldCheck.HasFlag(ShouldCheck.OTP) ? 1 : 0) .Received(shouldCheck.HasFlag(ShouldCheck.OTP) ? 1 : 0)
.ValidateAsync(Arg.Any<string>(), secret, Arg.Any<UserManager<User>>(), user); .ValidateAsync(Arg.Any<string>(), secret, Arg.Any<UserManager<User>>(), user);
@ -661,26 +608,25 @@ public class UserServiceTests
} }
[Theory, BitAutoData] [Theory, BitAutoData]
public async Task ResendNewDeviceVerificationEmail_SendsToken_Success( public async Task ResendNewDeviceVerificationEmail_SendsToken_Success(User user)
SutProvider<UserService> sutProvider, User user)
{ {
// Arrange // Arrange
var testPassword = "test_password"; var testPassword = "test_password";
var tokenProvider = SetupFakeTokenProvider(sutProvider, user);
SetupUserAndDevice(user, true); SetupUserAndDevice(user, true);
var sutProvider = new SutProvider<UserService>()
.CreateWithUserServiceCustomizations(user);
// Setup the fake password verification // Setup the fake password verification
var substitutedUserPasswordStore = Substitute.For<IUserPasswordStore<User>>(); sutProvider
substitutedUserPasswordStore .GetDependency<IUserPasswordStore<User>>()
.GetPasswordHashAsync(user, Arg.Any<CancellationToken>()) .GetPasswordHashAsync(user, Arg.Any<CancellationToken>())
.Returns((ci) => .Returns((ci) =>
{ {
return Task.FromResult("hashed_test_password"); return Task.FromResult("hashed_test_password");
}); });
sutProvider.SetDependency<IUserStore<User>>(substitutedUserPasswordStore, "store"); sutProvider.GetDependency<IPasswordHasher<User>>()
sutProvider.GetDependency<IPasswordHasher<User>>("passwordHasher")
.VerifyHashedPassword(user, "hashed_test_password", testPassword) .VerifyHashedPassword(user, "hashed_test_password", testPassword)
.Returns((ci) => .Returns((ci) =>
{ {
@ -695,10 +641,7 @@ public class UserServiceTests
context.DeviceType = DeviceType.Android; context.DeviceType = DeviceType.Android;
context.IpAddress = "1.1.1.1"; context.IpAddress = "1.1.1.1";
// HACK: SutProvider is being weird about not injecting the IPasswordHasher that I configured await sutProvider.Sut.ResendNewDeviceVerificationEmail(user.Email, testPassword);
var sut = RebuildSut(sutProvider);
await sut.ResendNewDeviceVerificationEmail(user.Email, testPassword);
await sutProvider.GetDependency<IMailService>() await sutProvider.GetDependency<IMailService>()
.Received(1) .Received(1)
@ -842,8 +785,15 @@ public class UserServiceTests
user.MasterPassword = null; user.MasterPassword = null;
} }
} }
}
private static IUserTwoFactorTokenProvider<User> SetupFakeTokenProvider(SutProvider<UserService> sutProvider, User user) public static class UserServiceSutProviderExtensions
{
/// <summary>
/// Arranges a fake token provider. Must call as part of a builder pattern that ends in Create(), as it modifies
/// the SutProvider build chain.
/// </summary>
private static SutProvider<UserService> SetFakeTokenProvider(this SutProvider<UserService> sutProvider, User user)
{ {
var fakeUserTwoFactorProvider = Substitute.For<IUserTwoFactorTokenProvider<User>>(); var fakeUserTwoFactorProvider = Substitute.For<IUserTwoFactorTokenProvider<User>>();
@ -859,8 +809,11 @@ public class UserServiceTests
.ValidateAsync(Arg.Any<string>(), "otp_token", Arg.Any<UserManager<User>>(), user) .ValidateAsync(Arg.Any<string>(), "otp_token", Arg.Any<UserManager<User>>(), user)
.Returns(true); .Returns(true);
sutProvider.GetDependency<IOptions<IdentityOptions>>() var fakeIdentityOptions = Substitute.For<IOptions<IdentityOptions>>();
.Value.Returns(new IdentityOptions
fakeIdentityOptions
.Value
.Returns(new IdentityOptions
{ {
Tokens = new TokenOptions Tokens = new TokenOptions
{ {
@ -874,54 +827,54 @@ public class UserServiceTests
} }
}); });
// The above arranging of dependencies is used in the constructor of UserManager sutProvider.SetDependency(fakeIdentityOptions);
// ref: https://github.com/dotnet/aspnetcore/blob/bfeb3bf9005c36b081d1e48725531ee0e15a9dfb/src/Identity/Extensions.Core/src/UserManager.cs#L103-L120 // Also set the fake provider dependency so that we can retrieve it easily via GetDependency
// since the constructor of the Sut has ran already (when injected) I need to recreate it to get it to run again sutProvider.SetDependency(fakeUserTwoFactorProvider);
sutProvider.Create();
return fakeUserTwoFactorProvider; return sutProvider;
} }
private IUserService RebuildSut(SutProvider<UserService> sutProvider) /// <summary>
/// Properly registers IUserPasswordStore as IUserStore so it's injected when the sut is initialized.
/// </summary>
/// <param name="sutProvider"></param>
/// <returns></returns>
private static SutProvider<UserService> SetUserPasswordStore(this SutProvider<UserService> sutProvider)
{ {
return new UserService( var substitutedUserPasswordStore = Substitute.For<IUserPasswordStore<User>>();
sutProvider.GetDependency<IUserRepository>(),
sutProvider.GetDependency<ICipherRepository>(), // IUserPasswordStore must be registered under the IUserStore parameter to be properly injected
sutProvider.GetDependency<IOrganizationUserRepository>(), // because this is what the constructor expects
sutProvider.GetDependency<IOrganizationRepository>(), sutProvider.SetDependency<IUserStore<User>>(substitutedUserPasswordStore);
sutProvider.GetDependency<IOrganizationDomainRepository>(),
sutProvider.GetDependency<IMailService>(), // Also store it under its own type for retrieval and configuration
sutProvider.GetDependency<IPushNotificationService>(), sutProvider.SetDependency(substitutedUserPasswordStore);
sutProvider.GetDependency<IUserStore<User>>(),
sutProvider.GetDependency<IOptions<IdentityOptions>>(), return sutProvider;
sutProvider.GetDependency<IPasswordHasher<User>>(),
sutProvider.GetDependency<IEnumerable<IUserValidator<User>>>(),
sutProvider.GetDependency<IEnumerable<IPasswordValidator<User>>>(),
sutProvider.GetDependency<ILookupNormalizer>(),
sutProvider.GetDependency<IdentityErrorDescriber>(),
sutProvider.GetDependency<IServiceProvider>(),
sutProvider.GetDependency<ILogger<UserManager<User>>>(),
sutProvider.GetDependency<ILicensingService>(),
sutProvider.GetDependency<IEventService>(),
sutProvider.GetDependency<IApplicationCacheService>(),
sutProvider.GetDependency<IDataProtectionProvider>(),
sutProvider.GetDependency<IPaymentService>(),
sutProvider.GetDependency<IPolicyRepository>(),
sutProvider.GetDependency<IPolicyService>(),
sutProvider.GetDependency<IFido2>(),
sutProvider.GetDependency<ICurrentContext>(),
sutProvider.GetDependency<IGlobalSettings>(),
sutProvider.GetDependency<IAcceptOrgUserCommand>(),
sutProvider.GetDependency<IProviderUserRepository>(),
sutProvider.GetDependency<IStripeSyncService>(),
new FakeDataProtectorTokenFactory<OrgUserInviteTokenable>(),
sutProvider.GetDependency<IFeatureService>(),
sutProvider.GetDependency<IPremiumUserBillingService>(),
sutProvider.GetDependency<IRemoveOrganizationUserCommand>(),
sutProvider.GetDependency<IRevokeNonCompliantOrganizationUserCommand>(),
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>(),
sutProvider.GetDependency<IDistributedCache>(),
sutProvider.GetDependency<IPolicyRequirementQuery>()
);
} }
/// <summary>
/// This is a hack: when autofixture initializes the sut in sutProvider, it overwrites the public
/// PasswordHasher property with a new substitute, so it loses the configured sutProvider mock.
/// This doesn't usually happen because our dependencies are not usually public.
/// Call this AFTER SutProvider.Create().
/// </summary>
private static SutProvider<UserService> FixPasswordHasherBug(this SutProvider<UserService> sutProvider)
{
// Get the configured sutProvider mock and assign it back to the public property in the base class
sutProvider.Sut.PasswordHasher = sutProvider.GetDependency<IPasswordHasher<User>>();
return sutProvider;
}
/// <summary>
/// A helper that combines all SutProvider configuration usually required for UserService.
/// Call this instead of SutProvider.Create, after any additional configuration your test needs.
/// </summary>
public static SutProvider<UserService> CreateWithUserServiceCustomizations(this SutProvider<UserService> sutProvider, User user)
=> sutProvider
.SetUserPasswordStore()
.SetFakeTokenProvider(user)
.Create()
.FixPasswordHasherBug();
} }