diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/EventIntegrationHandler.cs b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/EventIntegrationHandler.cs index e29d0eaaad..4202ba770e 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/EventIntegrationHandler.cs +++ b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/EventIntegrationHandler.cs @@ -1,11 +1,15 @@ using System.Text.Json; +using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Models.Data.EventIntegrations; using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Utilities; using Bit.Core.Enums; using Bit.Core.Models.Data; +using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Repositories; +using Bit.Core.Utilities; using Microsoft.Extensions.Logging; +using ZiggyCreatures.Caching.Fusion; namespace Bit.Core.Services; @@ -14,6 +18,7 @@ public class EventIntegrationHandler( IEventIntegrationPublisher eventIntegrationPublisher, IIntegrationFilterService integrationFilterService, IIntegrationConfigurationDetailsCache configurationCache, + IFusionCache cache, IGroupRepository groupRepository, IOrganizationRepository organizationRepository, IOrganizationUserRepository organizationUserRepository, @@ -87,13 +92,18 @@ public class EventIntegrationHandler( } } - private async Task BuildContextAsync(EventMessage eventMessage, string template) + internal async Task BuildContextAsync(EventMessage eventMessage, string template) { + // Note: All of these cache calls use the default options, including TTL of 30 minutes + var context = new IntegrationTemplateContext(eventMessage); if (IntegrationTemplateProcessor.TemplateRequiresGroup(template) && eventMessage.GroupId.HasValue) { - context.Group = await groupRepository.GetByIdAsync(eventMessage.GroupId.Value); + context.Group = await cache.GetOrSetAsync( + key: EventIntegrationsCacheConstants.BuildCacheKeyForGroup(eventMessage.GroupId.Value), + factory: async _ => await groupRepository.GetByIdAsync(eventMessage.GroupId.Value) + ); } if (eventMessage.OrganizationId is not Guid organizationId) @@ -103,25 +113,31 @@ public class EventIntegrationHandler( if (IntegrationTemplateProcessor.TemplateRequiresUser(template) && eventMessage.UserId.HasValue) { - context.User = await organizationUserRepository.GetDetailsByOrganizationIdUserIdAsync( - organizationId: organizationId, - userId: eventMessage.UserId.Value - ); + context.User = await GetUserFromCacheAsync(organizationId, eventMessage.UserId.Value); } if (IntegrationTemplateProcessor.TemplateRequiresActingUser(template) && eventMessage.ActingUserId.HasValue) { - context.ActingUser = await organizationUserRepository.GetDetailsByOrganizationIdUserIdAsync( - organizationId: organizationId, - userId: eventMessage.ActingUserId.Value - ); + context.ActingUser = await GetUserFromCacheAsync(organizationId, eventMessage.ActingUserId.Value); } if (IntegrationTemplateProcessor.TemplateRequiresOrganization(template)) { - context.Organization = await organizationRepository.GetByIdAsync(organizationId); + context.Organization = await cache.GetOrSetAsync( + key: EventIntegrationsCacheConstants.BuildCacheKeyForOrganization(organizationId), + factory: async _ => await organizationRepository.GetByIdAsync(organizationId) + ); } return context; } + + private async Task GetUserFromCacheAsync(Guid organizationId, Guid userId) => + await cache.GetOrSetAsync( + key: EventIntegrationsCacheConstants.BuildCacheKeyForOrganizationUser(organizationId, userId), + factory: async _ => await organizationUserRepository.GetDetailsByOrganizationIdUserIdAsync( + organizationId: organizationId, + userId: userId + ) + ); } diff --git a/src/Core/AdminConsole/Utilities/IntegrationTemplateProcessor.cs b/src/Core/AdminConsole/Utilities/IntegrationTemplateProcessor.cs index 62df3b2bc9..7fc8013c15 100644 --- a/src/Core/AdminConsole/Utilities/IntegrationTemplateProcessor.cs +++ b/src/Core/AdminConsole/Utilities/IntegrationTemplateProcessor.cs @@ -1,6 +1,4 @@ -#nullable enable - -using System.Text.RegularExpressions; +using System.Text.RegularExpressions; namespace Bit.Core.AdminConsole.Utilities; diff --git a/src/Core/Utilities/EventIntegrationsCacheConstants.cs b/src/Core/Utilities/EventIntegrationsCacheConstants.cs new file mode 100644 index 0000000000..f3ba99fd12 --- /dev/null +++ b/src/Core/Utilities/EventIntegrationsCacheConstants.cs @@ -0,0 +1,52 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Models.Data.Organizations.OrganizationUsers; + +namespace Bit.Core.Utilities; + +/// +/// Provides cache key generation helpers and cache name constants for event integration–related entities. +/// +public static class EventIntegrationsCacheConstants +{ + /// + /// The base cache name used for storing event integration data. + /// + public static readonly string CacheName = "EventIntegrations"; + + /// + /// Builds a deterministic cache key for a . + /// + /// The unique identifier of the group. + /// + /// A cache key for this Group. + /// + public static string BuildCacheKeyForGroup(Guid groupId) + { + return $"Group:{groupId:N}"; + } + + /// + /// Builds a deterministic cache key for an . + /// + /// The unique identifier of the organization. + /// + /// A cache key for the Organization. + /// + public static string BuildCacheKeyForOrganization(Guid organizationId) + { + return $"Organization:{organizationId:N}"; + } + + /// + /// Builds a deterministic cache key for an organization user . + /// + /// The unique identifier of the organization to which the user belongs. + /// The unique identifier of the user. + /// + /// A cache key for the user. + /// + public static string BuildCacheKeyForOrganizationUser(Guid organizationId, Guid userId) + { + return $"OrganizationUserUserDetails:{organizationId:N}:{userId:N}"; + } +} diff --git a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs index 9caa37b997..ad2cc0e8fa 100644 --- a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs +++ b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs @@ -86,6 +86,7 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using StackExchange.Redis; +using ZiggyCreatures.Caching.Fusion; using NoopRepos = Bit.Core.Repositories.Noop; using Role = Bit.Core.Entities.Role; using TableStorageRepos = Bit.Core.Repositories.TableStorage; @@ -890,6 +891,7 @@ public static class ServiceCollectionExtensions eventIntegrationPublisher: provider.GetRequiredService(), integrationFilterService: provider.GetRequiredService(), configurationCache: provider.GetRequiredService(), + cache: provider.GetRequiredKeyedService(EventIntegrationsCacheConstants.CacheName), groupRepository: provider.GetRequiredService(), organizationRepository: provider.GetRequiredService(), organizationUserRepository: provider.GetRequiredService(), @@ -934,6 +936,8 @@ public static class ServiceCollectionExtensions GlobalSettings globalSettings) { // Add common services + services.AddDistributedCache(globalSettings); + services.AddExtendedCache(EventIntegrationsCacheConstants.CacheName, globalSettings); services.TryAddSingleton(); services.TryAddSingleton(provider => provider.GetRequiredService()); @@ -1018,6 +1022,7 @@ public static class ServiceCollectionExtensions eventIntegrationPublisher: provider.GetRequiredService(), integrationFilterService: provider.GetRequiredService(), configurationCache: provider.GetRequiredService(), + cache: provider.GetRequiredKeyedService(EventIntegrationsCacheConstants.CacheName), groupRepository: provider.GetRequiredService(), organizationRepository: provider.GetRequiredService(), organizationUserRepository: provider.GetRequiredService(), diff --git a/test/Core.Test/AdminConsole/Services/EventIntegrationHandlerTests.cs b/test/Core.Test/AdminConsole/Services/EventIntegrationHandlerTests.cs index c556c1fae0..73566cff89 100644 --- a/test/Core.Test/AdminConsole/Services/EventIntegrationHandlerTests.cs +++ b/test/Core.Test/AdminConsole/Services/EventIntegrationHandlerTests.cs @@ -14,6 +14,7 @@ using Bit.Test.Common.Helpers; using Microsoft.Extensions.Logging; using NSubstitute; using Xunit; +using ZiggyCreatures.Caching.Fusion; namespace Bit.Core.Test.Services; @@ -25,7 +26,6 @@ public class EventIntegrationHandlerTests private const string _templateWithOrganization = "Org: #OrganizationName#"; private const string _templateWithUser = "#UserName#, #UserEmail#, #UserType#"; private const string _templateWithActingUser = "#ActingUserName#, #ActingUserEmail#, #ActingUserType#"; - private static readonly Guid _groupId = Guid.NewGuid(); private static readonly Guid _organizationId = Guid.NewGuid(); private static readonly Uri _uri = new Uri("https://localhost"); private static readonly Uri _uri2 = new Uri("https://example.com"); @@ -113,6 +113,232 @@ public class EventIntegrationHandlerTests return [config]; } + [Theory, BitAutoData] + public async Task BuildContextAsync_ActingUserIdPresent_UsesCache(EventMessage eventMessage, OrganizationUserUserDetails actingUser) + { + var sutProvider = GetSutProvider(OneConfiguration(_templateWithActingUser)); + var cache = sutProvider.GetDependency(); + + eventMessage.OrganizationId ??= Guid.NewGuid(); + eventMessage.ActingUserId ??= Guid.NewGuid(); + + cache.GetOrSetAsync( + key: Arg.Any(), + factory: Arg.Any, CancellationToken, Task>>() + ).Returns(actingUser); + + var context = await sutProvider.Sut.BuildContextAsync(eventMessage, _templateWithActingUser); + + await cache.Received(1).GetOrSetAsync( + key: Arg.Any(), + factory: Arg.Any, CancellationToken, Task>>() + ); + + Assert.Equal(actingUser, context.ActingUser); + } + + [Theory, BitAutoData] + public async Task BuildContextAsync_ActingUserIdNull_SkipsCache(EventMessage eventMessage) + { + var sutProvider = GetSutProvider(OneConfiguration(_templateWithActingUser)); + var cache = sutProvider.GetDependency(); + + eventMessage.OrganizationId ??= Guid.NewGuid(); + eventMessage.ActingUserId = null; + + var context = await sutProvider.Sut.BuildContextAsync(eventMessage, _templateWithActingUser); + + await cache.DidNotReceive().GetOrSetAsync( + key: Arg.Any(), + factory: Arg.Any, CancellationToken, Task>>() + ); + Assert.Null(context.ActingUser); + } + + [Theory, BitAutoData] + public async Task BuildContextAsync_ActingUserOrganizationIdNull_SkipsCache(EventMessage eventMessage) + { + var sutProvider = GetSutProvider(OneConfiguration(_templateWithActingUser)); + var cache = sutProvider.GetDependency(); + + eventMessage.OrganizationId = null; + eventMessage.ActingUserId ??= Guid.NewGuid(); + + var context = await sutProvider.Sut.BuildContextAsync(eventMessage, _templateWithActingUser); + + await cache.DidNotReceive().GetOrSetAsync( + key: Arg.Any(), + factory: Arg.Any, CancellationToken, Task>>() + ); + Assert.Null(context.ActingUser); + } + + [Theory, BitAutoData] + public async Task BuildContextAsync_GroupIdPresent_UsesCache(EventMessage eventMessage, Group group) + { + var sutProvider = GetSutProvider(OneConfiguration(_templateWithGroup)); + var cache = sutProvider.GetDependency(); + + eventMessage.GroupId ??= Guid.NewGuid(); + + cache.GetOrSetAsync( + key: Arg.Any(), + factory: Arg.Any, CancellationToken, Task>>() + ).Returns(group); + + var context = await sutProvider.Sut.BuildContextAsync(eventMessage, _templateWithGroup); + + await cache.Received(1).GetOrSetAsync( + key: Arg.Any(), + factory: Arg.Any, CancellationToken, Task>>() + ); + Assert.Equal(group, context.Group); + } + + [Theory, BitAutoData] + public async Task BuildContextAsync_GroupIdNull_SkipsCache(EventMessage eventMessage) + { + var sutProvider = GetSutProvider(OneConfiguration(_templateWithGroup)); + var cache = sutProvider.GetDependency(); + eventMessage.GroupId = null; + + var context = await sutProvider.Sut.BuildContextAsync(eventMessage, _templateWithGroup); + + await cache.DidNotReceive().GetOrSetAsync( + key: Arg.Any(), + factory: Arg.Any, CancellationToken, Task>>() + ); + Assert.Null(context.Group); + } + + [Theory, BitAutoData] + public async Task BuildContextAsync_OrganizationIdPresent_UsesCache(EventMessage eventMessage, Organization organization) + { + var sutProvider = GetSutProvider(OneConfiguration(_templateWithOrganization)); + var cache = sutProvider.GetDependency(); + + eventMessage.OrganizationId ??= Guid.NewGuid(); + + cache.GetOrSetAsync( + key: Arg.Any(), + factory: Arg.Any, CancellationToken, Task>>() + ).Returns(organization); + + var context = await sutProvider.Sut.BuildContextAsync(eventMessage, _templateWithOrganization); + + await cache.Received(1).GetOrSetAsync( + key: Arg.Any(), + factory: Arg.Any, CancellationToken, Task>>() + ); + Assert.Equal(organization, context.Organization); + } + + [Theory, BitAutoData] + public async Task BuildContextAsync_OrganizationIdNull_SkipsCache(EventMessage eventMessage) + { + var sutProvider = GetSutProvider(OneConfiguration(_templateWithOrganization)); + var cache = sutProvider.GetDependency(); + + eventMessage.OrganizationId = null; + + var context = await sutProvider.Sut.BuildContextAsync(eventMessage, _templateWithOrganization); + + await cache.DidNotReceive().GetOrSetAsync( + key: Arg.Any(), + factory: Arg.Any, CancellationToken, Task>>() + ); + Assert.Null(context.Organization); + } + + [Theory, BitAutoData] + public async Task BuildContextAsync_UserIdPresent_UsesCache(EventMessage eventMessage, OrganizationUserUserDetails userDetails) + { + var sutProvider = GetSutProvider(OneConfiguration(_templateWithUser)); + var cache = sutProvider.GetDependency(); + + eventMessage.OrganizationId ??= Guid.NewGuid(); + eventMessage.UserId ??= Guid.NewGuid(); + + cache.GetOrSetAsync( + key: Arg.Any(), + factory: Arg.Any, CancellationToken, Task>>() + ).Returns(userDetails); + + var context = await sutProvider.Sut.BuildContextAsync(eventMessage, _templateWithUser); + + await cache.Received(1).GetOrSetAsync( + key: Arg.Any(), + factory: Arg.Any, CancellationToken, Task>>() + ); + + Assert.Equal(userDetails, context.User); + } + + + [Theory, BitAutoData] + public async Task BuildContextAsync_UserIdNull_SkipsCache(EventMessage eventMessage) + { + var sutProvider = GetSutProvider(OneConfiguration(_templateWithUser)); + var cache = sutProvider.GetDependency(); + + eventMessage.OrganizationId = null; + eventMessage.UserId ??= Guid.NewGuid(); + + var context = await sutProvider.Sut.BuildContextAsync(eventMessage, _templateWithUser); + + await cache.DidNotReceive().GetOrSetAsync( + key: Arg.Any(), + factory: Arg.Any, CancellationToken, Task>>() + ); + + Assert.Null(context.User); + } + + [Theory, BitAutoData] + public async Task BuildContextAsync_OrganizationUserIdNull_SkipsCache(EventMessage eventMessage) + { + var sutProvider = GetSutProvider(OneConfiguration(_templateWithUser)); + var cache = sutProvider.GetDependency(); + + eventMessage.OrganizationId ??= Guid.NewGuid(); + eventMessage.UserId = null; + + var context = await sutProvider.Sut.BuildContextAsync(eventMessage, _templateWithUser); + + await cache.DidNotReceive().GetOrSetAsync( + key: Arg.Any(), + factory: Arg.Any, CancellationToken, Task>>() + ); + + Assert.Null(context.User); + } + + [Theory, BitAutoData] + public async Task BuildContextAsync_NoSpecialTokens_DoesNotCallAnyCache(EventMessage eventMessage) + { + var sutProvider = GetSutProvider(OneConfiguration(_templateWithUser)); + var cache = sutProvider.GetDependency(); + + eventMessage.ActingUserId ??= Guid.NewGuid(); + eventMessage.GroupId ??= Guid.NewGuid(); + eventMessage.OrganizationId ??= Guid.NewGuid(); + eventMessage.UserId ??= Guid.NewGuid(); + + await sutProvider.Sut.BuildContextAsync(eventMessage, _templateBase); + + await cache.DidNotReceive().GetOrSetAsync( + key: Arg.Any(), + factory: Arg.Any, CancellationToken, Task>>() + ); + await cache.DidNotReceive().GetOrSetAsync( + key: Arg.Any(), + factory: Arg.Any, CancellationToken, Task>>() + ); + await cache.DidNotReceive().GetOrSetAsync( + key: Arg.Any(), + factory: Arg.Any, CancellationToken, Task>>() + ); + } [Theory, BitAutoData] public async Task HandleEventAsync_BaseTemplateNoConfigurations_DoesNothing(EventMessage eventMessage) @@ -176,99 +402,6 @@ public class EventIntegrationHandlerTests await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetDetailsByOrganizationIdUserIdAsync(Arg.Any(), Arg.Any()); } - [Theory, BitAutoData] - public async Task HandleEventAsync_ActingUserTemplate_LoadsUserFromRepository(EventMessage eventMessage) - { - var sutProvider = GetSutProvider(OneConfiguration(_templateWithActingUser)); - var user = Substitute.For(); - user.Email = "test@example.com"; - user.Name = "Test"; - eventMessage.OrganizationId = _organizationId; - - sutProvider.GetDependency() - .GetDetailsByOrganizationIdUserIdAsync(Arg.Any(), Arg.Any()).Returns(user); - await sutProvider.Sut.HandleEventAsync(eventMessage); - - var expectedMessage = EventIntegrationHandlerTests.ExpectedMessage($"{user.Name}, {user.Email}, {user.Type}"); - - Assert.Single(_eventIntegrationPublisher.ReceivedCalls()); - await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is( - AssertHelper.AssertPropertyEqual(expectedMessage, new[] { "MessageId" }))); - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any()); - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any()); - await sutProvider.GetDependency().Received(1).GetDetailsByOrganizationIdUserIdAsync(Arg.Any(), eventMessage.ActingUserId ?? Guid.Empty); - } - - [Theory, BitAutoData] - public async Task HandleEventAsync_GroupTemplate_LoadsGroupFromRepository(EventMessage eventMessage) - { - var sutProvider = GetSutProvider(OneConfiguration(_templateWithGroup)); - var group = Substitute.For(); - group.Name = "Test"; - eventMessage.GroupId = _groupId; - eventMessage.OrganizationId = _organizationId; - - sutProvider.GetDependency().GetByIdAsync(Arg.Any()).Returns(group); - await sutProvider.Sut.HandleEventAsync(eventMessage); - - Assert.Single(_eventIntegrationPublisher.ReceivedCalls()); - - var expectedMessage = EventIntegrationHandlerTests.ExpectedMessage($"Group: {group.Name}"); - - Assert.Single(_eventIntegrationPublisher.ReceivedCalls()); - await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is( - AssertHelper.AssertPropertyEqual(expectedMessage, new[] { "MessageId" }))); - await sutProvider.GetDependency().Received(1).GetByIdAsync(eventMessage.GroupId ?? Guid.Empty); - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any()); - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetDetailsByOrganizationIdUserIdAsync(Arg.Any(), Arg.Any()); - } - - [Theory, BitAutoData] - public async Task HandleEventAsync_OrganizationTemplate_LoadsOrganizationFromRepository(EventMessage eventMessage) - { - var sutProvider = GetSutProvider(OneConfiguration(_templateWithOrganization)); - var organization = Substitute.For(); - organization.Name = "Test"; - eventMessage.OrganizationId = _organizationId; - - sutProvider.GetDependency().GetByIdAsync(Arg.Any()).Returns(organization); - await sutProvider.Sut.HandleEventAsync(eventMessage); - - Assert.Single(_eventIntegrationPublisher.ReceivedCalls()); - - var expectedMessage = EventIntegrationHandlerTests.ExpectedMessage($"Org: {organization.Name}"); - - Assert.Single(_eventIntegrationPublisher.ReceivedCalls()); - await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is( - AssertHelper.AssertPropertyEqual(expectedMessage, new[] { "MessageId" }))); - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any()); - await sutProvider.GetDependency().Received(1).GetByIdAsync(eventMessage.OrganizationId ?? Guid.Empty); - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetDetailsByOrganizationIdUserIdAsync(Arg.Any(), Arg.Any()); - } - - [Theory, BitAutoData] - public async Task HandleEventAsync_UserTemplate_LoadsUserFromRepository(EventMessage eventMessage) - { - var sutProvider = GetSutProvider(OneConfiguration(_templateWithUser)); - var user = Substitute.For(); - user.Email = "test@example.com"; - user.Name = "Test"; - eventMessage.OrganizationId = _organizationId; - - sutProvider.GetDependency() - .GetDetailsByOrganizationIdUserIdAsync(Arg.Any(), Arg.Any()).Returns(user); - await sutProvider.Sut.HandleEventAsync(eventMessage); - - var expectedMessage = EventIntegrationHandlerTests.ExpectedMessage($"{user.Name}, {user.Email}, {user.Type}"); - - Assert.Single(_eventIntegrationPublisher.ReceivedCalls()); - await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is( - AssertHelper.AssertPropertyEqual(expectedMessage, new[] { "MessageId" }))); - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any()); - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any()); - await sutProvider.GetDependency().Received(1).GetDetailsByOrganizationIdUserIdAsync(Arg.Any(), eventMessage.UserId ?? Guid.Empty); - } - [Theory, BitAutoData] public async Task HandleEventAsync_FilterReturnsFalse_DoesNothing(EventMessage eventMessage) { diff --git a/test/Core.Test/Utilities/EventIntegrationsCacheConstantsTests.cs b/test/Core.Test/Utilities/EventIntegrationsCacheConstantsTests.cs new file mode 100644 index 0000000000..051801e505 --- /dev/null +++ b/test/Core.Test/Utilities/EventIntegrationsCacheConstantsTests.cs @@ -0,0 +1,41 @@ +using Bit.Core.Utilities; +using Bit.Test.Common.AutoFixture.Attributes; +using Xunit; + +namespace Bit.Core.Test.Utilities; + +public class EventIntegrationsCacheConstantsTests +{ + [Theory, BitAutoData] + public void BuildCacheKeyForGroup_ReturnsExpectedKey(Guid groupId) + { + var expected = $"Group:{groupId:N}"; + var key = EventIntegrationsCacheConstants.BuildCacheKeyForGroup(groupId); + + Assert.Equal(expected, key); + } + + [Theory, BitAutoData] + public void BuildCacheKeyForOrganization_ReturnsExpectedKey(Guid orgId) + { + var expected = $"Organization:{orgId:N}"; + var key = EventIntegrationsCacheConstants.BuildCacheKeyForOrganization(orgId); + + Assert.Equal(expected, key); + } + + [Theory, BitAutoData] + public void BuildCacheKeyForOrganizationUser_ReturnsExpectedKey(Guid orgId, Guid userId) + { + var expected = $"OrganizationUserUserDetails:{orgId:N}:{userId:N}"; + var key = EventIntegrationsCacheConstants.BuildCacheKeyForOrganizationUser(orgId, userId); + + Assert.Equal(expected, key); + } + + [Fact] + public void CacheName_ReturnsExpected() + { + Assert.Equal("EventIntegrations", EventIntegrationsCacheConstants.CacheName); + } +}