From 0566de90d62fbf09046eca0465c479889827695d Mon Sep 17 00:00:00 2001 From: Jared McCannon Date: Wed, 11 Feb 2026 09:59:18 -0600 Subject: [PATCH] [PM-27145] - Block Auto Confirm Enable Admin Portal (#6981) * Extracted policy compliance checking for the organization out and added a check when attempting to enable auto user confirm via Admin Portal * Moved injection order. Fixed error message. --- .../Controllers/OrganizationsController.cs | 30 +- ...onOrganizationPolicyComplianceValidator.cs | 58 ++ ...izationPolicyComplianceValidatorRequest.cs | 3 + .../Enforcement/AutoConfirm/Errors.cs | 7 + ...onOrganizationPolicyComplianceValidator.cs | 28 + .../PolicyServiceCollectionExtensions.cs | 4 +- ...maticUserConfirmationPolicyEventHandler.cs | 65 +-- .../OrganizationsControllerTests.cs | 157 ++++- ...anizationPolicyComplianceValidatorTests.cs | 544 ++++++++++++++++++ ...UserConfirmationPolicyEventHandlerTests.cs | 352 +----------- 10 files changed, 863 insertions(+), 385 deletions(-) create mode 100644 src/Core/AdminConsole/OrganizationFeatures/Policies/Enforcement/AutoConfirm/AutomaticUserConfirmationOrganizationPolicyComplianceValidator.cs create mode 100644 src/Core/AdminConsole/OrganizationFeatures/Policies/Enforcement/AutoConfirm/AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest.cs create mode 100644 src/Core/AdminConsole/OrganizationFeatures/Policies/Enforcement/AutoConfirm/Errors.cs create mode 100644 src/Core/AdminConsole/OrganizationFeatures/Policies/Enforcement/AutoConfirm/IAutomaticUserConfirmationOrganizationPolicyComplianceValidator.cs create mode 100644 test/Core.Test/AdminConsole/OrganizationFeatures/Policies/Enforcement/AutoConfirm/AutomaticUserConfirmationOrganizationPolicyComplianceValidatorTests.cs diff --git a/src/Admin/AdminConsole/Controllers/OrganizationsController.cs b/src/Admin/AdminConsole/Controllers/OrganizationsController.cs index 1dbab08ca6..6415ef0815 100644 --- a/src/Admin/AdminConsole/Controllers/OrganizationsController.cs +++ b/src/Admin/AdminConsole/Controllers/OrganizationsController.cs @@ -10,8 +10,10 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Enforcement.AutoConfirm; using Bit.Core.AdminConsole.Providers.Interfaces; using Bit.Core.AdminConsole.Repositories; +using Bit.Core.AdminConsole.Utilities.v2; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Organizations.Services; @@ -59,6 +61,7 @@ public class OrganizationsController : Controller private readonly IPricingClient _pricingClient; private readonly IResendOrganizationInviteCommand _resendOrganizationInviteCommand; private readonly IOrganizationBillingService _organizationBillingService; + private readonly IAutomaticUserConfirmationOrganizationPolicyComplianceValidator _automaticUserConfirmationOrganizationPolicyComplianceValidator; public OrganizationsController( IOrganizationRepository organizationRepository, @@ -84,7 +87,8 @@ public class OrganizationsController : Controller IOrganizationInitiateDeleteCommand organizationInitiateDeleteCommand, IPricingClient pricingClient, IResendOrganizationInviteCommand resendOrganizationInviteCommand, - IOrganizationBillingService organizationBillingService) + IOrganizationBillingService organizationBillingService, + IAutomaticUserConfirmationOrganizationPolicyComplianceValidator automaticUserConfirmationOrganizationPolicyComplianceValidator) { _organizationRepository = organizationRepository; _organizationUserRepository = organizationUserRepository; @@ -110,6 +114,7 @@ public class OrganizationsController : Controller _pricingClient = pricingClient; _resendOrganizationInviteCommand = resendOrganizationInviteCommand; _organizationBillingService = organizationBillingService; + _automaticUserConfirmationOrganizationPolicyComplianceValidator = automaticUserConfirmationOrganizationPolicyComplianceValidator; } [RequirePermission(Permission.Org_List_View)] @@ -250,7 +255,8 @@ public class OrganizationsController : Controller BillingEmail = organization.BillingEmail, Status = organization.Status, PlanType = organization.PlanType, - Seats = organization.Seats + Seats = organization.Seats, + UseAutomaticUserConfirmation = organization.UseAutomaticUserConfirmation }; if (model.PlanType.HasValue) @@ -285,6 +291,13 @@ public class OrganizationsController : Controller return RedirectToAction("Edit", new { id }); } + if (await CheckOrganizationPolicyComplianceAsync(existingOrganizationData, organization) is { } error) + { + TempData["Error"] = error.Message; + + return RedirectToAction("Edit", new { id }); + } + await HandlePotentialProviderSeatScalingAsync( existingOrganizationData, model); @@ -312,6 +325,19 @@ public class OrganizationsController : Controller return RedirectToAction("Edit", new { id }); } + private async Task CheckOrganizationPolicyComplianceAsync(Organization existingOrganizationData, Organization updatedOrganization) + { + if (!existingOrganizationData.UseAutomaticUserConfirmation && updatedOrganization.UseAutomaticUserConfirmation) + { + var validationResult = await _automaticUserConfirmationOrganizationPolicyComplianceValidator.IsOrganizationCompliantAsync( + new AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest(existingOrganizationData.Id)); + + return validationResult.Match(error => error, _ => null); + } + + return null; + } + [HttpPost] [ValidateAntiForgeryToken] [RequirePermission(Permission.Org_Delete)] diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/Enforcement/AutoConfirm/AutomaticUserConfirmationOrganizationPolicyComplianceValidator.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/Enforcement/AutoConfirm/AutomaticUserConfirmationOrganizationPolicyComplianceValidator.cs new file mode 100644 index 0000000000..6762bc9014 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/Enforcement/AutoConfirm/AutomaticUserConfirmationOrganizationPolicyComplianceValidator.cs @@ -0,0 +1,58 @@ +using Bit.Core.AdminConsole.Repositories; +using Bit.Core.AdminConsole.Utilities.v2; +using Bit.Core.AdminConsole.Utilities.v2.Validation; +using Bit.Core.Enums; +using Bit.Core.Models.Data.Organizations.OrganizationUsers; +using Bit.Core.Repositories; +using static Bit.Core.AdminConsole.Utilities.v2.Validation.ValidationResultHelpers; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.Enforcement.AutoConfirm; + +public class AutomaticUserConfirmationOrganizationPolicyComplianceValidator( + IOrganizationUserRepository organizationUserRepository, + IProviderUserRepository providerUserRepository) + : IAutomaticUserConfirmationOrganizationPolicyComplianceValidator +{ + public async Task> + IsOrganizationCompliantAsync(AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest request) + { + var organizationUsers = await organizationUserRepository.GetManyDetailsByOrganizationAsync(request.OrganizationId); + + if (await ValidateUserComplianceWithSingleOrgAsync(request, organizationUsers) is { } singleOrgNonCompliant) + { + return Invalid(request, singleOrgNonCompliant); + } + + if (await ValidateNoProviderUsersAsync(organizationUsers) is { } orgHasProviderMember) + { + return Invalid(request, orgHasProviderMember); + } + + return Valid(request); + } + + private async Task ValidateUserComplianceWithSingleOrgAsync( + AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest request, + ICollection organizationUsers) + { + var userIds = organizationUsers + .Where(u => u.UserId is not null && u.Status != OrganizationUserStatusType.Invited) + .Select(u => u.UserId!.Value); + + var hasNonCompliantUser = (await organizationUserRepository.GetManyByManyUsersAsync(userIds)) + .Any(uo => uo.OrganizationId != request.OrganizationId + && uo.Status != OrganizationUserStatusType.Invited); + + return hasNonCompliantUser ? new UserNotCompliantWithSingleOrganization() : null; + } + + private async Task ValidateNoProviderUsersAsync(ICollection organizationUsers) + { + var userIds = organizationUsers.Where(x => x.UserId is not null) + .Select(x => x.UserId!.Value); + + return (await providerUserRepository.GetManyByManyUsersAsync(userIds)).Count != 0 + ? new ProviderExistsInOrganization() + : null; + } +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/Enforcement/AutoConfirm/AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/Enforcement/AutoConfirm/AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest.cs new file mode 100644 index 0000000000..bce44c7bed --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/Enforcement/AutoConfirm/AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest.cs @@ -0,0 +1,3 @@ +namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.Enforcement.AutoConfirm; + +public record AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest(Guid OrganizationId); diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/Enforcement/AutoConfirm/Errors.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/Enforcement/AutoConfirm/Errors.cs new file mode 100644 index 0000000000..f4e2240330 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/Enforcement/AutoConfirm/Errors.cs @@ -0,0 +1,7 @@ +using Bit.Core.AdminConsole.Utilities.v2; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.Enforcement.AutoConfirm; + +public record UserNotCompliantWithSingleOrganization() : BadRequestError("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."); + +public record ProviderExistsInOrganization() : BadRequestError("The organization has users with the Provider user type. Please remove provider users before enabling the Automatically confirm invited users policy."); diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/Enforcement/AutoConfirm/IAutomaticUserConfirmationOrganizationPolicyComplianceValidator.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/Enforcement/AutoConfirm/IAutomaticUserConfirmationOrganizationPolicyComplianceValidator.cs new file mode 100644 index 0000000000..bb33ddaa8f --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/Enforcement/AutoConfirm/IAutomaticUserConfirmationOrganizationPolicyComplianceValidator.cs @@ -0,0 +1,28 @@ +using Bit.Core.AdminConsole.Utilities.v2.Validation; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.Enforcement.AutoConfirm; + +/// +/// Validates that an organization meets the prerequisites for enabling the Automatic User Confirmation policy. +/// +/// +/// The following conditions must be met: +/// +/// All non-invited organization users belong only to this organization (Single Organization compliance) +/// No organization users are provider members +/// +/// +public interface IAutomaticUserConfirmationOrganizationPolicyComplianceValidator +{ + /// + /// Checks whether the organization is compliant with the Automatic User Confirmation policy prerequisites. + /// + /// The request containing the organization ID to validate. + /// + /// A that is valid if the organization is compliant, + /// or contains a or + /// error if validation fails. + /// + Task> + IsOrganizationCompliantAsync(AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest request); +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs index 6e0c3aa8d9..a7657dc714 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs @@ -21,12 +21,14 @@ public static class PolicyServiceCollectionExtensions services.AddScoped(); services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddPolicyValidators(); services.AddPolicyRequirements(); services.AddPolicySideEffects(); services.AddPolicyUpdateEvents(); - services.AddScoped(); } [Obsolete("Use AddPolicyUpdateEvents instead.")] diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/AutomaticUserConfirmationPolicyEventHandler.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/AutomaticUserConfirmationPolicyEventHandler.cs index 213d18c27d..6896cfaa22 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/AutomaticUserConfirmationPolicyEventHandler.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/AutomaticUserConfirmationPolicyEventHandler.cs @@ -1,11 +1,8 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Enforcement.AutoConfirm; 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.Models.Data.Organizations.OrganizationUsers; -using Bit.Core.Repositories; namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators; @@ -19,19 +16,11 @@ namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators; ///
  • No provider users exist
  • /// /// -public class AutomaticUserConfirmationPolicyEventHandler( - IOrganizationUserRepository organizationUserRepository, - IProviderUserRepository providerUserRepository) +public class AutomaticUserConfirmationPolicyEventHandler(IAutomaticUserConfirmationOrganizationPolicyComplianceValidator validator) : IPolicyValidator, IPolicyValidationEvent, IEnforceDependentPoliciesEvent { public PolicyType Type => PolicyType.AutomaticUserConfirmation; - 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) @@ -43,7 +32,11 @@ public class AutomaticUserConfirmationPolicyEventHandler( return string.Empty; } - return await ValidateEnablingPolicyAsync(policyUpdate.OrganizationId); + return (await validator.IsOrganizationCompliantAsync( + new AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest(policyUpdate.OrganizationId))) + .Match( + error => error.Message, + _ => string.Empty); } public async Task ValidateAsync(SavePolicyModel savePolicyModel, Policy? currentPolicy) => @@ -51,48 +44,4 @@ public class AutomaticUserConfirmationPolicyEventHandler( public Task OnSaveSideEffectsAsync(PolicyUpdate policyUpdate, Policy? currentPolicy) => Task.CompletedTask; - - private async Task ValidateEnablingPolicyAsync(Guid organizationId) - { - var organizationUsers = await organizationUserRepository.GetManyDetailsByOrganizationAsync(organizationId); - - var singleOrgValidationError = await ValidateUserComplianceWithSingleOrgAsync(organizationId, organizationUsers); - if (!string.IsNullOrWhiteSpace(singleOrgValidationError)) - { - return singleOrgValidationError; - } - - var providerValidationError = await ValidateNoProviderUsersAsync(organizationUsers); - if (!string.IsNullOrWhiteSpace(providerValidationError)) - { - return providerValidationError; - } - - return string.Empty; - } - - private async Task ValidateUserComplianceWithSingleOrgAsync(Guid organizationId, - ICollection organizationUsers) - { - var userIds = organizationUsers.Where( - u => u.UserId is not null && - u.Status != OrganizationUserStatusType.Invited) - .Select(u => u.UserId!.Value); - - var hasNonCompliantUser = (await organizationUserRepository.GetManyByManyUsersAsync(userIds)) - .Any(uo => uo.OrganizationId != organizationId - && uo.Status != OrganizationUserStatusType.Invited); - - return hasNonCompliantUser ? _usersNotCompliantWithSingleOrgErrorMessage : string.Empty; - } - - private async Task ValidateNoProviderUsersAsync(ICollection organizationUsers) - { - var userIds = organizationUsers.Where(x => x.UserId is not null) - .Select(x => x.UserId!.Value); - - return (await providerUserRepository.GetManyByManyUsersAsync(userIds)).Count != 0 - ? _providerUsersExistErrorMessage - : string.Empty; - } } diff --git a/test/Admin.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs b/test/Admin.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs index 84ef5c7f3d..7e56a28577 100644 --- a/test/Admin.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs +++ b/test/Admin.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs @@ -5,6 +5,7 @@ using Bit.Admin.Services; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Enums.Provider; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Enforcement.AutoConfirm; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Providers.Services; @@ -12,7 +13,11 @@ using Bit.Core.Enums; using Bit.Core.Repositories; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ViewFeatures; using NSubstitute; +using static Bit.Core.AdminConsole.Utilities.v2.Validation.ValidationResultHelpers; namespace Admin.Test.AdminConsole.Controllers; @@ -299,18 +304,164 @@ public class OrganizationsControllerTests .Returns(true); var organizationRepository = sutProvider.GetDependency(); - organizationRepository.GetByIdAsync(organization.Id).Returns(organization); + var request = new AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest(organization.Id); + sutProvider.GetDependency() + .IsOrganizationCompliantAsync(Arg.Any()) + .Returns(Valid(request)); + // Act _ = await sutProvider.Sut.Edit(organization.Id, update); // Assert await organizationRepository.Received(1).ReplaceAsync(Arg.Is(o => o.Id == organization.Id && o.UseAutomaticUserConfirmation == true)); + } - // Annul - await organizationRepository.DeleteAsync(organization); + [BitAutoData] + [SutProviderCustomize] + [Theory] + public async Task Edit_EnableUseAutomaticUserConfirmation_ValidationFails_RedirectsWithError( + Organization organization, + SutProvider sutProvider) + { + // Arrange + var update = new OrganizationEditModel + { + PlanType = PlanType.TeamsMonthly, + UseAutomaticUserConfirmation = true + }; + + organization.UseAutomaticUserConfirmation = false; + + sutProvider.GetDependency() + .UserHasPermission(Permission.Org_Plan_Edit) + .Returns(true); + + var organizationRepository = sutProvider.GetDependency(); + organizationRepository.GetByIdAsync(organization.Id).Returns(organization); + + var request = new AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest(organization.Id); + sutProvider.GetDependency() + .IsOrganizationCompliantAsync(Arg.Any()) + .Returns(Invalid(request, new UserNotCompliantWithSingleOrganization())); + + sutProvider.Sut.TempData = new TempDataDictionary(new DefaultHttpContext(), Substitute.For()); + + // Act + var result = await sutProvider.Sut.Edit(organization.Id, update); + + // Assert + var redirectResult = Assert.IsType(result); + Assert.Equal("Edit", redirectResult.ActionName); + Assert.Equal(organization.Id, redirectResult.RouteValues!["id"]); + + await organizationRepository.DidNotReceive().ReplaceAsync(Arg.Any()); + } + + [BitAutoData] + [SutProviderCustomize] + [Theory] + public async Task Edit_EnableUseAutomaticUserConfirmation_ProviderValidationFails_RedirectsWithError( + Organization organization, + SutProvider sutProvider) + { + // Arrange + var update = new OrganizationEditModel + { + PlanType = PlanType.TeamsMonthly, + UseAutomaticUserConfirmation = true + }; + + organization.UseAutomaticUserConfirmation = false; + + sutProvider.GetDependency() + .UserHasPermission(Permission.Org_Plan_Edit) + .Returns(true); + + var organizationRepository = sutProvider.GetDependency(); + organizationRepository.GetByIdAsync(organization.Id).Returns(organization); + + var request = new AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest(organization.Id); + sutProvider.GetDependency() + .IsOrganizationCompliantAsync(Arg.Any()) + .Returns(Invalid(request, new ProviderExistsInOrganization())); + + sutProvider.Sut.TempData = new TempDataDictionary(new DefaultHttpContext(), Substitute.For()); + + // Act + var result = await sutProvider.Sut.Edit(organization.Id, update); + + // Assert + var redirectResult = Assert.IsType(result); + Assert.Equal("Edit", redirectResult.ActionName); + + await organizationRepository.DidNotReceive().ReplaceAsync(Arg.Any()); + } + + [BitAutoData] + [SutProviderCustomize] + [Theory] + public async Task Edit_UseAutomaticUserConfirmation_NotChanged_DoesNotCallValidator( + SutProvider sutProvider) + { + // Arrange + var organizationId = new Guid(); + var update = new OrganizationEditModel + { + UseSecretsManager = false, + UseAutomaticUserConfirmation = false + }; + + var organization = new Organization + { + Id = organizationId, + UseAutomaticUserConfirmation = false + }; + + sutProvider.GetDependency().GetByIdAsync(organizationId) + .Returns(organization); + + // Act + _ = await sutProvider.Sut.Edit(organizationId, update); + + // Assert + await sutProvider.GetDependency() + .DidNotReceive() + .IsOrganizationCompliantAsync(Arg.Any()); + } + + [BitAutoData] + [SutProviderCustomize] + [Theory] + public async Task Edit_UseAutomaticUserConfirmation_AlreadyEnabled_DoesNotCallValidator( + SutProvider sutProvider) + { + // Arrange + var organizationId = new Guid(); + var update = new OrganizationEditModel + { + UseSecretsManager = false, + UseAutomaticUserConfirmation = true + }; + + var organization = new Organization + { + Id = organizationId, + UseAutomaticUserConfirmation = true + }; + + sutProvider.GetDependency().GetByIdAsync(organizationId) + .Returns(organization); + + // Act + _ = await sutProvider.Sut.Edit(organizationId, update); + + // Assert + await sutProvider.GetDependency() + .DidNotReceive() + .IsOrganizationCompliantAsync(Arg.Any()); } #endregion diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/Enforcement/AutoConfirm/AutomaticUserConfirmationOrganizationPolicyComplianceValidatorTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/Enforcement/AutoConfirm/AutomaticUserConfirmationOrganizationPolicyComplianceValidatorTests.cs new file mode 100644 index 0000000000..3376dea141 --- /dev/null +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/Enforcement/AutoConfirm/AutomaticUserConfirmationOrganizationPolicyComplianceValidatorTests.cs @@ -0,0 +1,544 @@ +using Bit.Core.AdminConsole.Entities.Provider; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Enforcement.AutoConfirm; +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.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.Enforcement.AutoConfirm; + +[SutProviderCustomize] +public class AutomaticUserConfirmationOrganizationPolicyComplianceValidatorTests +{ + [Theory, BitAutoData] + public async Task IsOrganizationCompliantAsync_AllUsersCompliant_NoProviders_ReturnsValid( + Guid organizationId, + Guid userId, + SutProvider sutProvider) + { + // Arrange + var orgUser = new OrganizationUserUserDetails + { + Id = Guid.NewGuid(), + OrganizationId = organizationId, + UserId = userId, + Status = OrganizationUserStatusType.Confirmed + }; + + sutProvider.GetDependency() + .GetManyDetailsByOrganizationAsync(organizationId) + .Returns([orgUser]); + + sutProvider.GetDependency() + .GetManyByManyUsersAsync(Arg.Any>()) + .Returns([]); + + sutProvider.GetDependency() + .GetManyByManyUsersAsync(Arg.Any>()) + .Returns([]); + + var request = new AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest(organizationId); + + // Act + var result = await sutProvider.Sut.IsOrganizationCompliantAsync(request); + + // Assert + Assert.True(result.IsValid); + } + + [Theory, BitAutoData] + public async Task IsOrganizationCompliantAsync_UserInAnotherOrg_ReturnsUserNotCompliantWithSingleOrganization( + Guid organizationId, + Guid userId, + SutProvider sutProvider) + { + // Arrange + var orgUser = new OrganizationUserUserDetails + { + Id = Guid.NewGuid(), + OrganizationId = organizationId, + UserId = userId, + Status = OrganizationUserStatusType.Confirmed + }; + + var otherOrgUser = new OrganizationUser + { + Id = Guid.NewGuid(), + OrganizationId = Guid.NewGuid(), // Different org + UserId = userId, + Status = OrganizationUserStatusType.Confirmed + }; + + sutProvider.GetDependency() + .GetManyDetailsByOrganizationAsync(organizationId) + .Returns([orgUser]); + + sutProvider.GetDependency() + .GetManyByManyUsersAsync(Arg.Any>()) + .Returns([otherOrgUser]); + + var request = new AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest(organizationId); + + // Act + var result = await sutProvider.Sut.IsOrganizationCompliantAsync(request); + + // Assert + Assert.True(result.IsError); + Assert.IsType(result.AsError); + } + + [Theory, BitAutoData] + public async Task IsOrganizationCompliantAsync_ProviderUsersExist_ReturnsProviderExistsInOrganization( + Guid organizationId, + Guid userId, + SutProvider sutProvider) + { + // Arrange + var orgUser = new OrganizationUserUserDetails + { + Id = Guid.NewGuid(), + OrganizationId = organizationId, + UserId = userId, + Status = OrganizationUserStatusType.Confirmed + }; + + var providerUser = new ProviderUser + { + Id = Guid.NewGuid(), + ProviderId = Guid.NewGuid(), + UserId = userId + }; + + sutProvider.GetDependency() + .GetManyDetailsByOrganizationAsync(organizationId) + .Returns([orgUser]); + + sutProvider.GetDependency() + .GetManyByManyUsersAsync(Arg.Any>()) + .Returns([]); + + sutProvider.GetDependency() + .GetManyByManyUsersAsync(Arg.Any>()) + .Returns([providerUser]); + + var request = new AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest(organizationId); + + // Act + var result = await sutProvider.Sut.IsOrganizationCompliantAsync(request); + + // Assert + Assert.True(result.IsError); + Assert.IsType(result.AsError); + } + + [Theory, BitAutoData] + public async Task IsOrganizationCompliantAsync_InvitedUsersExcluded_FromSingleOrgCheck( + Guid organizationId, + SutProvider sutProvider) + { + // Arrange - invited user has null UserId and Invited status + var invitedUser = new OrganizationUserUserDetails + { + Id = Guid.NewGuid(), + OrganizationId = organizationId, + UserId = null, + Status = OrganizationUserStatusType.Invited, + Email = "invited@example.com" + }; + + sutProvider.GetDependency() + .GetManyDetailsByOrganizationAsync(organizationId) + .Returns([invitedUser]); + + sutProvider.GetDependency() + .GetManyByManyUsersAsync(Arg.Any>()) + .Returns([]); + + var request = new AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest(organizationId); + + // Act + var result = await sutProvider.Sut.IsOrganizationCompliantAsync(request); + + // Assert + Assert.True(result.IsValid); + + // Invited users with null UserId should not trigger the single org query + await sutProvider.GetDependency() + .Received(1) + .GetManyByManyUsersAsync(Arg.Is>(ids => !ids.Any())); + } + + [Theory, BitAutoData] + public async Task IsOrganizationCompliantAsync_InvitedUserWithUserId_ExcludedFromSingleOrgCheck( + Guid organizationId, + Guid userId, + SutProvider sutProvider) + { + // Arrange - Invited status users are excluded regardless of UserId + var invitedUser = new OrganizationUserUserDetails + { + Id = Guid.NewGuid(), + OrganizationId = organizationId, + UserId = userId, + Status = OrganizationUserStatusType.Invited + }; + + sutProvider.GetDependency() + .GetManyDetailsByOrganizationAsync(organizationId) + .Returns([invitedUser]); + + sutProvider.GetDependency() + .GetManyByManyUsersAsync(Arg.Any>()) + .Returns([]); + + sutProvider.GetDependency() + .GetManyByManyUsersAsync(Arg.Any>()) + .Returns([]); + + var request = new AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest(organizationId); + + // Act + var result = await sutProvider.Sut.IsOrganizationCompliantAsync(request); + + // Assert + Assert.True(result.IsValid); + + // Invited users should not be included in the single org compliance query + await sutProvider.GetDependency() + .Received(1) + .GetManyByManyUsersAsync(Arg.Is>(ids => !ids.Any())); + } + + [Theory, BitAutoData] + public async Task IsOrganizationCompliantAsync_UserInAnotherOrgWithInvitedStatus_ReturnsValid( + Guid organizationId, + Guid userId, + SutProvider sutProvider) + { + // Arrange + var orgUser = new OrganizationUserUserDetails + { + Id = Guid.NewGuid(), + OrganizationId = organizationId, + UserId = userId, + Status = OrganizationUserStatusType.Confirmed + }; + + // User has an Invited status in another org - should not count as non-compliant + var otherOrgUser = new OrganizationUser + { + Id = Guid.NewGuid(), + OrganizationId = Guid.NewGuid(), + UserId = userId, + Status = OrganizationUserStatusType.Invited + }; + + sutProvider.GetDependency() + .GetManyDetailsByOrganizationAsync(organizationId) + .Returns([orgUser]); + + sutProvider.GetDependency() + .GetManyByManyUsersAsync(Arg.Any>()) + .Returns([otherOrgUser]); + + sutProvider.GetDependency() + .GetManyByManyUsersAsync(Arg.Any>()) + .Returns([]); + + var request = new AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest(organizationId); + + // Act + var result = await sutProvider.Sut.IsOrganizationCompliantAsync(request); + + // Assert + Assert.True(result.IsValid); + } + + [Theory, BitAutoData] + public async Task IsOrganizationCompliantAsync_SingleOrgViolationTakesPrecedence_OverProviderCheck( + Guid organizationId, + Guid userId, + SutProvider sutProvider) + { + // Arrange - user is in another org AND is a provider user + var orgUser = new OrganizationUserUserDetails + { + Id = Guid.NewGuid(), + OrganizationId = organizationId, + UserId = userId, + Status = OrganizationUserStatusType.Confirmed + }; + + var otherOrgUser = new OrganizationUser + { + Id = Guid.NewGuid(), + OrganizationId = Guid.NewGuid(), + UserId = userId, + Status = OrganizationUserStatusType.Confirmed + }; + + sutProvider.GetDependency() + .GetManyDetailsByOrganizationAsync(organizationId) + .Returns([orgUser]); + + sutProvider.GetDependency() + .GetManyByManyUsersAsync(Arg.Any>()) + .Returns([otherOrgUser]); + + var request = new AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest(organizationId); + + // Act + var result = await sutProvider.Sut.IsOrganizationCompliantAsync(request); + + // Assert + Assert.True(result.IsError); + Assert.IsType(result.AsError); + + // Provider check should not be called since single org check failed first + await sutProvider.GetDependency() + .DidNotReceive() + .GetManyByManyUsersAsync(Arg.Any>()); + } + + [Theory, BitAutoData] + public async Task IsOrganizationCompliantAsync_MixedUsers_OnlyNonInvitedChecked( + Guid organizationId, + Guid confirmedUserId, + Guid acceptedUserId, + SutProvider sutProvider) + { + // Arrange + var invitedUser = new OrganizationUserUserDetails + { + Id = Guid.NewGuid(), + OrganizationId = organizationId, + UserId = null, + Status = OrganizationUserStatusType.Invited, + Email = "invited@example.com" + }; + + var confirmedUser = new OrganizationUserUserDetails + { + Id = Guid.NewGuid(), + OrganizationId = organizationId, + UserId = confirmedUserId, + Status = OrganizationUserStatusType.Confirmed + }; + + var acceptedUser = new OrganizationUserUserDetails + { + Id = Guid.NewGuid(), + OrganizationId = organizationId, + UserId = acceptedUserId, + Status = OrganizationUserStatusType.Accepted + }; + + sutProvider.GetDependency() + .GetManyDetailsByOrganizationAsync(organizationId) + .Returns([invitedUser, confirmedUser, acceptedUser]); + + sutProvider.GetDependency() + .GetManyByManyUsersAsync(Arg.Any>()) + .Returns([]); + + sutProvider.GetDependency() + .GetManyByManyUsersAsync(Arg.Any>()) + .Returns([]); + + var request = new AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest(organizationId); + + // Act + var result = await sutProvider.Sut.IsOrganizationCompliantAsync(request); + + // Assert + Assert.True(result.IsValid); + + // Only confirmed and accepted users should be checked for single org compliance + await sutProvider.GetDependency() + .Received(1) + .GetManyByManyUsersAsync(Arg.Is>(ids => + ids.Count() == 2 && + ids.Contains(confirmedUserId) && + ids.Contains(acceptedUserId))); + } + + [Theory, BitAutoData] + public async Task IsOrganizationCompliantAsync_NoOrganizationUsers_ReturnsValid( + Guid organizationId, + SutProvider sutProvider) + { + // Arrange + sutProvider.GetDependency() + .GetManyDetailsByOrganizationAsync(organizationId) + .Returns([]); + + sutProvider.GetDependency() + .GetManyByManyUsersAsync(Arg.Any>()) + .Returns([]); + + sutProvider.GetDependency() + .GetManyByManyUsersAsync(Arg.Any>()) + .Returns([]); + + var request = new AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest(organizationId); + + // Act + var result = await sutProvider.Sut.IsOrganizationCompliantAsync(request); + + // Assert + Assert.True(result.IsValid); + } + + [Theory, BitAutoData] + public async Task IsOrganizationCompliantAsync_UserInSameOrgOnly_ReturnsValid( + Guid organizationId, + Guid userId, + SutProvider sutProvider) + { + // Arrange + var orgUser = new OrganizationUserUserDetails + { + Id = Guid.NewGuid(), + OrganizationId = organizationId, + UserId = userId, + Status = OrganizationUserStatusType.Confirmed + }; + + // User exists in the same org only (the GetManyByManyUsersAsync returns same-org entry) + var sameOrgUser = new OrganizationUser + { + Id = orgUser.Id, + OrganizationId = organizationId, + UserId = userId, + Status = OrganizationUserStatusType.Confirmed + }; + + sutProvider.GetDependency() + .GetManyDetailsByOrganizationAsync(organizationId) + .Returns([orgUser]); + + sutProvider.GetDependency() + .GetManyByManyUsersAsync(Arg.Any>()) + .Returns([sameOrgUser]); + + sutProvider.GetDependency() + .GetManyByManyUsersAsync(Arg.Any>()) + .Returns([]); + + var request = new AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest(organizationId); + + // Act + var result = await sutProvider.Sut.IsOrganizationCompliantAsync(request); + + // Assert + Assert.True(result.IsValid); + } + + [Theory, BitAutoData] + public async Task IsOrganizationCompliantAsync_ProviderCheckIncludesAllUsersWithUserIds( + Guid organizationId, + Guid userId1, + Guid userId2, + SutProvider sutProvider) + { + // Arrange - provider check includes users regardless of Invited status (only excludes null UserId) + var confirmedUser = new OrganizationUserUserDetails + { + Id = Guid.NewGuid(), + OrganizationId = organizationId, + UserId = userId1, + Status = OrganizationUserStatusType.Confirmed + }; + + var invitedUserWithNullId = new OrganizationUserUserDetails + { + Id = Guid.NewGuid(), + OrganizationId = organizationId, + UserId = null, + Status = OrganizationUserStatusType.Invited, + Email = "invited@example.com" + }; + + var acceptedUser = new OrganizationUserUserDetails + { + Id = Guid.NewGuid(), + OrganizationId = organizationId, + UserId = userId2, + Status = OrganizationUserStatusType.Accepted + }; + + sutProvider.GetDependency() + .GetManyDetailsByOrganizationAsync(organizationId) + .Returns([confirmedUser, invitedUserWithNullId, acceptedUser]); + + sutProvider.GetDependency() + .GetManyByManyUsersAsync(Arg.Any>()) + .Returns([]); + + sutProvider.GetDependency() + .GetManyByManyUsersAsync(Arg.Any>()) + .Returns([]); + + var request = new AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest(organizationId); + + // Act + var result = await sutProvider.Sut.IsOrganizationCompliantAsync(request); + + // Assert + Assert.True(result.IsValid); + + // Provider check should include all users with non-null UserIds (confirmed + accepted) + await sutProvider.GetDependency() + .Received(1) + .GetManyByManyUsersAsync(Arg.Is>(ids => + ids.Count() == 2 && + ids.Contains(userId1) && + ids.Contains(userId2))); + } + + [Theory, BitAutoData] + public async Task IsOrganizationCompliantAsync_RevokedUserInAnotherOrg_ReturnsUserNotCompliant( + Guid organizationId, + Guid userId, + SutProvider sutProvider) + { + // Arrange + var revokedUser = new OrganizationUserUserDetails + { + Id = Guid.NewGuid(), + OrganizationId = organizationId, + UserId = userId, + Status = OrganizationUserStatusType.Revoked + }; + + var otherOrgUser = new OrganizationUser + { + Id = Guid.NewGuid(), + OrganizationId = Guid.NewGuid(), + UserId = userId, + Status = OrganizationUserStatusType.Confirmed + }; + + sutProvider.GetDependency() + .GetManyDetailsByOrganizationAsync(organizationId) + .Returns([revokedUser]); + + sutProvider.GetDependency() + .GetManyByManyUsersAsync(Arg.Any>()) + .Returns([otherOrgUser]); + + var request = new AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest(organizationId); + + // Act + var result = await sutProvider.Sut.IsOrganizationCompliantAsync(request); + + // Assert + Assert.True(result.IsError); + Assert.IsType(result.AsError); + } +} diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/AutomaticUserConfirmationPolicyEventHandlerTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/AutomaticUserConfirmationPolicyEventHandlerTests.cs index e2c9de4d6f..45d3a0b6ee 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/AutomaticUserConfirmationPolicyEventHandlerTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/AutomaticUserConfirmationPolicyEventHandlerTests.cs @@ -1,19 +1,14 @@ 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.Enforcement.AutoConfirm; 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; +using static Bit.Core.AdminConsole.Utilities.v2.Validation.ValidationResultHelpers; namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyValidators; @@ -34,35 +29,14 @@ public class AutomaticUserConfirmationPolicyEventHandlerTests [Theory, BitAutoData] public async Task ValidateAsync_EnablingPolicy_UsersNotCompliantWithSingleOrg_ReturnsError( [PolicyUpdate(PolicyType.AutomaticUserConfirmation)] PolicyUpdate policyUpdate, - Guid nonCompliantUserId, SutProvider sutProvider) { // Arrange - var orgUser = new OrganizationUserUserDetails - { - Id = Guid.NewGuid(), - OrganizationId = policyUpdate.OrganizationId, - Type = OrganizationUserType.User, - Status = OrganizationUserStatusType.Confirmed, - UserId = nonCompliantUserId, - Email = "user@example.com" - }; + var request = new AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest(policyUpdate.OrganizationId); - var otherOrgUser = new OrganizationUser - { - Id = Guid.NewGuid(), - OrganizationId = Guid.NewGuid(), - UserId = nonCompliantUserId, - Status = OrganizationUserStatusType.Confirmed - }; - - sutProvider.GetDependency() - .GetManyDetailsByOrganizationAsync(policyUpdate.OrganizationId) - .Returns([orgUser]); - - sutProvider.GetDependency() - .GetManyByManyUsersAsync(Arg.Any>()) - .Returns([otherOrgUser]); + sutProvider.GetDependency() + .IsOrganizationCompliantAsync(Arg.Any()) + .Returns(Invalid(request, new UserNotCompliantWithSingleOrganization())); // Act var result = await sutProvider.Sut.ValidateAsync(policyUpdate, null); @@ -71,85 +45,17 @@ public class AutomaticUserConfirmationPolicyEventHandlerTests 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, - Guid userId, - SutProvider sutProvider) - { - // Arrange - var orgUser = new OrganizationUserUserDetails - { - Id = Guid.NewGuid(), - OrganizationId = policyUpdate.OrganizationId, - Type = OrganizationUserType.User, - Status = OrganizationUserStatusType.Confirmed, - UserId = userId, - }; - - 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() - .GetManyDetailsByOrganizationAsync(policyUpdate.OrganizationId) - .Returns([orgUser]); - - sutProvider.GetDependency() - .GetManyByManyUsersAsync(Arg.Any>()) - .Returns([otherOrgUser]); - - sutProvider.GetDependency() - .GetManyByManyUsersAsync(Arg.Any>()) - .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, - Guid userId, SutProvider sutProvider) { // Arrange - var orgUser = new OrganizationUserUserDetails - { - Id = Guid.NewGuid(), - OrganizationId = policyUpdate.OrganizationId, - Type = OrganizationUserType.User, - Status = OrganizationUserStatusType.Confirmed, - UserId = userId - }; + var request = new AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest(policyUpdate.OrganizationId); - var providerUser = new ProviderUser - { - Id = Guid.NewGuid(), - ProviderId = Guid.NewGuid(), - UserId = userId, - Status = ProviderUserStatusType.Confirmed - }; - - sutProvider.GetDependency() - .GetManyDetailsByOrganizationAsync(policyUpdate.OrganizationId) - .Returns([orgUser]); - - sutProvider.GetDependency() - .GetManyByManyUsersAsync(Arg.Any>()) - .Returns([]); - - sutProvider.GetDependency() - .GetManyByManyUsersAsync(Arg.Any>()) - .Returns([providerUser]); + sutProvider.GetDependency() + .IsOrganizationCompliantAsync(Arg.Any()) + .Returns(Invalid(request, new ProviderExistsInOrganization())); // Act var result = await sutProvider.Sut.ValidateAsync(policyUpdate, null); @@ -158,33 +64,17 @@ public class AutomaticUserConfirmationPolicyEventHandlerTests Assert.Contains("Provider user type", result, StringComparison.OrdinalIgnoreCase); } - [Theory, BitAutoData] public async Task ValidateAsync_EnablingPolicy_AllValidationsPassed_ReturnsEmptyString( [PolicyUpdate(PolicyType.AutomaticUserConfirmation)] PolicyUpdate policyUpdate, SutProvider sutProvider) { // Arrange - var orgUser = new OrganizationUserUserDetails - { - Id = Guid.NewGuid(), - OrganizationId = policyUpdate.OrganizationId, - Type = OrganizationUserType.User, - Status = OrganizationUserStatusType.Confirmed, - UserId = Guid.NewGuid() - }; + var request = new AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest(policyUpdate.OrganizationId); - sutProvider.GetDependency() - .GetManyDetailsByOrganizationAsync(policyUpdate.OrganizationId) - .Returns([orgUser]); - - sutProvider.GetDependency() - .GetManyByManyUsersAsync(Arg.Any>()) - .Returns([]); - - sutProvider.GetDependency() - .GetManyByManyUsersAsync(Arg.Any>()) - .Returns([]); + sutProvider.GetDependency() + .IsOrganizationCompliantAsync(Arg.Any()) + .Returns(Valid(request)); // Act var result = await sutProvider.Sut.ValidateAsync(policyUpdate, null); @@ -208,9 +98,9 @@ public class AutomaticUserConfirmationPolicyEventHandlerTests // Assert Assert.True(string.IsNullOrEmpty(result)); - await sutProvider.GetDependency() + await sutProvider.GetDependency() .DidNotReceive() - .GetManyDetailsByOrganizationAsync(Arg.Any()); + .IsOrganizationCompliantAsync(Arg.Any()); } [Theory, BitAutoData] @@ -227,212 +117,31 @@ public class AutomaticUserConfirmationPolicyEventHandlerTests // Assert Assert.True(string.IsNullOrEmpty(result)); - await sutProvider.GetDependency() + await sutProvider.GetDependency() .DidNotReceive() - .GetManyDetailsByOrganizationAsync(Arg.Any()); + .IsOrganizationCompliantAsync(Arg.Any()); } [Theory, BitAutoData] - public async Task ValidateAsync_EnablingPolicy_IncludesOwnersAndAdmins_InComplianceCheck( - [PolicyUpdate(PolicyType.AutomaticUserConfirmation)] PolicyUpdate policyUpdate, - Guid nonCompliantOwnerId, - SutProvider sutProvider) - { - // Arrange - var ownerUser = new OrganizationUserUserDetails - { - Id = Guid.NewGuid(), - OrganizationId = policyUpdate.OrganizationId, - Type = OrganizationUserType.Owner, - Status = OrganizationUserStatusType.Confirmed, - UserId = nonCompliantOwnerId, - }; - - var otherOrgUser = new OrganizationUser - { - Id = Guid.NewGuid(), - OrganizationId = Guid.NewGuid(), - UserId = nonCompliantOwnerId, - Status = OrganizationUserStatusType.Confirmed - }; - - 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( + public async Task ValidateAsync_EnablingPolicy_PassesCorrectOrganizationId( [PolicyUpdate(PolicyType.AutomaticUserConfirmation)] PolicyUpdate policyUpdate, SutProvider sutProvider) { // Arrange - var invitedUser = new OrganizationUserUserDetails - { - Id = Guid.NewGuid(), - OrganizationId = policyUpdate.OrganizationId, - Type = OrganizationUserType.User, - Status = OrganizationUserStatusType.Invited, - UserId = null, - Email = "invited@example.com" - }; + var request = new AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest(policyUpdate.OrganizationId); - sutProvider.GetDependency() - .GetManyDetailsByOrganizationAsync(policyUpdate.OrganizationId) - .Returns([invitedUser]); - - sutProvider.GetDependency() - .GetManyByManyUsersAsync(Arg.Any>()) - .Returns([]); + sutProvider.GetDependency() + .IsOrganizationCompliantAsync(Arg.Any()) + .Returns(Valid(request)); // Act - var result = await sutProvider.Sut.ValidateAsync(policyUpdate, null); + await sutProvider.Sut.ValidateAsync(policyUpdate, null); // Assert - Assert.True(string.IsNullOrEmpty(result)); - } - - [Theory, BitAutoData] - public async Task ValidateAsync_EnablingPolicy_MixedUsersWithNullUserId_HandlesCorrectly( - [PolicyUpdate(PolicyType.AutomaticUserConfirmation)] PolicyUpdate policyUpdate, - Guid confirmedUserId, - SutProvider sutProvider) - { - // Arrange - var invitedUser = new OrganizationUserUserDetails - { - Id = Guid.NewGuid(), - OrganizationId = policyUpdate.OrganizationId, - Type = OrganizationUserType.User, - Status = OrganizationUserStatusType.Invited, - UserId = null, - Email = "invited@example.com" - }; - - var confirmedUser = new OrganizationUserUserDetails - { - Id = Guid.NewGuid(), - OrganizationId = policyUpdate.OrganizationId, - Type = OrganizationUserType.User, - Status = OrganizationUserStatusType.Confirmed, - UserId = confirmedUserId, - Email = "confirmed@example.com" - }; - - sutProvider.GetDependency() - .GetManyDetailsByOrganizationAsync(policyUpdate.OrganizationId) - .Returns([invitedUser, confirmedUser]); - - sutProvider.GetDependency() - .GetManyByManyUsersAsync(Arg.Any>()) - .Returns([]); - - sutProvider.GetDependency() - .GetManyByManyUsersAsync(Arg.Any>()) - .Returns([]); - - // Act - var result = await sutProvider.Sut.ValidateAsync(policyUpdate, null); - - // Assert - Assert.True(string.IsNullOrEmpty(result)); - - await sutProvider.GetDependency() + await sutProvider.GetDependency() .Received(1) - .GetManyByManyUsersAsync(Arg.Is>(ids => ids.Count() == 1 && ids.First() == confirmedUserId)); - } - - [Theory, BitAutoData] - public async Task ValidateAsync_EnablingPolicy_RevokedUsersIncluded_InComplianceCheck( - [PolicyUpdate(PolicyType.AutomaticUserConfirmation)] PolicyUpdate policyUpdate, - SutProvider sutProvider) - { - // Arrange - var revokedUser = new OrganizationUserUserDetails - { - Id = Guid.NewGuid(), - OrganizationId = policyUpdate.OrganizationId, - Type = OrganizationUserType.User, - Status = OrganizationUserStatusType.Revoked, - UserId = Guid.NewGuid(), - }; - - var additionalOrgUser = new OrganizationUser - { - Id = Guid.NewGuid(), - OrganizationId = Guid.NewGuid(), - Type = OrganizationUserType.User, - Status = OrganizationUserStatusType.Revoked, - UserId = revokedUser.UserId, - }; - - var orgUserRepository = sutProvider.GetDependency(); - - orgUserRepository - .GetManyDetailsByOrganizationAsync(policyUpdate.OrganizationId) - .Returns([revokedUser]); - - orgUserRepository.GetManyByManyUsersAsync(Arg.Any>()) - .Returns([additionalOrgUser]); - - sutProvider.GetDependency() - .GetManyByManyUsersAsync(Arg.Any>()) - .Returns([]); - - // 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_AcceptedUsersIncluded_InComplianceCheck( - [PolicyUpdate(PolicyType.AutomaticUserConfirmation)] PolicyUpdate policyUpdate, - Guid nonCompliantUserId, - SutProvider sutProvider) - { - // Arrange - var acceptedUser = new OrganizationUserUserDetails - { - Id = Guid.NewGuid(), - OrganizationId = policyUpdate.OrganizationId, - Type = OrganizationUserType.User, - Status = OrganizationUserStatusType.Accepted, - UserId = nonCompliantUserId, - }; - - var otherOrgUser = new OrganizationUser - { - Id = Guid.NewGuid(), - OrganizationId = Guid.NewGuid(), - UserId = nonCompliantUserId, - Status = OrganizationUserStatusType.Confirmed - }; - - 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); + .IsOrganizationCompliantAsync(Arg.Is( + r => r.OrganizationId == policyUpdate.OrganizationId)); } [Theory, BitAutoData] @@ -442,10 +151,11 @@ public class AutomaticUserConfirmationPolicyEventHandlerTests { // Arrange var savePolicyModel = new SavePolicyModel(policyUpdate); + var request = new AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest(policyUpdate.OrganizationId); - sutProvider.GetDependency() - .GetManyDetailsByOrganizationAsync(policyUpdate.OrganizationId) - .Returns([]); + sutProvider.GetDependency() + .IsOrganizationCompliantAsync(Arg.Any()) + .Returns(Valid(request)); // Act var result = await sutProvider.Sut.ValidateAsync(savePolicyModel, null);