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_ReturnsFromInMemoryService( SutProvider sutProvider, IDictionary expectedResult) { // Arrange sutProvider.GetDependency() .GetOrganizationAbilitiesAsync() .Returns(expectedResult); // Act var result = await sutProvider.Sut.GetOrganizationAbilitiesAsync(); // Assert Assert.Equal(expectedResult, result); await sutProvider.GetDependency() .Received(1) .GetOrganizationAbilitiesAsync(); } [Theory, BitAutoData] public async Task GetOrganizationAbilityAsync_WhenFlagOff_ReturnsFromInMemoryService( SutProvider sutProvider, Guid orgId, OrganizationAbility expectedResult) { // Arrange sutProvider.GetDependency() .IsEnabled(FeatureFlagKeys.OrgAbilityExtendedCache) .Returns(false); 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() .DidNotReceiveWithAnyArgs() .GetOrganizationAbilityAsync(default); } [Theory, BitAutoData] public async Task GetOrganizationAbilityAsync_WhenFlagOn_ReturnsFromExtendedCacheService( SutProvider sutProvider, Guid orgId, OrganizationAbility expectedResult) { // Arrange sutProvider.GetDependency() .IsEnabled(FeatureFlagKeys.OrgAbilityExtendedCache) .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() .DidNotReceiveWithAnyArgs() .GetOrganizationAbilityAsync(default); } [Theory, BitAutoData] public async Task GetProviderAbilitiesAsync_ReturnsFromInMemoryService( SutProvider sutProvider, IDictionary expectedResult) { // Arrange sutProvider.GetDependency() .GetProviderAbilitiesAsync() .Returns(expectedResult); // Act var result = await sutProvider.Sut.GetProviderAbilitiesAsync(); // Assert Assert.Equal(expectedResult, result); await sutProvider.GetDependency() .Received(1) .GetProviderAbilitiesAsync(); } [Theory, BitAutoData] public async Task GetProviderAbilityAsync_WhenFeatureFlagEnabled_UsesExtendedCacheService( SutProvider sutProvider, Guid providerId, ProviderAbility expectedAbility) { // Arrange sutProvider.GetDependency() .IsEnabled(FeatureFlagKeys.ProviderAbilityExtendedCache) .Returns(true); sutProvider.GetDependency() .GetProviderAbilityAsync(providerId) .Returns(expectedAbility); // Act var result = await sutProvider.Sut.GetProviderAbilityAsync(providerId); // Assert Assert.Equal(expectedAbility, result); await sutProvider.GetDependency() .Received(1) .GetProviderAbilityAsync(providerId); await sutProvider.GetDependency() .DidNotReceive() .GetProviderAbilitiesAsync(); } [Theory, BitAutoData] public async Task GetProviderAbilityAsync_WhenFeatureFlagDisabled_UsesInMemoryService( SutProvider sutProvider, ProviderAbility providerAbility) { // Arrange sutProvider.GetDependency() .IsEnabled(FeatureFlagKeys.ProviderAbilityExtendedCache) .Returns(false); var allAbilities = new Dictionary { [providerAbility.Id] = providerAbility }; sutProvider.GetDependency() .GetProviderAbilitiesAsync() .Returns(allAbilities); // Act var result = await sutProvider.Sut.GetProviderAbilityAsync(providerAbility.Id); // Assert Assert.Equal(providerAbility, result); await sutProvider.GetDependency() .DidNotReceive() .GetProviderAbilityAsync(Arg.Any()); await sutProvider.GetDependency() .Received(1) .GetProviderAbilitiesAsync(); } [Theory, BitAutoData] public async Task UpsertProviderAbilityAsync_WhenFeatureFlagEnabled_UsesExtendedCacheService( SutProvider sutProvider, Provider provider) { // Arrange sutProvider.GetDependency() .IsEnabled(FeatureFlagKeys.ProviderAbilityExtendedCache) .Returns(true); // Act await sutProvider.Sut.UpsertProviderAbilityAsync(provider); // Assert await sutProvider.GetDependency() .Received(1) .UpsertProviderAbilityAsync(provider); await sutProvider.GetDependency() .DidNotReceive() .UpsertProviderAbilityAsync(Arg.Any()); } [Theory, BitAutoData] public async Task UpsertProviderAbilityAsync_WhenFeatureFlagDisabled_UsesInMemoryService( SutProvider sutProvider, Provider provider) { // Arrange sutProvider.GetDependency() .IsEnabled(FeatureFlagKeys.ProviderAbilityExtendedCache) .Returns(false); // Act await sutProvider.Sut.UpsertProviderAbilityAsync(provider); // Assert await sutProvider.GetDependency() .Received(1) .UpsertProviderAbilityAsync(provider); await sutProvider.GetDependency() .DidNotReceive() .UpsertProviderAbilityAsync(Arg.Any()); } [Theory, BitAutoData] public async Task DeleteProviderAbilityAsync_WhenFeatureFlagEnabled_UsesExtendedCacheService( SutProvider sutProvider, Guid providerId) { // Arrange sutProvider.GetDependency() .IsEnabled(FeatureFlagKeys.ProviderAbilityExtendedCache) .Returns(true); // Act await sutProvider.Sut.DeleteProviderAbilityAsync(providerId); // Assert await sutProvider.GetDependency() .Received(1) .DeleteProviderAbilityAsync(providerId); await sutProvider.GetDependency() .DidNotReceive() .DeleteProviderAbilityAsync(Arg.Any()); } [Theory, BitAutoData] public async Task DeleteProviderAbilityAsync_WhenFeatureFlagDisabled_UsesInMemoryService( SutProvider sutProvider, Guid providerId) { // Arrange sutProvider.GetDependency() .IsEnabled(FeatureFlagKeys.ProviderAbilityExtendedCache) .Returns(false); // Act await sutProvider.Sut.DeleteProviderAbilityAsync(providerId); // Assert await sutProvider.GetDependency() .Received(1) .DeleteProviderAbilityAsync(providerId); await sutProvider.GetDependency() .DidNotReceive() .DeleteProviderAbilityAsync(Arg.Any()); } [Theory, BitAutoData] public async Task GetProviderAbilitiesAsync_ReturnsOnlyMatchingAbilities( SutProvider sutProvider, ProviderAbility matchedAbility, ProviderAbility unmatchedAbility) { // Arrange var allAbilities = new Dictionary { [matchedAbility.Id] = matchedAbility, [unmatchedAbility.Id] = unmatchedAbility }; sutProvider.GetDependency() .GetProviderAbilitiesAsync() .Returns(allAbilities); // Act var result = await sutProvider.Sut.GetProviderAbilitiesAsync([matchedAbility.Id]); // Assert Assert.Single(result); Assert.Equal(matchedAbility, result[matchedAbility.Id]); } [Theory, BitAutoData] public async Task GetProviderAbilitiesAsync_WhenNoIdsMatched_ReturnsEmptyDictionary( SutProvider sutProvider, Guid missingProviderId) { // Arrange sutProvider.GetDependency() .GetProviderAbilitiesAsync() .Returns(new Dictionary()); // Act var result = await sutProvider.Sut.GetProviderAbilitiesAsync([missingProviderId]); // Assert Assert.Empty(result); } [Theory, BitAutoData] public async Task GetProviderAbilitiesAsync_WhenDuplicateIdsProvided_DoesNotThrowAndReturnsSingleEntry( SutProvider sutProvider, ProviderAbility ability) { // Arrange var allAbilities = new Dictionary { [ability.Id] = ability }; sutProvider.GetDependency() .GetProviderAbilitiesAsync() .Returns(allAbilities); // Act - passing the same ID twice simulates a provider with duplicate entries var result = await sutProvider.Sut.GetProviderAbilitiesAsync([ability.Id, ability.Id]); // Assert Assert.Single(result); Assert.Equal(ability, result[ability.Id]); } [Theory, BitAutoData] public async Task GetProviderAbilitiesAsync_WhenFlagOn_ReturnsFromExtendedCacheService( SutProvider sutProvider, ProviderAbility ability) { // Arrange var providerIds = new[] { ability.Id }; var expectedResult = new Dictionary { [ability.Id] = ability }; sutProvider.GetDependency() .IsEnabled(FeatureFlagKeys.ProviderAbilityExtendedCache) .Returns(true); sutProvider.GetDependency() .GetProviderAbilitiesAsync(providerIds) .Returns(expectedResult); // Act var result = await sutProvider.Sut.GetProviderAbilitiesAsync(providerIds); // Assert Assert.Single(result); Assert.Equal(ability, result[ability.Id]); await sutProvider.GetDependency() .Received(1) .GetProviderAbilitiesAsync(providerIds); await sutProvider.GetDependency() .DidNotReceiveWithAnyArgs() .GetProviderAbilitiesAsync(); } [Theory, BitAutoData] public async Task GetOrganizationAbilitiesAsync_WhenFlagOff_ReturnsFromInMemoryService( SutProvider sutProvider, OrganizationAbility matchedAbility, OrganizationAbility unmatchedAbility) { // Arrange sutProvider.GetDependency() .IsEnabled(FeatureFlagKeys.OrgAbilityExtendedCache) .Returns(false); var allAbilities = new Dictionary { [matchedAbility.Id] = matchedAbility, [unmatchedAbility.Id] = unmatchedAbility }; sutProvider.GetDependency() .GetOrganizationAbilitiesAsync() .Returns(allAbilities); // Act var result = await sutProvider.Sut.GetOrganizationAbilitiesAsync([matchedAbility.Id]); // Assert Assert.Single(result); Assert.Equal(matchedAbility, result[matchedAbility.Id]); await sutProvider.GetDependency() .Received(1) .GetOrganizationAbilitiesAsync(); await sutProvider.GetDependency() .DidNotReceiveWithAnyArgs() .GetOrganizationAbilitiesAsync(default); } [Theory, BitAutoData] public async Task GetOrganizationAbilitiesAsync_WhenFlagOn_ReturnsFromExtendedCacheService( SutProvider sutProvider, OrganizationAbility ability) { // Arrange var orgIds = new[] { ability.Id }; var expectedResult = new Dictionary { [ability.Id] = ability }; sutProvider.GetDependency() .IsEnabled(FeatureFlagKeys.OrgAbilityExtendedCache) .Returns(true); sutProvider.GetDependency() .GetOrganizationAbilitiesAsync(orgIds) .Returns(expectedResult); // Act var result = await sutProvider.Sut.GetOrganizationAbilitiesAsync(orgIds); // Assert Assert.Single(result); Assert.Equal(ability, result[ability.Id]); await sutProvider.GetDependency() .Received(1) .GetOrganizationAbilitiesAsync(orgIds); await sutProvider.GetDependency() .DidNotReceiveWithAnyArgs() .GetOrganizationAbilitiesAsync(); } [Theory, BitAutoData] public async Task GetOrganizationAbilitiesAsync_WhenFlagOff_WhenDuplicateIdsProvided_DoesNotThrowAndReturnsSingleEntry( SutProvider sutProvider, OrganizationAbility ability) { // Arrange sutProvider.GetDependency() .IsEnabled(FeatureFlagKeys.OrgAbilityExtendedCache) .Returns(false); var allAbilities = new Dictionary { [ability.Id] = ability }; sutProvider.GetDependency() .GetOrganizationAbilitiesAsync() .Returns(allAbilities); // Act - passing the same ID twice simulates a user with duplicate org memberships var result = await sutProvider.Sut.GetOrganizationAbilitiesAsync([ability.Id, ability.Id]); // Assert Assert.Single(result); Assert.Equal(ability, result[ability.Id]); } [Theory, BitAutoData] public async Task GetOrganizationAbilitiesAsync_WhenFlagOff_WhenNoIdsMatched_ReturnsEmptyDictionary( SutProvider sutProvider, Guid missingOrgId) { // Arrange sutProvider.GetDependency() .IsEnabled(FeatureFlagKeys.OrgAbilityExtendedCache) .Returns(false); sutProvider.GetDependency() .GetOrganizationAbilitiesAsync() .Returns(new Dictionary()); // Act var result = await sutProvider.Sut.GetOrganizationAbilitiesAsync([missingOrgId]); // Assert Assert.Empty(result); } [Theory, BitAutoData] public async Task UpsertOrganizationAbilityAsync_WhenFlagOff_CallsInMemoryService( SutProvider sutProvider, Organization organization) { // Arrange sutProvider.GetDependency() .IsEnabled(FeatureFlagKeys.OrgAbilityExtendedCache) .Returns(false); // Act await sutProvider.Sut.UpsertOrganizationAbilityAsync(organization); // Assert await sutProvider.GetDependency() .Received(1) .UpsertOrganizationAbilityAsync(organization); await sutProvider.GetDependency() .DidNotReceiveWithAnyArgs() .UpsertOrganizationAbilityAsync(default); } [Theory, BitAutoData] public async Task UpsertOrganizationAbilityAsync_WhenFlagOn_CallsExtendedCacheService( SutProvider sutProvider, Organization organization) { // Arrange sutProvider.GetDependency() .IsEnabled(FeatureFlagKeys.OrgAbilityExtendedCache) .Returns(true); // Act await sutProvider.Sut.UpsertOrganizationAbilityAsync(organization); // Assert await sutProvider.GetDependency() .Received(1) .UpsertOrganizationAbilityAsync(organization); await sutProvider.GetDependency() .DidNotReceiveWithAnyArgs() .UpsertOrganizationAbilityAsync(default); } [Theory, BitAutoData] public async Task DeleteOrganizationAbilityAsync_WhenFlagOff_CallsInMemoryService( SutProvider sutProvider, Guid organizationId) { // Arrange sutProvider.GetDependency() .IsEnabled(FeatureFlagKeys.OrgAbilityExtendedCache) .Returns(false); // Act await sutProvider.Sut.DeleteOrganizationAbilityAsync(organizationId); // Assert await sutProvider.GetDependency() .Received(1) .DeleteOrganizationAbilityAsync(organizationId); await sutProvider.GetDependency() .DidNotReceiveWithAnyArgs() .DeleteOrganizationAbilityAsync(default); } [Theory, BitAutoData] public async Task DeleteOrganizationAbilityAsync_WhenFlagOn_CallsExtendedCacheService( SutProvider sutProvider, Guid organizationId) { // Arrange sutProvider.GetDependency() .IsEnabled(FeatureFlagKeys.OrgAbilityExtendedCache) .Returns(true); // Act await sutProvider.Sut.DeleteOrganizationAbilityAsync(organizationId); // Assert await sutProvider.GetDependency() .Received(1) .DeleteOrganizationAbilityAsync(organizationId); await sutProvider.GetDependency() .DidNotReceiveWithAnyArgs() .DeleteOrganizationAbilityAsync(default); } [Theory, BitAutoData] public async Task BaseUpsertOrganizationAbilityAsync_WhenFlagOff_CallsServiceBusCache( Organization organization) { // Arrange var currentCacheService = CreateCurrentCacheMockService(); var extendedCacheService = Substitute.For(); var providerAbilityCacheService = Substitute.For(); var featureService = Substitute.For(); featureService.IsEnabled(FeatureFlagKeys.OrgAbilityExtendedCache).Returns(false); var sut = new FeatureRoutedCacheService(currentCacheService, extendedCacheService, providerAbilityCacheService, featureService); // Act await sut.BaseUpsertOrganizationAbilityAsync(organization); // Assert await currentCacheService .Received(1) .BaseUpsertOrganizationAbilityAsync(organization); await extendedCacheService .DidNotReceiveWithAnyArgs() .UpsertOrganizationAbilityAsync(default); } [Theory, BitAutoData] public async Task BaseUpsertOrganizationAbilityAsync_WhenFlagOn_CallsExtendedCacheService( SutProvider sutProvider, Organization organization) { // Arrange sutProvider.GetDependency() .IsEnabled(FeatureFlagKeys.OrgAbilityExtendedCache) .Returns(true); // Act await sutProvider.Sut.BaseUpsertOrganizationAbilityAsync(organization); // Assert await sutProvider.GetDependency() .Received(1) .UpsertOrganizationAbilityAsync(organization); } [Theory, BitAutoData] public async Task BaseUpsertOrganizationAbilityAsync_WhenFlagOff_AndServiceIsNotServiceBusCache_ThrowsException( SutProvider sutProvider, Organization organization) { // Arrange sutProvider.GetDependency() .IsEnabled(FeatureFlagKeys.OrgAbilityExtendedCache) .Returns(false); // Act var ex = await Assert.ThrowsAsync( () => sutProvider.Sut.BaseUpsertOrganizationAbilityAsync(organization)); // Assert Assert.Equal(ExpectedErrorMessage, ex.Message); } [Theory, BitAutoData] public async Task BaseDeleteOrganizationAbilityAsync_WhenFlagOff_CallsServiceBusCache( Guid organizationId) { // Arrange var currentCacheService = CreateCurrentCacheMockService(); var extendedCacheService = Substitute.For(); var providerAbilityCacheService = Substitute.For(); var featureService = Substitute.For(); featureService.IsEnabled(FeatureFlagKeys.OrgAbilityExtendedCache).Returns(false); var sut = new FeatureRoutedCacheService(currentCacheService, extendedCacheService, providerAbilityCacheService, featureService); // Act await sut.BaseDeleteOrganizationAbilityAsync(organizationId); // Assert await currentCacheService .Received(1) .BaseDeleteOrganizationAbilityAsync(organizationId); await extendedCacheService .DidNotReceiveWithAnyArgs() .DeleteOrganizationAbilityAsync(default); } [Theory, BitAutoData] public async Task BaseDeleteOrganizationAbilityAsync_WhenFlagOn_CallsExtendedCacheService( SutProvider sutProvider, Guid organizationId) { // Arrange sutProvider.GetDependency() .IsEnabled(FeatureFlagKeys.OrgAbilityExtendedCache) .Returns(true); // Act await sutProvider.Sut.BaseDeleteOrganizationAbilityAsync(organizationId); // Assert await sutProvider.GetDependency() .Received(1) .DeleteOrganizationAbilityAsync(organizationId); } [Theory, BitAutoData] public async Task BaseDeleteOrganizationAbilityAsync_WhenFlagOff_AndServiceIsNotServiceBusCache_ThrowsException( SutProvider sutProvider, Guid organizationId) { // Arrange sutProvider.GetDependency() .IsEnabled(FeatureFlagKeys.OrgAbilityExtendedCache) .Returns(false); // Act var ex = await Assert.ThrowsAsync(() => sutProvider.Sut.BaseDeleteOrganizationAbilityAsync(organizationId)); // Assert Assert.Equal(ExpectedErrorMessage, ex.Message); } /// /// Our SUT uses a method that is not part of IVCurrentInMemoryApplicationCacheService, /// so AutoFixture's auto-created mock won't work. /// private static InMemoryServiceBusApplicationCacheService CreateCurrentCacheMockService() { return Substitute.For( Substitute.For(), Substitute.For(), 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" } }); } private static string ExpectedErrorMessage => "Expected inMemoryApplicationCacheService to be of type InMemoryServiceBusApplicationCacheService"; }