1
0
mirror of https://github.com/bitwarden/server synced 2026-02-27 09:53:42 +00:00

Ac/pm 32125/remove ivnextinmemoryapplicationcacheservice (#7067)

This commit is contained in:
Jimmy Vo
2026-02-26 09:30:06 -05:00
committed by GitHub
parent 5eb7bd28f6
commit 47b60ef6cd
9 changed files with 81 additions and 1060 deletions

View File

@@ -1,19 +0,0 @@
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);
}

View File

@@ -1,137 +0,0 @@
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;
}
}
}

View File

@@ -140,7 +140,6 @@ public static class FeatureFlagKeys
public const string CreateDefaultLocation = "pm-19467-create-default-location";
public const string AutomaticConfirmUsers = "pm-19934-auto-confirm-organization-users";
public const string ScimRevokeV2 = "pm-32394-scim-revoke-put-v2";
public const string PM23845_VNextApplicationCache = "pm-24957-refactor-memory-application-cache";
public const string DefaultUserCollectionRestore = "pm-30883-my-items-restored-users";
public const string RefactorMembersComponent = "pm-29503-refactor-members-inheritance";
public const string BulkReinviteUI = "pm-28416-bulk-reinvite-ux-improvements";

View File

@@ -7,10 +7,12 @@ namespace Bit.Core.Services;
public interface IApplicationCacheService
{
[Obsolete("We are transitioning to a new cache pattern. Please consult the Admin Console team before using.", false)]
Task<IDictionary<Guid, OrganizationAbility>> GetOrganizationAbilitiesAsync();
#nullable enable
Task<OrganizationAbility?> GetOrganizationAbilityAsync(Guid orgId);
#nullable disable
[Obsolete("We are transitioning to a new cache pattern. Please consult the Admin Console team before using.", false)]
Task<IDictionary<Guid, ProviderAbility>> GetProviderAbilitiesAsync();
Task UpsertOrganizationAbilityAsync(Organization organization);
Task UpsertProviderAbilityAsync(Provider provider);

View File

@@ -6,147 +6,52 @@ 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)
IVCurrentInMemoryApplicationCacheService inMemoryApplicationCacheService)
: IApplicationCacheService
{
public async Task<IDictionary<Guid, OrganizationAbility>> GetOrganizationAbilitiesAsync()
{
if (featureService.IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache))
{
return await vNextInMemoryApplicationCacheService.GetOrganizationAbilitiesAsync();
}
public Task<IDictionary<Guid, OrganizationAbility>> GetOrganizationAbilitiesAsync() =>
inMemoryApplicationCacheService.GetOrganizationAbilitiesAsync();
return await inMemoryApplicationCacheService.GetOrganizationAbilitiesAsync();
}
public Task<OrganizationAbility?> GetOrganizationAbilityAsync(Guid orgId) =>
inMemoryApplicationCacheService.GetOrganizationAbilityAsync(orgId);
public async Task<OrganizationAbility?> GetOrganizationAbilityAsync(Guid orgId)
{
if (featureService.IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache))
{
return await vNextInMemoryApplicationCacheService.GetOrganizationAbilityAsync(orgId);
}
return await inMemoryApplicationCacheService.GetOrganizationAbilityAsync(orgId);
}
public Task<IDictionary<Guid, ProviderAbility>> GetProviderAbilitiesAsync() =>
inMemoryApplicationCacheService.GetProviderAbilitiesAsync();
public async Task<IDictionary<Guid, ProviderAbility>> GetProviderAbilitiesAsync()
{
if (featureService.IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache))
{
return await vNextInMemoryApplicationCacheService.GetProviderAbilitiesAsync();
}
return await inMemoryApplicationCacheService.GetProviderAbilitiesAsync();
}
public Task UpsertOrganizationAbilityAsync(Organization organization) =>
inMemoryApplicationCacheService.UpsertOrganizationAbilityAsync(organization);
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 Task UpsertProviderAbilityAsync(Provider provider) =>
inMemoryApplicationCacheService.UpsertProviderAbilityAsync(provider);
public async Task UpsertProviderAbilityAsync(Provider provider)
{
if (featureService.IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache))
{
await vNextInMemoryApplicationCacheService.UpsertProviderAbilityAsync(provider);
}
else
{
await inMemoryApplicationCacheService.UpsertProviderAbilityAsync(provider);
}
}
public Task DeleteOrganizationAbilityAsync(Guid organizationId) =>
inMemoryApplicationCacheService.DeleteOrganizationAbilityAsync(organizationId);
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 Task DeleteProviderAbilityAsync(Guid providerId) =>
inMemoryApplicationCacheService.DeleteProviderAbilityAsync(providerId);
public async Task BaseUpsertOrganizationAbilityAsync(Organization organization)
{
if (featureService.IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache))
if (inMemoryApplicationCacheService is InMemoryServiceBusApplicationCacheService serviceBusCache)
{
await vNextInMemoryApplicationCacheService.UpsertOrganizationAbilityAsync(organization);
await serviceBusCache.BaseUpsertOrganizationAbilityAsync(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)}");
}
throw new InvalidOperationException($"Expected {nameof(inMemoryApplicationCacheService)} to be of type {nameof(InMemoryServiceBusApplicationCacheService)}");
}
}
public async Task BaseDeleteOrganizationAbilityAsync(Guid organizationId)
{
if (featureService.IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache))
if (inMemoryApplicationCacheService is InMemoryServiceBusApplicationCacheService serviceBusCache)
{
await vNextInMemoryApplicationCacheService.DeleteOrganizationAbilityAsync(organizationId);
await serviceBusCache.BaseDeleteOrganizationAbilityAsync(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)}");
}
throw new InvalidOperationException($"Expected {nameof(inMemoryApplicationCacheService)} to be of type {nameof(InMemoryServiceBusApplicationCacheService)}");
}
}
}

View File

@@ -56,17 +56,14 @@ public class Startup
var usingServiceBusAppCache = CoreHelpers.SettingHasValue(globalSettings.ServiceBus.ConnectionString) &&
CoreHelpers.SettingHasValue(globalSettings.ServiceBus.ApplicationCacheTopicName);
services.AddScoped<IApplicationCacheService, FeatureRoutedCacheService>();
services.AddSingleton<IVNextInMemoryApplicationCacheService, VNextInMemoryApplicationCacheService>();
if (usingServiceBusAppCache)
{
services.AddSingleton<IVCurrentInMemoryApplicationCacheService, InMemoryServiceBusApplicationCacheService>();
services.AddSingleton<IApplicationCacheServiceBusMessaging, ServiceBusApplicationCacheMessaging>();
}
else
{
services.AddSingleton<IVCurrentInMemoryApplicationCacheService, InMemoryApplicationCacheService>();
services.AddSingleton<IApplicationCacheServiceBusMessaging, NoOpApplicationCacheMessaging>();
}
services.AddEventWriteServices(globalSettings);

View File

@@ -292,19 +292,16 @@ 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<IVCurrentInMemoryApplicationCacheService, InMemoryServiceBusApplicationCacheService>();
services.AddSingleton<IApplicationCacheServiceBusMessaging, ServiceBusApplicationCacheMessaging>();
}
else
{
services.AddSingleton<IVCurrentInMemoryApplicationCacheService, InMemoryApplicationCacheService>();
services.AddSingleton<IApplicationCacheServiceBusMessaging, NoOpApplicationCacheMessaging>();
}
var awsConfigured = CoreHelpers.SettingHasValue(globalSettings.Amazon?.AccessKeySecret);

View File

@@ -1,403 +0,0 @@
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));
}

View File

@@ -19,41 +19,11 @@ namespace Bit.Core.Test.Services.Implementations;
public class FeatureRoutedCacheServiceTests
{
[Theory, BitAutoData]
public async Task GetOrganizationAbilitiesAsync_WhenFeatureIsEnabled_ReturnsFromVNextService(
public async Task GetOrganizationAbilitiesAsync_ReturnsFromInMemoryService(
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);
@@ -66,51 +36,15 @@ public class FeatureRoutedCacheServiceTests
await sutProvider.GetDependency<IVCurrentInMemoryApplicationCacheService>()
.Received(1)
.GetOrganizationAbilitiesAsync();
await sutProvider.GetDependency<IVNextInMemoryApplicationCacheService>()
.DidNotReceive()
.GetOrganizationAbilitiesAsync();
}
[Theory, BitAutoData]
public async Task GetOrganizationAbilityAsync_WhenFeatureIsEnabled_ReturnsFromVNextService(
public async Task GetOrganizationAbilityAsync_ReturnsFromInMemoryService(
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);
@@ -123,49 +57,14 @@ public class FeatureRoutedCacheServiceTests
await sutProvider.GetDependency<IVCurrentInMemoryApplicationCacheService>()
.Received(1)
.GetOrganizationAbilityAsync(orgId);
await sutProvider.GetDependency<IVNextInMemoryApplicationCacheService>()
.DidNotReceive()
.GetOrganizationAbilityAsync(orgId);
}
[Theory, BitAutoData]
public async Task GetProviderAbilitiesAsync_WhenFeatureIsEnabled_ReturnsFromVNextService(
public async Task GetProviderAbilitiesAsync_ReturnsFromInMemoryService(
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);
@@ -178,45 +77,13 @@ public class FeatureRoutedCacheServiceTests
await sutProvider.GetDependency<IVCurrentInMemoryApplicationCacheService>()
.Received(1)
.GetProviderAbilitiesAsync();
await sutProvider.GetDependency<IVNextInMemoryApplicationCacheService>()
.DidNotReceive()
.GetProviderAbilitiesAsync();
}
[Theory, BitAutoData]
public async Task UpsertOrganizationAbilityAsync_WhenFeatureIsEnabled_CallsVNextService(
public async Task UpsertOrganizationAbilityAsync_CallsInMemoryService(
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);
@@ -224,45 +91,13 @@ public class FeatureRoutedCacheServiceTests
await sutProvider.GetDependency<IVCurrentInMemoryApplicationCacheService>()
.Received(1)
.UpsertOrganizationAbilityAsync(organization);
await sutProvider.GetDependency<IVNextInMemoryApplicationCacheService>()
.DidNotReceive()
.GetProviderAbilitiesAsync();
}
[Theory, BitAutoData]
public async Task UpsertProviderAbilityAsync_WhenFeatureIsEnabled_CallsVNextService(
public async Task UpsertProviderAbilityAsync_CallsInMemoryService(
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);
@@ -270,45 +105,13 @@ public class FeatureRoutedCacheServiceTests
await sutProvider.GetDependency<IVCurrentInMemoryApplicationCacheService>()
.Received(1)
.UpsertProviderAbilityAsync(provider);
await sutProvider.GetDependency<IVNextInMemoryApplicationCacheService>()
.DidNotReceive()
.UpsertProviderAbilityAsync(provider);
}
[Theory, BitAutoData]
public async Task DeleteOrganizationAbilityAsync_WhenFeatureIsEnabled_CallsVNextService(
public async Task DeleteOrganizationAbilityAsync_CallsInMemoryService(
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);
@@ -316,45 +119,13 @@ public class FeatureRoutedCacheServiceTests
await sutProvider.GetDependency<IVCurrentInMemoryApplicationCacheService>()
.Received(1)
.DeleteOrganizationAbilityAsync(organizationId);
await sutProvider.GetDependency<IVNextInMemoryApplicationCacheService>()
.DidNotReceive()
.DeleteOrganizationAbilityAsync(organizationId);
}
[Theory, BitAutoData]
public async Task DeleteProviderAbilityAsync_WhenFeatureIsEnabled_CallsVNextService(
public async Task DeleteProviderAbilityAsync_CallsInMemoryService(
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);
@@ -362,56 +133,18 @@ public class FeatureRoutedCacheServiceTests
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,
public async Task BaseUpsertOrganizationAbilityAsync_CallsServiceBusCache(
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>());
var sut = new FeatureRoutedCacheService(currentCacheService);
// Act
await sutProvider.BaseUpsertOrganizationAbilityAsync(organization);
await sut.BaseUpsertOrganizationAbilityAsync(organization);
// Assert
await currentCacheService
@@ -419,14 +152,56 @@ public class FeatureRoutedCacheServiceTests
.BaseUpsertOrganizationAbilityAsync(organization);
}
[Theory, BitAutoData]
public async Task BaseUpsertOrganizationAbilityAsync_WhenServiceIsNotServiceBusCache_ThrowsException(
SutProvider<FeatureRoutedCacheService> sutProvider,
Organization organization)
{
// Act
var ex = await Assert.ThrowsAsync<InvalidOperationException>(
() => sutProvider.Sut.BaseUpsertOrganizationAbilityAsync(organization));
// Assert
Assert.Equal(ExpectedErrorMessage, ex.Message);
}
[Theory, BitAutoData]
public async Task BaseDeleteOrganizationAbilityAsync_CallsServiceBusCache(
Guid organizationId)
{
// Arrange
var currentCacheService = CreateCurrentCacheMockService();
var sut = new FeatureRoutedCacheService(currentCacheService);
// Act
await sut.BaseDeleteOrganizationAbilityAsync(organizationId);
// Assert
await currentCacheService
.Received(1)
.BaseDeleteOrganizationAbilityAsync(organizationId);
}
[Theory, BitAutoData]
public async Task BaseDeleteOrganizationAbilityAsync_WhenServiceIsNotServiceBusCache_ThrowsException(
SutProvider<FeatureRoutedCacheService> sutProvider,
Guid organizationId)
{
// Act
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() =>
sutProvider.Sut.BaseDeleteOrganizationAbilityAsync(organizationId));
// Assert
Assert.Equal(ExpectedErrorMessage, ex.Message);
}
/// <summary>
/// Our SUT is using a method that is not part of the IVCurrentInMemoryApplicationCacheService,
/// so AutoFixtures auto-created mock wont work.
/// Our SUT uses a method that is not part of IVCurrentInMemoryApplicationCacheService,
/// so AutoFixture's auto-created mock won't work.
/// </summary>
/// <returns></returns>
private static InMemoryServiceBusApplicationCacheService CreateCurrentCacheMockService()
{
var currentCacheService = Substitute.For<InMemoryServiceBusApplicationCacheService>(
return Substitute.For<InMemoryServiceBusApplicationCacheService>(
Substitute.For<IOrganizationRepository>(),
Substitute.For<IProviderRepository>(),
new GlobalSettings
@@ -439,103 +214,8 @@ public class FeatureRoutedCacheServiceTests
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);
}
private static string ExpectedErrorMessage =>
"Expected inMemoryApplicationCacheService to be of type InMemoryServiceBusApplicationCacheService";
}