1
0
mirror of https://github.com/bitwarden/server synced 2025-12-15 15:53:59 +00:00

[PM-26683] Migrate individual policy handlers/validators to the new Policy Update Events pattern (#6458)

* Implement IOnPolicyPreUpdateEvent for FreeFamiliesForEnterprisePolicyValidator and add corresponding unit tests

* Implement IEnforceDependentPoliciesEvent in MaximumVaultTimeoutPolicyValidator

* Rename test methods in FreeFamiliesForEnterprisePolicyValidatorTests for consistency

* Implement IPolicyValidationEvent and IEnforceDependentPoliciesEvent in RequireSsoPolicyValidator and enhance unit tests

* Implement IPolicyValidationEvent and IEnforceDependentPoliciesEvent in ResetPasswordPolicyValidator and add unit tests

* Implement IOnPolicyPreUpdateEvent in TwoFactorAuthenticationPolicyValidator and add unit tests

* Implement IPolicyValidationEvent and IOnPolicyPreUpdateEvent in SingleOrgPolicyValidator with corresponding unit tests

* Implement IOnPolicyPostUpdateEvent in OrganizationDataOwnershipPolicyValidator and add unit tests for ExecutePostUpsertSideEffectAsync

* Refactor policy validation logic in VNextSavePolicyCommand to simplify enabling and disabling requirements checks

* Refactor VNextSavePolicyCommand to replace IEnforceDependentPoliciesEvent with IPolicyUpdateEvent and update related tests

* Add AddPolicyUpdateEvents method and update service registration for policy update events
This commit is contained in:
Rui Tomé
2025-10-16 10:18:37 +01:00
committed by GitHub
parent 0fb7099620
commit 132db95fb7
16 changed files with 721 additions and 89 deletions

View File

@@ -72,4 +72,65 @@ public class FreeFamiliesForEnterprisePolicyValidatorTests
organizationSponsorships[0].SponsoredOrganizationId.ToString(), organization.Name);
}
[Theory, BitAutoData]
public async Task ExecutePreUpsertSideEffectAsync_DoesNotNotifyUserWhenPolicyDisabled(
Organization organization,
List<OrganizationSponsorship> organizationSponsorships,
[PolicyUpdate(PolicyType.FreeFamiliesSponsorshipPolicy)] PolicyUpdate policyUpdate,
[Policy(PolicyType.FreeFamiliesSponsorshipPolicy, true)] Policy policy,
SutProvider<FreeFamiliesForEnterprisePolicyValidator> sutProvider)
{
policy.Enabled = true;
policyUpdate.Enabled = false;
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(policyUpdate.OrganizationId)
.Returns(organization);
sutProvider.GetDependency<IOrganizationSponsorshipRepository>()
.GetManyBySponsoringOrganizationAsync(policyUpdate.OrganizationId)
.Returns(organizationSponsorships);
var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel());
await sutProvider.Sut.ExecutePreUpsertSideEffectAsync(savePolicyModel, policy);
await sutProvider.GetDependency<IMailService>()
.DidNotReceiveWithAnyArgs()
.SendFamiliesForEnterpriseRemoveSponsorshipsEmailAsync(default, default, default, default);
}
[Theory, BitAutoData]
public async Task ExecutePreUpsertSideEffectAsync_DoesNotifyUserWhenPolicyEnabled(
Organization organization,
List<OrganizationSponsorship> organizationSponsorships,
[PolicyUpdate(PolicyType.FreeFamiliesSponsorshipPolicy)] PolicyUpdate policyUpdate,
[Policy(PolicyType.FreeFamiliesSponsorshipPolicy, false)] Policy policy,
SutProvider<FreeFamiliesForEnterprisePolicyValidator> sutProvider)
{
policy.Enabled = false;
policyUpdate.Enabled = true;
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(policyUpdate.OrganizationId)
.Returns(organization);
sutProvider.GetDependency<IOrganizationSponsorshipRepository>()
.GetManyBySponsoringOrganizationAsync(policyUpdate.OrganizationId)
.Returns(organizationSponsorships);
var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel());
await sutProvider.Sut.ExecutePreUpsertSideEffectAsync(savePolicyModel, policy);
var offerAcceptanceDate = organizationSponsorships[0].ValidUntil!.Value.AddDays(-7).ToString("MM/dd/yyyy");
await sutProvider.GetDependency<IMailService>()
.Received(1)
.SendFamiliesForEnterpriseRemoveSponsorshipsEmailAsync(
organizationSponsorships[0].FriendlyName,
offerAcceptanceDate,
organizationSponsorships[0].SponsoredOrganizationId.ToString(),
organization.Name);
}
}

View File

@@ -274,4 +274,176 @@ public class OrganizationDataOwnershipPolicyValidatorTests
return sut;
}
[Theory, BitAutoData]
public async Task ExecutePostUpsertSideEffectAsync_FeatureFlagDisabled_DoesNothing(
[PolicyUpdate(PolicyType.OrganizationDataOwnership, false)] PolicyUpdate policyUpdate,
[Policy(PolicyType.OrganizationDataOwnership, false)] Policy postUpdatedPolicy,
[Policy(PolicyType.OrganizationDataOwnership, false)] Policy previousPolicyState,
SutProvider<OrganizationDataOwnershipPolicyValidator> sutProvider)
{
// Arrange
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.CreateDefaultLocation)
.Returns(false);
var policyRequest = new SavePolicyModel(policyUpdate, null, new OrganizationModelOwnershipPolicyModel(_defaultUserCollectionName));
// Act
await sutProvider.Sut.ExecutePostUpsertSideEffectAsync(policyRequest, postUpdatedPolicy, previousPolicyState);
// Assert
await sutProvider.GetDependency<ICollectionRepository>()
.DidNotReceiveWithAnyArgs()
.UpsertDefaultCollectionsAsync(default, default, default);
}
[Theory, BitAutoData]
public async Task ExecutePostUpsertSideEffectAsync_PolicyAlreadyEnabled_DoesNothing(
[PolicyUpdate(PolicyType.OrganizationDataOwnership, true)] PolicyUpdate policyUpdate,
[Policy(PolicyType.OrganizationDataOwnership, true)] Policy postUpdatedPolicy,
[Policy(PolicyType.OrganizationDataOwnership, true)] Policy previousPolicyState,
SutProvider<OrganizationDataOwnershipPolicyValidator> sutProvider)
{
// Arrange
postUpdatedPolicy.OrganizationId = policyUpdate.OrganizationId;
previousPolicyState.OrganizationId = policyUpdate.OrganizationId;
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.CreateDefaultLocation)
.Returns(true);
var policyRequest = new SavePolicyModel(policyUpdate, null, new OrganizationModelOwnershipPolicyModel(_defaultUserCollectionName));
// Act
await sutProvider.Sut.ExecutePostUpsertSideEffectAsync(policyRequest, postUpdatedPolicy, previousPolicyState);
// Assert
await sutProvider.GetDependency<ICollectionRepository>()
.DidNotReceiveWithAnyArgs()
.UpsertDefaultCollectionsAsync(default, default, default);
}
[Theory, BitAutoData]
public async Task ExecutePostUpsertSideEffectAsync_PolicyBeingDisabled_DoesNothing(
[PolicyUpdate(PolicyType.OrganizationDataOwnership, false)] PolicyUpdate policyUpdate,
[Policy(PolicyType.OrganizationDataOwnership, false)] Policy postUpdatedPolicy,
[Policy(PolicyType.OrganizationDataOwnership)] Policy previousPolicyState,
SutProvider<OrganizationDataOwnershipPolicyValidator> sutProvider)
{
// Arrange
previousPolicyState.OrganizationId = policyUpdate.OrganizationId;
postUpdatedPolicy.OrganizationId = policyUpdate.OrganizationId;
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.CreateDefaultLocation)
.Returns(true);
var policyRequest = new SavePolicyModel(policyUpdate, null, new OrganizationModelOwnershipPolicyModel(_defaultUserCollectionName));
// Act
await sutProvider.Sut.ExecutePostUpsertSideEffectAsync(policyRequest, postUpdatedPolicy, previousPolicyState);
// Assert
await sutProvider.GetDependency<ICollectionRepository>()
.DidNotReceiveWithAnyArgs()
.UpsertDefaultCollectionsAsync(default, default, default);
}
[Theory, BitAutoData]
public async Task ExecutePostUpsertSideEffectAsync_WhenNoUsersExist_DoNothing(
[PolicyUpdate(PolicyType.OrganizationDataOwnership, true)] PolicyUpdate policyUpdate,
[Policy(PolicyType.OrganizationDataOwnership, true)] Policy postUpdatedPolicy,
[Policy(PolicyType.OrganizationDataOwnership, false)] Policy previousPolicyState,
OrganizationDataOwnershipPolicyRequirementFactory factory)
{
// Arrange
postUpdatedPolicy.OrganizationId = policyUpdate.OrganizationId;
previousPolicyState.OrganizationId = policyUpdate.OrganizationId;
var policyRepository = ArrangePolicyRepository([]);
var collectionRepository = Substitute.For<ICollectionRepository>();
var sut = ArrangeSut(factory, policyRepository, collectionRepository);
var policyRequest = new SavePolicyModel(policyUpdate, null, new OrganizationModelOwnershipPolicyModel(_defaultUserCollectionName));
// Act
await sut.ExecutePostUpsertSideEffectAsync(policyRequest, postUpdatedPolicy, previousPolicyState);
// Assert
await collectionRepository
.DidNotReceiveWithAnyArgs()
.UpsertDefaultCollectionsAsync(
default,
default,
default);
await policyRepository
.Received(1)
.GetPolicyDetailsByOrganizationIdAsync(
policyUpdate.OrganizationId,
PolicyType.OrganizationDataOwnership);
}
[Theory]
[BitMemberAutoData(nameof(ShouldUpsertDefaultCollectionsTestCases))]
public async Task ExecutePostUpsertSideEffectAsync_WithRequirements_ShouldUpsertDefaultCollections(
Policy postUpdatedPolicy,
Policy? previousPolicyState,
[PolicyUpdate(PolicyType.OrganizationDataOwnership)] PolicyUpdate policyUpdate,
[OrganizationPolicyDetails(PolicyType.OrganizationDataOwnership)] IEnumerable<OrganizationPolicyDetails> orgPolicyDetails,
OrganizationDataOwnershipPolicyRequirementFactory factory)
{
// Arrange
var orgPolicyDetailsList = orgPolicyDetails.ToList();
foreach (var policyDetail in orgPolicyDetailsList)
{
policyDetail.OrganizationId = policyUpdate.OrganizationId;
}
var policyRepository = ArrangePolicyRepository(orgPolicyDetailsList);
var collectionRepository = Substitute.For<ICollectionRepository>();
var sut = ArrangeSut(factory, policyRepository, collectionRepository);
var policyRequest = new SavePolicyModel(policyUpdate, null, new OrganizationModelOwnershipPolicyModel(_defaultUserCollectionName));
// Act
await sut.ExecutePostUpsertSideEffectAsync(policyRequest, postUpdatedPolicy, previousPolicyState);
// Assert
await collectionRepository
.Received(1)
.UpsertDefaultCollectionsAsync(
policyUpdate.OrganizationId,
Arg.Is<IEnumerable<Guid>>(ids => ids.Count() == 3),
_defaultUserCollectionName);
}
[Theory]
[BitMemberAutoData(nameof(WhenDefaultCollectionsDoesNotExistTestCases))]
public async Task ExecutePostUpsertSideEffectAsync_WhenDefaultCollectionNameIsInvalid_DoesNothing(
IPolicyMetadataModel metadata,
[PolicyUpdate(PolicyType.OrganizationDataOwnership)] PolicyUpdate policyUpdate,
[Policy(PolicyType.OrganizationDataOwnership, true)] Policy postUpdatedPolicy,
[Policy(PolicyType.OrganizationDataOwnership, false)] Policy previousPolicyState,
SutProvider<OrganizationDataOwnershipPolicyValidator> sutProvider)
{
// Arrange
postUpdatedPolicy.OrganizationId = policyUpdate.OrganizationId;
previousPolicyState.OrganizationId = policyUpdate.OrganizationId;
policyUpdate.Enabled = true;
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.CreateDefaultLocation)
.Returns(true);
var policyRequest = new SavePolicyModel(policyUpdate, null, metadata);
// Act
await sutProvider.Sut.ExecutePostUpsertSideEffectAsync(policyRequest, postUpdatedPolicy, previousPolicyState);
// Assert
await sutProvider.GetDependency<ICollectionRepository>()
.DidNotReceiveWithAnyArgs()
.UpsertDefaultCollectionsAsync(default, default, default);
}
}

View File

@@ -72,4 +72,66 @@ public class RequireSsoPolicyValidatorTests
var result = await sutProvider.Sut.ValidateAsync(policyUpdate, policy);
Assert.True(string.IsNullOrEmpty(result));
}
[Theory, BitAutoData]
public async Task ValidateAsync_WithSavePolicyModel_DisablingPolicy_KeyConnectorEnabled_ValidationError(
[PolicyUpdate(PolicyType.RequireSso, false)] PolicyUpdate policyUpdate,
[Policy(PolicyType.RequireSso)] Policy policy,
SutProvider<RequireSsoPolicyValidator> sutProvider)
{
policy.OrganizationId = policyUpdate.OrganizationId;
var ssoConfig = new SsoConfig { Enabled = true };
ssoConfig.SetData(new SsoConfigurationData { MemberDecryptionType = MemberDecryptionType.KeyConnector });
sutProvider.GetDependency<ISsoConfigRepository>()
.GetByOrganizationIdAsync(policyUpdate.OrganizationId)
.Returns(ssoConfig);
var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel());
var result = await sutProvider.Sut.ValidateAsync(savePolicyModel, policy);
Assert.Contains("Key Connector is enabled", result, StringComparison.OrdinalIgnoreCase);
}
[Theory, BitAutoData]
public async Task ValidateAsync_WithSavePolicyModel_DisablingPolicy_TdeEnabled_ValidationError(
[PolicyUpdate(PolicyType.RequireSso, false)] PolicyUpdate policyUpdate,
[Policy(PolicyType.RequireSso)] Policy policy,
SutProvider<RequireSsoPolicyValidator> sutProvider)
{
policy.OrganizationId = policyUpdate.OrganizationId;
var ssoConfig = new SsoConfig { Enabled = true };
ssoConfig.SetData(new SsoConfigurationData { MemberDecryptionType = MemberDecryptionType.TrustedDeviceEncryption });
sutProvider.GetDependency<ISsoConfigRepository>()
.GetByOrganizationIdAsync(policyUpdate.OrganizationId)
.Returns(ssoConfig);
var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel());
var result = await sutProvider.Sut.ValidateAsync(savePolicyModel, policy);
Assert.Contains("Trusted device encryption is on", result, StringComparison.OrdinalIgnoreCase);
}
[Theory, BitAutoData]
public async Task ValidateAsync_WithSavePolicyModel_DisablingPolicy_DecryptionOptionsNotEnabled_Success(
[PolicyUpdate(PolicyType.RequireSso, false)] PolicyUpdate policyUpdate,
[Policy(PolicyType.RequireSso)] Policy policy,
SutProvider<RequireSsoPolicyValidator> sutProvider)
{
policy.OrganizationId = policyUpdate.OrganizationId;
var ssoConfig = new SsoConfig { Enabled = false };
sutProvider.GetDependency<ISsoConfigRepository>()
.GetByOrganizationIdAsync(policyUpdate.OrganizationId)
.Returns(ssoConfig);
var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel());
var result = await sutProvider.Sut.ValidateAsync(savePolicyModel, policy);
Assert.True(string.IsNullOrEmpty(result));
}
}

View File

@@ -68,4 +68,59 @@ public class ResetPasswordPolicyValidatorTests
var result = await sutProvider.Sut.ValidateAsync(policyUpdate, policy);
Assert.True(string.IsNullOrEmpty(result));
}
[Theory]
[BitAutoData(true, false)]
[BitAutoData(false, true)]
[BitAutoData(false, false)]
public async Task ValidateAsync_WithSavePolicyModel_DisablingPolicy_TdeEnabled_ValidationError(
bool policyEnabled,
bool autoEnrollEnabled,
[PolicyUpdate(PolicyType.ResetPassword)] PolicyUpdate policyUpdate,
[Policy(PolicyType.ResetPassword)] Policy policy,
SutProvider<ResetPasswordPolicyValidator> sutProvider)
{
policyUpdate.Enabled = policyEnabled;
policyUpdate.SetDataModel(new ResetPasswordDataModel
{
AutoEnrollEnabled = autoEnrollEnabled
});
policy.OrganizationId = policyUpdate.OrganizationId;
var ssoConfig = new SsoConfig { Enabled = true };
ssoConfig.SetData(new SsoConfigurationData { MemberDecryptionType = MemberDecryptionType.TrustedDeviceEncryption });
sutProvider.GetDependency<ISsoConfigRepository>()
.GetByOrganizationIdAsync(policyUpdate.OrganizationId)
.Returns(ssoConfig);
var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel());
var result = await sutProvider.Sut.ValidateAsync(savePolicyModel, policy);
Assert.Contains("Trusted device encryption is on and requires this policy.", result, StringComparison.OrdinalIgnoreCase);
}
[Theory, BitAutoData]
public async Task ValidateAsync_WithSavePolicyModel_DisablingPolicy_TdeNotEnabled_Success(
[PolicyUpdate(PolicyType.ResetPassword, false)] PolicyUpdate policyUpdate,
[Policy(PolicyType.ResetPassword)] Policy policy,
SutProvider<ResetPasswordPolicyValidator> sutProvider)
{
policyUpdate.SetDataModel(new ResetPasswordDataModel
{
AutoEnrollEnabled = false
});
policy.OrganizationId = policyUpdate.OrganizationId;
var ssoConfig = new SsoConfig { Enabled = false };
sutProvider.GetDependency<ISsoConfigRepository>()
.GetByOrganizationIdAsync(policyUpdate.OrganizationId)
.Returns(ssoConfig);
var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel());
var result = await sutProvider.Sut.ValidateAsync(savePolicyModel, policy);
Assert.True(string.IsNullOrEmpty(result));
}
}

View File

@@ -1,5 +1,6 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
@@ -145,4 +146,135 @@ public class SingleOrgPolicyValidatorTests
.Received(1)
.SendOrganizationUserRevokedForPolicySingleOrgEmailAsync(organization.DisplayName(), nonCompliantUser.Email);
}
[Theory, BitAutoData]
public async Task ValidateAsync_WithSavePolicyModel_DisablingPolicy_KeyConnectorEnabled_ValidationError(
[PolicyUpdate(PolicyType.SingleOrg, false)] PolicyUpdate policyUpdate,
[Policy(PolicyType.SingleOrg)] Policy policy,
SutProvider<SingleOrgPolicyValidator> sutProvider)
{
policy.OrganizationId = policyUpdate.OrganizationId;
var ssoConfig = new SsoConfig { Enabled = true };
ssoConfig.SetData(new SsoConfigurationData { MemberDecryptionType = MemberDecryptionType.KeyConnector });
sutProvider.GetDependency<ISsoConfigRepository>()
.GetByOrganizationIdAsync(policyUpdate.OrganizationId)
.Returns(ssoConfig);
var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel());
var result = await sutProvider.Sut.ValidateAsync(savePolicyModel, policy);
Assert.Contains("Key Connector is enabled", result, StringComparison.OrdinalIgnoreCase);
}
[Theory, BitAutoData]
public async Task ValidateAsync_WithSavePolicyModel_DisablingPolicy_KeyConnectorNotEnabled_Success(
[PolicyUpdate(PolicyType.SingleOrg, false)] PolicyUpdate policyUpdate,
[Policy(PolicyType.SingleOrg)] Policy policy,
SutProvider<SingleOrgPolicyValidator> sutProvider)
{
policy.OrganizationId = policyUpdate.OrganizationId;
var ssoConfig = new SsoConfig { Enabled = false };
sutProvider.GetDependency<ISsoConfigRepository>()
.GetByOrganizationIdAsync(policyUpdate.OrganizationId)
.Returns(ssoConfig);
sutProvider.GetDependency<IOrganizationHasVerifiedDomainsQuery>()
.HasVerifiedDomainsAsync(policyUpdate.OrganizationId)
.Returns(false);
var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel());
var result = await sutProvider.Sut.ValidateAsync(savePolicyModel, policy);
Assert.True(string.IsNullOrEmpty(result));
}
[Theory, BitAutoData]
public async Task ExecutePreUpsertSideEffectAsync_RevokesNonCompliantUsers(
[PolicyUpdate(PolicyType.SingleOrg)] PolicyUpdate policyUpdate,
[Policy(PolicyType.SingleOrg, false)] Policy policy,
Guid savingUserId,
Guid nonCompliantUserId,
Organization organization,
SutProvider<SingleOrgPolicyValidator> sutProvider)
{
policy.OrganizationId = organization.Id = policyUpdate.OrganizationId;
var compliantUser1 = new OrganizationUserUserDetails
{
Id = Guid.NewGuid(),
OrganizationId = organization.Id,
Type = OrganizationUserType.User,
Status = OrganizationUserStatusType.Confirmed,
UserId = new Guid(),
Email = "user1@example.com"
};
var compliantUser2 = new OrganizationUserUserDetails
{
Id = Guid.NewGuid(),
OrganizationId = organization.Id,
Type = OrganizationUserType.User,
Status = OrganizationUserStatusType.Confirmed,
UserId = new Guid(),
Email = "user2@example.com"
};
var nonCompliantUser = new OrganizationUserUserDetails
{
Id = Guid.NewGuid(),
OrganizationId = organization.Id,
Type = OrganizationUserType.User,
Status = OrganizationUserStatusType.Confirmed,
UserId = nonCompliantUserId,
Email = "user3@example.com"
};
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyDetailsByOrganizationAsync(policyUpdate.OrganizationId)
.Returns([compliantUser1, compliantUser2, nonCompliantUser]);
var otherOrganizationUser = new OrganizationUser
{
Id = Guid.NewGuid(),
OrganizationId = new Guid(),
UserId = nonCompliantUserId,
Status = OrganizationUserStatusType.Confirmed
};
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyByManyUsersAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(nonCompliantUserId)))
.Returns([otherOrganizationUser]);
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(savingUserId);
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(policyUpdate.OrganizationId).Returns(organization);
sutProvider.GetDependency<IRevokeNonCompliantOrganizationUserCommand>()
.RevokeNonCompliantOrganizationUsersAsync(Arg.Any<RevokeOrganizationUsersRequest>())
.Returns(new CommandResult());
var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel());
await sutProvider.Sut.ExecutePreUpsertSideEffectAsync(savePolicyModel, policy);
await sutProvider.GetDependency<IRevokeNonCompliantOrganizationUserCommand>()
.Received(1)
.RevokeNonCompliantOrganizationUsersAsync(
Arg.Is<RevokeOrganizationUsersRequest>(r =>
r.OrganizationId == organization.Id &&
r.OrganizationUsers.Count() == 1 &&
r.OrganizationUsers.First().Id == nonCompliantUser.Id));
await sutProvider.GetDependency<IMailService>()
.DidNotReceive()
.SendOrganizationUserRevokedForPolicySingleOrgEmailAsync(organization.DisplayName(), compliantUser1.Email);
await sutProvider.GetDependency<IMailService>()
.DidNotReceive()
.SendOrganizationUserRevokedForPolicySingleOrgEmailAsync(organization.DisplayName(), compliantUser2.Email);
await sutProvider.GetDependency<IMailService>()
.Received(1)
.SendOrganizationUserRevokedForPolicySingleOrgEmailAsync(organization.DisplayName(), nonCompliantUser.Email);
}
}

View File

@@ -136,4 +136,124 @@ public class TwoFactorAuthenticationPolicyValidatorTests
.SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(organization.DisplayName(),
compliantUser.Email);
}
[Theory, BitAutoData]
public async Task ExecutePreUpsertSideEffectAsync_GivenNonCompliantUsersWithoutMasterPassword_Throws(
Organization organization,
[PolicyUpdate(PolicyType.TwoFactorAuthentication)] PolicyUpdate policyUpdate,
[Policy(PolicyType.TwoFactorAuthentication, false)] Policy policy,
SutProvider<TwoFactorAuthenticationPolicyValidator> sutProvider)
{
policy.OrganizationId = organization.Id = policyUpdate.OrganizationId;
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
var orgUserDetailUserWithout2Fa = new OrganizationUserUserDetails
{
Id = Guid.NewGuid(),
Status = OrganizationUserStatusType.Confirmed,
Type = OrganizationUserType.User,
Email = "user3@test.com",
Name = "TEST",
UserId = Guid.NewGuid(),
HasMasterPassword = false
};
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyDetailsByOrganizationAsync(policyUpdate.OrganizationId)
.Returns([orgUserDetailUserWithout2Fa]);
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
.TwoFactorIsEnabledAsync(Arg.Any<IEnumerable<OrganizationUserUserDetails>>())
.Returns(new List<(OrganizationUserUserDetails user, bool hasTwoFactor)>()
{
(orgUserDetailUserWithout2Fa, false),
});
var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel());
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.ExecutePreUpsertSideEffectAsync(savePolicyModel, policy));
Assert.Equal(TwoFactorAuthenticationPolicyValidator.NonCompliantMembersWillLoseAccessMessage, exception.Message);
}
[Theory, BitAutoData]
public async Task ExecutePreUpsertSideEffectAsync_RevokesOnlyNonCompliantUsers(
Organization organization,
[PolicyUpdate(PolicyType.TwoFactorAuthentication)] PolicyUpdate policyUpdate,
[Policy(PolicyType.TwoFactorAuthentication, false)] Policy policy,
SutProvider<TwoFactorAuthenticationPolicyValidator> sutProvider)
{
// Arrange
policy.OrganizationId = policyUpdate.OrganizationId;
organization.Id = policyUpdate.OrganizationId;
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
var nonCompliantUser = new OrganizationUserUserDetails
{
Id = Guid.NewGuid(),
Status = OrganizationUserStatusType.Confirmed,
Type = OrganizationUserType.User,
Email = "user3@test.com",
Name = "TEST",
UserId = Guid.NewGuid(),
HasMasterPassword = true
};
var compliantUser = new OrganizationUserUserDetails
{
Id = Guid.NewGuid(),
Status = OrganizationUserStatusType.Confirmed,
Type = OrganizationUserType.User,
Email = "user4@test.com",
Name = "TEST",
UserId = Guid.NewGuid(),
HasMasterPassword = true
};
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyDetailsByOrganizationAsync(policyUpdate.OrganizationId)
.Returns([nonCompliantUser, compliantUser]);
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
.TwoFactorIsEnabledAsync(Arg.Any<IEnumerable<OrganizationUserUserDetails>>())
.Returns(new List<(OrganizationUserUserDetails user, bool hasTwoFactor)>()
{
(nonCompliantUser, false),
(compliantUser, true)
});
sutProvider.GetDependency<IRevokeNonCompliantOrganizationUserCommand>()
.RevokeNonCompliantOrganizationUsersAsync(Arg.Any<RevokeOrganizationUsersRequest>())
.Returns(new CommandResult());
var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel());
// Act
await sutProvider.Sut.ExecutePreUpsertSideEffectAsync(savePolicyModel, policy);
// Assert
await sutProvider.GetDependency<IRevokeNonCompliantOrganizationUserCommand>()
.Received(1)
.RevokeNonCompliantOrganizationUsersAsync(Arg.Any<RevokeOrganizationUsersRequest>());
await sutProvider.GetDependency<IRevokeNonCompliantOrganizationUserCommand>()
.Received(1)
.RevokeNonCompliantOrganizationUsersAsync(Arg.Is<RevokeOrganizationUsersRequest>(req =>
req.OrganizationId == policyUpdate.OrganizationId &&
req.OrganizationUsers.SequenceEqual(new[] { nonCompliantUser })
));
await sutProvider.GetDependency<IMailService>()
.Received(1)
.SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(organization.DisplayName(),
nonCompliantUser.Email);
// Did not send out an email for compliantUser
await sutProvider.GetDependency<IMailService>()
.Received(0)
.SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(organization.DisplayName(),
compliantUser.Email);
}
}

View File

@@ -28,9 +28,10 @@ public class VNextSavePolicyCommandTests
// Arrange
var fakePolicyValidationEvent = new FakeSingleOrgValidationEvent();
fakePolicyValidationEvent.ValidateAsyncMock(Arg.Any<SavePolicyModel>(), Arg.Any<Policy>()).Returns("");
var sutProvider = SutProviderFactory(
[new FakeSingleOrgDependencyEvent()],
[fakePolicyValidationEvent]);
var sutProvider = SutProviderFactory([
new FakeSingleOrgDependencyEvent(),
fakePolicyValidationEvent
]);
var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel());
@@ -71,9 +72,10 @@ public class VNextSavePolicyCommandTests
// Arrange
var fakePolicyValidationEvent = new FakeSingleOrgValidationEvent();
fakePolicyValidationEvent.ValidateAsyncMock(Arg.Any<SavePolicyModel>(), Arg.Any<Policy>()).Returns("");
var sutProvider = SutProviderFactory(
[new FakeSingleOrgDependencyEvent()],
[fakePolicyValidationEvent]);
var sutProvider = SutProviderFactory([
new FakeSingleOrgDependencyEvent(),
fakePolicyValidationEvent
]);
var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel());
@@ -110,23 +112,6 @@ public class VNextSavePolicyCommandTests
p.RevisionDate == revisionDate));
}
[Fact]
public void Constructor_DuplicatePolicyDependencyEvents_Throws()
{
// Arrange & Act
var exception = Assert.Throws<Exception>(() =>
new VNextSavePolicyCommand(
Substitute.For<IApplicationCacheService>(),
Substitute.For<IEventService>(),
Substitute.For<IPolicyRepository>(),
[new FakeSingleOrgDependencyEvent(), new FakeSingleOrgDependencyEvent()],
Substitute.For<TimeProvider>(),
Substitute.For<IPolicyEventHandlerFactory>()));
// Assert
Assert.Contains("Duplicate PolicyValidationEvent for SingleOrg policy", exception.Message);
}
[Theory, BitAutoData]
public async Task SaveAsync_OrganizationDoesNotExist_ThrowsBadRequest([PolicyUpdate(PolicyType.ActivateAutofill)] PolicyUpdate policyUpdate)
{
@@ -366,9 +351,10 @@ public class VNextSavePolicyCommandTests
// Arrange
var fakePolicyValidationEvent = new FakeSingleOrgValidationEvent();
fakePolicyValidationEvent.ValidateAsyncMock(Arg.Any<SavePolicyModel>(), Arg.Any<Policy>()).Returns("Validation error!");
var sutProvider = SutProviderFactory(
[new FakeSingleOrgDependencyEvent()],
[fakePolicyValidationEvent]);
var sutProvider = SutProviderFactory([
new FakeSingleOrgDependencyEvent(),
fakePolicyValidationEvent
]);
var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel());
@@ -392,20 +378,20 @@ public class VNextSavePolicyCommandTests
}
/// <summary>
/// Returns a new SutProvider with the PolicyDependencyEvents registered in the Sut.
/// Returns a new SutProvider with the PolicyUpdateEvents registered in the Sut.
/// </summary>
private static SutProvider<VNextSavePolicyCommand> SutProviderFactory(
IEnumerable<IEnforceDependentPoliciesEvent>? policyDependencyEvents = null,
IEnumerable<IPolicyValidationEvent>? policyValidationEvents = null)
IEnumerable<IPolicyUpdateEvent>? policyUpdateEvents = null)
{
var policyEventHandlerFactory = Substitute.For<IPolicyEventHandlerFactory>();
var handlers = policyUpdateEvents ?? [];
// Setup factory to return handlers based on type
policyEventHandlerFactory.GetHandler<IEnforceDependentPoliciesEvent>(Arg.Any<PolicyType>())
.Returns(callInfo =>
{
var policyType = callInfo.Arg<PolicyType>();
var handler = policyDependencyEvents?.FirstOrDefault(e => e.Type == policyType);
var handler = handlers.OfType<IEnforceDependentPoliciesEvent>().FirstOrDefault(e => e.Type == policyType);
return handler != null ? OneOf.OneOf<IEnforceDependentPoliciesEvent, None>.FromT0(handler) : OneOf.OneOf<IEnforceDependentPoliciesEvent, None>.FromT1(new None());
});
@@ -413,7 +399,7 @@ public class VNextSavePolicyCommandTests
.Returns(callInfo =>
{
var policyType = callInfo.Arg<PolicyType>();
var handler = policyValidationEvents?.FirstOrDefault(e => e.Type == policyType);
var handler = handlers.OfType<IPolicyValidationEvent>().FirstOrDefault(e => e.Type == policyType);
return handler != null ? OneOf.OneOf<IPolicyValidationEvent, None>.FromT0(handler) : OneOf.OneOf<IPolicyValidationEvent, None>.FromT1(new None());
});
@@ -425,7 +411,7 @@ public class VNextSavePolicyCommandTests
return new SutProvider<VNextSavePolicyCommand>()
.WithFakeTimeProvider()
.SetDependency(policyDependencyEvents ?? [])
.SetDependency(handlers)
.SetDependency(policyEventHandlerFactory)
.Create();
}