mirror of
https://github.com/bitwarden/server
synced 2025-12-06 00:03:34 +00:00
[PM-23845] Update cache service to handle concurrency (#6170)
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
|
||||
namespace Bit.Core.AdminConsole.AbilitiesCache;
|
||||
|
||||
public interface IApplicationCacheServiceBusMessaging
|
||||
{
|
||||
Task NotifyOrganizationAbilityUpsertedAsync(Organization organization);
|
||||
Task NotifyOrganizationAbilityDeletedAsync(Guid organizationId);
|
||||
Task NotifyProviderAbilityDeletedAsync(Guid providerId);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Models.Data.Provider;
|
||||
using Bit.Core.Models.Data.Organizations;
|
||||
|
||||
namespace Bit.Core.AdminConsole.AbilitiesCache;
|
||||
|
||||
public interface IVCurrentInMemoryApplicationCacheService
|
||||
{
|
||||
Task<IDictionary<Guid, OrganizationAbility>> GetOrganizationAbilitiesAsync();
|
||||
#nullable enable
|
||||
Task<OrganizationAbility?> GetOrganizationAbilityAsync(Guid orgId);
|
||||
#nullable disable
|
||||
Task<IDictionary<Guid, ProviderAbility>> GetProviderAbilitiesAsync();
|
||||
Task UpsertOrganizationAbilityAsync(Organization organization);
|
||||
Task UpsertProviderAbilityAsync(Provider provider);
|
||||
Task DeleteOrganizationAbilityAsync(Guid organizationId);
|
||||
Task DeleteProviderAbilityAsync(Guid providerId);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Models.Data.Provider;
|
||||
using Bit.Core.Models.Data.Organizations;
|
||||
|
||||
namespace Bit.Core.AdminConsole.AbilitiesCache;
|
||||
|
||||
public interface IVNextInMemoryApplicationCacheService
|
||||
{
|
||||
Task<IDictionary<Guid, OrganizationAbility>> GetOrganizationAbilitiesAsync();
|
||||
#nullable enable
|
||||
Task<OrganizationAbility?> GetOrganizationAbilityAsync(Guid orgId);
|
||||
#nullable disable
|
||||
Task<IDictionary<Guid, ProviderAbility>> GetProviderAbilitiesAsync();
|
||||
Task UpsertOrganizationAbilityAsync(Organization organization);
|
||||
Task UpsertProviderAbilityAsync(Provider provider);
|
||||
Task DeleteOrganizationAbilityAsync(Guid organizationId);
|
||||
Task DeleteProviderAbilityAsync(Guid providerId);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
|
||||
namespace Bit.Core.AdminConsole.AbilitiesCache;
|
||||
|
||||
public class NoOpApplicationCacheMessaging : IApplicationCacheServiceBusMessaging
|
||||
{
|
||||
public Task NotifyOrganizationAbilityUpsertedAsync(Organization organization)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task NotifyOrganizationAbilityDeletedAsync(Guid organizationId)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task NotifyProviderAbilityDeletedAsync(Guid providerId)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
using Azure.Messaging.ServiceBus;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Core.AdminConsole.AbilitiesCache;
|
||||
|
||||
public class ServiceBusApplicationCacheMessaging : IApplicationCacheServiceBusMessaging
|
||||
{
|
||||
private readonly ServiceBusSender _topicMessageSender;
|
||||
private readonly string _subName;
|
||||
|
||||
public ServiceBusApplicationCacheMessaging(
|
||||
GlobalSettings globalSettings)
|
||||
{
|
||||
_subName = CoreHelpers.GetApplicationCacheServiceBusSubscriptionName(globalSettings);
|
||||
var serviceBusClient = new ServiceBusClient(globalSettings.ServiceBus.ConnectionString);
|
||||
_topicMessageSender = serviceBusClient.CreateSender(globalSettings.ServiceBus.ApplicationCacheTopicName);
|
||||
}
|
||||
|
||||
public async Task NotifyOrganizationAbilityUpsertedAsync(Organization organization)
|
||||
{
|
||||
var message = new ServiceBusMessage
|
||||
{
|
||||
Subject = _subName,
|
||||
ApplicationProperties =
|
||||
{
|
||||
{ "type", (byte)ApplicationCacheMessageType.UpsertOrganizationAbility },
|
||||
{ "id", organization.Id },
|
||||
}
|
||||
};
|
||||
await _topicMessageSender.SendMessageAsync(message);
|
||||
}
|
||||
|
||||
public async Task NotifyOrganizationAbilityDeletedAsync(Guid organizationId)
|
||||
{
|
||||
var message = new ServiceBusMessage
|
||||
{
|
||||
Subject = _subName,
|
||||
ApplicationProperties =
|
||||
{
|
||||
{ "type", (byte)ApplicationCacheMessageType.DeleteOrganizationAbility },
|
||||
{ "id", organizationId },
|
||||
}
|
||||
};
|
||||
await _topicMessageSender.SendMessageAsync(message);
|
||||
}
|
||||
|
||||
public async Task NotifyProviderAbilityDeletedAsync(Guid providerId)
|
||||
{
|
||||
var message = new ServiceBusMessage
|
||||
{
|
||||
Subject = _subName,
|
||||
ApplicationProperties =
|
||||
{
|
||||
{ "type", (byte)ApplicationCacheMessageType.DeleteProviderAbility },
|
||||
{ "id", providerId },
|
||||
}
|
||||
};
|
||||
await _topicMessageSender.SendMessageAsync(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
using System.Collections.Concurrent;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Models.Data.Provider;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Models.Data.Organizations;
|
||||
using Bit.Core.Repositories;
|
||||
|
||||
namespace Bit.Core.AdminConsole.AbilitiesCache;
|
||||
|
||||
public class VNextInMemoryApplicationCacheService(
|
||||
IOrganizationRepository organizationRepository,
|
||||
IProviderRepository providerRepository,
|
||||
TimeProvider timeProvider) : IVNextInMemoryApplicationCacheService
|
||||
{
|
||||
private ConcurrentDictionary<Guid, OrganizationAbility> _orgAbilities = new();
|
||||
private readonly SemaphoreSlim _orgInitLock = new(1, 1);
|
||||
private DateTimeOffset _lastOrgAbilityRefresh = DateTimeOffset.MinValue;
|
||||
|
||||
private ConcurrentDictionary<Guid, ProviderAbility> _providerAbilities = new();
|
||||
private readonly SemaphoreSlim _providerInitLock = new(1, 1);
|
||||
private DateTimeOffset _lastProviderAbilityRefresh = DateTimeOffset.MinValue;
|
||||
|
||||
private readonly TimeSpan _refreshInterval = TimeSpan.FromMinutes(10);
|
||||
|
||||
public virtual async Task<IDictionary<Guid, OrganizationAbility>> GetOrganizationAbilitiesAsync()
|
||||
{
|
||||
await InitOrganizationAbilitiesAsync();
|
||||
return _orgAbilities;
|
||||
}
|
||||
|
||||
public async Task<OrganizationAbility?> GetOrganizationAbilityAsync(Guid organizationId)
|
||||
{
|
||||
(await GetOrganizationAbilitiesAsync())
|
||||
.TryGetValue(organizationId, out var organizationAbility);
|
||||
return organizationAbility;
|
||||
}
|
||||
|
||||
public virtual async Task<IDictionary<Guid, ProviderAbility>> GetProviderAbilitiesAsync()
|
||||
{
|
||||
await InitProviderAbilitiesAsync();
|
||||
return _providerAbilities;
|
||||
}
|
||||
|
||||
public virtual async Task UpsertProviderAbilityAsync(Provider provider)
|
||||
{
|
||||
await InitProviderAbilitiesAsync();
|
||||
_providerAbilities.AddOrUpdate(
|
||||
provider.Id,
|
||||
static (_, provider) => new ProviderAbility(provider),
|
||||
static (_, _, provider) => new ProviderAbility(provider),
|
||||
provider);
|
||||
}
|
||||
|
||||
public virtual async Task UpsertOrganizationAbilityAsync(Organization organization)
|
||||
{
|
||||
await InitOrganizationAbilitiesAsync();
|
||||
|
||||
_orgAbilities.AddOrUpdate(
|
||||
organization.Id,
|
||||
static (_, organization) => new OrganizationAbility(organization),
|
||||
static (_, _, organization) => new OrganizationAbility(organization),
|
||||
organization);
|
||||
}
|
||||
|
||||
public virtual Task DeleteOrganizationAbilityAsync(Guid organizationId)
|
||||
{
|
||||
_orgAbilities.TryRemove(organizationId, out _);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public virtual Task DeleteProviderAbilityAsync(Guid providerId)
|
||||
{
|
||||
_providerAbilities.TryRemove(providerId, out _);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task InitOrganizationAbilitiesAsync() =>
|
||||
await InitAbilitiesAsync<OrganizationAbility>(
|
||||
dict => _orgAbilities = dict,
|
||||
() => _lastOrgAbilityRefresh,
|
||||
dt => _lastOrgAbilityRefresh = dt,
|
||||
_orgInitLock,
|
||||
async () => await organizationRepository.GetManyAbilitiesAsync(),
|
||||
_refreshInterval,
|
||||
ability => ability.Id);
|
||||
|
||||
private async Task InitProviderAbilitiesAsync() =>
|
||||
await InitAbilitiesAsync<ProviderAbility>(
|
||||
dict => _providerAbilities = dict,
|
||||
() => _lastProviderAbilityRefresh,
|
||||
dateTime => _lastProviderAbilityRefresh = dateTime,
|
||||
_providerInitLock,
|
||||
async () => await providerRepository.GetManyAbilitiesAsync(),
|
||||
_refreshInterval,
|
||||
ability => ability.Id);
|
||||
|
||||
|
||||
private async Task InitAbilitiesAsync<TAbility>(
|
||||
Action<ConcurrentDictionary<Guid, TAbility>> setCache,
|
||||
Func<DateTimeOffset> getLastRefresh,
|
||||
Action<DateTimeOffset> setLastRefresh,
|
||||
SemaphoreSlim @lock,
|
||||
Func<Task<IEnumerable<TAbility>>> fetchFunc,
|
||||
TimeSpan refreshInterval,
|
||||
Func<TAbility, Guid> getId)
|
||||
{
|
||||
if (SkipRefresh())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await @lock.WaitAsync();
|
||||
try
|
||||
{
|
||||
if (SkipRefresh())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var sources = await fetchFunc();
|
||||
var abilities = new ConcurrentDictionary<Guid, TAbility>(
|
||||
sources.ToDictionary(getId));
|
||||
setCache(abilities);
|
||||
setLastRefresh(timeProvider.GetUtcNow());
|
||||
}
|
||||
finally
|
||||
{
|
||||
@lock.Release();
|
||||
}
|
||||
|
||||
bool SkipRefresh()
|
||||
{
|
||||
return timeProvider.GetUtcNow() - getLastRefresh() <= refreshInterval;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -131,6 +131,8 @@ public static class FeatureFlagKeys
|
||||
public const string EventBasedOrganizationIntegrations = "event-based-organization-integrations";
|
||||
public const string SeparateCustomRolePermissions = "pm-19917-separate-custom-role-permissions";
|
||||
public const string CreateDefaultLocation = "pm-19467-create-default-location";
|
||||
public const string DirectoryConnectorPreventUserRemoval = "pm-24592-directory-connector-prevent-user-removal";
|
||||
public const string PM23845_VNextApplicationCache = "pm-24957-refactor-memory-application-cache";
|
||||
public const string CipherRepositoryBulkResourceCreation = "pm-24951-cipher-repository-bulk-resource-creation-service";
|
||||
public const string CollectionVaultRefactor = "pm-25030-resolve-ts-upgrade-errors";
|
||||
public const string DeleteClaimedUserAccountRefactor = "pm-25094-refactor-delete-managed-organization-user-command";
|
||||
|
||||
@@ -3,6 +3,7 @@ using Azure.Messaging.ServiceBus.Administration;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Services.Implementations;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
@@ -14,7 +15,7 @@ namespace Bit.Core.HostedServices;
|
||||
|
||||
public class ApplicationCacheHostedService : IHostedService, IDisposable
|
||||
{
|
||||
private readonly InMemoryServiceBusApplicationCacheService? _applicationCacheService;
|
||||
private readonly FeatureRoutedCacheService? _applicationCacheService;
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
protected readonly ILogger<ApplicationCacheHostedService> _logger;
|
||||
private readonly ServiceBusClient _serviceBusClient;
|
||||
@@ -34,7 +35,7 @@ public class ApplicationCacheHostedService : IHostedService, IDisposable
|
||||
{
|
||||
_topicName = globalSettings.ServiceBus.ApplicationCacheTopicName;
|
||||
_subName = CoreHelpers.GetApplicationCacheServiceBusSubscriptionName(globalSettings);
|
||||
_applicationCacheService = applicationCacheService as InMemoryServiceBusApplicationCacheService;
|
||||
_applicationCacheService = applicationCacheService as FeatureRoutedCacheService;
|
||||
_organizationRepository = organizationRepository;
|
||||
_logger = logger;
|
||||
_serviceBusClient = new ServiceBusClient(globalSettings.ServiceBus.ConnectionString);
|
||||
|
||||
152
src/Core/Services/Implementations/FeatureRoutedCacheService.cs
Normal file
152
src/Core/Services/Implementations/FeatureRoutedCacheService.cs
Normal file
@@ -0,0 +1,152 @@
|
||||
using Bit.Core.AdminConsole.AbilitiesCache;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Models.Data.Provider;
|
||||
using Bit.Core.Models.Data.Organizations;
|
||||
|
||||
namespace Bit.Core.Services.Implementations;
|
||||
|
||||
/// <summary>
|
||||
/// A feature-flagged routing service for application caching that bridges the gap between
|
||||
/// scoped dependency injection (IFeatureService) and singleton services (cache implementations).
|
||||
/// This service allows dynamic routing between IVCurrentInMemoryApplicationCacheService and
|
||||
/// IVNextInMemoryApplicationCacheService based on the PM23845_VNextApplicationCache feature flag.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This service is necessary because:
|
||||
/// - IFeatureService is registered as Scoped in the DI container
|
||||
/// - IVNextInMemoryApplicationCacheService and IVCurrentInMemoryApplicationCacheService are registered as Singleton
|
||||
/// - We need to evaluate feature flags at request time while maintaining singleton cache behavior
|
||||
///
|
||||
/// The service acts as a scoped proxy that can access the scoped IFeatureService while
|
||||
/// delegating actual cache operations to the appropriate singleton implementation.
|
||||
/// </remarks>
|
||||
public class FeatureRoutedCacheService(
|
||||
IFeatureService featureService,
|
||||
IVNextInMemoryApplicationCacheService vNextInMemoryApplicationCacheService,
|
||||
IVCurrentInMemoryApplicationCacheService inMemoryApplicationCacheService,
|
||||
IApplicationCacheServiceBusMessaging serviceBusMessaging)
|
||||
: IApplicationCacheService
|
||||
{
|
||||
public async Task<IDictionary<Guid, OrganizationAbility>> GetOrganizationAbilitiesAsync()
|
||||
{
|
||||
if (featureService.IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache))
|
||||
{
|
||||
return await vNextInMemoryApplicationCacheService.GetOrganizationAbilitiesAsync();
|
||||
}
|
||||
|
||||
return await inMemoryApplicationCacheService.GetOrganizationAbilitiesAsync();
|
||||
}
|
||||
|
||||
public async Task<OrganizationAbility?> GetOrganizationAbilityAsync(Guid orgId)
|
||||
{
|
||||
if (featureService.IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache))
|
||||
{
|
||||
return await vNextInMemoryApplicationCacheService.GetOrganizationAbilityAsync(orgId);
|
||||
}
|
||||
return await inMemoryApplicationCacheService.GetOrganizationAbilityAsync(orgId);
|
||||
}
|
||||
|
||||
public async Task<IDictionary<Guid, ProviderAbility>> GetProviderAbilitiesAsync()
|
||||
{
|
||||
if (featureService.IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache))
|
||||
{
|
||||
return await vNextInMemoryApplicationCacheService.GetProviderAbilitiesAsync();
|
||||
}
|
||||
return await inMemoryApplicationCacheService.GetProviderAbilitiesAsync();
|
||||
}
|
||||
|
||||
public async Task UpsertOrganizationAbilityAsync(Organization organization)
|
||||
{
|
||||
if (featureService.IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache))
|
||||
{
|
||||
await vNextInMemoryApplicationCacheService.UpsertOrganizationAbilityAsync(organization);
|
||||
await serviceBusMessaging.NotifyOrganizationAbilityUpsertedAsync(organization);
|
||||
}
|
||||
else
|
||||
{
|
||||
await inMemoryApplicationCacheService.UpsertOrganizationAbilityAsync(organization);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task UpsertProviderAbilityAsync(Provider provider)
|
||||
{
|
||||
if (featureService.IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache))
|
||||
{
|
||||
await vNextInMemoryApplicationCacheService.UpsertProviderAbilityAsync(provider);
|
||||
}
|
||||
else
|
||||
{
|
||||
await inMemoryApplicationCacheService.UpsertProviderAbilityAsync(provider);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task DeleteOrganizationAbilityAsync(Guid organizationId)
|
||||
{
|
||||
if (featureService.IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache))
|
||||
{
|
||||
await vNextInMemoryApplicationCacheService.DeleteOrganizationAbilityAsync(organizationId);
|
||||
await serviceBusMessaging.NotifyOrganizationAbilityDeletedAsync(organizationId);
|
||||
}
|
||||
else
|
||||
{
|
||||
await inMemoryApplicationCacheService.DeleteOrganizationAbilityAsync(organizationId);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task DeleteProviderAbilityAsync(Guid providerId)
|
||||
{
|
||||
if (featureService.IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache))
|
||||
{
|
||||
await vNextInMemoryApplicationCacheService.DeleteProviderAbilityAsync(providerId);
|
||||
await serviceBusMessaging.NotifyProviderAbilityDeletedAsync(providerId);
|
||||
}
|
||||
else
|
||||
{
|
||||
await inMemoryApplicationCacheService.DeleteProviderAbilityAsync(providerId);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public async Task BaseUpsertOrganizationAbilityAsync(Organization organization)
|
||||
{
|
||||
if (featureService.IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache))
|
||||
{
|
||||
await vNextInMemoryApplicationCacheService.UpsertOrganizationAbilityAsync(organization);
|
||||
}
|
||||
else
|
||||
{
|
||||
// NOTE: This is a temporary workaround InMemoryServiceBusApplicationCacheService legacy implementation.
|
||||
// Avoid using this approach in new code.
|
||||
if (inMemoryApplicationCacheService is InMemoryServiceBusApplicationCacheService serviceBusCache)
|
||||
{
|
||||
await serviceBusCache.BaseUpsertOrganizationAbilityAsync(organization);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException($"Expected {nameof(inMemoryApplicationCacheService)} to be of type {nameof(InMemoryServiceBusApplicationCacheService)}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task BaseDeleteOrganizationAbilityAsync(Guid organizationId)
|
||||
{
|
||||
if (featureService.IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache))
|
||||
{
|
||||
await vNextInMemoryApplicationCacheService.DeleteOrganizationAbilityAsync(organizationId);
|
||||
}
|
||||
else
|
||||
{
|
||||
// NOTE: This is a temporary workaround InMemoryServiceBusApplicationCacheService legacy implementation.
|
||||
// Avoid using this approach in new code.
|
||||
if (inMemoryApplicationCacheService is InMemoryServiceBusApplicationCacheService serviceBusCache)
|
||||
{
|
||||
await serviceBusCache.BaseDeleteOrganizationAbilityAsync(organizationId);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException($"Expected {nameof(inMemoryApplicationCacheService)} to be of type {nameof(InMemoryServiceBusApplicationCacheService)}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
// FIXME: Update this file to be null safe and then delete the line below
|
||||
#nullable disable
|
||||
|
||||
using Bit.Core.AdminConsole.AbilitiesCache;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Models.Data.Provider;
|
||||
@@ -10,7 +11,7 @@ using Bit.Core.Repositories;
|
||||
|
||||
namespace Bit.Core.Services;
|
||||
|
||||
public class InMemoryApplicationCacheService : IApplicationCacheService
|
||||
public class InMemoryApplicationCacheService : IVCurrentInMemoryApplicationCacheService
|
||||
{
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
private readonly IProviderRepository _providerRepository;
|
||||
|
||||
@@ -8,9 +8,8 @@ using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Core.Services;
|
||||
|
||||
public class InMemoryServiceBusApplicationCacheService : InMemoryApplicationCacheService, IApplicationCacheService
|
||||
public class InMemoryServiceBusApplicationCacheService : InMemoryApplicationCacheService
|
||||
{
|
||||
private readonly ServiceBusClient _serviceBusClient;
|
||||
private readonly ServiceBusSender _topicMessageSender;
|
||||
private readonly string _subName;
|
||||
|
||||
@@ -21,7 +20,7 @@ public class InMemoryServiceBusApplicationCacheService : InMemoryApplicationCach
|
||||
: base(organizationRepository, providerRepository)
|
||||
{
|
||||
_subName = CoreHelpers.GetApplicationCacheServiceBusSubscriptionName(globalSettings);
|
||||
_serviceBusClient = new ServiceBusClient(globalSettings.ServiceBus.ConnectionString);
|
||||
|
||||
_topicMessageSender = new ServiceBusClient(globalSettings.ServiceBus.ConnectionString).CreateSender(globalSettings.ServiceBus.ApplicationCacheTopicName);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
using System.Globalization;
|
||||
using Bit.Core.AdminConsole.AbilitiesCache;
|
||||
using Bit.Core.Auth.IdentityServer;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Services.Implementations;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.SharedWeb.Utilities;
|
||||
@@ -52,13 +54,18 @@ public class Startup
|
||||
// Services
|
||||
var usingServiceBusAppCache = CoreHelpers.SettingHasValue(globalSettings.ServiceBus.ConnectionString) &&
|
||||
CoreHelpers.SettingHasValue(globalSettings.ServiceBus.ApplicationCacheTopicName);
|
||||
services.AddScoped<IApplicationCacheService, FeatureRoutedCacheService>();
|
||||
services.AddSingleton<IVNextInMemoryApplicationCacheService, VNextInMemoryApplicationCacheService>();
|
||||
|
||||
if (usingServiceBusAppCache)
|
||||
{
|
||||
services.AddSingleton<IApplicationCacheService, InMemoryServiceBusApplicationCacheService>();
|
||||
services.AddSingleton<IVCurrentInMemoryApplicationCacheService, InMemoryServiceBusApplicationCacheService>();
|
||||
services.AddSingleton<IApplicationCacheServiceBusMessaging, ServiceBusApplicationCacheMessaging>();
|
||||
}
|
||||
else
|
||||
{
|
||||
services.AddSingleton<IApplicationCacheService, InMemoryApplicationCacheService>();
|
||||
services.AddSingleton<IVCurrentInMemoryApplicationCacheService, InMemoryApplicationCacheService>();
|
||||
services.AddSingleton<IApplicationCacheServiceBusMessaging, NoOpApplicationCacheMessaging>();
|
||||
}
|
||||
|
||||
services.AddEventWriteServices(globalSettings);
|
||||
|
||||
@@ -6,6 +6,7 @@ using System.Reflection;
|
||||
using System.Security.Claims;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using AspNetCoreRateLimit;
|
||||
using Bit.Core.AdminConsole.AbilitiesCache;
|
||||
using Bit.Core.AdminConsole.Models.Business.Tokenables;
|
||||
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
@@ -41,6 +42,7 @@ using Bit.Core.Resources;
|
||||
using Bit.Core.SecretsManager.Repositories;
|
||||
using Bit.Core.SecretsManager.Repositories.Noop;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Services.Implementations;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Tokens;
|
||||
using Bit.Core.Tools.ImportFeatures;
|
||||
@@ -247,14 +249,19 @@ public static class ServiceCollectionExtensions
|
||||
services.AddOptionality();
|
||||
services.AddTokenizers();
|
||||
|
||||
services.AddSingleton<IVNextInMemoryApplicationCacheService, VNextInMemoryApplicationCacheService>();
|
||||
services.AddScoped<IApplicationCacheService, FeatureRoutedCacheService>();
|
||||
|
||||
if (CoreHelpers.SettingHasValue(globalSettings.ServiceBus.ConnectionString) &&
|
||||
CoreHelpers.SettingHasValue(globalSettings.ServiceBus.ApplicationCacheTopicName))
|
||||
{
|
||||
services.AddSingleton<IApplicationCacheService, InMemoryServiceBusApplicationCacheService>();
|
||||
services.AddSingleton<IVCurrentInMemoryApplicationCacheService, InMemoryServiceBusApplicationCacheService>();
|
||||
services.AddSingleton<IApplicationCacheServiceBusMessaging, ServiceBusApplicationCacheMessaging>();
|
||||
}
|
||||
else
|
||||
{
|
||||
services.AddSingleton<IApplicationCacheService, InMemoryApplicationCacheService>();
|
||||
services.AddSingleton<IVCurrentInMemoryApplicationCacheService, InMemoryApplicationCacheService>();
|
||||
services.AddSingleton<IApplicationCacheServiceBusMessaging, NoOpApplicationCacheMessaging>();
|
||||
}
|
||||
|
||||
var awsConfigured = CoreHelpers.SettingHasValue(globalSettings.Amazon?.AccessKeySecret);
|
||||
|
||||
@@ -0,0 +1,403 @@
|
||||
using System.Collections.Concurrent;
|
||||
using Bit.Core.AdminConsole.AbilitiesCache;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Models.Data.Provider;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Models.Data.Organizations;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.AdminConsole.AbilitiesCache;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class VNextInMemoryApplicationCacheServiceTests
|
||||
|
||||
{
|
||||
[Theory, BitAutoData]
|
||||
public async Task GetOrganizationAbilitiesAsync_FirstCall_LoadsFromRepository(
|
||||
ICollection<OrganizationAbility> organizationAbilities,
|
||||
SutProvider<VNextInMemoryApplicationCacheService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetManyAbilitiesAsync()
|
||||
.Returns(organizationAbilities);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.GetOrganizationAbilitiesAsync();
|
||||
|
||||
// Assert
|
||||
Assert.IsType<ConcurrentDictionary<Guid, OrganizationAbility>>(result);
|
||||
Assert.Equal(organizationAbilities.Count, result.Count);
|
||||
foreach (var ability in organizationAbilities)
|
||||
{
|
||||
Assert.True(result.TryGetValue(ability.Id, out var actualAbility));
|
||||
Assert.Equal(ability, actualAbility);
|
||||
}
|
||||
await sutProvider.GetDependency<IOrganizationRepository>().Received(1).GetManyAbilitiesAsync();
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task GetOrganizationAbilitiesAsync_SecondCall_UsesCachedValue(
|
||||
List<OrganizationAbility> organizationAbilities,
|
||||
SutProvider<VNextInMemoryApplicationCacheService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetManyAbilitiesAsync()
|
||||
.Returns(organizationAbilities);
|
||||
|
||||
// Act
|
||||
var firstCall = await sutProvider.Sut.GetOrganizationAbilitiesAsync();
|
||||
var secondCall = await sutProvider.Sut.GetOrganizationAbilitiesAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Same(firstCall, secondCall);
|
||||
await sutProvider.GetDependency<IOrganizationRepository>().Received(1).GetManyAbilitiesAsync();
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task GetOrganizationAbilityAsync_ExistingId_ReturnsAbility(
|
||||
List<OrganizationAbility> organizationAbilities,
|
||||
SutProvider<VNextInMemoryApplicationCacheService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var targetAbility = organizationAbilities.First();
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetManyAbilitiesAsync()
|
||||
.Returns(organizationAbilities);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.GetOrganizationAbilityAsync(targetAbility.Id);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(targetAbility, result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task GetOrganizationAbilityAsync_NonExistingId_ReturnsNull(
|
||||
List<OrganizationAbility> organizationAbilities,
|
||||
Guid nonExistingId,
|
||||
SutProvider<VNextInMemoryApplicationCacheService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetManyAbilitiesAsync()
|
||||
.Returns(organizationAbilities);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.GetOrganizationAbilityAsync(nonExistingId);
|
||||
|
||||
// Assert
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task GetProviderAbilitiesAsync_FirstCall_LoadsFromRepository(
|
||||
List<ProviderAbility> providerAbilities,
|
||||
SutProvider<VNextInMemoryApplicationCacheService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IProviderRepository>()
|
||||
.GetManyAbilitiesAsync()
|
||||
.Returns(providerAbilities);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.GetProviderAbilitiesAsync();
|
||||
|
||||
// Assert
|
||||
Assert.IsType<ConcurrentDictionary<Guid, ProviderAbility>>(result);
|
||||
Assert.Equal(providerAbilities.Count, result.Count);
|
||||
foreach (var ability in providerAbilities)
|
||||
{
|
||||
Assert.True(result.TryGetValue(ability.Id, out var actualAbility));
|
||||
Assert.Equal(ability, actualAbility);
|
||||
}
|
||||
await sutProvider.GetDependency<IProviderRepository>().Received(1).GetManyAbilitiesAsync();
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task GetProviderAbilitiesAsync_SecondCall_UsesCachedValue(
|
||||
List<ProviderAbility> providerAbilities,
|
||||
SutProvider<VNextInMemoryApplicationCacheService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IProviderRepository>()
|
||||
.GetManyAbilitiesAsync()
|
||||
.Returns(providerAbilities);
|
||||
|
||||
// Act
|
||||
var firstCall = await sutProvider.Sut.GetProviderAbilitiesAsync();
|
||||
var secondCall = await sutProvider.Sut.GetProviderAbilitiesAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Same(firstCall, secondCall);
|
||||
await sutProvider.GetDependency<IProviderRepository>().Received(1).GetManyAbilitiesAsync();
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpsertOrganizationAbilityAsync_NewOrganization_AddsToCache(
|
||||
Organization organization,
|
||||
List<OrganizationAbility> existingAbilities,
|
||||
SutProvider<VNextInMemoryApplicationCacheService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetManyAbilitiesAsync()
|
||||
.Returns(existingAbilities);
|
||||
await sutProvider.Sut.GetOrganizationAbilitiesAsync();
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.UpsertOrganizationAbilityAsync(organization);
|
||||
|
||||
// Assert
|
||||
var result = await sutProvider.Sut.GetOrganizationAbilitiesAsync();
|
||||
Assert.True(result.ContainsKey(organization.Id));
|
||||
Assert.Equal(organization.Id, result[organization.Id].Id);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpsertOrganizationAbilityAsync_ExistingOrganization_UpdatesCache(
|
||||
Organization organization,
|
||||
List<OrganizationAbility> existingAbilities,
|
||||
SutProvider<VNextInMemoryApplicationCacheService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
existingAbilities.Add(new OrganizationAbility { Id = organization.Id });
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetManyAbilitiesAsync()
|
||||
.Returns(existingAbilities);
|
||||
await sutProvider.Sut.GetOrganizationAbilitiesAsync();
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.UpsertOrganizationAbilityAsync(organization);
|
||||
|
||||
// Assert
|
||||
var result = await sutProvider.Sut.GetOrganizationAbilitiesAsync();
|
||||
Assert.True(result.ContainsKey(organization.Id));
|
||||
Assert.Equal(organization.Id, result[organization.Id].Id);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpsertProviderAbilityAsync_NewProvider_AddsToCache(
|
||||
Provider provider,
|
||||
List<ProviderAbility> existingAbilities,
|
||||
SutProvider<VNextInMemoryApplicationCacheService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IProviderRepository>()
|
||||
.GetManyAbilitiesAsync()
|
||||
.Returns(existingAbilities);
|
||||
await sutProvider.Sut.GetProviderAbilitiesAsync();
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.UpsertProviderAbilityAsync(provider);
|
||||
|
||||
// Assert
|
||||
var result = await sutProvider.Sut.GetProviderAbilitiesAsync();
|
||||
Assert.True(result.ContainsKey(provider.Id));
|
||||
Assert.Equal(provider.Id, result[provider.Id].Id);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task DeleteOrganizationAbilityAsync_ExistingId_RemovesFromCache(
|
||||
List<OrganizationAbility> organizationAbilities,
|
||||
SutProvider<VNextInMemoryApplicationCacheService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var targetAbility = organizationAbilities.First();
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetManyAbilitiesAsync()
|
||||
.Returns(organizationAbilities);
|
||||
await sutProvider.Sut.GetOrganizationAbilitiesAsync();
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.DeleteOrganizationAbilityAsync(targetAbility.Id);
|
||||
|
||||
// Assert
|
||||
var result = await sutProvider.Sut.GetOrganizationAbilitiesAsync();
|
||||
Assert.False(result.ContainsKey(targetAbility.Id));
|
||||
}
|
||||
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task DeleteProviderAbilityAsync_ExistingId_RemovesFromCache(
|
||||
List<ProviderAbility> providerAbilities,
|
||||
SutProvider<VNextInMemoryApplicationCacheService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var targetAbility = providerAbilities.First();
|
||||
sutProvider.GetDependency<IProviderRepository>()
|
||||
.GetManyAbilitiesAsync()
|
||||
.Returns(providerAbilities);
|
||||
await sutProvider.Sut.GetProviderAbilitiesAsync();
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.DeleteProviderAbilityAsync(targetAbility.Id);
|
||||
|
||||
// Assert
|
||||
var result = await sutProvider.Sut.GetProviderAbilitiesAsync();
|
||||
Assert.False(result.ContainsKey(targetAbility.Id));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ConcurrentAccess_GetOrganizationAbilities_ThreadSafe(
|
||||
List<OrganizationAbility> organizationAbilities,
|
||||
SutProvider<VNextInMemoryApplicationCacheService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetManyAbilitiesAsync()
|
||||
.Returns(organizationAbilities);
|
||||
|
||||
var results = new ConcurrentBag<IDictionary<Guid, OrganizationAbility>>();
|
||||
|
||||
const int iterationCount = 100;
|
||||
|
||||
|
||||
// Act
|
||||
await Parallel.ForEachAsync(
|
||||
Enumerable.Range(0, iterationCount),
|
||||
async (_, _) =>
|
||||
{
|
||||
var result = await sutProvider.Sut.GetOrganizationAbilitiesAsync();
|
||||
results.Add(result);
|
||||
});
|
||||
|
||||
// Assert
|
||||
var firstCall = results.First();
|
||||
Assert.Equal(iterationCount, results.Count);
|
||||
Assert.All(results, result => Assert.Same(firstCall, result));
|
||||
await sutProvider.GetDependency<IOrganizationRepository>().Received(1).GetManyAbilitiesAsync();
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task GetOrganizationAbilitiesAsync_AfterRefreshInterval_RefreshesFromRepository(
|
||||
List<OrganizationAbility> organizationAbilities,
|
||||
List<OrganizationAbility> updatedAbilities)
|
||||
{
|
||||
// Arrange
|
||||
var sutProvider = new SutProvider<VNextInMemoryApplicationCacheService>()
|
||||
.WithFakeTimeProvider()
|
||||
.Create();
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetManyAbilitiesAsync()
|
||||
.Returns(organizationAbilities, updatedAbilities);
|
||||
|
||||
var firstCall = await sutProvider.Sut.GetOrganizationAbilitiesAsync();
|
||||
|
||||
const int pastIntervalInMinutes = 11;
|
||||
SimulateTimeLapseAfterFirstCall(sutProvider, pastIntervalInMinutes);
|
||||
|
||||
// Act
|
||||
var secondCall = await sutProvider.Sut.GetOrganizationAbilitiesAsync();
|
||||
|
||||
// Assert
|
||||
Assert.NotSame(firstCall, secondCall);
|
||||
Assert.Equal(updatedAbilities.Count, secondCall.Count);
|
||||
await sutProvider.GetDependency<IOrganizationRepository>().Received(2).GetManyAbilitiesAsync();
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task GetProviderAbilitiesAsync_AfterRefreshInterval_RefreshesFromRepository(
|
||||
List<ProviderAbility> providerAbilities,
|
||||
List<ProviderAbility> updatedAbilities)
|
||||
{
|
||||
// Arrange
|
||||
var sutProvider = new SutProvider<VNextInMemoryApplicationCacheService>()
|
||||
.WithFakeTimeProvider()
|
||||
.Create();
|
||||
|
||||
sutProvider.GetDependency<IProviderRepository>()
|
||||
.GetManyAbilitiesAsync()
|
||||
.Returns(providerAbilities, updatedAbilities);
|
||||
|
||||
var firstCall = await sutProvider.Sut.GetProviderAbilitiesAsync();
|
||||
const int pastIntervalMinutes = 15;
|
||||
SimulateTimeLapseAfterFirstCall(sutProvider, pastIntervalMinutes);
|
||||
|
||||
// Act
|
||||
var secondCall = await sutProvider.Sut.GetProviderAbilitiesAsync();
|
||||
|
||||
// Assert
|
||||
Assert.NotSame(firstCall, secondCall);
|
||||
Assert.Equal(updatedAbilities.Count, secondCall.Count);
|
||||
await sutProvider.GetDependency<IProviderRepository>().Received(2).GetManyAbilitiesAsync();
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> WhenCacheIsWithinIntervalTestCases =>
|
||||
[
|
||||
[5, 1],
|
||||
[10, 1],
|
||||
];
|
||||
|
||||
[Theory]
|
||||
[BitMemberAutoData(nameof(WhenCacheIsWithinIntervalTestCases))]
|
||||
public async Task GetOrganizationAbilitiesAsync_WhenCacheIsWithinInterval(
|
||||
int pastIntervalInMinutes,
|
||||
int expectCacheHit,
|
||||
List<OrganizationAbility> organizationAbilities)
|
||||
{
|
||||
// Arrange
|
||||
var sutProvider = new SutProvider<VNextInMemoryApplicationCacheService>()
|
||||
.WithFakeTimeProvider()
|
||||
.Create();
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetManyAbilitiesAsync()
|
||||
.Returns(organizationAbilities);
|
||||
|
||||
var firstCall = await sutProvider.Sut.GetOrganizationAbilitiesAsync();
|
||||
|
||||
SimulateTimeLapseAfterFirstCall(sutProvider, pastIntervalInMinutes);
|
||||
|
||||
// Act
|
||||
var secondCall = await sutProvider.Sut.GetOrganizationAbilitiesAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Same(firstCall, secondCall);
|
||||
Assert.Equal(organizationAbilities.Count, secondCall.Count);
|
||||
await sutProvider.GetDependency<IOrganizationRepository>().Received(expectCacheHit).GetManyAbilitiesAsync();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitMemberAutoData(nameof(WhenCacheIsWithinIntervalTestCases))]
|
||||
public async Task GetProviderAbilitiesAsync_WhenCacheIsWithinInterval(
|
||||
int pastIntervalInMinutes,
|
||||
int expectCacheHit,
|
||||
List<ProviderAbility> providerAbilities)
|
||||
{
|
||||
// Arrange
|
||||
var sutProvider = new SutProvider<VNextInMemoryApplicationCacheService>()
|
||||
.WithFakeTimeProvider()
|
||||
.Create();
|
||||
|
||||
sutProvider.GetDependency<IProviderRepository>()
|
||||
.GetManyAbilitiesAsync()
|
||||
.Returns(providerAbilities);
|
||||
|
||||
var firstCall = await sutProvider.Sut.GetProviderAbilitiesAsync();
|
||||
|
||||
SimulateTimeLapseAfterFirstCall(sutProvider, pastIntervalInMinutes);
|
||||
|
||||
// Act
|
||||
var secondCall = await sutProvider.Sut.GetProviderAbilitiesAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Same(firstCall, secondCall);
|
||||
Assert.Equal(providerAbilities.Count, secondCall.Count);
|
||||
await sutProvider.GetDependency<IProviderRepository>().Received(expectCacheHit).GetManyAbilitiesAsync();
|
||||
}
|
||||
|
||||
private static void SimulateTimeLapseAfterFirstCall(SutProvider<VNextInMemoryApplicationCacheService> sutProvider, int pastIntervalInMinutes) =>
|
||||
sutProvider
|
||||
.GetDependency<FakeTimeProvider>()
|
||||
.Advance(TimeSpan.FromMinutes(pastIntervalInMinutes));
|
||||
|
||||
}
|
||||
@@ -0,0 +1,541 @@
|
||||
using Bit.Core.AdminConsole.AbilitiesCache;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Models.Data.Provider;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Models.Data.Organizations;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Services.Implementations;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
using GlobalSettings = Bit.Core.Settings.GlobalSettings;
|
||||
|
||||
namespace Bit.Core.Test.Services.Implementations;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class FeatureRoutedCacheServiceTests
|
||||
{
|
||||
[Theory, BitAutoData]
|
||||
public async Task GetOrganizationAbilitiesAsync_WhenFeatureIsEnabled_ReturnsFromVNextService(
|
||||
SutProvider<FeatureRoutedCacheService> sutProvider,
|
||||
IDictionary<Guid, OrganizationAbility> expectedResult)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<IVNextInMemoryApplicationCacheService>()
|
||||
.GetOrganizationAbilitiesAsync()
|
||||
.Returns(expectedResult);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.GetOrganizationAbilitiesAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedResult, result);
|
||||
await sutProvider.GetDependency<IVNextInMemoryApplicationCacheService>()
|
||||
.Received(1)
|
||||
.GetOrganizationAbilitiesAsync();
|
||||
|
||||
await sutProvider.GetDependency<IVCurrentInMemoryApplicationCacheService>()
|
||||
.DidNotReceive()
|
||||
.GetOrganizationAbilitiesAsync();
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task GetOrganizationAbilitiesAsync_WhenFeatureIsDisabled_ReturnsFromInMemoryService(
|
||||
SutProvider<FeatureRoutedCacheService> sutProvider,
|
||||
IDictionary<Guid, OrganizationAbility> expectedResult)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache)
|
||||
.Returns(false);
|
||||
sutProvider.GetDependency<IVCurrentInMemoryApplicationCacheService>()
|
||||
.GetOrganizationAbilitiesAsync()
|
||||
.Returns(expectedResult);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.GetOrganizationAbilitiesAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedResult, result);
|
||||
await sutProvider.GetDependency<IVCurrentInMemoryApplicationCacheService>()
|
||||
.Received(1)
|
||||
.GetOrganizationAbilitiesAsync();
|
||||
|
||||
await sutProvider.GetDependency<IVNextInMemoryApplicationCacheService>()
|
||||
.DidNotReceive()
|
||||
.GetOrganizationAbilitiesAsync();
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task GetOrganizationAbilityAsync_WhenFeatureIsEnabled_ReturnsFromVNextService(
|
||||
SutProvider<FeatureRoutedCacheService> sutProvider,
|
||||
Guid orgId,
|
||||
OrganizationAbility expectedResult)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<IVNextInMemoryApplicationCacheService>()
|
||||
.GetOrganizationAbilityAsync(orgId)
|
||||
.Returns(expectedResult);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.GetOrganizationAbilityAsync(orgId);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedResult, result);
|
||||
await sutProvider.GetDependency<IVNextInMemoryApplicationCacheService>()
|
||||
.Received(1)
|
||||
.GetOrganizationAbilityAsync(orgId);
|
||||
|
||||
await sutProvider.GetDependency<IVCurrentInMemoryApplicationCacheService>()
|
||||
.DidNotReceive()
|
||||
.GetOrganizationAbilityAsync(orgId);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task GetOrganizationAbilityAsync_WhenFeatureIsDisabled_ReturnsFromInMemoryService(
|
||||
SutProvider<FeatureRoutedCacheService> sutProvider,
|
||||
Guid orgId,
|
||||
OrganizationAbility expectedResult)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache)
|
||||
.Returns(false);
|
||||
|
||||
sutProvider.GetDependency<IVCurrentInMemoryApplicationCacheService>()
|
||||
.GetOrganizationAbilityAsync(orgId)
|
||||
.Returns(expectedResult);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.GetOrganizationAbilityAsync(orgId);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedResult, result);
|
||||
await sutProvider.GetDependency<IVCurrentInMemoryApplicationCacheService>()
|
||||
.Received(1)
|
||||
.GetOrganizationAbilityAsync(orgId);
|
||||
|
||||
await sutProvider.GetDependency<IVNextInMemoryApplicationCacheService>()
|
||||
.DidNotReceive()
|
||||
.GetOrganizationAbilityAsync(orgId);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task GetProviderAbilitiesAsync_WhenFeatureIsEnabled_ReturnsFromVNextService(
|
||||
SutProvider<FeatureRoutedCacheService> sutProvider,
|
||||
IDictionary<Guid, ProviderAbility> expectedResult)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<IVNextInMemoryApplicationCacheService>()
|
||||
.GetProviderAbilitiesAsync()
|
||||
.Returns(expectedResult);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.GetProviderAbilitiesAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedResult, result);
|
||||
await sutProvider.GetDependency<IVNextInMemoryApplicationCacheService>()
|
||||
.Received(1)
|
||||
.GetProviderAbilitiesAsync();
|
||||
|
||||
await sutProvider.GetDependency<IVCurrentInMemoryApplicationCacheService>()
|
||||
.DidNotReceive()
|
||||
.GetProviderAbilitiesAsync();
|
||||
}
|
||||
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task GetProviderAbilitiesAsync_WhenFeatureIsDisabled_ReturnsFromInMemoryService(
|
||||
SutProvider<FeatureRoutedCacheService> sutProvider,
|
||||
IDictionary<Guid, ProviderAbility> expectedResult)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache)
|
||||
.Returns(false);
|
||||
sutProvider.GetDependency<IVCurrentInMemoryApplicationCacheService>()
|
||||
.GetProviderAbilitiesAsync()
|
||||
.Returns(expectedResult);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.GetProviderAbilitiesAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedResult, result);
|
||||
await sutProvider.GetDependency<IVCurrentInMemoryApplicationCacheService>()
|
||||
.Received(1)
|
||||
.GetProviderAbilitiesAsync();
|
||||
|
||||
await sutProvider.GetDependency<IVNextInMemoryApplicationCacheService>()
|
||||
.DidNotReceive()
|
||||
.GetProviderAbilitiesAsync();
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpsertOrganizationAbilityAsync_WhenFeatureIsEnabled_CallsVNextService(
|
||||
SutProvider<FeatureRoutedCacheService> sutProvider,
|
||||
Organization organization)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache)
|
||||
.Returns(true);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.UpsertOrganizationAbilityAsync(organization);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IVNextInMemoryApplicationCacheService>()
|
||||
.Received(1)
|
||||
.UpsertOrganizationAbilityAsync(organization);
|
||||
|
||||
await sutProvider.GetDependency<IVCurrentInMemoryApplicationCacheService>()
|
||||
.DidNotReceive()
|
||||
.GetProviderAbilitiesAsync();
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpsertOrganizationAbilityAsync_WhenFeatureIsDisabled_CallsInMemoryService(
|
||||
SutProvider<FeatureRoutedCacheService> sutProvider,
|
||||
Organization organization)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache)
|
||||
.Returns(false);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.UpsertOrganizationAbilityAsync(organization);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IVCurrentInMemoryApplicationCacheService>()
|
||||
.Received(1)
|
||||
.UpsertOrganizationAbilityAsync(organization);
|
||||
|
||||
await sutProvider.GetDependency<IVNextInMemoryApplicationCacheService>()
|
||||
.DidNotReceive()
|
||||
.GetProviderAbilitiesAsync();
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpsertProviderAbilityAsync_WhenFeatureIsEnabled_CallsVNextService(
|
||||
SutProvider<FeatureRoutedCacheService> sutProvider,
|
||||
Provider provider)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache)
|
||||
.Returns(true);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.UpsertProviderAbilityAsync(provider);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IVNextInMemoryApplicationCacheService>()
|
||||
.Received(1)
|
||||
.UpsertProviderAbilityAsync(provider);
|
||||
|
||||
await sutProvider.GetDependency<IVCurrentInMemoryApplicationCacheService>()
|
||||
.DidNotReceive()
|
||||
.UpsertProviderAbilityAsync(provider);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpsertProviderAbilityAsync_WhenFeatureIsDisabled_CallsInMemoryService(
|
||||
SutProvider<FeatureRoutedCacheService> sutProvider,
|
||||
Provider provider)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache)
|
||||
.Returns(false);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.UpsertProviderAbilityAsync(provider);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IVCurrentInMemoryApplicationCacheService>()
|
||||
.Received(1)
|
||||
.UpsertProviderAbilityAsync(provider);
|
||||
|
||||
await sutProvider.GetDependency<IVNextInMemoryApplicationCacheService>()
|
||||
.DidNotReceive()
|
||||
.UpsertProviderAbilityAsync(provider);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task DeleteOrganizationAbilityAsync_WhenFeatureIsEnabled_CallsVNextService(
|
||||
SutProvider<FeatureRoutedCacheService> sutProvider,
|
||||
Guid organizationId)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache)
|
||||
.Returns(true);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.DeleteOrganizationAbilityAsync(organizationId);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IVNextInMemoryApplicationCacheService>()
|
||||
.Received(1)
|
||||
.DeleteOrganizationAbilityAsync(organizationId);
|
||||
|
||||
await sutProvider.GetDependency<IVCurrentInMemoryApplicationCacheService>()
|
||||
.DidNotReceive()
|
||||
.DeleteOrganizationAbilityAsync(organizationId);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task DeleteOrganizationAbilityAsync_WhenFeatureIsDisabled_CallsInMemoryService(
|
||||
SutProvider<FeatureRoutedCacheService> sutProvider,
|
||||
Guid organizationId)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache)
|
||||
.Returns(false);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.DeleteOrganizationAbilityAsync(organizationId);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IVCurrentInMemoryApplicationCacheService>()
|
||||
.Received(1)
|
||||
.DeleteOrganizationAbilityAsync(organizationId);
|
||||
|
||||
await sutProvider.GetDependency<IVNextInMemoryApplicationCacheService>()
|
||||
.DidNotReceive()
|
||||
.DeleteOrganizationAbilityAsync(organizationId);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task DeleteProviderAbilityAsync_WhenFeatureIsEnabled_CallsVNextService(
|
||||
SutProvider<FeatureRoutedCacheService> sutProvider,
|
||||
Guid providerId)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache)
|
||||
.Returns(true);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.DeleteProviderAbilityAsync(providerId);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IVNextInMemoryApplicationCacheService>()
|
||||
.Received(1)
|
||||
.DeleteProviderAbilityAsync(providerId);
|
||||
|
||||
await sutProvider.GetDependency<IVCurrentInMemoryApplicationCacheService>()
|
||||
.DidNotReceive()
|
||||
.DeleteProviderAbilityAsync(providerId);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task DeleteProviderAbilityAsync_WhenFeatureIsDisabled_CallsInMemoryService(
|
||||
SutProvider<FeatureRoutedCacheService> sutProvider,
|
||||
Guid providerId)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache)
|
||||
.Returns(false);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.DeleteProviderAbilityAsync(providerId);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IVCurrentInMemoryApplicationCacheService>()
|
||||
.Received(1)
|
||||
.DeleteProviderAbilityAsync(providerId);
|
||||
|
||||
await sutProvider.GetDependency<IVNextInMemoryApplicationCacheService>()
|
||||
.DidNotReceive()
|
||||
.DeleteProviderAbilityAsync(providerId);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task BaseUpsertOrganizationAbilityAsync_WhenFeatureIsEnabled_CallsVNextService(
|
||||
SutProvider<FeatureRoutedCacheService> sutProvider,
|
||||
Organization organization)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache)
|
||||
.Returns(true);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.BaseUpsertOrganizationAbilityAsync(organization);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IVNextInMemoryApplicationCacheService>()
|
||||
.Received(1)
|
||||
.UpsertOrganizationAbilityAsync(organization);
|
||||
|
||||
await sutProvider.GetDependency<IVCurrentInMemoryApplicationCacheService>()
|
||||
.DidNotReceive()
|
||||
.UpsertOrganizationAbilityAsync(organization);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task BaseUpsertOrganizationAbilityAsync_WhenFeatureIsDisabled_CallsServiceBusCache(
|
||||
Organization organization)
|
||||
{
|
||||
// Arrange
|
||||
var featureService = Substitute.For<IFeatureService>();
|
||||
|
||||
var currentCacheService = CreateCurrentCacheMockService();
|
||||
|
||||
featureService
|
||||
.IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache)
|
||||
.Returns(false);
|
||||
|
||||
var sutProvider = Substitute.For<FeatureRoutedCacheService>(
|
||||
featureService,
|
||||
Substitute.For<IVNextInMemoryApplicationCacheService>(),
|
||||
currentCacheService,
|
||||
Substitute.For<IApplicationCacheServiceBusMessaging>());
|
||||
|
||||
// Act
|
||||
await sutProvider.BaseUpsertOrganizationAbilityAsync(organization);
|
||||
|
||||
// Assert
|
||||
await currentCacheService
|
||||
.Received(1)
|
||||
.BaseUpsertOrganizationAbilityAsync(organization);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Our SUT is using a method that is not part of the IVCurrentInMemoryApplicationCacheService,
|
||||
/// so AutoFixture’s auto-created mock won’t work.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private static InMemoryServiceBusApplicationCacheService CreateCurrentCacheMockService()
|
||||
{
|
||||
var currentCacheService = Substitute.For<InMemoryServiceBusApplicationCacheService>(
|
||||
Substitute.For<IOrganizationRepository>(),
|
||||
Substitute.For<IProviderRepository>(),
|
||||
new GlobalSettings
|
||||
{
|
||||
ProjectName = "BitwardenTest",
|
||||
ServiceBus = new GlobalSettings.ServiceBusSettings
|
||||
{
|
||||
ConnectionString = "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=test;SharedAccessKey=test",
|
||||
ApplicationCacheTopicName = "test-topic",
|
||||
ApplicationCacheSubscriptionName = "test-subscription"
|
||||
}
|
||||
});
|
||||
return currentCacheService;
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task BaseUpsertOrganizationAbilityAsync_WhenFeatureIsDisabled_AndServiceIsNotServiceBusCache_ThrowsException(
|
||||
SutProvider<FeatureRoutedCacheService> sutProvider,
|
||||
Organization organization)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache)
|
||||
.Returns(false);
|
||||
|
||||
// Act
|
||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(
|
||||
() => sutProvider.Sut.BaseUpsertOrganizationAbilityAsync(organization));
|
||||
|
||||
// Assert
|
||||
Assert.Equal(
|
||||
ExpectedErrorMessage,
|
||||
ex.Message);
|
||||
}
|
||||
|
||||
private static string ExpectedErrorMessage
|
||||
{
|
||||
get => "Expected inMemoryApplicationCacheService to be of type InMemoryServiceBusApplicationCacheService";
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task BaseDeleteOrganizationAbilityAsync_WhenFeatureIsEnabled_CallsVNextService(
|
||||
SutProvider<FeatureRoutedCacheService> sutProvider,
|
||||
Guid organizationId)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache)
|
||||
.Returns(true);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.BaseDeleteOrganizationAbilityAsync(organizationId);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IVNextInMemoryApplicationCacheService>()
|
||||
.Received(1)
|
||||
.DeleteOrganizationAbilityAsync(organizationId);
|
||||
|
||||
await sutProvider.GetDependency<IVCurrentInMemoryApplicationCacheService>()
|
||||
.DidNotReceive()
|
||||
.DeleteOrganizationAbilityAsync(organizationId);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task BaseDeleteOrganizationAbilityAsync_WhenFeatureIsDisabled_CallsServiceBusCache(
|
||||
Guid organizationId)
|
||||
{
|
||||
// Arrange
|
||||
var featureService = Substitute.For<IFeatureService>();
|
||||
|
||||
var currentCacheService = CreateCurrentCacheMockService();
|
||||
|
||||
featureService
|
||||
.IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache)
|
||||
.Returns(false);
|
||||
|
||||
var sutProvider = Substitute.For<FeatureRoutedCacheService>(
|
||||
featureService,
|
||||
Substitute.For<IVNextInMemoryApplicationCacheService>(),
|
||||
currentCacheService,
|
||||
Substitute.For<IApplicationCacheServiceBusMessaging>());
|
||||
|
||||
// Act
|
||||
await sutProvider.BaseDeleteOrganizationAbilityAsync(organizationId);
|
||||
|
||||
// Assert
|
||||
await currentCacheService
|
||||
.Received(1)
|
||||
.BaseDeleteOrganizationAbilityAsync(organizationId);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task
|
||||
BaseDeleteOrganizationAbilityAsync_WhenFeatureIsDisabled_AndServiceIsNotServiceBusCache_ThrowsException(
|
||||
SutProvider<FeatureRoutedCacheService> sutProvider,
|
||||
Guid organizationId)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache)
|
||||
.Returns(false);
|
||||
|
||||
// Act
|
||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() =>
|
||||
sutProvider.Sut.BaseDeleteOrganizationAbilityAsync(organizationId));
|
||||
|
||||
// Assert
|
||||
Assert.Equal(
|
||||
ExpectedErrorMessage,
|
||||
ex.Message);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user