diff --git a/src/Core/AdminConsole/AbilitiesCache/IVNextInMemoryApplicationCacheService.cs b/src/Core/AdminConsole/AbilitiesCache/IVNextInMemoryApplicationCacheService.cs deleted file mode 100644 index 57109ba6a7..0000000000 --- a/src/Core/AdminConsole/AbilitiesCache/IVNextInMemoryApplicationCacheService.cs +++ /dev/null @@ -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> GetOrganizationAbilitiesAsync(); -#nullable enable - Task GetOrganizationAbilityAsync(Guid orgId); -#nullable disable - Task> GetProviderAbilitiesAsync(); - Task UpsertOrganizationAbilityAsync(Organization organization); - Task UpsertProviderAbilityAsync(Provider provider); - Task DeleteOrganizationAbilityAsync(Guid organizationId); - Task DeleteProviderAbilityAsync(Guid providerId); -} diff --git a/src/Core/AdminConsole/AbilitiesCache/VNextInMemoryApplicationCacheService.cs b/src/Core/AdminConsole/AbilitiesCache/VNextInMemoryApplicationCacheService.cs deleted file mode 100644 index 409074e3b2..0000000000 --- a/src/Core/AdminConsole/AbilitiesCache/VNextInMemoryApplicationCacheService.cs +++ /dev/null @@ -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 _orgAbilities = new(); - private readonly SemaphoreSlim _orgInitLock = new(1, 1); - private DateTimeOffset _lastOrgAbilityRefresh = DateTimeOffset.MinValue; - - private ConcurrentDictionary _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> GetOrganizationAbilitiesAsync() - { - await InitOrganizationAbilitiesAsync(); - return _orgAbilities; - } - - public async Task GetOrganizationAbilityAsync(Guid organizationId) - { - (await GetOrganizationAbilitiesAsync()) - .TryGetValue(organizationId, out var organizationAbility); - return organizationAbility; - } - - public virtual async Task> 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( - dict => _orgAbilities = dict, - () => _lastOrgAbilityRefresh, - dt => _lastOrgAbilityRefresh = dt, - _orgInitLock, - async () => await organizationRepository.GetManyAbilitiesAsync(), - _refreshInterval, - ability => ability.Id); - - private async Task InitProviderAbilitiesAsync() => - await InitAbilitiesAsync( - dict => _providerAbilities = dict, - () => _lastProviderAbilityRefresh, - dateTime => _lastProviderAbilityRefresh = dateTime, - _providerInitLock, - async () => await providerRepository.GetManyAbilitiesAsync(), - _refreshInterval, - ability => ability.Id); - - - private async Task InitAbilitiesAsync( - Action> setCache, - Func getLastRefresh, - Action setLastRefresh, - SemaphoreSlim @lock, - Func>> fetchFunc, - TimeSpan refreshInterval, - Func getId) - { - if (SkipRefresh()) - { - return; - } - - await @lock.WaitAsync(); - try - { - if (SkipRefresh()) - { - return; - } - - var sources = await fetchFunc(); - var abilities = new ConcurrentDictionary( - sources.ToDictionary(getId)); - setCache(abilities); - setLastRefresh(timeProvider.GetUtcNow()); - } - finally - { - @lock.Release(); - } - - bool SkipRefresh() - { - return timeProvider.GetUtcNow() - getLastRefresh() <= refreshInterval; - } - } -} diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 2eef68cb42..a0e8482d66 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -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"; diff --git a/src/Core/Services/IApplicationCacheService.cs b/src/Core/Services/IApplicationCacheService.cs index 1236335d75..23a58b48f3 100644 --- a/src/Core/Services/IApplicationCacheService.cs +++ b/src/Core/Services/IApplicationCacheService.cs @@ -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> GetOrganizationAbilitiesAsync(); #nullable enable Task GetOrganizationAbilityAsync(Guid orgId); #nullable disable + [Obsolete("We are transitioning to a new cache pattern. Please consult the Admin Console team before using.", false)] Task> GetProviderAbilitiesAsync(); Task UpsertOrganizationAbilityAsync(Organization organization); Task UpsertProviderAbilityAsync(Provider provider); diff --git a/src/Core/Services/Implementations/FeatureRoutedCacheService.cs b/src/Core/Services/Implementations/FeatureRoutedCacheService.cs index b6294a28f8..abd57a4f3a 100644 --- a/src/Core/Services/Implementations/FeatureRoutedCacheService.cs +++ b/src/Core/Services/Implementations/FeatureRoutedCacheService.cs @@ -6,147 +6,52 @@ using Bit.Core.Models.Data.Organizations; namespace Bit.Core.Services.Implementations; -/// -/// 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. -/// -/// -/// 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. -/// public class FeatureRoutedCacheService( - IFeatureService featureService, - IVNextInMemoryApplicationCacheService vNextInMemoryApplicationCacheService, - IVCurrentInMemoryApplicationCacheService inMemoryApplicationCacheService, - IApplicationCacheServiceBusMessaging serviceBusMessaging) + IVCurrentInMemoryApplicationCacheService inMemoryApplicationCacheService) : IApplicationCacheService { - public async Task> GetOrganizationAbilitiesAsync() - { - if (featureService.IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache)) - { - return await vNextInMemoryApplicationCacheService.GetOrganizationAbilitiesAsync(); - } + public Task> GetOrganizationAbilitiesAsync() => + inMemoryApplicationCacheService.GetOrganizationAbilitiesAsync(); - return await inMemoryApplicationCacheService.GetOrganizationAbilitiesAsync(); - } + public Task GetOrganizationAbilityAsync(Guid orgId) => + inMemoryApplicationCacheService.GetOrganizationAbilityAsync(orgId); - public async Task GetOrganizationAbilityAsync(Guid orgId) - { - if (featureService.IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache)) - { - return await vNextInMemoryApplicationCacheService.GetOrganizationAbilityAsync(orgId); - } - return await inMemoryApplicationCacheService.GetOrganizationAbilityAsync(orgId); - } + public Task> GetProviderAbilitiesAsync() => + inMemoryApplicationCacheService.GetProviderAbilitiesAsync(); - public async Task> 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)}"); } } } diff --git a/src/Events/Startup.cs b/src/Events/Startup.cs index d97d65c2ed..cf8534d134 100644 --- a/src/Events/Startup.cs +++ b/src/Events/Startup.cs @@ -56,17 +56,14 @@ public class Startup var usingServiceBusAppCache = CoreHelpers.SettingHasValue(globalSettings.ServiceBus.ConnectionString) && CoreHelpers.SettingHasValue(globalSettings.ServiceBus.ApplicationCacheTopicName); services.AddScoped(); - services.AddSingleton(); if (usingServiceBusAppCache) { services.AddSingleton(); - services.AddSingleton(); } else { services.AddSingleton(); - services.AddSingleton(); } services.AddEventWriteServices(globalSettings); diff --git a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs index 988e884813..42f5d9e655 100644 --- a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs +++ b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs @@ -292,19 +292,16 @@ public static class ServiceCollectionExtensions services.AddOptionality(); services.AddTokenizers(); - services.AddSingleton(); services.AddScoped(); if (CoreHelpers.SettingHasValue(globalSettings.ServiceBus.ConnectionString) && CoreHelpers.SettingHasValue(globalSettings.ServiceBus.ApplicationCacheTopicName)) { services.AddSingleton(); - services.AddSingleton(); } else { services.AddSingleton(); - services.AddSingleton(); } var awsConfigured = CoreHelpers.SettingHasValue(globalSettings.Amazon?.AccessKeySecret); diff --git a/test/Core.Test/AdminConsole/AbilitiesCache/VNextInMemoryApplicationCacheServiceTests.cs b/test/Core.Test/AdminConsole/AbilitiesCache/VNextInMemoryApplicationCacheServiceTests.cs deleted file mode 100644 index afd3dccda3..0000000000 --- a/test/Core.Test/AdminConsole/AbilitiesCache/VNextInMemoryApplicationCacheServiceTests.cs +++ /dev/null @@ -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 organizationAbilities, - SutProvider sutProvider) - { - // Arrange - sutProvider.GetDependency() - .GetManyAbilitiesAsync() - .Returns(organizationAbilities); - - // Act - var result = await sutProvider.Sut.GetOrganizationAbilitiesAsync(); - - // Assert - Assert.IsType>(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().Received(1).GetManyAbilitiesAsync(); - } - - [Theory, BitAutoData] - public async Task GetOrganizationAbilitiesAsync_SecondCall_UsesCachedValue( - List organizationAbilities, - SutProvider sutProvider) - { - // Arrange - sutProvider.GetDependency() - .GetManyAbilitiesAsync() - .Returns(organizationAbilities); - - // Act - var firstCall = await sutProvider.Sut.GetOrganizationAbilitiesAsync(); - var secondCall = await sutProvider.Sut.GetOrganizationAbilitiesAsync(); - - // Assert - Assert.Same(firstCall, secondCall); - await sutProvider.GetDependency().Received(1).GetManyAbilitiesAsync(); - } - - [Theory, BitAutoData] - public async Task GetOrganizationAbilityAsync_ExistingId_ReturnsAbility( - List organizationAbilities, - SutProvider sutProvider) - { - // Arrange - var targetAbility = organizationAbilities.First(); - sutProvider.GetDependency() - .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 organizationAbilities, - Guid nonExistingId, - SutProvider sutProvider) - { - // Arrange - sutProvider.GetDependency() - .GetManyAbilitiesAsync() - .Returns(organizationAbilities); - - // Act - var result = await sutProvider.Sut.GetOrganizationAbilityAsync(nonExistingId); - - // Assert - Assert.Null(result); - } - - [Theory, BitAutoData] - public async Task GetProviderAbilitiesAsync_FirstCall_LoadsFromRepository( - List providerAbilities, - SutProvider sutProvider) - { - // Arrange - sutProvider.GetDependency() - .GetManyAbilitiesAsync() - .Returns(providerAbilities); - - // Act - var result = await sutProvider.Sut.GetProviderAbilitiesAsync(); - - // Assert - Assert.IsType>(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().Received(1).GetManyAbilitiesAsync(); - } - - [Theory, BitAutoData] - public async Task GetProviderAbilitiesAsync_SecondCall_UsesCachedValue( - List providerAbilities, - SutProvider sutProvider) - { - // Arrange - sutProvider.GetDependency() - .GetManyAbilitiesAsync() - .Returns(providerAbilities); - - // Act - var firstCall = await sutProvider.Sut.GetProviderAbilitiesAsync(); - var secondCall = await sutProvider.Sut.GetProviderAbilitiesAsync(); - - // Assert - Assert.Same(firstCall, secondCall); - await sutProvider.GetDependency().Received(1).GetManyAbilitiesAsync(); - } - - [Theory, BitAutoData] - public async Task UpsertOrganizationAbilityAsync_NewOrganization_AddsToCache( - Organization organization, - List existingAbilities, - SutProvider sutProvider) - { - // Arrange - sutProvider.GetDependency() - .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 existingAbilities, - SutProvider sutProvider) - { - // Arrange - existingAbilities.Add(new OrganizationAbility { Id = organization.Id }); - sutProvider.GetDependency() - .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 existingAbilities, - SutProvider sutProvider) - { - // Arrange - sutProvider.GetDependency() - .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 organizationAbilities, - SutProvider sutProvider) - { - // Arrange - var targetAbility = organizationAbilities.First(); - sutProvider.GetDependency() - .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 providerAbilities, - SutProvider sutProvider) - { - // Arrange - var targetAbility = providerAbilities.First(); - sutProvider.GetDependency() - .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 organizationAbilities, - SutProvider sutProvider) - { - // Arrange - sutProvider.GetDependency() - .GetManyAbilitiesAsync() - .Returns(organizationAbilities); - - var results = new ConcurrentBag>(); - - 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().Received(1).GetManyAbilitiesAsync(); - } - - [Theory, BitAutoData] - public async Task GetOrganizationAbilitiesAsync_AfterRefreshInterval_RefreshesFromRepository( - List organizationAbilities, - List updatedAbilities) - { - // Arrange - var sutProvider = new SutProvider() - .WithFakeTimeProvider() - .Create(); - - sutProvider.GetDependency() - .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().Received(2).GetManyAbilitiesAsync(); - } - - [Theory, BitAutoData] - public async Task GetProviderAbilitiesAsync_AfterRefreshInterval_RefreshesFromRepository( - List providerAbilities, - List updatedAbilities) - { - // Arrange - var sutProvider = new SutProvider() - .WithFakeTimeProvider() - .Create(); - - sutProvider.GetDependency() - .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().Received(2).GetManyAbilitiesAsync(); - } - - public static IEnumerable WhenCacheIsWithinIntervalTestCases => - [ - [5, 1], - [10, 1], - ]; - - [Theory] - [BitMemberAutoData(nameof(WhenCacheIsWithinIntervalTestCases))] - public async Task GetOrganizationAbilitiesAsync_WhenCacheIsWithinInterval( - int pastIntervalInMinutes, - int expectCacheHit, - List organizationAbilities) - { - // Arrange - var sutProvider = new SutProvider() - .WithFakeTimeProvider() - .Create(); - - sutProvider.GetDependency() - .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().Received(expectCacheHit).GetManyAbilitiesAsync(); - } - - [Theory] - [BitMemberAutoData(nameof(WhenCacheIsWithinIntervalTestCases))] - public async Task GetProviderAbilitiesAsync_WhenCacheIsWithinInterval( - int pastIntervalInMinutes, - int expectCacheHit, - List providerAbilities) - { - // Arrange - var sutProvider = new SutProvider() - .WithFakeTimeProvider() - .Create(); - - sutProvider.GetDependency() - .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().Received(expectCacheHit).GetManyAbilitiesAsync(); - } - - private static void SimulateTimeLapseAfterFirstCall(SutProvider sutProvider, int pastIntervalInMinutes) => - sutProvider - .GetDependency() - .Advance(TimeSpan.FromMinutes(pastIntervalInMinutes)); - -} diff --git a/test/Core.Test/Services/Implementations/FeatureRoutedCacheServiceTests.cs b/test/Core.Test/Services/Implementations/FeatureRoutedCacheServiceTests.cs index 3309f2bf23..01ca333ad7 100644 --- a/test/Core.Test/Services/Implementations/FeatureRoutedCacheServiceTests.cs +++ b/test/Core.Test/Services/Implementations/FeatureRoutedCacheServiceTests.cs @@ -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 sutProvider, IDictionary expectedResult) { // Arrange - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache) - .Returns(true); - sutProvider.GetDependency() - .GetOrganizationAbilitiesAsync() - .Returns(expectedResult); - - // Act - var result = await sutProvider.Sut.GetOrganizationAbilitiesAsync(); - - // Assert - Assert.Equal(expectedResult, result); - await sutProvider.GetDependency() - .Received(1) - .GetOrganizationAbilitiesAsync(); - - await sutProvider.GetDependency() - .DidNotReceive() - .GetOrganizationAbilitiesAsync(); - } - - [Theory, BitAutoData] - public async Task GetOrganizationAbilitiesAsync_WhenFeatureIsDisabled_ReturnsFromInMemoryService( - SutProvider sutProvider, - IDictionary expectedResult) - { - // Arrange - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache) - .Returns(false); sutProvider.GetDependency() .GetOrganizationAbilitiesAsync() .Returns(expectedResult); @@ -66,51 +36,15 @@ public class FeatureRoutedCacheServiceTests await sutProvider.GetDependency() .Received(1) .GetOrganizationAbilitiesAsync(); - - await sutProvider.GetDependency() - .DidNotReceive() - .GetOrganizationAbilitiesAsync(); } [Theory, BitAutoData] - public async Task GetOrganizationAbilityAsync_WhenFeatureIsEnabled_ReturnsFromVNextService( + public async Task GetOrganizationAbilityAsync_ReturnsFromInMemoryService( SutProvider sutProvider, Guid orgId, OrganizationAbility expectedResult) { // Arrange - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache) - .Returns(true); - sutProvider.GetDependency() - .GetOrganizationAbilityAsync(orgId) - .Returns(expectedResult); - - // Act - var result = await sutProvider.Sut.GetOrganizationAbilityAsync(orgId); - - // Assert - Assert.Equal(expectedResult, result); - await sutProvider.GetDependency() - .Received(1) - .GetOrganizationAbilityAsync(orgId); - - await sutProvider.GetDependency() - .DidNotReceive() - .GetOrganizationAbilityAsync(orgId); - } - - [Theory, BitAutoData] - public async Task GetOrganizationAbilityAsync_WhenFeatureIsDisabled_ReturnsFromInMemoryService( - SutProvider sutProvider, - Guid orgId, - OrganizationAbility expectedResult) - { - // Arrange - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache) - .Returns(false); - sutProvider.GetDependency() .GetOrganizationAbilityAsync(orgId) .Returns(expectedResult); @@ -123,49 +57,14 @@ public class FeatureRoutedCacheServiceTests await sutProvider.GetDependency() .Received(1) .GetOrganizationAbilityAsync(orgId); - - await sutProvider.GetDependency() - .DidNotReceive() - .GetOrganizationAbilityAsync(orgId); } [Theory, BitAutoData] - public async Task GetProviderAbilitiesAsync_WhenFeatureIsEnabled_ReturnsFromVNextService( + public async Task GetProviderAbilitiesAsync_ReturnsFromInMemoryService( SutProvider sutProvider, IDictionary expectedResult) { // Arrange - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache) - .Returns(true); - sutProvider.GetDependency() - .GetProviderAbilitiesAsync() - .Returns(expectedResult); - - // Act - var result = await sutProvider.Sut.GetProviderAbilitiesAsync(); - - // Assert - Assert.Equal(expectedResult, result); - await sutProvider.GetDependency() - .Received(1) - .GetProviderAbilitiesAsync(); - - await sutProvider.GetDependency() - .DidNotReceive() - .GetProviderAbilitiesAsync(); - } - - - [Theory, BitAutoData] - public async Task GetProviderAbilitiesAsync_WhenFeatureIsDisabled_ReturnsFromInMemoryService( - SutProvider sutProvider, - IDictionary expectedResult) - { - // Arrange - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache) - .Returns(false); sutProvider.GetDependency() .GetProviderAbilitiesAsync() .Returns(expectedResult); @@ -178,45 +77,13 @@ public class FeatureRoutedCacheServiceTests await sutProvider.GetDependency() .Received(1) .GetProviderAbilitiesAsync(); - - await sutProvider.GetDependency() - .DidNotReceive() - .GetProviderAbilitiesAsync(); } [Theory, BitAutoData] - public async Task UpsertOrganizationAbilityAsync_WhenFeatureIsEnabled_CallsVNextService( + public async Task UpsertOrganizationAbilityAsync_CallsInMemoryService( SutProvider sutProvider, Organization organization) { - // Arrange - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache) - .Returns(true); - - // Act - await sutProvider.Sut.UpsertOrganizationAbilityAsync(organization); - - // Assert - await sutProvider.GetDependency() - .Received(1) - .UpsertOrganizationAbilityAsync(organization); - - await sutProvider.GetDependency() - .DidNotReceive() - .GetProviderAbilitiesAsync(); - } - - [Theory, BitAutoData] - public async Task UpsertOrganizationAbilityAsync_WhenFeatureIsDisabled_CallsInMemoryService( - SutProvider sutProvider, - Organization organization) - { - // Arrange - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache) - .Returns(false); - // Act await sutProvider.Sut.UpsertOrganizationAbilityAsync(organization); @@ -224,45 +91,13 @@ public class FeatureRoutedCacheServiceTests await sutProvider.GetDependency() .Received(1) .UpsertOrganizationAbilityAsync(organization); - - await sutProvider.GetDependency() - .DidNotReceive() - .GetProviderAbilitiesAsync(); } [Theory, BitAutoData] - public async Task UpsertProviderAbilityAsync_WhenFeatureIsEnabled_CallsVNextService( + public async Task UpsertProviderAbilityAsync_CallsInMemoryService( SutProvider sutProvider, Provider provider) { - // Arrange - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache) - .Returns(true); - - // Act - await sutProvider.Sut.UpsertProviderAbilityAsync(provider); - - // Assert - await sutProvider.GetDependency() - .Received(1) - .UpsertProviderAbilityAsync(provider); - - await sutProvider.GetDependency() - .DidNotReceive() - .UpsertProviderAbilityAsync(provider); - } - - [Theory, BitAutoData] - public async Task UpsertProviderAbilityAsync_WhenFeatureIsDisabled_CallsInMemoryService( - SutProvider sutProvider, - Provider provider) - { - // Arrange - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache) - .Returns(false); - // Act await sutProvider.Sut.UpsertProviderAbilityAsync(provider); @@ -270,45 +105,13 @@ public class FeatureRoutedCacheServiceTests await sutProvider.GetDependency() .Received(1) .UpsertProviderAbilityAsync(provider); - - await sutProvider.GetDependency() - .DidNotReceive() - .UpsertProviderAbilityAsync(provider); } [Theory, BitAutoData] - public async Task DeleteOrganizationAbilityAsync_WhenFeatureIsEnabled_CallsVNextService( + public async Task DeleteOrganizationAbilityAsync_CallsInMemoryService( SutProvider sutProvider, Guid organizationId) { - // Arrange - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache) - .Returns(true); - - // Act - await sutProvider.Sut.DeleteOrganizationAbilityAsync(organizationId); - - // Assert - await sutProvider.GetDependency() - .Received(1) - .DeleteOrganizationAbilityAsync(organizationId); - - await sutProvider.GetDependency() - .DidNotReceive() - .DeleteOrganizationAbilityAsync(organizationId); - } - - [Theory, BitAutoData] - public async Task DeleteOrganizationAbilityAsync_WhenFeatureIsDisabled_CallsInMemoryService( - SutProvider sutProvider, - Guid organizationId) - { - // Arrange - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache) - .Returns(false); - // Act await sutProvider.Sut.DeleteOrganizationAbilityAsync(organizationId); @@ -316,45 +119,13 @@ public class FeatureRoutedCacheServiceTests await sutProvider.GetDependency() .Received(1) .DeleteOrganizationAbilityAsync(organizationId); - - await sutProvider.GetDependency() - .DidNotReceive() - .DeleteOrganizationAbilityAsync(organizationId); } [Theory, BitAutoData] - public async Task DeleteProviderAbilityAsync_WhenFeatureIsEnabled_CallsVNextService( + public async Task DeleteProviderAbilityAsync_CallsInMemoryService( SutProvider sutProvider, Guid providerId) { - // Arrange - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache) - .Returns(true); - - // Act - await sutProvider.Sut.DeleteProviderAbilityAsync(providerId); - - // Assert - await sutProvider.GetDependency() - .Received(1) - .DeleteProviderAbilityAsync(providerId); - - await sutProvider.GetDependency() - .DidNotReceive() - .DeleteProviderAbilityAsync(providerId); - } - - [Theory, BitAutoData] - public async Task DeleteProviderAbilityAsync_WhenFeatureIsDisabled_CallsInMemoryService( - SutProvider sutProvider, - Guid providerId) - { - // Arrange - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache) - .Returns(false); - // Act await sutProvider.Sut.DeleteProviderAbilityAsync(providerId); @@ -362,56 +133,18 @@ public class FeatureRoutedCacheServiceTests await sutProvider.GetDependency() .Received(1) .DeleteProviderAbilityAsync(providerId); - - await sutProvider.GetDependency() - .DidNotReceive() - .DeleteProviderAbilityAsync(providerId); } [Theory, BitAutoData] - public async Task BaseUpsertOrganizationAbilityAsync_WhenFeatureIsEnabled_CallsVNextService( - SutProvider sutProvider, + public async Task BaseUpsertOrganizationAbilityAsync_CallsServiceBusCache( Organization organization) { // Arrange - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache) - .Returns(true); - - // Act - await sutProvider.Sut.BaseUpsertOrganizationAbilityAsync(organization); - - // Assert - await sutProvider.GetDependency() - .Received(1) - .UpsertOrganizationAbilityAsync(organization); - - await sutProvider.GetDependency() - .DidNotReceive() - .UpsertOrganizationAbilityAsync(organization); - } - - [Theory, BitAutoData] - public async Task BaseUpsertOrganizationAbilityAsync_WhenFeatureIsDisabled_CallsServiceBusCache( - Organization organization) - { - // Arrange - var featureService = Substitute.For(); - var currentCacheService = CreateCurrentCacheMockService(); - - featureService - .IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache) - .Returns(false); - - var sutProvider = Substitute.For( - featureService, - Substitute.For(), - currentCacheService, - Substitute.For()); + 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 sutProvider, + Organization organization) + { + // Act + var ex = await Assert.ThrowsAsync( + () => 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 sutProvider, + Guid organizationId) + { + // Act + var ex = await Assert.ThrowsAsync(() => + sutProvider.Sut.BaseDeleteOrganizationAbilityAsync(organizationId)); + + // Assert + Assert.Equal(ExpectedErrorMessage, ex.Message); + } + /// - /// Our SUT is using a method that is not part of the IVCurrentInMemoryApplicationCacheService, - /// so AutoFixture’s auto-created mock won’t work. + /// Our SUT uses a method that is not part of IVCurrentInMemoryApplicationCacheService, + /// so AutoFixture's auto-created mock won't work. /// - /// private static InMemoryServiceBusApplicationCacheService CreateCurrentCacheMockService() { - var currentCacheService = Substitute.For( + return Substitute.For( Substitute.For(), Substitute.For(), new GlobalSettings @@ -439,103 +214,8 @@ public class FeatureRoutedCacheServiceTests ApplicationCacheSubscriptionName = "test-subscription" } }); - return currentCacheService; } - [Theory, BitAutoData] - public async Task BaseUpsertOrganizationAbilityAsync_WhenFeatureIsDisabled_AndServiceIsNotServiceBusCache_ThrowsException( - SutProvider sutProvider, - Organization organization) - { - // Arrange - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache) - .Returns(false); - - // Act - var ex = await Assert.ThrowsAsync( - () => 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 sutProvider, - Guid organizationId) - { - // Arrange - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache) - .Returns(true); - - // Act - await sutProvider.Sut.BaseDeleteOrganizationAbilityAsync(organizationId); - - // Assert - await sutProvider.GetDependency() - .Received(1) - .DeleteOrganizationAbilityAsync(organizationId); - - await sutProvider.GetDependency() - .DidNotReceive() - .DeleteOrganizationAbilityAsync(organizationId); - } - - [Theory, BitAutoData] - public async Task BaseDeleteOrganizationAbilityAsync_WhenFeatureIsDisabled_CallsServiceBusCache( - Guid organizationId) - { - // Arrange - var featureService = Substitute.For(); - - var currentCacheService = CreateCurrentCacheMockService(); - - featureService - .IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache) - .Returns(false); - - var sutProvider = Substitute.For( - featureService, - Substitute.For(), - currentCacheService, - Substitute.For()); - - // Act - await sutProvider.BaseDeleteOrganizationAbilityAsync(organizationId); - - // Assert - await currentCacheService - .Received(1) - .BaseDeleteOrganizationAbilityAsync(organizationId); - } - - [Theory, BitAutoData] - public async Task - BaseDeleteOrganizationAbilityAsync_WhenFeatureIsDisabled_AndServiceIsNotServiceBusCache_ThrowsException( - SutProvider sutProvider, - Guid organizationId) - { - // Arrange - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache) - .Returns(false); - - // Act - var ex = await Assert.ThrowsAsync(() => - sutProvider.Sut.BaseDeleteOrganizationAbilityAsync(organizationId)); - - // Assert - Assert.Equal( - ExpectedErrorMessage, - ex.Message); - } + private static string ExpectedErrorMessage => + "Expected inMemoryApplicationCacheService to be of type InMemoryServiceBusApplicationCacheService"; }