mirror of
https://github.com/bitwarden/server
synced 2025-12-06 00:03:34 +00:00
Ac/pm 25823/vnext policy upsert pattern (#6426)
This commit is contained in:
@@ -0,0 +1,208 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Services;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.Implementations;
|
||||
|
||||
public class VNextSavePolicyCommand(
|
||||
IApplicationCacheService applicationCacheService,
|
||||
IEventService eventService,
|
||||
IPolicyRepository policyRepository,
|
||||
IEnumerable<IEnforceDependentPoliciesEvent> policyValidationEventHandlers,
|
||||
TimeProvider timeProvider,
|
||||
IPolicyEventHandlerFactory policyEventHandlerFactory)
|
||||
: IVNextSavePolicyCommand
|
||||
{
|
||||
private readonly IReadOnlyDictionary<PolicyType, IEnforceDependentPoliciesEvent> _policyValidationEvents = MapToDictionary(policyValidationEventHandlers);
|
||||
|
||||
private static Dictionary<PolicyType, IEnforceDependentPoliciesEvent> MapToDictionary(IEnumerable<IEnforceDependentPoliciesEvent> policyValidationEventHandlers)
|
||||
{
|
||||
var policyValidationEventsDict = new Dictionary<PolicyType, IEnforceDependentPoliciesEvent>();
|
||||
foreach (var policyValidationEvent in policyValidationEventHandlers)
|
||||
{
|
||||
if (!policyValidationEventsDict.TryAdd(policyValidationEvent.Type, policyValidationEvent))
|
||||
{
|
||||
throw new Exception($"Duplicate PolicyValidationEvent for {policyValidationEvent.Type} policy.");
|
||||
}
|
||||
}
|
||||
return policyValidationEventsDict;
|
||||
}
|
||||
|
||||
public async Task<Policy> SaveAsync(SavePolicyModel policyRequest)
|
||||
{
|
||||
var policyUpdateRequest = policyRequest.PolicyUpdate;
|
||||
var organizationId = policyUpdateRequest.OrganizationId;
|
||||
|
||||
await EnsureOrganizationCanUsePolicyAsync(organizationId);
|
||||
|
||||
var savedPoliciesDict = await GetCurrentPolicyStateAsync(organizationId);
|
||||
|
||||
var currentPolicy = savedPoliciesDict.GetValueOrDefault(policyUpdateRequest.Type);
|
||||
|
||||
ValidatePolicyDependencies(policyUpdateRequest, currentPolicy, savedPoliciesDict);
|
||||
|
||||
await ValidateTargetedPolicyAsync(policyRequest, currentPolicy);
|
||||
|
||||
await ExecutePreUpsertSideEffectAsync(policyRequest, currentPolicy);
|
||||
|
||||
var upsertedPolicy = await UpsertPolicyAsync(policyUpdateRequest);
|
||||
|
||||
await eventService.LogPolicyEventAsync(upsertedPolicy, EventType.Policy_Updated);
|
||||
|
||||
await ExecutePostUpsertSideEffectAsync(policyRequest, upsertedPolicy, currentPolicy);
|
||||
|
||||
return upsertedPolicy;
|
||||
}
|
||||
|
||||
private async Task EnsureOrganizationCanUsePolicyAsync(Guid organizationId)
|
||||
{
|
||||
var org = await applicationCacheService.GetOrganizationAbilityAsync(organizationId);
|
||||
if (org == null)
|
||||
{
|
||||
throw new BadRequestException("Organization not found");
|
||||
}
|
||||
|
||||
if (!org.UsePolicies)
|
||||
{
|
||||
throw new BadRequestException("This organization cannot use policies.");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<Policy> UpsertPolicyAsync(PolicyUpdate policyUpdateRequest)
|
||||
{
|
||||
var policy = await policyRepository.GetByOrganizationIdTypeAsync(policyUpdateRequest.OrganizationId, policyUpdateRequest.Type)
|
||||
?? new Policy
|
||||
{
|
||||
OrganizationId = policyUpdateRequest.OrganizationId,
|
||||
Type = policyUpdateRequest.Type,
|
||||
CreationDate = timeProvider.GetUtcNow().UtcDateTime
|
||||
};
|
||||
|
||||
policy.Enabled = policyUpdateRequest.Enabled;
|
||||
policy.Data = policyUpdateRequest.Data;
|
||||
policy.RevisionDate = timeProvider.GetUtcNow().UtcDateTime;
|
||||
|
||||
await policyRepository.UpsertAsync(policy);
|
||||
|
||||
return policy;
|
||||
}
|
||||
|
||||
private async Task ValidateTargetedPolicyAsync(SavePolicyModel policyRequest,
|
||||
Policy? currentPolicy)
|
||||
{
|
||||
await ExecutePolicyEventAsync<IPolicyValidationEvent>(
|
||||
policyRequest.PolicyUpdate.Type,
|
||||
async validator =>
|
||||
{
|
||||
var validationError = await validator.ValidateAsync(policyRequest, currentPolicy);
|
||||
if (!string.IsNullOrEmpty(validationError))
|
||||
{
|
||||
throw new BadRequestException(validationError);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void ValidatePolicyDependencies(
|
||||
PolicyUpdate policyUpdateRequest,
|
||||
Policy? currentPolicy,
|
||||
Dictionary<PolicyType, Policy> savedPoliciesDict)
|
||||
{
|
||||
var result = policyEventHandlerFactory.GetHandler<IEnforceDependentPoliciesEvent>(policyUpdateRequest.Type);
|
||||
|
||||
result.Switch(
|
||||
validator =>
|
||||
{
|
||||
var isCurrentlyEnabled = currentPolicy?.Enabled == true;
|
||||
|
||||
switch (policyUpdateRequest.Enabled)
|
||||
{
|
||||
case true when !isCurrentlyEnabled:
|
||||
ValidateEnablingRequirements(validator, savedPoliciesDict);
|
||||
return;
|
||||
case false when isCurrentlyEnabled:
|
||||
ValidateDisablingRequirements(validator, policyUpdateRequest.Type, savedPoliciesDict);
|
||||
break;
|
||||
}
|
||||
},
|
||||
_ => { });
|
||||
}
|
||||
|
||||
private void ValidateDisablingRequirements(
|
||||
IEnforceDependentPoliciesEvent validator,
|
||||
PolicyType policyType,
|
||||
Dictionary<PolicyType, Policy> savedPoliciesDict)
|
||||
{
|
||||
var dependentPolicyTypes = _policyValidationEvents.Values
|
||||
.Where(otherValidator => otherValidator.RequiredPolicies.Contains(policyType))
|
||||
.Select(otherValidator => otherValidator.Type)
|
||||
.Where(otherPolicyType => savedPoliciesDict.TryGetValue(otherPolicyType, out var savedPolicy) &&
|
||||
savedPolicy.Enabled)
|
||||
.ToList();
|
||||
|
||||
switch (dependentPolicyTypes)
|
||||
{
|
||||
case { Count: 1 }:
|
||||
throw new BadRequestException($"Turn off the {dependentPolicyTypes.First().GetName()} policy because it requires the {validator.Type.GetName()} policy.");
|
||||
case { Count: > 1 }:
|
||||
throw new BadRequestException($"Turn off all of the policies that require the {validator.Type.GetName()} policy.");
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateEnablingRequirements(
|
||||
IEnforceDependentPoliciesEvent validator,
|
||||
Dictionary<PolicyType, Policy> savedPoliciesDict)
|
||||
{
|
||||
var missingRequiredPolicyTypes = validator.RequiredPolicies
|
||||
.Where(requiredPolicyType => savedPoliciesDict.GetValueOrDefault(requiredPolicyType) is not { Enabled: true })
|
||||
.ToList();
|
||||
|
||||
if (missingRequiredPolicyTypes.Count != 0)
|
||||
{
|
||||
throw new BadRequestException($"Turn on the {missingRequiredPolicyTypes.First().GetName()} policy because it is required for the {validator.Type.GetName()} policy.");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ExecutePreUpsertSideEffectAsync(
|
||||
SavePolicyModel policyRequest,
|
||||
Policy? currentPolicy)
|
||||
{
|
||||
await ExecutePolicyEventAsync<IOnPolicyPreUpdateEvent>(
|
||||
policyRequest.PolicyUpdate.Type,
|
||||
handler => handler.ExecutePreUpsertSideEffectAsync(policyRequest, currentPolicy));
|
||||
}
|
||||
private async Task ExecutePostUpsertSideEffectAsync(
|
||||
SavePolicyModel policyRequest,
|
||||
Policy postUpsertedPolicyState,
|
||||
Policy? previousPolicyState)
|
||||
{
|
||||
await ExecutePolicyEventAsync<IOnPolicyPostUpdateEvent>(
|
||||
policyRequest.PolicyUpdate.Type,
|
||||
handler => handler.ExecutePostUpsertSideEffectAsync(
|
||||
policyRequest,
|
||||
postUpsertedPolicyState,
|
||||
previousPolicyState));
|
||||
}
|
||||
|
||||
private async Task ExecutePolicyEventAsync<T>(PolicyType type, Func<T, Task> func) where T : IPolicyUpdateEvent
|
||||
{
|
||||
var handler = policyEventHandlerFactory.GetHandler<T>(type);
|
||||
|
||||
await handler.Match(
|
||||
async h => await func(h),
|
||||
_ => Task.CompletedTask
|
||||
);
|
||||
}
|
||||
|
||||
private async Task<Dictionary<PolicyType, Policy>> GetCurrentPolicyStateAsync(Guid organizationId)
|
||||
{
|
||||
var savedPolicies = await policyRepository.GetManyByOrganizationIdAsync(organizationId);
|
||||
// Note: policies may be missing from this dict if they have never been enabled
|
||||
var savedPoliciesDict = savedPolicies.ToDictionary(p => p.Type);
|
||||
return savedPoliciesDict;
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,8 @@ public record PolicyUpdate
|
||||
public PolicyType Type { get; set; }
|
||||
public string? Data { get; set; }
|
||||
public bool Enabled { get; set; }
|
||||
|
||||
[Obsolete("Please use SavePolicyModel.PerformedBy instead.")]
|
||||
public IActingUser? PerformedBy { get; set; }
|
||||
|
||||
public T GetDataModel<T>() where T : IPolicyDataModel, new()
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Implementations;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.AdminConsole.Services.Implementations;
|
||||
@@ -13,7 +15,9 @@ public static class PolicyServiceCollectionExtensions
|
||||
{
|
||||
services.AddScoped<IPolicyService, PolicyService>();
|
||||
services.AddScoped<ISavePolicyCommand, SavePolicyCommand>();
|
||||
services.AddScoped<IVNextSavePolicyCommand, VNextSavePolicyCommand>();
|
||||
services.AddScoped<IPolicyRequirementQuery, PolicyRequirementQuery>();
|
||||
services.AddScoped<IPolicyEventHandlerFactory, PolicyEventHandlerHandlerFactory>();
|
||||
|
||||
services.AddPolicyValidators();
|
||||
services.AddPolicyRequirements();
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces;
|
||||
|
||||
public interface IEnforceDependentPoliciesEvent : IPolicyUpdateEvent
|
||||
{
|
||||
/// <summary>
|
||||
/// PolicyTypes that must be enabled before this policy can be enabled, if any.
|
||||
/// These dependencies will be checked when this policy is enabled and when any required policy is disabled.
|
||||
/// </summary>
|
||||
public IEnumerable<PolicyType> RequiredPolicies { get; }
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces;
|
||||
public interface IOnPolicyPostUpdateEvent : IPolicyUpdateEvent
|
||||
{
|
||||
/// <summary>
|
||||
/// Performs side effects after a policy has been upserted.
|
||||
/// For example, this can be used for cleanup tasks or notifications.
|
||||
/// </summary>
|
||||
/// <param name="policyRequest">The policy save request</param>
|
||||
/// <param name="postUpsertedPolicyState">The policy after it was upserted</param>
|
||||
/// <param name="previousPolicyState">The policy state before it was updated, if any</param>
|
||||
public Task ExecutePostUpsertSideEffectAsync(
|
||||
SavePolicyModel policyRequest,
|
||||
Policy postUpsertedPolicyState,
|
||||
Policy? previousPolicyState);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces;
|
||||
|
||||
public interface IOnPolicyPreUpdateEvent : IPolicyUpdateEvent
|
||||
{
|
||||
/// <summary>
|
||||
/// Performs side effects before a policy is upserted.
|
||||
/// For example, this can be used to remove non-compliant users from the organization.
|
||||
/// </summary>
|
||||
/// <param name="policyRequest">The policy save request containing the policy update and metadata</param>
|
||||
/// <param name="currentPolicy">The current policy, if any</param>
|
||||
public Task ExecutePreUpsertSideEffectAsync(
|
||||
SavePolicyModel policyRequest,
|
||||
Policy? currentPolicy);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
#nullable enable
|
||||
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using OneOf;
|
||||
using OneOf.Types;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Provides policy-specific event handlers used during the save workflow in <see cref="IVNextSavePolicyCommand"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Supported handlers:
|
||||
/// - <see cref="IEnforceDependentPoliciesEvent"/> for dependency checks
|
||||
/// - <see cref="IPolicyValidationEvent"/> for custom validation
|
||||
/// - <see cref="IOnPolicyPreUpdateEvent"/> for pre-save logic
|
||||
/// - <see cref="IOnPolicyPostUpdateEvent"/> for post-save logic
|
||||
/// </remarks>
|
||||
public interface IPolicyEventHandlerFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the event handler for the given policy type and handler interface.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Handler type implementing <see cref="IPolicyUpdateEvent"/>.</typeparam>
|
||||
/// <param name="policyType">The policy type to resolve.</param>
|
||||
/// <returns>
|
||||
/// <see cref="OneOf{T, None}"/> — the handler if available, or None if not implemented.
|
||||
/// </returns>
|
||||
OneOf<T, None> GetHandler<T>(PolicyType policyType) where T : IPolicyUpdateEvent;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces;
|
||||
|
||||
public interface IPolicyUpdateEvent
|
||||
{
|
||||
/// <summary>
|
||||
/// The policy type that the associated handler will handle.
|
||||
/// </summary>
|
||||
public PolicyType Type { get; }
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces;
|
||||
|
||||
public interface IPolicyValidationEvent : IPolicyUpdateEvent
|
||||
{
|
||||
/// <summary>
|
||||
/// Performs side effects after a policy is validated but before it is saved.
|
||||
/// For example, this can be used to remove non-compliant users from the organization.
|
||||
/// Implementation is optional; by default, it will not perform any side effects.
|
||||
/// </summary>
|
||||
/// <param name="policyRequest">The policy save request containing the policy update and metadata</param>
|
||||
/// <param name="currentPolicy">The current policy, if any</param>
|
||||
public Task<string> ValidateAsync(
|
||||
SavePolicyModel policyRequest,
|
||||
Policy? currentPolicy);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
|
||||
using Microsoft.Azure.NotificationHubs.Messaging;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Handles creating or updating organization policies with validation and side effect execution.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Workflow:
|
||||
/// 1. Validates organization can use policies
|
||||
/// 2. Validates required and dependent policies
|
||||
/// 3. Runs policy-specific validation (<see cref="IPolicyValidationEvent"/>)
|
||||
/// 4. Executes pre-save logic (<see cref="IOnPolicyPreUpdateEvent"/>)
|
||||
/// 5. Saves the policy
|
||||
/// 6. Logs the event
|
||||
/// 7. Executes post-save logic (<see cref="IOnPolicyPostUpdateEvent"/>)
|
||||
/// </remarks>
|
||||
public interface IVNextSavePolicyCommand
|
||||
{
|
||||
/// <summary>
|
||||
/// Performs the necessary validations, saves the policy and any side effects
|
||||
/// </summary>
|
||||
/// <param name="policyRequest">Policy data, acting user, and metadata.</param>
|
||||
/// <returns>The saved policy with updated revision and applied changes.</returns>
|
||||
/// <exception cref="BadRequestException">
|
||||
/// Thrown if:
|
||||
/// - The organization can’t use policies
|
||||
/// - Dependent policies are missing or block changes
|
||||
/// - Custom validation fails
|
||||
/// </exception>
|
||||
Task<Policy> SaveAsync(SavePolicyModel policyRequest);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces;
|
||||
using OneOf;
|
||||
using OneOf.Types;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents;
|
||||
|
||||
public class PolicyEventHandlerHandlerFactory(
|
||||
IEnumerable<IPolicyUpdateEvent> allEventHandlers) : IPolicyEventHandlerFactory
|
||||
{
|
||||
public OneOf<T, None> GetHandler<T>(PolicyType policyType) where T : IPolicyUpdateEvent
|
||||
{
|
||||
var tEventHandlers = allEventHandlers.OfType<T>().ToList();
|
||||
|
||||
var matchingHandlers = tEventHandlers.Where(h => h.Type == policyType).ToList();
|
||||
|
||||
if (matchingHandlers.Count > 1)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Multiple {nameof(IPolicyUpdateEvent)} handlers of type {typeof(T).Name} found for {nameof(PolicyType)} {policyType}. " +
|
||||
$"Expected one {typeof(T).Name} handler per {nameof(PolicyType)}.");
|
||||
}
|
||||
|
||||
var policyTEventHandler = matchingHandlers.SingleOrDefault();
|
||||
if (policyTEventHandler is null)
|
||||
{
|
||||
return new None();
|
||||
}
|
||||
|
||||
return policyTEventHandler;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces;
|
||||
using OneOf.Types;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies;
|
||||
|
||||
public class PolicyEventHandlerHandlerFactoryTests
|
||||
{
|
||||
[Fact]
|
||||
public void GetHandler_ReturnsHandler_WhenHandlerExists()
|
||||
{
|
||||
// Arrange
|
||||
var expectedHandler = new FakeSingleOrgDependencyEvent();
|
||||
var factory = new PolicyEventHandlerHandlerFactory([expectedHandler]);
|
||||
|
||||
// Act
|
||||
var result = factory.GetHandler<IEnforceDependentPoliciesEvent>(PolicyType.SingleOrg);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsT0);
|
||||
Assert.Equal(expectedHandler, result.AsT0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetHandler_ReturnsNone_WhenHandlerDoesNotExist()
|
||||
{
|
||||
// Arrange
|
||||
var factory = new PolicyEventHandlerHandlerFactory([new FakeSingleOrgDependencyEvent()]);
|
||||
|
||||
// Act
|
||||
var result = factory.GetHandler<IEnforceDependentPoliciesEvent>(PolicyType.RequireSso);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsT1);
|
||||
Assert.IsType<None>(result.AsT1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetHandler_ReturnsNone_WhenHandlerTypeDoesNotMatch()
|
||||
{
|
||||
// Arrange
|
||||
var factory = new PolicyEventHandlerHandlerFactory([new FakeSingleOrgDependencyEvent()]);
|
||||
|
||||
// Act
|
||||
var result = factory.GetHandler<IPolicyValidationEvent>(PolicyType.SingleOrg);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsT1);
|
||||
Assert.IsType<None>(result.AsT1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetHandler_ReturnsCorrectHandler_WhenMultipleHandlerTypesExist()
|
||||
{
|
||||
// Arrange
|
||||
var dependencyEvent = new FakeSingleOrgDependencyEvent();
|
||||
var validationEvent = new FakeSingleOrgValidationEvent();
|
||||
var factory = new PolicyEventHandlerHandlerFactory([dependencyEvent, validationEvent]);
|
||||
|
||||
// Act
|
||||
var dependencyResult = factory.GetHandler<IEnforceDependentPoliciesEvent>(PolicyType.SingleOrg);
|
||||
var validationResult = factory.GetHandler<IPolicyValidationEvent>(PolicyType.SingleOrg);
|
||||
|
||||
// Assert
|
||||
Assert.True(dependencyResult.IsT0);
|
||||
Assert.Equal(dependencyEvent, dependencyResult.AsT0);
|
||||
|
||||
Assert.True(validationResult.IsT0);
|
||||
Assert.Equal(validationEvent, validationResult.AsT0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetHandler_ReturnsCorrectHandler_WhenMultiplePolicyTypesExist()
|
||||
{
|
||||
// Arrange
|
||||
var singleOrgEvent = new FakeSingleOrgDependencyEvent();
|
||||
var requireSsoEvent = new FakeRequireSsoDependencyEvent();
|
||||
var factory = new PolicyEventHandlerHandlerFactory([singleOrgEvent, requireSsoEvent]);
|
||||
|
||||
// Act
|
||||
var singleOrgResult = factory.GetHandler<IEnforceDependentPoliciesEvent>(PolicyType.SingleOrg);
|
||||
var requireSsoResult = factory.GetHandler<IEnforceDependentPoliciesEvent>(PolicyType.RequireSso);
|
||||
|
||||
// Assert
|
||||
Assert.True(singleOrgResult.IsT0);
|
||||
Assert.Equal(singleOrgEvent, singleOrgResult.AsT0);
|
||||
|
||||
Assert.True(requireSsoResult.IsT0);
|
||||
Assert.Equal(requireSsoEvent, requireSsoResult.AsT0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetHandler_Throws_WhenDuplicateHandlersExist()
|
||||
{
|
||||
// Arrange
|
||||
var factory = new PolicyEventHandlerHandlerFactory([
|
||||
new FakeSingleOrgDependencyEvent(),
|
||||
new FakeSingleOrgDependencyEvent()
|
||||
]);
|
||||
|
||||
// Act & Assert
|
||||
var exception = Assert.Throws<InvalidOperationException>(() =>
|
||||
factory.GetHandler<IEnforceDependentPoliciesEvent>(PolicyType.SingleOrg));
|
||||
|
||||
Assert.Contains("Multiple IPolicyUpdateEvent handlers of type IEnforceDependentPoliciesEvent found for PolicyType SingleOrg", exception.Message);
|
||||
Assert.Contains("Expected one IEnforceDependentPoliciesEvent handler per PolicyType", exception.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetHandler_ReturnsNone_WhenNoHandlersProvided()
|
||||
{
|
||||
// Arrange
|
||||
var factory = new PolicyEventHandlerHandlerFactory([]);
|
||||
|
||||
// Act
|
||||
var result = factory.GetHandler<IEnforceDependentPoliciesEvent>(PolicyType.SingleOrg);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsT1);
|
||||
Assert.IsType<None>(result.AsT1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces;
|
||||
using NSubstitute;
|
||||
|
||||
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies;
|
||||
|
||||
public class FakeSingleOrgDependencyEvent : IEnforceDependentPoliciesEvent
|
||||
{
|
||||
public PolicyType Type => PolicyType.SingleOrg;
|
||||
public IEnumerable<PolicyType> RequiredPolicies => [];
|
||||
}
|
||||
|
||||
public class FakeRequireSsoDependencyEvent : IEnforceDependentPoliciesEvent
|
||||
{
|
||||
public PolicyType Type => PolicyType.RequireSso;
|
||||
public IEnumerable<PolicyType> RequiredPolicies => [PolicyType.SingleOrg];
|
||||
}
|
||||
|
||||
public class FakeVaultTimeoutDependencyEvent : IEnforceDependentPoliciesEvent
|
||||
{
|
||||
public PolicyType Type => PolicyType.MaximumVaultTimeout;
|
||||
public IEnumerable<PolicyType> RequiredPolicies => [PolicyType.SingleOrg];
|
||||
}
|
||||
|
||||
public class FakeSingleOrgValidationEvent : IPolicyValidationEvent
|
||||
{
|
||||
public PolicyType Type => PolicyType.SingleOrg;
|
||||
|
||||
public readonly Func<SavePolicyModel, Policy?, Task<string>> ValidateAsyncMock = Substitute.For<Func<SavePolicyModel, Policy?, Task<string>>>();
|
||||
|
||||
public Task<string> ValidateAsync(SavePolicyModel policyRequest, Policy? currentPolicy)
|
||||
{
|
||||
return ValidateAsyncMock(policyRequest, currentPolicy);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,471 @@
|
||||
#nullable enable
|
||||
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Implementations;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Data.Organizations;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Test.AdminConsole.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using NSubstitute;
|
||||
using OneOf.Types;
|
||||
using Xunit;
|
||||
using EventType = Bit.Core.Enums.EventType;
|
||||
|
||||
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies;
|
||||
|
||||
public class VNextSavePolicyCommandTests
|
||||
{
|
||||
[Theory, BitAutoData]
|
||||
public async Task SaveAsync_NewPolicy_Success([PolicyUpdate(PolicyType.SingleOrg)] PolicyUpdate policyUpdate)
|
||||
{
|
||||
// Arrange
|
||||
var fakePolicyValidationEvent = new FakeSingleOrgValidationEvent();
|
||||
fakePolicyValidationEvent.ValidateAsyncMock(Arg.Any<SavePolicyModel>(), Arg.Any<Policy>()).Returns("");
|
||||
var sutProvider = SutProviderFactory(
|
||||
[new FakeSingleOrgDependencyEvent()],
|
||||
[fakePolicyValidationEvent]);
|
||||
|
||||
var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel());
|
||||
|
||||
var newPolicy = new Policy
|
||||
{
|
||||
Type = policyUpdate.Type,
|
||||
OrganizationId = policyUpdate.OrganizationId,
|
||||
Enabled = false
|
||||
};
|
||||
|
||||
ArrangeOrganization(sutProvider, policyUpdate);
|
||||
sutProvider.GetDependency<IPolicyRepository>().GetManyByOrganizationIdAsync(policyUpdate.OrganizationId).Returns([newPolicy]);
|
||||
|
||||
var creationDate = sutProvider.GetDependency<FakeTimeProvider>().Start;
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.SaveAsync(savePolicyModel);
|
||||
|
||||
// Assert
|
||||
await fakePolicyValidationEvent.ValidateAsyncMock
|
||||
.Received(1)
|
||||
.Invoke(Arg.Any<SavePolicyModel>(), Arg.Any<Policy>());
|
||||
|
||||
await AssertPolicySavedAsync(sutProvider, policyUpdate);
|
||||
|
||||
await sutProvider.GetDependency<IPolicyRepository>()
|
||||
.Received(1)
|
||||
.UpsertAsync(Arg.Is<Policy>(p =>
|
||||
p.CreationDate == creationDate &&
|
||||
p.RevisionDate == creationDate));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task SaveAsync_ExistingPolicy_Success(
|
||||
[PolicyUpdate(PolicyType.SingleOrg)] PolicyUpdate policyUpdate,
|
||||
[Policy(PolicyType.SingleOrg, false)] Policy currentPolicy)
|
||||
{
|
||||
// Arrange
|
||||
var fakePolicyValidationEvent = new FakeSingleOrgValidationEvent();
|
||||
fakePolicyValidationEvent.ValidateAsyncMock(Arg.Any<SavePolicyModel>(), Arg.Any<Policy>()).Returns("");
|
||||
var sutProvider = SutProviderFactory(
|
||||
[new FakeSingleOrgDependencyEvent()],
|
||||
[fakePolicyValidationEvent]);
|
||||
|
||||
var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel());
|
||||
|
||||
currentPolicy.OrganizationId = policyUpdate.OrganizationId;
|
||||
sutProvider.GetDependency<IPolicyRepository>()
|
||||
.GetByOrganizationIdTypeAsync(policyUpdate.OrganizationId, policyUpdate.Type)
|
||||
.Returns(currentPolicy);
|
||||
|
||||
ArrangeOrganization(sutProvider, policyUpdate);
|
||||
sutProvider.GetDependency<IPolicyRepository>()
|
||||
.GetManyByOrganizationIdAsync(policyUpdate.OrganizationId)
|
||||
.Returns([currentPolicy]);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.SaveAsync(savePolicyModel);
|
||||
|
||||
// Assert
|
||||
await fakePolicyValidationEvent.ValidateAsyncMock
|
||||
.Received(1)
|
||||
.Invoke(Arg.Any<SavePolicyModel>(), currentPolicy);
|
||||
|
||||
await AssertPolicySavedAsync(sutProvider, policyUpdate);
|
||||
|
||||
|
||||
var revisionDate = sutProvider.GetDependency<FakeTimeProvider>().Start;
|
||||
|
||||
await sutProvider.GetDependency<IPolicyRepository>()
|
||||
.Received(1)
|
||||
.UpsertAsync(Arg.Is<Policy>(p =>
|
||||
p.Id == currentPolicy.Id &&
|
||||
p.OrganizationId == currentPolicy.OrganizationId &&
|
||||
p.Type == currentPolicy.Type &&
|
||||
p.CreationDate == currentPolicy.CreationDate &&
|
||||
p.RevisionDate == revisionDate));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_DuplicatePolicyDependencyEvents_Throws()
|
||||
{
|
||||
// Arrange & Act
|
||||
var exception = Assert.Throws<Exception>(() =>
|
||||
new VNextSavePolicyCommand(
|
||||
Substitute.For<IApplicationCacheService>(),
|
||||
Substitute.For<IEventService>(),
|
||||
Substitute.For<IPolicyRepository>(),
|
||||
[new FakeSingleOrgDependencyEvent(), new FakeSingleOrgDependencyEvent()],
|
||||
Substitute.For<TimeProvider>(),
|
||||
Substitute.For<IPolicyEventHandlerFactory>()));
|
||||
|
||||
// Assert
|
||||
Assert.Contains("Duplicate PolicyValidationEvent for SingleOrg policy", exception.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task SaveAsync_OrganizationDoesNotExist_ThrowsBadRequest([PolicyUpdate(PolicyType.ActivateAutofill)] PolicyUpdate policyUpdate)
|
||||
{
|
||||
// Arrange
|
||||
var sutProvider = SutProviderFactory();
|
||||
var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel());
|
||||
|
||||
sutProvider.GetDependency<IApplicationCacheService>()
|
||||
.GetOrganizationAbilityAsync(policyUpdate.OrganizationId)
|
||||
.Returns(Task.FromResult<OrganizationAbility?>(null));
|
||||
|
||||
// Act
|
||||
var badRequestException = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.SaveAsync(savePolicyModel));
|
||||
|
||||
// Assert
|
||||
Assert.Contains("Organization not found", badRequestException.Message, StringComparison.OrdinalIgnoreCase);
|
||||
await AssertPolicyNotSavedAsync(sutProvider);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task SaveAsync_OrganizationCannotUsePolicies_ThrowsBadRequest([PolicyUpdate(PolicyType.ActivateAutofill)] PolicyUpdate policyUpdate)
|
||||
{
|
||||
// Arrange
|
||||
var sutProvider = SutProviderFactory();
|
||||
var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel());
|
||||
|
||||
sutProvider.GetDependency<IApplicationCacheService>()
|
||||
.GetOrganizationAbilityAsync(policyUpdate.OrganizationId)
|
||||
.Returns(new OrganizationAbility
|
||||
{
|
||||
Id = policyUpdate.OrganizationId,
|
||||
UsePolicies = false
|
||||
});
|
||||
|
||||
// Act
|
||||
var badRequestException = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.SaveAsync(savePolicyModel));
|
||||
|
||||
// Assert
|
||||
Assert.Contains("cannot use policies", badRequestException.Message, StringComparison.OrdinalIgnoreCase);
|
||||
await AssertPolicyNotSavedAsync(sutProvider);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task SaveAsync_RequiredPolicyIsNull_Throws(
|
||||
[PolicyUpdate(PolicyType.RequireSso)] PolicyUpdate policyUpdate)
|
||||
{
|
||||
// Arrange
|
||||
var sutProvider = SutProviderFactory(
|
||||
[
|
||||
new FakeRequireSsoDependencyEvent(),
|
||||
new FakeSingleOrgDependencyEvent()
|
||||
]);
|
||||
|
||||
var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel());
|
||||
|
||||
var requireSsoPolicy = new Policy
|
||||
{
|
||||
Type = PolicyType.RequireSso,
|
||||
OrganizationId = policyUpdate.OrganizationId,
|
||||
Enabled = false
|
||||
};
|
||||
|
||||
ArrangeOrganization(sutProvider, policyUpdate);
|
||||
sutProvider.GetDependency<IPolicyRepository>()
|
||||
.GetManyByOrganizationIdAsync(policyUpdate.OrganizationId)
|
||||
.Returns([requireSsoPolicy]);
|
||||
|
||||
// Act
|
||||
var badRequestException = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.SaveAsync(savePolicyModel));
|
||||
|
||||
// Assert
|
||||
Assert.Contains("Turn on the Single organization policy because it is required for the Require single sign-on authentication policy", badRequestException.Message, StringComparison.OrdinalIgnoreCase);
|
||||
await AssertPolicyNotSavedAsync(sutProvider);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task SaveAsync_RequiredPolicyNotEnabled_Throws(
|
||||
[PolicyUpdate(PolicyType.RequireSso)] PolicyUpdate policyUpdate,
|
||||
[Policy(PolicyType.SingleOrg, false)] Policy singleOrgPolicy)
|
||||
{
|
||||
// Arrange
|
||||
var sutProvider = SutProviderFactory(
|
||||
[
|
||||
new FakeRequireSsoDependencyEvent(),
|
||||
new FakeSingleOrgDependencyEvent()
|
||||
]);
|
||||
|
||||
var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel());
|
||||
|
||||
var requireSsoPolicy = new Policy
|
||||
{
|
||||
Type = PolicyType.RequireSso,
|
||||
OrganizationId = policyUpdate.OrganizationId,
|
||||
Enabled = false
|
||||
};
|
||||
|
||||
ArrangeOrganization(sutProvider, policyUpdate);
|
||||
sutProvider.GetDependency<IPolicyRepository>()
|
||||
.GetManyByOrganizationIdAsync(policyUpdate.OrganizationId)
|
||||
.Returns([singleOrgPolicy, requireSsoPolicy]);
|
||||
|
||||
// Act
|
||||
var badRequestException = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.SaveAsync(savePolicyModel));
|
||||
|
||||
// Assert
|
||||
Assert.Contains("Turn on the Single organization policy because it is required for the Require single sign-on authentication policy", badRequestException.Message, StringComparison.OrdinalIgnoreCase);
|
||||
await AssertPolicyNotSavedAsync(sutProvider);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task SaveAsync_RequiredPolicyEnabled_Success(
|
||||
[PolicyUpdate(PolicyType.RequireSso)] PolicyUpdate policyUpdate,
|
||||
[Policy(PolicyType.SingleOrg)] Policy singleOrgPolicy)
|
||||
{
|
||||
// Arrange
|
||||
var sutProvider = SutProviderFactory(
|
||||
[
|
||||
new FakeRequireSsoDependencyEvent(),
|
||||
new FakeSingleOrgDependencyEvent()
|
||||
]);
|
||||
|
||||
var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel());
|
||||
|
||||
var requireSsoPolicy = new Policy
|
||||
{
|
||||
Type = PolicyType.RequireSso,
|
||||
OrganizationId = policyUpdate.OrganizationId,
|
||||
Enabled = false
|
||||
};
|
||||
|
||||
ArrangeOrganization(sutProvider, policyUpdate);
|
||||
sutProvider.GetDependency<IPolicyRepository>()
|
||||
.GetManyByOrganizationIdAsync(policyUpdate.OrganizationId)
|
||||
.Returns([singleOrgPolicy, requireSsoPolicy]);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.SaveAsync(savePolicyModel);
|
||||
|
||||
// Assert
|
||||
await AssertPolicySavedAsync(sutProvider, policyUpdate);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task SaveAsync_DependentPolicyIsEnabled_Throws(
|
||||
[PolicyUpdate(PolicyType.SingleOrg, false)] PolicyUpdate policyUpdate,
|
||||
[Policy(PolicyType.SingleOrg)] Policy currentPolicy,
|
||||
[Policy(PolicyType.RequireSso)] Policy requireSsoPolicy)
|
||||
{
|
||||
// Arrange
|
||||
var sutProvider = SutProviderFactory(
|
||||
[
|
||||
new FakeRequireSsoDependencyEvent(),
|
||||
new FakeSingleOrgDependencyEvent()
|
||||
]);
|
||||
|
||||
var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel());
|
||||
|
||||
ArrangeOrganization(sutProvider, policyUpdate);
|
||||
sutProvider.GetDependency<IPolicyRepository>()
|
||||
.GetManyByOrganizationIdAsync(policyUpdate.OrganizationId)
|
||||
.Returns([currentPolicy, requireSsoPolicy]);
|
||||
|
||||
// Act
|
||||
var badRequestException = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.SaveAsync(savePolicyModel));
|
||||
|
||||
// Assert
|
||||
Assert.Contains("Turn off the Require single sign-on authentication policy because it requires the Single organization policy", badRequestException.Message, StringComparison.OrdinalIgnoreCase);
|
||||
await AssertPolicyNotSavedAsync(sutProvider);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task SaveAsync_MultipleDependentPoliciesAreEnabled_Throws(
|
||||
[PolicyUpdate(PolicyType.SingleOrg, false)] PolicyUpdate policyUpdate,
|
||||
[Policy(PolicyType.SingleOrg)] Policy currentPolicy,
|
||||
[Policy(PolicyType.RequireSso)] Policy requireSsoPolicy,
|
||||
[Policy(PolicyType.MaximumVaultTimeout)] Policy vaultTimeoutPolicy)
|
||||
{
|
||||
// Arrange
|
||||
var sutProvider = SutProviderFactory(
|
||||
[
|
||||
new FakeRequireSsoDependencyEvent(),
|
||||
new FakeSingleOrgDependencyEvent(),
|
||||
new FakeVaultTimeoutDependencyEvent()
|
||||
]);
|
||||
|
||||
var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel());
|
||||
|
||||
ArrangeOrganization(sutProvider, policyUpdate);
|
||||
sutProvider.GetDependency<IPolicyRepository>()
|
||||
.GetManyByOrganizationIdAsync(policyUpdate.OrganizationId)
|
||||
.Returns([currentPolicy, requireSsoPolicy, vaultTimeoutPolicy]);
|
||||
|
||||
// Act
|
||||
var badRequestException = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.SaveAsync(savePolicyModel));
|
||||
|
||||
// Assert
|
||||
Assert.Contains("Turn off all of the policies that require the Single organization policy", badRequestException.Message, StringComparison.OrdinalIgnoreCase);
|
||||
await AssertPolicyNotSavedAsync(sutProvider);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task SaveAsync_DependentPolicyNotEnabled_Success(
|
||||
[PolicyUpdate(PolicyType.SingleOrg, false)] PolicyUpdate policyUpdate,
|
||||
[Policy(PolicyType.SingleOrg)] Policy currentPolicy,
|
||||
[Policy(PolicyType.RequireSso, false)] Policy requireSsoPolicy)
|
||||
{
|
||||
// Arrange
|
||||
var sutProvider = SutProviderFactory(
|
||||
[
|
||||
new FakeRequireSsoDependencyEvent(),
|
||||
new FakeSingleOrgDependencyEvent()
|
||||
]);
|
||||
|
||||
var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel());
|
||||
|
||||
ArrangeOrganization(sutProvider, policyUpdate);
|
||||
sutProvider.GetDependency<IPolicyRepository>()
|
||||
.GetManyByOrganizationIdAsync(policyUpdate.OrganizationId)
|
||||
.Returns([currentPolicy, requireSsoPolicy]);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.SaveAsync(savePolicyModel);
|
||||
|
||||
// Assert
|
||||
await AssertPolicySavedAsync(sutProvider, policyUpdate);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task SaveAsync_ThrowsOnValidationError([PolicyUpdate(PolicyType.SingleOrg)] PolicyUpdate policyUpdate)
|
||||
{
|
||||
// Arrange
|
||||
var fakePolicyValidationEvent = new FakeSingleOrgValidationEvent();
|
||||
fakePolicyValidationEvent.ValidateAsyncMock(Arg.Any<SavePolicyModel>(), Arg.Any<Policy>()).Returns("Validation error!");
|
||||
var sutProvider = SutProviderFactory(
|
||||
[new FakeSingleOrgDependencyEvent()],
|
||||
[fakePolicyValidationEvent]);
|
||||
|
||||
var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel());
|
||||
|
||||
var singleOrgPolicy = new Policy
|
||||
{
|
||||
Type = PolicyType.SingleOrg,
|
||||
OrganizationId = policyUpdate.OrganizationId,
|
||||
Enabled = false
|
||||
};
|
||||
|
||||
ArrangeOrganization(sutProvider, policyUpdate);
|
||||
sutProvider.GetDependency<IPolicyRepository>().GetManyByOrganizationIdAsync(policyUpdate.OrganizationId).Returns([singleOrgPolicy]);
|
||||
|
||||
// Act
|
||||
var badRequestException = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.SaveAsync(savePolicyModel));
|
||||
|
||||
// Assert
|
||||
Assert.Contains("Validation error!", badRequestException.Message, StringComparison.OrdinalIgnoreCase);
|
||||
await AssertPolicyNotSavedAsync(sutProvider);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a new SutProvider with the PolicyDependencyEvents registered in the Sut.
|
||||
/// </summary>
|
||||
private static SutProvider<VNextSavePolicyCommand> SutProviderFactory(
|
||||
IEnumerable<IEnforceDependentPoliciesEvent>? policyDependencyEvents = null,
|
||||
IEnumerable<IPolicyValidationEvent>? policyValidationEvents = null)
|
||||
{
|
||||
var policyEventHandlerFactory = Substitute.For<IPolicyEventHandlerFactory>();
|
||||
|
||||
// Setup factory to return handlers based on type
|
||||
policyEventHandlerFactory.GetHandler<IEnforceDependentPoliciesEvent>(Arg.Any<PolicyType>())
|
||||
.Returns(callInfo =>
|
||||
{
|
||||
var policyType = callInfo.Arg<PolicyType>();
|
||||
var handler = policyDependencyEvents?.FirstOrDefault(e => e.Type == policyType);
|
||||
return handler != null ? OneOf.OneOf<IEnforceDependentPoliciesEvent, None>.FromT0(handler) : OneOf.OneOf<IEnforceDependentPoliciesEvent, None>.FromT1(new None());
|
||||
});
|
||||
|
||||
policyEventHandlerFactory.GetHandler<IPolicyValidationEvent>(Arg.Any<PolicyType>())
|
||||
.Returns(callInfo =>
|
||||
{
|
||||
var policyType = callInfo.Arg<PolicyType>();
|
||||
var handler = policyValidationEvents?.FirstOrDefault(e => e.Type == policyType);
|
||||
return handler != null ? OneOf.OneOf<IPolicyValidationEvent, None>.FromT0(handler) : OneOf.OneOf<IPolicyValidationEvent, None>.FromT1(new None());
|
||||
});
|
||||
|
||||
policyEventHandlerFactory.GetHandler<IOnPolicyPreUpdateEvent>(Arg.Any<PolicyType>())
|
||||
.Returns(new None());
|
||||
|
||||
policyEventHandlerFactory.GetHandler<IOnPolicyPostUpdateEvent>(Arg.Any<PolicyType>())
|
||||
.Returns(new None());
|
||||
|
||||
return new SutProvider<VNextSavePolicyCommand>()
|
||||
.WithFakeTimeProvider()
|
||||
.SetDependency(policyDependencyEvents ?? [])
|
||||
.SetDependency(policyEventHandlerFactory)
|
||||
.Create();
|
||||
}
|
||||
|
||||
private static void ArrangeOrganization(SutProvider<VNextSavePolicyCommand> sutProvider, PolicyUpdate policyUpdate)
|
||||
{
|
||||
sutProvider.GetDependency<IApplicationCacheService>()
|
||||
.GetOrganizationAbilityAsync(policyUpdate.OrganizationId)
|
||||
.Returns(new OrganizationAbility
|
||||
{
|
||||
Id = policyUpdate.OrganizationId,
|
||||
UsePolicies = true
|
||||
});
|
||||
}
|
||||
|
||||
private static async Task AssertPolicyNotSavedAsync(SutProvider<VNextSavePolicyCommand> sutProvider)
|
||||
{
|
||||
await sutProvider.GetDependency<IPolicyRepository>()
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.UpsertAsync(default!);
|
||||
|
||||
await sutProvider.GetDependency<IEventService>()
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.LogPolicyEventAsync(default, default);
|
||||
}
|
||||
|
||||
private static async Task AssertPolicySavedAsync(SutProvider<VNextSavePolicyCommand> sutProvider, PolicyUpdate policyUpdate)
|
||||
{
|
||||
await sutProvider.GetDependency<IPolicyRepository>().Received(1).UpsertAsync(ExpectedPolicy());
|
||||
|
||||
await sutProvider.GetDependency<IEventService>().Received(1)
|
||||
.LogPolicyEventAsync(ExpectedPolicy(), EventType.Policy_Updated);
|
||||
|
||||
return;
|
||||
|
||||
Policy ExpectedPolicy() => Arg.Is<Policy>(
|
||||
p =>
|
||||
p.Type == policyUpdate.Type
|
||||
&& p.OrganizationId == policyUpdate.OrganizationId
|
||||
&& p.Enabled == policyUpdate.Enabled
|
||||
&& p.Data == policyUpdate.Data);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user