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)); }