diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/IPolicyValidator.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/IPolicyValidator.cs
index 6aef9f248b..d3df63b6ac 100644
--- a/src/Core/AdminConsole/OrganizationFeatures/Policies/IPolicyValidator.cs
+++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/IPolicyValidator.cs
@@ -9,6 +9,10 @@ namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies;
///
/// Defines behavior and functionality for a given PolicyType.
///
+///
+/// All methods defined in this interface are for the PolicyService#SavePolicy method. This needs to be supported until
+/// we successfully refactor policy validators over to policy validation handlers
+///
public interface IPolicyValidator
{
///
diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs
index f3dbc83706..7c1987865a 100644
--- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs
+++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs
@@ -53,6 +53,7 @@ public static class PolicyServiceCollectionExtensions
services.AddScoped();
services.AddScoped();
services.AddScoped();
+ services.AddScoped();
}
private static void AddPolicyRequirements(this IServiceCollection services)
diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyUpdateEvents/Interfaces/IEnforceDependentPoliciesEvent.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyUpdateEvents/Interfaces/IEnforceDependentPoliciesEvent.cs
index 798417ae7c..0e2bdc3d69 100644
--- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyUpdateEvents/Interfaces/IEnforceDependentPoliciesEvent.cs
+++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyUpdateEvents/Interfaces/IEnforceDependentPoliciesEvent.cs
@@ -2,6 +2,13 @@
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces;
+///
+/// Represents all policies required to be enabled before the given policy can be enabled.
+///
+///
+/// This interface is intended for policy event handlers that mandate the activation of other policies
+/// as prerequisites for enabling the associated policy.
+///
public interface IEnforceDependentPoliciesEvent : IPolicyUpdateEvent
{
///
diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyUpdateEvents/Interfaces/IOnPolicyPreUpdateEvent.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyUpdateEvents/Interfaces/IOnPolicyPreUpdateEvent.cs
index 278a17f35e..4167a392e4 100644
--- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyUpdateEvents/Interfaces/IOnPolicyPreUpdateEvent.cs
+++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyUpdateEvents/Interfaces/IOnPolicyPreUpdateEvent.cs
@@ -3,6 +3,12 @@ using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces;
+///
+/// Represents all side effects that should be executed before a policy is upserted.
+///
+///
+/// This should be added to policy handlers that need to perform side effects before policy upserts.
+///
public interface IOnPolicyPreUpdateEvent : IPolicyUpdateEvent
{
///
diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyUpdateEvents/Interfaces/IPolicyUpdateEvent.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyUpdateEvents/Interfaces/IPolicyUpdateEvent.cs
index ded1a14f1a..a568658d4d 100644
--- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyUpdateEvents/Interfaces/IPolicyUpdateEvent.cs
+++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyUpdateEvents/Interfaces/IPolicyUpdateEvent.cs
@@ -2,6 +2,12 @@
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces;
+///
+/// Represents the policy to be upserted.
+///
+///
+/// This is used for the VNextSavePolicyCommand. All policy handlers should implement this interface.
+///
public interface IPolicyUpdateEvent
{
///
diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyUpdateEvents/Interfaces/IPolicyValidationEvent.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyUpdateEvents/Interfaces/IPolicyValidationEvent.cs
index 6d486e1fa0..ee401ef813 100644
--- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyUpdateEvents/Interfaces/IPolicyValidationEvent.cs
+++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyUpdateEvents/Interfaces/IPolicyValidationEvent.cs
@@ -3,12 +3,17 @@ using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces;
+///
+/// Represents all validations that need to be run to enable or disable the given policy.
+///
+///
+/// This is used for the VNextSavePolicyCommand. This optional but should be implemented for all policies that have
+/// certain requirements for the given organization.
+///
public interface IPolicyValidationEvent : IPolicyUpdateEvent
{
///
- /// Performs side effects after a policy is validated but before it is saved.
- /// For example, this can be used to remove non-compliant users from the organization.
- /// Implementation is optional; by default, it will not perform any side effects.
+ /// Performs any validations required to enable or disable the policy.
///
/// The policy save request containing the policy update and metadata
/// The current policy, if any
diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/AutomaticUserConfirmationPolicyEventHandler.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/AutomaticUserConfirmationPolicyEventHandler.cs
new file mode 100644
index 0000000000..c0d302df02
--- /dev/null
+++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/AutomaticUserConfirmationPolicyEventHandler.cs
@@ -0,0 +1,131 @@
+using Bit.Core.AdminConsole.Entities;
+using Bit.Core.AdminConsole.Enums;
+using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
+using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces;
+using Bit.Core.AdminConsole.Repositories;
+using Bit.Core.Enums;
+using Bit.Core.Repositories;
+
+namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;
+
+///
+/// Represents an event handler for the Automatic User Confirmation policy.
+///
+/// This class validates that the following conditions are met:
+///
+/// - The Single organization policy is enabled
+/// - All organization users are compliant with the Single organization policy
+/// - No provider users exist
+///
+///
+/// This class also performs side effects when the policy is being enabled or disabled. They are:
+///
+/// - Sets the UseAutomaticUserConfirmation organization feature to match the policy update
+///
+///
+public class AutomaticUserConfirmationPolicyEventHandler(
+ IOrganizationUserRepository organizationUserRepository,
+ IProviderUserRepository providerUserRepository,
+ IPolicyRepository policyRepository,
+ IOrganizationRepository organizationRepository,
+ TimeProvider timeProvider)
+ : IPolicyValidator, IPolicyValidationEvent, IOnPolicyPreUpdateEvent, IEnforceDependentPoliciesEvent
+{
+ public PolicyType Type => PolicyType.AutomaticUserConfirmation;
+ public async Task ExecutePreUpsertSideEffectAsync(SavePolicyModel policyRequest, Policy? currentPolicy) =>
+ await OnSaveSideEffectsAsync(policyRequest.PolicyUpdate, currentPolicy);
+
+ private const string _singleOrgPolicyNotEnabledErrorMessage =
+ "The Single organization policy must be enabled before enabling the Automatically confirm invited users policy.";
+
+ private const string _usersNotCompliantWithSingleOrgErrorMessage =
+ "All organization users must be compliant with the Single organization policy before enabling the Automatically confirm invited users policy. Please remove users who are members of multiple organizations.";
+
+ private const string _providerUsersExistErrorMessage =
+ "The organization has users with the Provider user type. Please remove provider users before enabling the Automatically confirm invited users policy.";
+
+ public IEnumerable RequiredPolicies => [PolicyType.SingleOrg];
+
+ public async Task ValidateAsync(PolicyUpdate policyUpdate, Policy? currentPolicy)
+ {
+ var isNotEnablingPolicy = policyUpdate is not { Enabled: true };
+ var policyAlreadyEnabled = currentPolicy is { Enabled: true };
+ if (isNotEnablingPolicy || policyAlreadyEnabled)
+ {
+ return string.Empty;
+ }
+
+ return await ValidateEnablingPolicyAsync(policyUpdate.OrganizationId);
+ }
+
+ public async Task ValidateAsync(SavePolicyModel savePolicyModel, Policy? currentPolicy) =>
+ await ValidateAsync(savePolicyModel.PolicyUpdate, currentPolicy);
+
+ public async Task OnSaveSideEffectsAsync(PolicyUpdate policyUpdate, Policy? currentPolicy)
+ {
+ var organization = await organizationRepository.GetByIdAsync(policyUpdate.OrganizationId);
+
+ if (organization is not null)
+ {
+ organization.UseAutomaticUserConfirmation = policyUpdate.Enabled;
+ organization.RevisionDate = timeProvider.GetUtcNow().UtcDateTime;
+ await organizationRepository.UpsertAsync(organization);
+ }
+ }
+
+ private async Task ValidateEnablingPolicyAsync(Guid organizationId)
+ {
+ var singleOrgValidationError = await ValidateSingleOrgPolicyComplianceAsync(organizationId);
+ if (!string.IsNullOrWhiteSpace(singleOrgValidationError))
+ {
+ return singleOrgValidationError;
+ }
+
+ var providerValidationError = await ValidateNoProviderUsersAsync(organizationId);
+ if (!string.IsNullOrWhiteSpace(providerValidationError))
+ {
+ return providerValidationError;
+ }
+
+ return string.Empty;
+ }
+
+ private async Task ValidateSingleOrgPolicyComplianceAsync(Guid organizationId)
+ {
+ var singleOrgPolicy = await policyRepository.GetByOrganizationIdTypeAsync(organizationId, PolicyType.SingleOrg);
+ if (singleOrgPolicy is not { Enabled: true })
+ {
+ return _singleOrgPolicyNotEnabledErrorMessage;
+ }
+
+ return await ValidateUserComplianceWithSingleOrgAsync(organizationId);
+ }
+
+ private async Task ValidateUserComplianceWithSingleOrgAsync(Guid organizationId)
+ {
+ var organizationUsers = (await organizationUserRepository.GetManyDetailsByOrganizationAsync(organizationId))
+ .Where(ou => ou.Status != OrganizationUserStatusType.Invited &&
+ ou.Status != OrganizationUserStatusType.Revoked &&
+ ou.UserId.HasValue)
+ .ToList();
+
+ if (organizationUsers.Count == 0)
+ {
+ return string.Empty;
+ }
+
+ var hasNonCompliantUser = (await organizationUserRepository.GetManyByManyUsersAsync(
+ organizationUsers.Select(ou => ou.UserId!.Value)))
+ .Any(uo => uo.OrganizationId != organizationId &&
+ uo.Status != OrganizationUserStatusType.Invited);
+
+ return hasNonCompliantUser ? _usersNotCompliantWithSingleOrgErrorMessage : string.Empty;
+ }
+
+ private async Task ValidateNoProviderUsersAsync(Guid organizationId)
+ {
+ var providerUsers = await providerUserRepository.GetManyByOrganizationAsync(organizationId);
+
+ return providerUsers.Count > 0 ? _providerUsersExistErrorMessage : string.Empty;
+ }
+}
diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/AutomaticUserConfirmationPolicyEventHandlerTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/AutomaticUserConfirmationPolicyEventHandlerTests.cs
new file mode 100644
index 0000000000..4781127a3d
--- /dev/null
+++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/AutomaticUserConfirmationPolicyEventHandlerTests.cs
@@ -0,0 +1,628 @@
+using Bit.Core.AdminConsole.Entities;
+using Bit.Core.AdminConsole.Entities.Provider;
+using Bit.Core.AdminConsole.Enums;
+using Bit.Core.AdminConsole.Enums.Provider;
+using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
+using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;
+using Bit.Core.AdminConsole.Repositories;
+using Bit.Core.Entities;
+using Bit.Core.Enums;
+using Bit.Core.Models.Data.Organizations.OrganizationUsers;
+using Bit.Core.Repositories;
+using Bit.Core.Test.AdminConsole.AutoFixture;
+using Bit.Test.Common.AutoFixture;
+using Bit.Test.Common.AutoFixture.Attributes;
+using NSubstitute;
+using Xunit;
+
+namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;
+
+[SutProviderCustomize]
+public class AutomaticUserConfirmationPolicyEventHandlerTests
+{
+ [Theory, BitAutoData]
+ public async Task ValidateAsync_EnablingPolicy_SingleOrgNotEnabled_ReturnsError(
+ [PolicyUpdate(PolicyType.AutomaticUserConfirmation)] PolicyUpdate policyUpdate,
+ SutProvider sutProvider)
+ {
+ // Arrange
+ sutProvider.GetDependency()
+ .GetByOrganizationIdTypeAsync(policyUpdate.OrganizationId, PolicyType.SingleOrg)
+ .Returns((Policy?)null);
+
+ // Act
+ var result = await sutProvider.Sut.ValidateAsync(policyUpdate, null);
+
+ // Assert
+ Assert.Contains("Single organization policy must be enabled", result, StringComparison.OrdinalIgnoreCase);
+ }
+
+ [Theory, BitAutoData]
+ public async Task ValidateAsync_EnablingPolicy_SingleOrgPolicyDisabled_ReturnsError(
+ [PolicyUpdate(PolicyType.AutomaticUserConfirmation)] PolicyUpdate policyUpdate,
+ [Policy(PolicyType.SingleOrg, false)] Policy singleOrgPolicy,
+ SutProvider sutProvider)
+ {
+ // Arrange
+ singleOrgPolicy.OrganizationId = policyUpdate.OrganizationId;
+
+ sutProvider.GetDependency()
+ .GetByOrganizationIdTypeAsync(policyUpdate.OrganizationId, PolicyType.SingleOrg)
+ .Returns(singleOrgPolicy);
+
+ // Act
+ var result = await sutProvider.Sut.ValidateAsync(policyUpdate, null);
+
+ // Assert
+ Assert.Contains("Single organization policy must be enabled", result, StringComparison.OrdinalIgnoreCase);
+ }
+
+ [Theory, BitAutoData]
+ public async Task ValidateAsync_EnablingPolicy_UsersNotCompliantWithSingleOrg_ReturnsError(
+ [PolicyUpdate(PolicyType.AutomaticUserConfirmation)] PolicyUpdate policyUpdate,
+ [Policy(PolicyType.SingleOrg)] Policy singleOrgPolicy,
+ Guid nonCompliantUserId,
+ SutProvider sutProvider)
+ {
+ // Arrange
+ singleOrgPolicy.OrganizationId = policyUpdate.OrganizationId;
+
+ var orgUser = new OrganizationUserUserDetails
+ {
+ Id = Guid.NewGuid(),
+ OrganizationId = policyUpdate.OrganizationId,
+ Type = OrganizationUserType.User,
+ Status = OrganizationUserStatusType.Confirmed,
+ UserId = nonCompliantUserId,
+ Email = "user@example.com"
+ };
+
+ var otherOrgUser = new OrganizationUser
+ {
+ Id = Guid.NewGuid(),
+ OrganizationId = Guid.NewGuid(),
+ UserId = nonCompliantUserId,
+ Status = OrganizationUserStatusType.Confirmed
+ };
+
+ sutProvider.GetDependency()
+ .GetByOrganizationIdTypeAsync(policyUpdate.OrganizationId, PolicyType.SingleOrg)
+ .Returns(singleOrgPolicy);
+
+ sutProvider.GetDependency()
+ .GetManyDetailsByOrganizationAsync(policyUpdate.OrganizationId)
+ .Returns([orgUser]);
+
+ sutProvider.GetDependency()
+ .GetManyByManyUsersAsync(Arg.Any>())
+ .Returns([otherOrgUser]);
+
+ // Act
+ var result = await sutProvider.Sut.ValidateAsync(policyUpdate, null);
+
+ // Assert
+ Assert.Contains("compliant with the Single organization policy", result, StringComparison.OrdinalIgnoreCase);
+ }
+
+ [Theory, BitAutoData]
+ public async Task ValidateAsync_EnablingPolicy_UserWithInvitedStatusInOtherOrg_ValidationPasses(
+ [PolicyUpdate(PolicyType.AutomaticUserConfirmation)] PolicyUpdate policyUpdate,
+ [Policy(PolicyType.SingleOrg)] Policy singleOrgPolicy,
+ Guid userId,
+ SutProvider sutProvider)
+ {
+ // Arrange
+ singleOrgPolicy.OrganizationId = policyUpdate.OrganizationId;
+
+ var orgUser = new OrganizationUserUserDetails
+ {
+ Id = Guid.NewGuid(),
+ OrganizationId = policyUpdate.OrganizationId,
+ Type = OrganizationUserType.User,
+ Status = OrganizationUserStatusType.Confirmed,
+ UserId = userId,
+ Email = "test@email.com"
+ };
+
+ var otherOrgUser = new OrganizationUser
+ {
+ Id = Guid.NewGuid(),
+ OrganizationId = Guid.NewGuid(),
+ UserId = null, // invited users do not have a user id
+ Status = OrganizationUserStatusType.Invited,
+ Email = orgUser.Email
+ };
+
+ sutProvider.GetDependency()
+ .GetByOrganizationIdTypeAsync(policyUpdate.OrganizationId, PolicyType.SingleOrg)
+ .Returns(singleOrgPolicy);
+
+ sutProvider.GetDependency()
+ .GetManyDetailsByOrganizationAsync(policyUpdate.OrganizationId)
+ .Returns([orgUser]);
+
+ sutProvider.GetDependency()
+ .GetManyByManyUsersAsync(Arg.Any>())
+ .Returns([otherOrgUser]);
+
+ sutProvider.GetDependency()
+ .GetManyByOrganizationAsync(policyUpdate.OrganizationId)
+ .Returns([]);
+
+ // Act
+ var result = await sutProvider.Sut.ValidateAsync(policyUpdate, null);
+
+ // Assert
+ Assert.True(string.IsNullOrEmpty(result));
+ }
+
+ [Theory, BitAutoData]
+ public async Task ValidateAsync_EnablingPolicy_ProviderUsersExist_ReturnsError(
+ [PolicyUpdate(PolicyType.AutomaticUserConfirmation)] PolicyUpdate policyUpdate,
+ [Policy(PolicyType.SingleOrg)] Policy singleOrgPolicy,
+ SutProvider sutProvider)
+ {
+ // Arrange
+ singleOrgPolicy.OrganizationId = policyUpdate.OrganizationId;
+
+ var providerUser = new ProviderUser
+ {
+ Id = Guid.NewGuid(),
+ ProviderId = Guid.NewGuid(),
+ UserId = Guid.NewGuid(),
+ Status = ProviderUserStatusType.Confirmed
+ };
+
+ sutProvider.GetDependency()
+ .GetByOrganizationIdTypeAsync(policyUpdate.OrganizationId, PolicyType.SingleOrg)
+ .Returns(singleOrgPolicy);
+
+ sutProvider.GetDependency()
+ .GetManyDetailsByOrganizationAsync(policyUpdate.OrganizationId)
+ .Returns([]);
+
+ sutProvider.GetDependency()
+ .GetManyByOrganizationAsync(policyUpdate.OrganizationId)
+ .Returns([providerUser]);
+
+ // Act
+ var result = await sutProvider.Sut.ValidateAsync(policyUpdate, null);
+
+ // Assert
+ Assert.Contains("Provider user type", result, StringComparison.OrdinalIgnoreCase);
+ }
+
+
+ [Theory, BitAutoData]
+ public async Task ValidateAsync_EnablingPolicy_AllValidationsPassed_ReturnsEmptyString(
+ [PolicyUpdate(PolicyType.AutomaticUserConfirmation)] PolicyUpdate policyUpdate,
+ [Policy(PolicyType.SingleOrg)] Policy singleOrgPolicy,
+ SutProvider sutProvider)
+ {
+ // Arrange
+ singleOrgPolicy.OrganizationId = policyUpdate.OrganizationId;
+
+ var orgUser = new OrganizationUserUserDetails
+ {
+ Id = Guid.NewGuid(),
+ OrganizationId = policyUpdate.OrganizationId,
+ Type = OrganizationUserType.User,
+ Status = OrganizationUserStatusType.Confirmed,
+ UserId = Guid.NewGuid(),
+ Email = "user@example.com"
+ };
+
+ sutProvider.GetDependency()
+ .GetByOrganizationIdTypeAsync(policyUpdate.OrganizationId, PolicyType.SingleOrg)
+ .Returns(singleOrgPolicy);
+
+ sutProvider.GetDependency()
+ .GetManyDetailsByOrganizationAsync(policyUpdate.OrganizationId)
+ .Returns([orgUser]);
+
+ sutProvider.GetDependency()
+ .GetManyByManyUsersAsync(Arg.Any>())
+ .Returns([]);
+
+ sutProvider.GetDependency()
+ .GetManyByOrganizationAsync(policyUpdate.OrganizationId)
+ .Returns([]);
+
+ // Act
+ var result = await sutProvider.Sut.ValidateAsync(policyUpdate, null);
+
+ // Assert
+ Assert.True(string.IsNullOrEmpty(result));
+ }
+
+ [Theory, BitAutoData]
+ public async Task ValidateAsync_PolicyAlreadyEnabled_ReturnsEmptyString(
+ [PolicyUpdate(PolicyType.AutomaticUserConfirmation)] PolicyUpdate policyUpdate,
+ [Policy(PolicyType.AutomaticUserConfirmation)] Policy currentPolicy,
+ SutProvider sutProvider)
+ {
+ // Arrange
+ currentPolicy.OrganizationId = policyUpdate.OrganizationId;
+
+ // Act
+ var result = await sutProvider.Sut.ValidateAsync(policyUpdate, currentPolicy);
+
+ // Assert
+ Assert.True(string.IsNullOrEmpty(result));
+ await sutProvider.GetDependency()
+ .DidNotReceive()
+ .GetByOrganizationIdTypeAsync(Arg.Any(), Arg.Any());
+ }
+
+ [Theory, BitAutoData]
+ public async Task ValidateAsync_DisablingPolicy_ReturnsEmptyString(
+ [PolicyUpdate(PolicyType.AutomaticUserConfirmation, false)] PolicyUpdate policyUpdate,
+ [Policy(PolicyType.AutomaticUserConfirmation)] Policy currentPolicy,
+ SutProvider sutProvider)
+ {
+ // Arrange
+ currentPolicy.OrganizationId = policyUpdate.OrganizationId;
+
+ // Act
+ var result = await sutProvider.Sut.ValidateAsync(policyUpdate, currentPolicy);
+
+ // Assert
+ Assert.True(string.IsNullOrEmpty(result));
+ await sutProvider.GetDependency()
+ .DidNotReceive()
+ .GetByOrganizationIdTypeAsync(Arg.Any(), Arg.Any());
+ }
+
+ [Theory, BitAutoData]
+ public async Task ValidateAsync_EnablingPolicy_IncludesOwnersAndAdmins_InComplianceCheck(
+ [PolicyUpdate(PolicyType.AutomaticUserConfirmation)] PolicyUpdate policyUpdate,
+ [Policy(PolicyType.SingleOrg)] Policy singleOrgPolicy,
+ Guid nonCompliantOwnerId,
+ SutProvider sutProvider)
+ {
+ // Arrange
+ singleOrgPolicy.OrganizationId = policyUpdate.OrganizationId;
+
+ var ownerUser = new OrganizationUserUserDetails
+ {
+ Id = Guid.NewGuid(),
+ OrganizationId = policyUpdate.OrganizationId,
+ Type = OrganizationUserType.Owner,
+ Status = OrganizationUserStatusType.Confirmed,
+ UserId = nonCompliantOwnerId,
+ Email = "owner@example.com"
+ };
+
+ var otherOrgUser = new OrganizationUser
+ {
+ Id = Guid.NewGuid(),
+ OrganizationId = Guid.NewGuid(),
+ UserId = nonCompliantOwnerId,
+ Status = OrganizationUserStatusType.Confirmed
+ };
+
+ sutProvider.GetDependency()
+ .GetByOrganizationIdTypeAsync(policyUpdate.OrganizationId, PolicyType.SingleOrg)
+ .Returns(singleOrgPolicy);
+
+ sutProvider.GetDependency()
+ .GetManyDetailsByOrganizationAsync(policyUpdate.OrganizationId)
+ .Returns([ownerUser]);
+
+ sutProvider.GetDependency()
+ .GetManyByManyUsersAsync(Arg.Any>())
+ .Returns([otherOrgUser]);
+
+ // Act
+ var result = await sutProvider.Sut.ValidateAsync(policyUpdate, null);
+
+ // Assert
+ Assert.Contains("compliant with the Single organization policy", result, StringComparison.OrdinalIgnoreCase);
+ }
+
+ [Theory, BitAutoData]
+ public async Task ValidateAsync_EnablingPolicy_InvitedUsersExcluded_FromComplianceCheck(
+ [PolicyUpdate(PolicyType.AutomaticUserConfirmation)] PolicyUpdate policyUpdate,
+ [Policy(PolicyType.SingleOrg)] Policy singleOrgPolicy,
+ SutProvider sutProvider)
+ {
+ // Arrange
+ singleOrgPolicy.OrganizationId = policyUpdate.OrganizationId;
+
+ var invitedUser = new OrganizationUserUserDetails
+ {
+ Id = Guid.NewGuid(),
+ OrganizationId = policyUpdate.OrganizationId,
+ Type = OrganizationUserType.User,
+ Status = OrganizationUserStatusType.Invited,
+ UserId = Guid.NewGuid(),
+ Email = "invited@example.com"
+ };
+
+ sutProvider.GetDependency()
+ .GetByOrganizationIdTypeAsync(policyUpdate.OrganizationId, PolicyType.SingleOrg)
+ .Returns(singleOrgPolicy);
+
+ sutProvider.GetDependency()
+ .GetManyDetailsByOrganizationAsync(policyUpdate.OrganizationId)
+ .Returns([invitedUser]);
+
+ sutProvider.GetDependency()
+ .GetManyByOrganizationAsync(policyUpdate.OrganizationId)
+ .Returns([]);
+
+ // Act
+ var result = await sutProvider.Sut.ValidateAsync(policyUpdate, null);
+
+ // Assert
+ Assert.True(string.IsNullOrEmpty(result));
+ }
+
+ [Theory, BitAutoData]
+ public async Task ValidateAsync_EnablingPolicy_RevokedUsersExcluded_FromComplianceCheck(
+ [PolicyUpdate(PolicyType.AutomaticUserConfirmation)] PolicyUpdate policyUpdate,
+ [Policy(PolicyType.SingleOrg)] Policy singleOrgPolicy,
+ SutProvider sutProvider)
+ {
+ // Arrange
+ singleOrgPolicy.OrganizationId = policyUpdate.OrganizationId;
+
+ var revokedUser = new OrganizationUserUserDetails
+ {
+ Id = Guid.NewGuid(),
+ OrganizationId = policyUpdate.OrganizationId,
+ Type = OrganizationUserType.User,
+ Status = OrganizationUserStatusType.Revoked,
+ UserId = Guid.NewGuid(),
+ Email = "revoked@example.com"
+ };
+
+ sutProvider.GetDependency()
+ .GetByOrganizationIdTypeAsync(policyUpdate.OrganizationId, PolicyType.SingleOrg)
+ .Returns(singleOrgPolicy);
+
+ sutProvider.GetDependency()
+ .GetManyDetailsByOrganizationAsync(policyUpdate.OrganizationId)
+ .Returns([revokedUser]);
+
+ sutProvider.GetDependency()
+ .GetManyByOrganizationAsync(policyUpdate.OrganizationId)
+ .Returns([]);
+
+ // Act
+ var result = await sutProvider.Sut.ValidateAsync(policyUpdate, null);
+
+ // Assert
+ Assert.True(string.IsNullOrEmpty(result));
+ }
+
+ [Theory, BitAutoData]
+ public async Task ValidateAsync_EnablingPolicy_AcceptedUsersIncluded_InComplianceCheck(
+ [PolicyUpdate(PolicyType.AutomaticUserConfirmation)] PolicyUpdate policyUpdate,
+ [Policy(PolicyType.SingleOrg)] Policy singleOrgPolicy,
+ Guid nonCompliantUserId,
+ SutProvider sutProvider)
+ {
+ // Arrange
+ singleOrgPolicy.OrganizationId = policyUpdate.OrganizationId;
+
+ var acceptedUser = new OrganizationUserUserDetails
+ {
+ Id = Guid.NewGuid(),
+ OrganizationId = policyUpdate.OrganizationId,
+ Type = OrganizationUserType.User,
+ Status = OrganizationUserStatusType.Accepted,
+ UserId = nonCompliantUserId,
+ Email = "accepted@example.com"
+ };
+
+ var otherOrgUser = new OrganizationUser
+ {
+ Id = Guid.NewGuid(),
+ OrganizationId = Guid.NewGuid(),
+ UserId = nonCompliantUserId,
+ Status = OrganizationUserStatusType.Confirmed
+ };
+
+ sutProvider.GetDependency()
+ .GetByOrganizationIdTypeAsync(policyUpdate.OrganizationId, PolicyType.SingleOrg)
+ .Returns(singleOrgPolicy);
+
+ sutProvider.GetDependency()
+ .GetManyDetailsByOrganizationAsync(policyUpdate.OrganizationId)
+ .Returns([acceptedUser]);
+
+ sutProvider.GetDependency()
+ .GetManyByManyUsersAsync(Arg.Any>())
+ .Returns([otherOrgUser]);
+
+ // Act
+ var result = await sutProvider.Sut.ValidateAsync(policyUpdate, null);
+
+ // Assert
+ Assert.Contains("compliant with the Single organization policy", result, StringComparison.OrdinalIgnoreCase);
+ }
+
+ [Theory, BitAutoData]
+ public async Task ValidateAsync_EnablingPolicy_EmptyOrganization_ReturnsEmptyString(
+ [PolicyUpdate(PolicyType.AutomaticUserConfirmation)] PolicyUpdate policyUpdate,
+ [Policy(PolicyType.SingleOrg)] Policy singleOrgPolicy,
+ SutProvider sutProvider)
+ {
+ // Arrange
+ singleOrgPolicy.OrganizationId = policyUpdate.OrganizationId;
+
+ sutProvider.GetDependency()
+ .GetByOrganizationIdTypeAsync(policyUpdate.OrganizationId, PolicyType.SingleOrg)
+ .Returns(singleOrgPolicy);
+
+ sutProvider.GetDependency()
+ .GetManyDetailsByOrganizationAsync(policyUpdate.OrganizationId)
+ .Returns([]);
+
+ sutProvider.GetDependency()
+ .GetManyByOrganizationAsync(policyUpdate.OrganizationId)
+ .Returns([]);
+
+ // Act
+ var result = await sutProvider.Sut.ValidateAsync(policyUpdate, null);
+
+ // Assert
+ Assert.True(string.IsNullOrEmpty(result));
+ }
+
+ [Theory, BitAutoData]
+ public async Task ValidateAsync_WithSavePolicyModel_CallsValidateWithPolicyUpdate(
+ [PolicyUpdate(PolicyType.AutomaticUserConfirmation)] PolicyUpdate policyUpdate,
+ [Policy(PolicyType.SingleOrg)] Policy singleOrgPolicy,
+ SutProvider sutProvider)
+ {
+ // Arrange
+ singleOrgPolicy.OrganizationId = policyUpdate.OrganizationId;
+
+ var savePolicyModel = new SavePolicyModel(policyUpdate);
+
+ sutProvider.GetDependency()
+ .GetByOrganizationIdTypeAsync(policyUpdate.OrganizationId, PolicyType.SingleOrg)
+ .Returns(singleOrgPolicy);
+
+ sutProvider.GetDependency()
+ .GetManyDetailsByOrganizationAsync(policyUpdate.OrganizationId)
+ .Returns([]);
+
+ sutProvider.GetDependency()
+ .GetManyByOrganizationAsync(policyUpdate.OrganizationId)
+ .Returns([]);
+
+ // Act
+ var result = await sutProvider.Sut.ValidateAsync(savePolicyModel, null);
+
+ // Assert
+ Assert.True(string.IsNullOrEmpty(result));
+ }
+
+ [Theory, BitAutoData]
+ public async Task OnSaveSideEffectsAsync_EnablingPolicy_SetsUseAutomaticUserConfirmationToTrue(
+ [PolicyUpdate(PolicyType.AutomaticUserConfirmation)] PolicyUpdate policyUpdate,
+ Organization organization,
+ SutProvider sutProvider)
+ {
+ // Arrange
+ organization.Id = policyUpdate.OrganizationId;
+ organization.UseAutomaticUserConfirmation = false;
+
+ sutProvider.GetDependency()
+ .GetByIdAsync(policyUpdate.OrganizationId)
+ .Returns(organization);
+
+ // Act
+ await sutProvider.Sut.OnSaveSideEffectsAsync(policyUpdate, null);
+
+ // Assert
+ await sutProvider.GetDependency()
+ .Received(1)
+ .UpsertAsync(Arg.Is(o =>
+ o.Id == organization.Id &&
+ o.UseAutomaticUserConfirmation == true &&
+ o.RevisionDate > DateTime.MinValue));
+ }
+
+ [Theory, BitAutoData]
+ public async Task OnSaveSideEffectsAsync_DisablingPolicy_SetsUseAutomaticUserConfirmationToFalse(
+ [PolicyUpdate(PolicyType.AutomaticUserConfirmation, false)] PolicyUpdate policyUpdate,
+ Organization organization,
+ SutProvider sutProvider)
+ {
+ // Arrange
+ organization.Id = policyUpdate.OrganizationId;
+ organization.UseAutomaticUserConfirmation = true;
+
+ sutProvider.GetDependency()
+ .GetByIdAsync(policyUpdate.OrganizationId)
+ .Returns(organization);
+
+ // Act
+ await sutProvider.Sut.OnSaveSideEffectsAsync(policyUpdate, null);
+
+ // Assert
+ await sutProvider.GetDependency()
+ .Received(1)
+ .UpsertAsync(Arg.Is(o =>
+ o.Id == organization.Id &&
+ o.UseAutomaticUserConfirmation == false &&
+ o.RevisionDate > DateTime.MinValue));
+ }
+
+ [Theory, BitAutoData]
+ public async Task OnSaveSideEffectsAsync_OrganizationNotFound_DoesNotThrowException(
+ [PolicyUpdate(PolicyType.AutomaticUserConfirmation)] PolicyUpdate policyUpdate,
+ SutProvider sutProvider)
+ {
+ // Arrange
+ sutProvider.GetDependency()
+ .GetByIdAsync(policyUpdate.OrganizationId)
+ .Returns((Organization?)null);
+
+ // Act
+ await sutProvider.Sut.OnSaveSideEffectsAsync(policyUpdate, null);
+
+ // Assert
+ await sutProvider.GetDependency()
+ .DidNotReceive()
+ .UpsertAsync(Arg.Any());
+ }
+
+ [Theory, BitAutoData]
+ public async Task ExecutePreUpsertSideEffectAsync_CallsOnSaveSideEffectsAsync(
+ [PolicyUpdate(PolicyType.AutomaticUserConfirmation)] PolicyUpdate policyUpdate,
+ [Policy(PolicyType.AutomaticUserConfirmation)] Policy currentPolicy,
+ Organization organization,
+ SutProvider sutProvider)
+ {
+ // Arrange
+ organization.Id = policyUpdate.OrganizationId;
+ currentPolicy.OrganizationId = policyUpdate.OrganizationId;
+
+ var savePolicyModel = new SavePolicyModel(policyUpdate);
+
+ sutProvider.GetDependency()
+ .GetByIdAsync(policyUpdate.OrganizationId)
+ .Returns(organization);
+
+ // Act
+ await sutProvider.Sut.ExecutePreUpsertSideEffectAsync(savePolicyModel, currentPolicy);
+
+ // Assert
+ await sutProvider.GetDependency()
+ .Received(1)
+ .UpsertAsync(Arg.Is(o =>
+ o.Id == organization.Id &&
+ o.UseAutomaticUserConfirmation == policyUpdate.Enabled));
+ }
+
+ [Theory, BitAutoData]
+ public async Task OnSaveSideEffectsAsync_UpdatesRevisionDate(
+ [PolicyUpdate(PolicyType.AutomaticUserConfirmation)] PolicyUpdate policyUpdate,
+ Organization organization,
+ SutProvider sutProvider)
+ {
+ // Arrange
+ organization.Id = policyUpdate.OrganizationId;
+ var originalRevisionDate = DateTime.UtcNow.AddDays(-1);
+ organization.RevisionDate = originalRevisionDate;
+
+ sutProvider.GetDependency()
+ .GetByIdAsync(policyUpdate.OrganizationId)
+ .Returns(organization);
+
+ // Act
+ await sutProvider.Sut.OnSaveSideEffectsAsync(policyUpdate, null);
+
+ // Assert
+ await sutProvider.GetDependency()
+ .Received(1)
+ .UpsertAsync(Arg.Is(o =>
+ o.Id == organization.Id &&
+ o.RevisionDate > originalRevisionDate));
+ }
+}