diff --git a/src/Api/AdminConsole/Controllers/PoliciesController.cs b/src/Api/AdminConsole/Controllers/PoliciesController.cs index 9087cd908741..92f2558c16cf 100644 --- a/src/Api/AdminConsole/Controllers/PoliciesController.cs +++ b/src/Api/AdminConsole/Controllers/PoliciesController.cs @@ -8,6 +8,7 @@ using Bit.Api.AdminConsole.Models.Response.Organizations; using Bit.Api.Models.Response; using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.Policies; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces; @@ -90,7 +91,35 @@ public async Task> GetAll(string orgId) var policies = await _policyRepository.GetManyByOrganizationIdAsync(orgIdGuid); - return new ListResponseModel(policies.Select(p => new PolicyResponseModel(p))); + // Once migration from legacy Send policies > SendControls has run, replace the rest of this method with: + // return new ListResponseModel(policies.Select(p => new PolicyResponseModel(p))); + var responses = policies.Select(p => new PolicyResponseModel(p)).ToList(); + + if (policies.Any(p => p.Type == PolicyType.SendControls)) + { + return new ListResponseModel(responses); + } + + var sendControlsStatus = await _policyQuery.RunAsync(orgIdGuid, PolicyType.SendControls); + if (!sendControlsStatus.Enabled) + { + return new ListResponseModel(responses); + } + + var data = sendControlsStatus.GetDataModel(); + responses.Add(new PolicyResponseModel + { + OrganizationId = sendControlsStatus.OrganizationId, + Type = sendControlsStatus.Type, + Enabled = sendControlsStatus.Enabled, + Data = new Dictionary + { + ["disableSend"] = data.DisableSend, + ["disableHideEmail"] = data.DisableHideEmail, + }, + }); + + return new ListResponseModel(responses); } [AllowAnonymous] diff --git a/src/Core/AdminConsole/Enums/PolicyType.cs b/src/Core/AdminConsole/Enums/PolicyType.cs index bd6daf7cdffa..ad61782521ab 100644 --- a/src/Core/AdminConsole/Enums/PolicyType.cs +++ b/src/Core/AdminConsole/Enums/PolicyType.cs @@ -8,7 +8,11 @@ public enum PolicyType : byte SingleOrg = 3, RequireSso = 4, OrganizationDataOwnership = 5, + // Deprecated: superseded by SendControls (20) when pm-31885-send-controls flag is active. + // Do not add [Obsolete] until the flag is retired. DisableSend = 6, + // Deprecated: superseded by SendControls (20) when pm-31885-send-controls flag is active. + // Do not add [Obsolete] until the flag is retired. SendOptions = 7, ResetPassword = 8, MaximumVaultTimeout = 9, @@ -22,6 +26,10 @@ public enum PolicyType : byte AutotypeDefaultSetting = 17, AutomaticUserConfirmation = 18, BlockClaimedDomainAccountCreation = 19, + /// + /// Supersedes DisableSend (6) and SendOptions (7) when the pm-31885-send-controls feature flag is active. + /// + SendControls = 20, } public static class PolicyTypeExtensions @@ -54,6 +62,7 @@ public static string GetName(this PolicyType type) PolicyType.AutotypeDefaultSetting => "Autotype default setting", PolicyType.AutomaticUserConfirmation => "Automatically confirm invited users", PolicyType.BlockClaimedDomainAccountCreation => "Block account creation for claimed domains", + PolicyType.SendControls => "Send controls", }; } } diff --git a/src/Core/AdminConsole/Models/Data/Organizations/Policies/SendControlsPolicyData.cs b/src/Core/AdminConsole/Models/Data/Organizations/Policies/SendControlsPolicyData.cs new file mode 100644 index 000000000000..42d55aa40c4e --- /dev/null +++ b/src/Core/AdminConsole/Models/Data/Organizations/Policies/SendControlsPolicyData.cs @@ -0,0 +1,11 @@ +using System.ComponentModel.DataAnnotations; + +namespace Bit.Core.AdminConsole.Models.Data.Organizations.Policies; + +public class SendControlsPolicyData : IPolicyDataModel +{ + [Display(Name = "DisableSend")] + public bool DisableSend { get; set; } + [Display(Name = "DisableHideEmail")] + public bool DisableHideEmail { get; set; } +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/PolicyQuery.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/PolicyQuery.cs index 0ee6f9ab06ac..da0bf681c0d0 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/PolicyQuery.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/PolicyQuery.cs @@ -1,6 +1,7 @@ using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Utilities; namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.Implementations; @@ -9,6 +10,44 @@ public class PolicyQuery(IPolicyRepository policyRepository) : IPolicyQuery public async Task RunAsync(Guid organizationId, PolicyType policyType) { var dbPolicy = await policyRepository.GetByOrganizationIdTypeAsync(organizationId, policyType); + + // Remove this block and SynthesizeSendControlsStatusAsync once migration has run + if (dbPolicy == null && policyType == PolicyType.SendControls) + { + return await SynthesizeSendControlsStatusAsync(organizationId); + } + return new PolicyStatus(organizationId, policyType, dbPolicy); } + + /// + /// When no SendControls policy row exists in the database, synthesizes a PolicyStatus + /// from the legacy DisableSend and SendOptions policies. This supports lazy migration + /// from the two legacy policies into the unified SendControls policy without requiring + /// a database migration script. + /// + private async Task SynthesizeSendControlsStatusAsync(Guid organizationId) + { + var disableSendPolicy = await policyRepository.GetByOrganizationIdTypeAsync( + organizationId, PolicyType.DisableSend); + var sendOptionsPolicy = await policyRepository.GetByOrganizationIdTypeAsync( + organizationId, PolicyType.SendOptions); + + var disableSend = disableSendPolicy?.Enabled ?? false; + var disableHideEmail = sendOptionsPolicy?.Enabled == true + && sendOptionsPolicy.GetDataModel().DisableHideEmail; + var enabled = disableSend || disableHideEmail; + + var data = new SendControlsPolicyData + { + DisableSend = disableSend, + DisableHideEmail = disableHideEmail, + }; + + return new PolicyStatus(organizationId, PolicyType.SendControls) + { + Enabled = enabled, + Data = CoreHelpers.ClassToJsonData(data), + }; + } } diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SendControlsPolicyRequirement.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SendControlsPolicyRequirement.cs new file mode 100644 index 000000000000..229a39028c86 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SendControlsPolicyRequirement.cs @@ -0,0 +1,40 @@ +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; + +/// +/// Policy requirements for the Send Controls policy. +/// Supersedes DisableSend and SendOptions when the pm-31885-send-controls feature flag is active. +/// +public class SendControlsPolicyRequirement : IPolicyRequirement +{ + /// + /// Indicates whether Send is disabled for the user. If true, the user should not be able to create or edit Sends. + /// They may still delete existing Sends. + /// + public bool DisableSend { get; init; } + + /// + /// Indicates whether the user is prohibited from hiding their email from the recipient of a Send. + /// + public bool DisableHideEmail { get; init; } +} + +public class SendControlsPolicyRequirementFactory : BasePolicyRequirementFactory +{ + public override PolicyType PolicyType => PolicyType.SendControls; + + public override SendControlsPolicyRequirement Create(IEnumerable policyDetails) + { + return policyDetails + .Select(p => p.GetDataModel()) + .Aggregate( + new SendControlsPolicyRequirement(), + (result, data) => new SendControlsPolicyRequirement + { + DisableSend = result.DisableSend || data.DisableSend, + DisableHideEmail = result.DisableHideEmail || data.DisableHideEmail, + }); + } +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs index db529d03b685..c79416a125b4 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs @@ -63,12 +63,16 @@ private static void AddPolicyUpdateEvents(this IServiceCollection services) services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); } private static void AddPolicyRequirements(this IServiceCollection services) { services.AddScoped, DisableSendPolicyRequirementFactory>(); services.AddScoped, SendOptionsPolicyRequirementFactory>(); + services.AddScoped, SendControlsPolicyRequirementFactory>(); services.AddScoped, ResetPasswordPolicyRequirementFactory>(); services.AddScoped, OrganizationDataOwnershipPolicyRequirementFactory>(); services.AddScoped, RequireSsoPolicyRequirementFactory>(); diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/DisableSendSyncPolicyEvent.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/DisableSendSyncPolicyEvent.cs new file mode 100644 index 000000000000..2f6b1ada8052 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/DisableSendSyncPolicyEvent.cs @@ -0,0 +1,54 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces; +using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Utilities; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators; + +/// +/// Syncs changes to the DisableSend policy into the SendControls policy row. +/// Runs regardless of the pm-31885-send-controls feature flag to ensure SendControls +/// always stays current for when the flag is eventually enabled. +/// +public class DisableSendSyncPolicyEvent(IPolicyRepository policyRepository) : IOnPolicyPostUpdateEvent +{ + public PolicyType Type => PolicyType.DisableSend; + + public async Task ExecutePostUpsertSideEffectAsync( + SavePolicyModel policyRequest, + Policy postUpsertedPolicyState, + Policy? previousPolicyState) + { + var organizationId = policyRequest.PolicyUpdate.OrganizationId; + + // Step 1: sync DisableSend.Enabled -> SendControlsPolicy.Data.DisableSend + var sendControlsPolicy = await policyRepository.GetByOrganizationIdTypeAsync( + organizationId, PolicyType.SendControls) ?? new Policy + { + Id = CoreHelpers.GenerateComb(), + OrganizationId = organizationId, + Type = PolicyType.SendControls, + }; + + var sendOptionsPolicy = await policyRepository.GetByOrganizationIdTypeAsync( + organizationId, PolicyType.SendOptions); + + var sendControlsPolicyData = sendControlsPolicy.GetDataModel(); + sendControlsPolicyData.DisableSend = postUpsertedPolicyState.Enabled; + if (sendOptionsPolicy?.Enabled == true) + { + sendControlsPolicyData.DisableHideEmail = + sendOptionsPolicy.GetDataModel().DisableHideEmail; + } + + sendControlsPolicy.SetDataModel(sendControlsPolicyData); + + // Step 2: sync Enabled status. SendControlsPolicy is enabled if either legacy policy is enabled + sendControlsPolicy.Enabled = postUpsertedPolicyState.Enabled || (sendOptionsPolicy?.Enabled ?? false); + + await policyRepository.UpsertAsync(sendControlsPolicy); + } +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SendControlsSyncPolicyEvent.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SendControlsSyncPolicyEvent.cs new file mode 100644 index 000000000000..ab186a42ad06 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SendControlsSyncPolicyEvent.cs @@ -0,0 +1,74 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces; +using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Utilities; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators; + +/// +/// When the pm-31885-send-controls flag is active, syncs changes to the SendControls policy +/// back into the legacy DisableSend and SendOptions policy rows, enabling safe rollback. +/// +public class SendControlsSyncPolicyEvent( + IPolicyRepository policyRepository, + TimeProvider timeProvider) : IOnPolicyPostUpdateEvent +{ + public PolicyType Type => PolicyType.SendControls; + + public async Task ExecutePostUpsertSideEffectAsync( + SavePolicyModel policyRequest, + Policy postUpsertedPolicyState, + Policy? previousPolicyState) + { + var policyUpdate = policyRequest.PolicyUpdate; + + var sendControlsPolicy = await policyRepository.GetByOrganizationIdTypeAsync( + policyUpdate.OrganizationId, PolicyType.SendControls) ?? new Policy + { + Id = CoreHelpers.GenerateComb(), + OrganizationId = policyUpdate.OrganizationId, + Type = PolicyType.SendControls, + }; + + var sendControlsPolicyData = + sendControlsPolicy.GetDataModel(); + + await UpsertLegacyPolicyAsync( + policyRequest.PolicyUpdate.OrganizationId, + PolicyType.DisableSend, + enabled: postUpsertedPolicyState.Enabled && sendControlsPolicyData.DisableSend, + policyData: null); + + var sendOptionsData = new SendOptionsPolicyData { DisableHideEmail = sendControlsPolicyData.DisableHideEmail }; + await UpsertLegacyPolicyAsync( + policyRequest.PolicyUpdate.OrganizationId, + PolicyType.SendOptions, + enabled: postUpsertedPolicyState.Enabled && sendControlsPolicyData.DisableHideEmail, + policyData: CoreHelpers.ClassToJsonData(sendOptionsData)); + } + + private async Task UpsertLegacyPolicyAsync( + Guid organizationId, + PolicyType type, + bool enabled, + string? policyData) + { + var existing = await policyRepository.GetByOrganizationIdTypeAsync(organizationId, type); + + var policy = existing ?? new Policy { OrganizationId = organizationId, Type = type, }; + + if (existing == null) + { + policy.SetNewId(); + } + + policy.Enabled = enabled; + policy.Data = policyData; + policy.RevisionDate = timeProvider.GetUtcNow().UtcDateTime; + + await policyRepository.UpsertAsync(policy); + } +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SendOptionsSyncPolicyEvent.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SendOptionsSyncPolicyEvent.cs new file mode 100644 index 000000000000..6e3486c38444 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SendOptionsSyncPolicyEvent.cs @@ -0,0 +1,48 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces; +using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Utilities; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators; + +/// +/// Syncs changes to the SendOptions policy into the SendControls policy row. +/// Runs regardless of the pm-31885-send-controls feature flag to ensure SendControls +/// always stays current for when the flag is eventually enabled. +/// +public class SendOptionsSyncPolicyEvent(IPolicyRepository policyRepository) : IOnPolicyPostUpdateEvent +{ + public PolicyType Type => PolicyType.SendOptions; + + public async Task ExecutePostUpsertSideEffectAsync( + SavePolicyModel policyRequest, + Policy postUpsertedPolicyState, + Policy? previousPolicyState) + { + var organizationId = policyRequest.PolicyUpdate.OrganizationId; + + // Step 1: sync SendOptionsPolicy.Data.DisableHideEmail -> SendControlsPolicy.Data.DisableHideEmail + var sendControlsPolicy = await policyRepository.GetByOrganizationIdTypeAsync( + organizationId, PolicyType.SendControls) ?? new Policy + { + Id = CoreHelpers.GenerateComb(), + OrganizationId = organizationId, + Type = PolicyType.SendControls, + }; + + var sendControlsPolicyData = sendControlsPolicy.GetDataModel(); + sendControlsPolicyData.DisableHideEmail = postUpsertedPolicyState.GetDataModel().DisableHideEmail; + sendControlsPolicy.SetDataModel(sendControlsPolicyData); + + // Step 2: sync Enabled status. SendControlsPolicy is enabled if either legacy policy is enabled + // Optimization: DisableSendPolicy.Enabled maps to SendControlsPolicy.Data.DisableSend - so we can use that + // as a proxy for that legacy policy state + sendControlsPolicy.Enabled = postUpsertedPolicyState.Enabled || + sendControlsPolicyData.DisableSend; + + await policyRepository.UpsertAsync(sendControlsPolicy); + } +} diff --git a/src/Core/Tools/SendFeatures/Services/SendValidationService.cs b/src/Core/Tools/SendFeatures/Services/SendValidationService.cs index 506cb00160ef..017f982b2a10 100644 --- a/src/Core/Tools/SendFeatures/Services/SendValidationService.cs +++ b/src/Core/Tools/SendFeatures/Services/SendValidationService.cs @@ -18,6 +18,7 @@ public class SendValidationService : ISendValidationService private readonly IUserRepository _userRepository; private readonly IOrganizationRepository _organizationRepository; private readonly IUserService _userService; + private readonly IFeatureService _featureService; private readonly GlobalSettings _globalSettings; private readonly IPolicyRequirementQuery _policyRequirementQuery; private readonly IPricingClient _pricingClient; @@ -26,6 +27,7 @@ public SendValidationService( IUserRepository userRepository, IOrganizationRepository organizationRepository, IUserService userService, + IFeatureService featureService, IPolicyRequirementQuery policyRequirementQuery, GlobalSettings globalSettings, IPricingClient pricingClient) @@ -33,6 +35,7 @@ public SendValidationService( _userRepository = userRepository; _organizationRepository = organizationRepository; _userService = userService; + _featureService = featureService; _policyRequirementQuery = policyRequirementQuery; _globalSettings = globalSettings; _pricingClient = pricingClient; @@ -47,13 +50,39 @@ public async Task ValidateUserCanSaveAsync(Guid? userId, Send send) return; } - var disableSendRequirement = await _policyRequirementQuery.GetAsync(userId.Value); + #region Fetch Policy Requirements Async + var sendControlsTask = _policyRequirementQuery.GetAsync(userId.Value); + var disableSendTask = _policyRequirementQuery.GetAsync(userId.Value); + var sendOptionsTask = _policyRequirementQuery.GetAsync(userId.Value); + + await Task.WhenAll(sendControlsTask, disableSendTask, sendOptionsTask); + + var sendControlsRequirement = sendControlsTask.Result; + var disableSendRequirement = disableSendTask.Result; + var sendOptionsRequirement = sendOptionsTask.Result; + #endregion + + if (_featureService.IsEnabled(FeatureFlagKeys.SendControls)) + { + if (sendControlsRequirement.DisableSend || disableSendRequirement.DisableSend) + { + throw new BadRequestException("Due to an Enterprise Policy, you are only able to delete an existing Send."); + } + + if ((sendControlsRequirement.DisableHideEmail || sendOptionsRequirement.DisableHideEmail) + && send.HideEmail.GetValueOrDefault()) + { + throw new BadRequestException("Due to an Enterprise Policy, you are not allowed to hide your email address from recipients when creating or editing a Send."); + } + + return; + } + if (disableSendRequirement.DisableSend) { throw new BadRequestException("Due to an Enterprise Policy, you are only able to delete an existing Send."); } - var sendOptionsRequirement = await _policyRequirementQuery.GetAsync(userId.Value); if (sendOptionsRequirement.DisableHideEmail && send.HideEmail.GetValueOrDefault()) { throw new BadRequestException("Due to an Enterprise Policy, you are not allowed to hide your email address from recipients when creating or editing a Send."); diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SendControlsPolicyRequirementFactoryTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SendControlsPolicyRequirementFactoryTests.cs new file mode 100644 index 000000000000..c717aa2cf0ed --- /dev/null +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SendControlsPolicyRequirementFactoryTests.cs @@ -0,0 +1,99 @@ +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; +using Bit.Core.Test.AdminConsole.AutoFixture; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Xunit; + +namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; + +[SutProviderCustomize] +public class SendControlsPolicyRequirementFactoryTests +{ + [Theory, BitAutoData] + public void DisableSend_IsFalse_IfNoPolicies(SutProvider sutProvider) + { + var actual = sutProvider.Sut.Create([]); + + Assert.False(actual.DisableSend); + } + + [Theory, BitAutoData] + public void DisableSend_IsFalse_WhenNotConfigured( + [PolicyDetails(PolicyType.SendControls)] PolicyDetails[] policies, + SutProvider sutProvider) + { + foreach (var policy in policies) + { + policy.SetDataModel(new SendControlsPolicyData { DisableSend = false, DisableHideEmail = false }); + } + + var actual = sutProvider.Sut.Create(policies); + + Assert.False(actual.DisableSend); + } + + [Theory, BitAutoData] + public void DisableSend_IsTrue_IfAnyPolicyHasDisableSend( + [PolicyDetails(PolicyType.SendControls)] PolicyDetails[] policies, + SutProvider sutProvider) + { + policies[0].SetDataModel(new SendControlsPolicyData { DisableSend = true, DisableHideEmail = false }); + policies[1].SetDataModel(new SendControlsPolicyData { DisableSend = false, DisableHideEmail = false }); + + var actual = sutProvider.Sut.Create(policies); + + Assert.True(actual.DisableSend); + } + + [Theory, BitAutoData] + public void DisableHideEmail_IsFalse_IfNoPolicies(SutProvider sutProvider) + { + var actual = sutProvider.Sut.Create([]); + + Assert.False(actual.DisableHideEmail); + } + + [Theory, BitAutoData] + public void DisableHideEmail_IsFalse_WhenNotConfigured( + [PolicyDetails(PolicyType.SendControls)] PolicyDetails[] policies, + SutProvider sutProvider) + { + foreach (var policy in policies) + { + policy.SetDataModel(new SendControlsPolicyData { DisableSend = false, DisableHideEmail = false }); + } + + var actual = sutProvider.Sut.Create(policies); + + Assert.False(actual.DisableHideEmail); + } + + [Theory, BitAutoData] + public void DisableHideEmail_IsTrue_IfAnyPolicyHasDisableHideEmail( + [PolicyDetails(PolicyType.SendControls)] PolicyDetails[] policies, + SutProvider sutProvider) + { + policies[0].SetDataModel(new SendControlsPolicyData { DisableSend = false, DisableHideEmail = true }); + policies[1].SetDataModel(new SendControlsPolicyData { DisableSend = false, DisableHideEmail = false }); + + var actual = sutProvider.Sut.Create(policies); + + Assert.True(actual.DisableHideEmail); + } + + [Theory, BitAutoData] + public void BothFields_AreOrAggregatedAcrossMultiplePolicies( + [PolicyDetails(PolicyType.SendControls)] PolicyDetails[] policies, + SutProvider sutProvider) + { + policies[0].SetDataModel(new SendControlsPolicyData { DisableSend = true, DisableHideEmail = false }); + policies[1].SetDataModel(new SendControlsPolicyData { DisableSend = false, DisableHideEmail = true }); + + var actual = sutProvider.Sut.Create(policies); + + Assert.True(actual.DisableSend); + Assert.True(actual.DisableHideEmail); + } +} diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/DisableSendSyncPolicyEventTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/DisableSendSyncPolicyEventTests.cs new file mode 100644 index 000000000000..e2bb203974bc --- /dev/null +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/DisableSendSyncPolicyEventTests.cs @@ -0,0 +1,137 @@ +#nullable enable + +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators; +using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Test.AdminConsole.AutoFixture; +using Bit.Core.Utilities; +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 DisableSendSyncPolicyEventTests +{ + [Theory, BitAutoData] + public async Task ExecutePostUpsertSideEffectAsync_CreatesNewSendControlsPolicy_WhenNoneExists( + [PolicyUpdate(PolicyType.DisableSend, enabled: true)] PolicyUpdate policyUpdate, + [Policy(PolicyType.DisableSend, enabled: true)] Policy postUpsertedPolicy, + SutProvider sutProvider) + { + postUpsertedPolicy.OrganizationId = policyUpdate.OrganizationId; + sutProvider.GetDependency() + .GetByOrganizationIdTypeAsync(policyUpdate.OrganizationId, PolicyType.SendControls) + .Returns((Policy?)null); + sutProvider.GetDependency() + .GetByOrganizationIdTypeAsync(policyUpdate.OrganizationId, PolicyType.SendOptions) + .Returns((Policy?)null); + + await sutProvider.Sut.ExecutePostUpsertSideEffectAsync( + new SavePolicyModel(policyUpdate), postUpsertedPolicy, null); + + await sutProvider.GetDependency() + .Received(1) + .UpsertAsync(Arg.Is(p => + p.OrganizationId == policyUpdate.OrganizationId && + p.Type == PolicyType.SendControls && + p.Enabled == true && + (CoreHelpers.LoadClassFromJsonData(p.Data)!.DisableSend == true))); + } + + [Theory, BitAutoData] + public async Task ExecutePostUpsertSideEffectAsync_UpdatesExistingSendControlsPolicy( + [PolicyUpdate(PolicyType.DisableSend, enabled: false)] PolicyUpdate policyUpdate, + [Policy(PolicyType.DisableSend, enabled: false)] Policy postUpsertedPolicy, + [Policy(PolicyType.SendControls, enabled: true)] Policy existingSendControlsPolicy, + SutProvider sutProvider) + { + postUpsertedPolicy.OrganizationId = policyUpdate.OrganizationId; + existingSendControlsPolicy.OrganizationId = policyUpdate.OrganizationId; + existingSendControlsPolicy.SetDataModel(new SendControlsPolicyData { DisableSend = true, DisableHideEmail = false }); + + sutProvider.GetDependency() + .GetByOrganizationIdTypeAsync(policyUpdate.OrganizationId, PolicyType.SendControls) + .Returns(existingSendControlsPolicy); + sutProvider.GetDependency() + .GetByOrganizationIdTypeAsync(policyUpdate.OrganizationId, PolicyType.SendOptions) + .Returns((Policy?)null); + + await sutProvider.Sut.ExecutePostUpsertSideEffectAsync( + new SavePolicyModel(policyUpdate), postUpsertedPolicy, null); + + await sutProvider.GetDependency() + .Received(1) + .UpsertAsync(Arg.Is(p => + p.Id == existingSendControlsPolicy.Id && + p.Enabled == false && + (CoreHelpers.LoadClassFromJsonData(p.Data)!.DisableSend == false))); + } + + [Theory, BitAutoData] + public async Task ExecutePostUpsertSideEffectAsync_SendControlsRemainsEnabled_WhenSendOptionsStillEnabled( + [PolicyUpdate(PolicyType.DisableSend, enabled: false)] PolicyUpdate policyUpdate, + [Policy(PolicyType.DisableSend, enabled: false)] Policy postUpsertedPolicy, + [Policy(PolicyType.SendControls, enabled: true)] Policy existingSendControlsPolicy, + [Policy(PolicyType.SendOptions, enabled: true)] Policy existingSendOptionsPolicy, + SutProvider sutProvider) + { + postUpsertedPolicy.OrganizationId = policyUpdate.OrganizationId; + existingSendControlsPolicy.OrganizationId = policyUpdate.OrganizationId; + existingSendOptionsPolicy.OrganizationId = policyUpdate.OrganizationId; + existingSendOptionsPolicy.SetDataModel(new SendOptionsPolicyData { DisableHideEmail = true }); + + sutProvider.GetDependency() + .GetByOrganizationIdTypeAsync(policyUpdate.OrganizationId, PolicyType.SendControls) + .Returns(existingSendControlsPolicy); + // DisableSend is being turned off, but SendOptions is still enabled + sutProvider.GetDependency() + .GetByOrganizationIdTypeAsync(policyUpdate.OrganizationId, PolicyType.SendOptions) + .Returns(existingSendOptionsPolicy); + + await sutProvider.Sut.ExecutePostUpsertSideEffectAsync( + new SavePolicyModel(policyUpdate), postUpsertedPolicy, null); + + await sutProvider.GetDependency() + .Received(1) + .UpsertAsync(Arg.Is(p => + p.Enabled == true && // stays enabled because SendOptions is still enabled + CoreHelpers.LoadClassFromJsonData(p.Data)!.DisableHideEmail == true)); + } + + [Theory, BitAutoData] + public async Task ExecutePostUpsertSideEffectAsync_SendControlsEnabled_WhenSendOptionsEnabled_AndSendControlsDidNotExist( + [PolicyUpdate(PolicyType.DisableSend, enabled: true)] PolicyUpdate policyUpdate, + [Policy(PolicyType.DisableSend, enabled: true)] Policy postUpsertedPolicy, + [Policy(PolicyType.SendOptions, enabled: true)] Policy existingSendOptionsPolicy, + SutProvider sutProvider) + { + postUpsertedPolicy.OrganizationId = policyUpdate.OrganizationId; + existingSendOptionsPolicy.OrganizationId = policyUpdate.OrganizationId; + existingSendOptionsPolicy.SetDataModel(new SendOptionsPolicyData { DisableHideEmail = true }); + + sutProvider.GetDependency() + .GetByOrganizationIdTypeAsync(policyUpdate.OrganizationId, PolicyType.SendControls) + .Returns((Policy?)null); + sutProvider.GetDependency() + .GetByOrganizationIdTypeAsync(policyUpdate.OrganizationId, PolicyType.SendOptions) + .Returns(existingSendOptionsPolicy); + + await sutProvider.Sut.ExecutePostUpsertSideEffectAsync( + new SavePolicyModel(policyUpdate), postUpsertedPolicy, null); + + await sutProvider.GetDependency() + .Received(1) + .UpsertAsync(Arg.Is(p => + p.OrganizationId == policyUpdate.OrganizationId && + p.Type == PolicyType.SendControls && + p.Enabled == true && + CoreHelpers.LoadClassFromJsonData(p.Data)!.DisableSend == true && + CoreHelpers.LoadClassFromJsonData(p.Data)!.DisableHideEmail == true)); + } +} diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SendControlsSyncPolicyEventTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SendControlsSyncPolicyEventTests.cs new file mode 100644 index 000000000000..f483c967c59e --- /dev/null +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SendControlsSyncPolicyEventTests.cs @@ -0,0 +1,144 @@ +#nullable enable + +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators; +using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Test.AdminConsole.AutoFixture; +using Bit.Core.Utilities; +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 SendControlsSyncPolicyEventTests +{ + [Theory, BitAutoData] + public async Task ExecutePostUpsertSideEffectAsync_SyncsDisableSend_ToLegacyDisableSendPolicy( + [PolicyUpdate(PolicyType.SendControls, enabled: true)] PolicyUpdate policyUpdate, + [Policy(PolicyType.SendControls, enabled: true)] Policy postUpsertedPolicy, + SutProvider sutProvider) + { + postUpsertedPolicy.OrganizationId = policyUpdate.OrganizationId; + postUpsertedPolicy.SetDataModel(new SendControlsPolicyData { DisableSend = true, DisableHideEmail = false }); + + sutProvider.GetDependency() + .GetByOrganizationIdTypeAsync(policyUpdate.OrganizationId, PolicyType.SendControls) + .Returns(postUpsertedPolicy); + sutProvider.GetDependency() + .GetByOrganizationIdTypeAsync(policyUpdate.OrganizationId, Arg.Is(t => t != PolicyType.SendControls)) + .Returns((Policy?)null); + + await sutProvider.Sut.ExecutePostUpsertSideEffectAsync( + new SavePolicyModel(policyUpdate), postUpsertedPolicy, null); + + await sutProvider.GetDependency() + .Received(1) + .UpsertAsync(Arg.Is(p => + p.OrganizationId == policyUpdate.OrganizationId && + p.Type == PolicyType.DisableSend && + p.Enabled == true)); + } + + [Theory, BitAutoData] + public async Task ExecutePostUpsertSideEffectAsync_SyncsDisableHideEmail_ToLegacySendOptionsPolicy( + [PolicyUpdate(PolicyType.SendControls, enabled: true)] PolicyUpdate policyUpdate, + [Policy(PolicyType.SendControls, enabled: true)] Policy postUpsertedPolicy, + SutProvider sutProvider) + { + postUpsertedPolicy.OrganizationId = policyUpdate.OrganizationId; + postUpsertedPolicy.SetDataModel(new SendControlsPolicyData { DisableSend = false, DisableHideEmail = true }); + + sutProvider.GetDependency() + .GetByOrganizationIdTypeAsync(policyUpdate.OrganizationId, PolicyType.SendControls) + .Returns(postUpsertedPolicy); + sutProvider.GetDependency() + .GetByOrganizationIdTypeAsync(policyUpdate.OrganizationId, Arg.Is(t => t != PolicyType.SendControls)) + .Returns((Policy?)null); + + await sutProvider.Sut.ExecutePostUpsertSideEffectAsync( + new SavePolicyModel(policyUpdate), postUpsertedPolicy, null); + + await sutProvider.GetDependency() + .Received(1) + .UpsertAsync(Arg.Is(p => + p.OrganizationId == policyUpdate.OrganizationId && + p.Type == PolicyType.SendOptions && + p.Enabled == true && + (CoreHelpers.LoadClassFromJsonData(p.Data)!.DisableHideEmail == true))); + } + + [Theory, BitAutoData] + public async Task ExecutePostUpsertSideEffectAsync_DisablesLegacyPolicies_WhenSendControlsPolicyDisabled( + [PolicyUpdate(PolicyType.SendControls, enabled: false)] PolicyUpdate policyUpdate, + [Policy(PolicyType.SendControls, enabled: false)] Policy postUpsertedPolicy, + SutProvider sutProvider) + { + postUpsertedPolicy.OrganizationId = policyUpdate.OrganizationId; + postUpsertedPolicy.SetDataModel(new SendControlsPolicyData { DisableSend = true, DisableHideEmail = true }); + + sutProvider.GetDependency() + .GetByOrganizationIdTypeAsync(policyUpdate.OrganizationId, PolicyType.SendControls) + .Returns(postUpsertedPolicy); + sutProvider.GetDependency() + .GetByOrganizationIdTypeAsync(policyUpdate.OrganizationId, Arg.Is(t => t != PolicyType.SendControls)) + .Returns((Policy?)null); + + await sutProvider.Sut.ExecutePostUpsertSideEffectAsync( + new SavePolicyModel(policyUpdate), postUpsertedPolicy, null); + + await sutProvider.GetDependency() + .Received(1) + .UpsertAsync(Arg.Is(p => + p.Type == PolicyType.DisableSend && + p.Enabled == false)); + await sutProvider.GetDependency() + .Received(1) + .UpsertAsync(Arg.Is(p => + p.Type == PolicyType.SendOptions && + p.Enabled == false)); + } + + [Theory, BitAutoData] + public async Task ExecutePostUpsertSideEffectAsync_UpdatesExistingLegacyPolicies( + [PolicyUpdate(PolicyType.SendControls, enabled: true)] PolicyUpdate policyUpdate, + [Policy(PolicyType.SendControls, enabled: true)] Policy postUpsertedPolicy, + [Policy(PolicyType.DisableSend, enabled: false)] Policy existingDisableSendPolicy, + [Policy(PolicyType.SendOptions, enabled: false)] Policy existingSendOptionsPolicy, + SutProvider sutProvider) + { + postUpsertedPolicy.OrganizationId = policyUpdate.OrganizationId; + existingDisableSendPolicy.OrganizationId = policyUpdate.OrganizationId; + existingSendOptionsPolicy.OrganizationId = policyUpdate.OrganizationId; + postUpsertedPolicy.SetDataModel(new SendControlsPolicyData { DisableSend = true, DisableHideEmail = true }); + + sutProvider.GetDependency() + .GetByOrganizationIdTypeAsync(policyUpdate.OrganizationId, PolicyType.SendControls) + .Returns(postUpsertedPolicy); + sutProvider.GetDependency() + .GetByOrganizationIdTypeAsync(policyUpdate.OrganizationId, PolicyType.DisableSend) + .Returns(existingDisableSendPolicy); + sutProvider.GetDependency() + .GetByOrganizationIdTypeAsync(policyUpdate.OrganizationId, PolicyType.SendOptions) + .Returns(existingSendOptionsPolicy); + + await sutProvider.Sut.ExecutePostUpsertSideEffectAsync( + new SavePolicyModel(policyUpdate), postUpsertedPolicy, null); + + await sutProvider.GetDependency() + .Received(1) + .UpsertAsync(Arg.Is(p => + p.Id == existingDisableSendPolicy.Id && + p.Enabled == true)); + await sutProvider.GetDependency() + .Received(1) + .UpsertAsync(Arg.Is(p => + p.Id == existingSendOptionsPolicy.Id && + p.Enabled == true)); + } +} diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SendOptionsSyncPolicyEventTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SendOptionsSyncPolicyEventTests.cs new file mode 100644 index 000000000000..e32a3c023436 --- /dev/null +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SendOptionsSyncPolicyEventTests.cs @@ -0,0 +1,98 @@ +#nullable enable + +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators; +using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Test.AdminConsole.AutoFixture; +using Bit.Core.Utilities; +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 SendOptionsSyncPolicyEventTests +{ + [Theory, BitAutoData] + public async Task ExecutePostUpsertSideEffectAsync_ClearsDisableHideEmail_WhenPolicyDisabled( + [PolicyUpdate(PolicyType.SendOptions, enabled: false)] PolicyUpdate policyUpdate, + [Policy(PolicyType.SendOptions, enabled: false)] Policy postUpsertedPolicy, + [Policy(PolicyType.SendControls, enabled: true)] Policy existingSendControlsPolicy, + SutProvider sutProvider) + { + postUpsertedPolicy.OrganizationId = policyUpdate.OrganizationId; + postUpsertedPolicy.SetDataModel(new SendOptionsPolicyData { DisableHideEmail = false }); + existingSendControlsPolicy.OrganizationId = policyUpdate.OrganizationId; + existingSendControlsPolicy.SetDataModel(new SendControlsPolicyData { DisableSend = false, DisableHideEmail = true }); + + sutProvider.GetDependency() + .GetByOrganizationIdTypeAsync(policyUpdate.OrganizationId, PolicyType.SendControls) + .Returns(existingSendControlsPolicy); + + await sutProvider.Sut.ExecutePostUpsertSideEffectAsync( + new SavePolicyModel(policyUpdate), postUpsertedPolicy, null); + + await sutProvider.GetDependency() + .Received(1) + .UpsertAsync(Arg.Is(p => + p.Enabled == false && + (CoreHelpers.LoadClassFromJsonData(p.Data)!.DisableHideEmail == false))); + } + + [Theory, BitAutoData] + public async Task ExecutePostUpsertSideEffectAsync_UpdatesExistingSendControlsPolicy( + [PolicyUpdate(PolicyType.SendOptions, enabled: true)] PolicyUpdate policyUpdate, + [Policy(PolicyType.SendOptions, enabled: true)] Policy postUpsertedPolicy, + [Policy(PolicyType.SendControls, enabled: false)] Policy existingSendControlsPolicy, + SutProvider sutProvider) + { + postUpsertedPolicy.OrganizationId = policyUpdate.OrganizationId; + postUpsertedPolicy.SetDataModel(new SendOptionsPolicyData { DisableHideEmail = true }); + existingSendControlsPolicy.OrganizationId = policyUpdate.OrganizationId; + existingSendControlsPolicy.SetDataModel(new SendControlsPolicyData { DisableSend = false, DisableHideEmail = false }); + + sutProvider.GetDependency() + .GetByOrganizationIdTypeAsync(policyUpdate.OrganizationId, PolicyType.SendControls) + .Returns(existingSendControlsPolicy); + + await sutProvider.Sut.ExecutePostUpsertSideEffectAsync( + new SavePolicyModel(policyUpdate), postUpsertedPolicy, null); + + await sutProvider.GetDependency() + .Received(1) + .UpsertAsync(Arg.Is(p => + p.Id == existingSendControlsPolicy.Id && + p.Enabled == true && + (CoreHelpers.LoadClassFromJsonData(p.Data)!.DisableHideEmail == true))); + } + + [Theory, BitAutoData] + public async Task ExecutePostUpsertSideEffectAsync_CreatesNewSendControlsPolicy_WhenNoneExists( + [PolicyUpdate(PolicyType.SendOptions, enabled: true)] PolicyUpdate policyUpdate, + [Policy(PolicyType.SendOptions, enabled: true)] Policy postUpsertedPolicy, + SutProvider sutProvider) + { + postUpsertedPolicy.OrganizationId = policyUpdate.OrganizationId; + postUpsertedPolicy.SetDataModel(new SendOptionsPolicyData { DisableHideEmail = true }); + + sutProvider.GetDependency() + .GetByOrganizationIdTypeAsync(policyUpdate.OrganizationId, PolicyType.SendControls) + .Returns((Policy?)null); + + await sutProvider.Sut.ExecutePostUpsertSideEffectAsync( + new SavePolicyModel(policyUpdate), postUpsertedPolicy, null); + + await sutProvider.GetDependency() + .Received(1) + .UpsertAsync(Arg.Is(p => + p.OrganizationId == policyUpdate.OrganizationId && + p.Type == PolicyType.SendControls && + p.Enabled == true && + CoreHelpers.LoadClassFromJsonData(p.Data)!.DisableHideEmail == true)); + } +} diff --git a/test/Core.Test/Tools/Services/SendValidationServiceTests.cs b/test/Core.Test/Tools/Services/SendValidationServiceTests.cs index cf721e9f6a2d..fc6fd487a93c 100644 --- a/test/Core.Test/Tools/Services/SendValidationServiceTests.cs +++ b/test/Core.Test/Tools/Services/SendValidationServiceTests.cs @@ -179,4 +179,93 @@ public async Task ValidateUserCanSaveAsync_WhenPoliciesDoNotApply_Success( // No exception implies success await sutProvider.Sut.ValidateUserCanSaveAsync(userId, send); } + + [Theory, BitAutoData] + public async Task ValidateUserCanSaveAsync_WhenSendControlsFlagEnabled_ThrowsWhenDisableSendApplies( + SutProvider sutProvider, Send send, Guid userId) + { + send.HideEmail = false; + sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.SendControls).Returns(true); + sutProvider.GetDependency().GetAsync(userId) + .Returns(new SendControlsPolicyRequirement { DisableSend = true, DisableHideEmail = false }); + sutProvider.GetDependency().GetAsync(userId) + .Returns(new DisableSendPolicyRequirement { DisableSend = false }); + sutProvider.GetDependency().GetAsync(userId) + .Returns(new SendOptionsPolicyRequirement { DisableHideEmail = false }); + + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.ValidateUserCanSaveAsync(userId, send)); + Assert.Contains("you are only able to delete an existing Send", exception.Message); + } + + [Theory, BitAutoData] + public async Task ValidateUserCanSaveAsync_WhenSendControlsFlagEnabled_ThrowsWhenLegacyDisableSendApplies( + SutProvider sutProvider, Send send, Guid userId) + { + send.HideEmail = false; + sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.SendControls).Returns(true); + sutProvider.GetDependency().GetAsync(userId) + .Returns(new SendControlsPolicyRequirement { DisableSend = false, DisableHideEmail = false }); + sutProvider.GetDependency().GetAsync(userId) + .Returns(new DisableSendPolicyRequirement { DisableSend = true }); + sutProvider.GetDependency().GetAsync(userId) + .Returns(new SendOptionsPolicyRequirement { DisableHideEmail = false }); + + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.ValidateUserCanSaveAsync(userId, send)); + Assert.Contains("you are only able to delete an existing Send", exception.Message); + } + + [Theory, BitAutoData] + public async Task ValidateUserCanSaveAsync_WhenSendControlsFlagEnabled_ThrowsWhenDisableHideEmailApplies( + SutProvider sutProvider, Send send, Guid userId) + { + send.HideEmail = true; + sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.SendControls).Returns(true); + sutProvider.GetDependency().GetAsync(userId) + .Returns(new SendControlsPolicyRequirement { DisableSend = false, DisableHideEmail = true }); + sutProvider.GetDependency().GetAsync(userId) + .Returns(new DisableSendPolicyRequirement { DisableSend = false }); + sutProvider.GetDependency().GetAsync(userId) + .Returns(new SendOptionsPolicyRequirement { DisableHideEmail = false }); + + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.ValidateUserCanSaveAsync(userId, send)); + Assert.Contains("you are not allowed to hide your email address", exception.Message); + } + + [Theory, BitAutoData] + public async Task ValidateUserCanSaveAsync_WhenSendControlsFlagEnabled_ThrowsWhenLegacySendOptionsDisableHideEmailApplies( + SutProvider sutProvider, Send send, Guid userId) + { + send.HideEmail = true; + sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.SendControls).Returns(true); + sutProvider.GetDependency().GetAsync(userId) + .Returns(new SendControlsPolicyRequirement { DisableSend = false, DisableHideEmail = false }); + sutProvider.GetDependency().GetAsync(userId) + .Returns(new DisableSendPolicyRequirement { DisableSend = false }); + sutProvider.GetDependency().GetAsync(userId) + .Returns(new SendOptionsPolicyRequirement { DisableHideEmail = true }); + + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.ValidateUserCanSaveAsync(userId, send)); + Assert.Contains("you are not allowed to hide your email address", exception.Message); + } + + [Theory, BitAutoData] + public async Task ValidateUserCanSaveAsync_WhenSendControlsFlagEnabled_NoPolicyRestrictions_Success( + SutProvider sutProvider, Send send, Guid userId) + { + send.HideEmail = true; + sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.SendControls).Returns(true); + sutProvider.GetDependency().GetAsync(userId) + .Returns(new SendControlsPolicyRequirement { DisableSend = false, DisableHideEmail = false }); + sutProvider.GetDependency().GetAsync(userId) + .Returns(new DisableSendPolicyRequirement { DisableSend = false }); + sutProvider.GetDependency().GetAsync(userId) + .Returns(new SendOptionsPolicyRequirement { DisableHideEmail = false }); + + // No exception implies success + await sutProvider.Sut.ValidateUserCanSaveAsync(userId, send); + } }