1
0
mirror of https://github.com/bitwarden/server synced 2025-12-06 00:03:34 +00:00

[PM-12642] Add new ExtendedCache to add caching to template parameters (#6608)

* Add new ExtendedCache to add caching to template parameters

* Added Cache constants for building consistent keys/name, clarified that we are using defaults including TTL, removed as much fusion cache references as possible
This commit is contained in:
Brant DeBow
2025-11-25 10:58:39 -05:00
committed by GitHub
parent f0f10bcb95
commit 1413dd7689
6 changed files with 353 additions and 108 deletions

View File

@@ -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<T>(
IEventIntegrationPublisher eventIntegrationPublisher,
IIntegrationFilterService integrationFilterService,
IIntegrationConfigurationDetailsCache configurationCache,
IFusionCache cache,
IGroupRepository groupRepository,
IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository,
@@ -87,13 +92,18 @@ public class EventIntegrationHandler<T>(
}
}
private async Task<IntegrationTemplateContext> BuildContextAsync(EventMessage eventMessage, string template)
internal async Task<IntegrationTemplateContext> 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<Group?>(
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<T>(
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<Organization?>(
key: EventIntegrationsCacheConstants.BuildCacheKeyForOrganization(organizationId),
factory: async _ => await organizationRepository.GetByIdAsync(organizationId)
);
}
return context;
}
private async Task<OrganizationUserUserDetails?> GetUserFromCacheAsync(Guid organizationId, Guid userId) =>
await cache.GetOrSetAsync<OrganizationUserUserDetails?>(
key: EventIntegrationsCacheConstants.BuildCacheKeyForOrganizationUser(organizationId, userId),
factory: async _ => await organizationUserRepository.GetDetailsByOrganizationIdUserIdAsync(
organizationId: organizationId,
userId: userId
)
);
}

View File

@@ -1,6 +1,4 @@
#nullable enable
using System.Text.RegularExpressions;
using System.Text.RegularExpressions;
namespace Bit.Core.AdminConsole.Utilities;

View File

@@ -0,0 +1,52 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
namespace Bit.Core.Utilities;
/// <summary>
/// Provides cache key generation helpers and cache name constants for event integrationrelated entities.
/// </summary>
public static class EventIntegrationsCacheConstants
{
/// <summary>
/// The base cache name used for storing event integration data.
/// </summary>
public static readonly string CacheName = "EventIntegrations";
/// <summary>
/// Builds a deterministic cache key for a <see cref="Group"/>.
/// </summary>
/// <param name="groupId">The unique identifier of the group.</param>
/// <returns>
/// A cache key for this Group.
/// </returns>
public static string BuildCacheKeyForGroup(Guid groupId)
{
return $"Group:{groupId:N}";
}
/// <summary>
/// Builds a deterministic cache key for an <see cref="Organization"/>.
/// </summary>
/// <param name="organizationId">The unique identifier of the organization.</param>
/// <returns>
/// A cache key for the Organization.
/// </returns>
public static string BuildCacheKeyForOrganization(Guid organizationId)
{
return $"Organization:{organizationId:N}";
}
/// <summary>
/// Builds a deterministic cache key for an organization user <see cref="OrganizationUserUserDetails"/>.
/// </summary>
/// <param name="organizationId">The unique identifier of the organization to which the user belongs.</param>
/// <param name="userId">The unique identifier of the user.</param>
/// <returns>
/// A cache key for the user.
/// </returns>
public static string BuildCacheKeyForOrganizationUser(Guid organizationId, Guid userId)
{
return $"OrganizationUserUserDetails:{organizationId:N}:{userId:N}";
}
}

View File

@@ -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<IEventIntegrationPublisher>(),
integrationFilterService: provider.GetRequiredService<IIntegrationFilterService>(),
configurationCache: provider.GetRequiredService<IIntegrationConfigurationDetailsCache>(),
cache: provider.GetRequiredKeyedService<IFusionCache>(EventIntegrationsCacheConstants.CacheName),
groupRepository: provider.GetRequiredService<IGroupRepository>(),
organizationRepository: provider.GetRequiredService<IOrganizationRepository>(),
organizationUserRepository: provider.GetRequiredService<IOrganizationUserRepository>(),
@@ -934,6 +936,8 @@ public static class ServiceCollectionExtensions
GlobalSettings globalSettings)
{
// Add common services
services.AddDistributedCache(globalSettings);
services.AddExtendedCache(EventIntegrationsCacheConstants.CacheName, globalSettings);
services.TryAddSingleton<IntegrationConfigurationDetailsCacheService>();
services.TryAddSingleton<IIntegrationConfigurationDetailsCache>(provider =>
provider.GetRequiredService<IntegrationConfigurationDetailsCacheService>());
@@ -1018,6 +1022,7 @@ public static class ServiceCollectionExtensions
eventIntegrationPublisher: provider.GetRequiredService<IEventIntegrationPublisher>(),
integrationFilterService: provider.GetRequiredService<IIntegrationFilterService>(),
configurationCache: provider.GetRequiredService<IIntegrationConfigurationDetailsCache>(),
cache: provider.GetRequiredKeyedService<IFusionCache>(EventIntegrationsCacheConstants.CacheName),
groupRepository: provider.GetRequiredService<IGroupRepository>(),
organizationRepository: provider.GetRequiredService<IOrganizationRepository>(),
organizationUserRepository: provider.GetRequiredService<IOrganizationUserRepository>(),

View File

@@ -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<IFusionCache>();
eventMessage.OrganizationId ??= Guid.NewGuid();
eventMessage.ActingUserId ??= Guid.NewGuid();
cache.GetOrSetAsync(
key: Arg.Any<string>(),
factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<OrganizationUserUserDetails?>, CancellationToken, Task<OrganizationUserUserDetails?>>>()
).Returns(actingUser);
var context = await sutProvider.Sut.BuildContextAsync(eventMessage, _templateWithActingUser);
await cache.Received(1).GetOrSetAsync(
key: Arg.Any<string>(),
factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<OrganizationUserUserDetails?>, CancellationToken, Task<OrganizationUserUserDetails?>>>()
);
Assert.Equal(actingUser, context.ActingUser);
}
[Theory, BitAutoData]
public async Task BuildContextAsync_ActingUserIdNull_SkipsCache(EventMessage eventMessage)
{
var sutProvider = GetSutProvider(OneConfiguration(_templateWithActingUser));
var cache = sutProvider.GetDependency<IFusionCache>();
eventMessage.OrganizationId ??= Guid.NewGuid();
eventMessage.ActingUserId = null;
var context = await sutProvider.Sut.BuildContextAsync(eventMessage, _templateWithActingUser);
await cache.DidNotReceive().GetOrSetAsync(
key: Arg.Any<string>(),
factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<OrganizationUserUserDetails?>, CancellationToken, Task<OrganizationUserUserDetails?>>>()
);
Assert.Null(context.ActingUser);
}
[Theory, BitAutoData]
public async Task BuildContextAsync_ActingUserOrganizationIdNull_SkipsCache(EventMessage eventMessage)
{
var sutProvider = GetSutProvider(OneConfiguration(_templateWithActingUser));
var cache = sutProvider.GetDependency<IFusionCache>();
eventMessage.OrganizationId = null;
eventMessage.ActingUserId ??= Guid.NewGuid();
var context = await sutProvider.Sut.BuildContextAsync(eventMessage, _templateWithActingUser);
await cache.DidNotReceive().GetOrSetAsync(
key: Arg.Any<string>(),
factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<OrganizationUserUserDetails?>, CancellationToken, Task<OrganizationUserUserDetails?>>>()
);
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<IFusionCache>();
eventMessage.GroupId ??= Guid.NewGuid();
cache.GetOrSetAsync(
key: Arg.Any<string>(),
factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<Group?>, CancellationToken, Task<Group?>>>()
).Returns(group);
var context = await sutProvider.Sut.BuildContextAsync(eventMessage, _templateWithGroup);
await cache.Received(1).GetOrSetAsync(
key: Arg.Any<string>(),
factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<Group?>, CancellationToken, Task<Group?>>>()
);
Assert.Equal(group, context.Group);
}
[Theory, BitAutoData]
public async Task BuildContextAsync_GroupIdNull_SkipsCache(EventMessage eventMessage)
{
var sutProvider = GetSutProvider(OneConfiguration(_templateWithGroup));
var cache = sutProvider.GetDependency<IFusionCache>();
eventMessage.GroupId = null;
var context = await sutProvider.Sut.BuildContextAsync(eventMessage, _templateWithGroup);
await cache.DidNotReceive().GetOrSetAsync(
key: Arg.Any<string>(),
factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<Group?>, CancellationToken, Task<Group?>>>()
);
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<IFusionCache>();
eventMessage.OrganizationId ??= Guid.NewGuid();
cache.GetOrSetAsync(
key: Arg.Any<string>(),
factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<Organization?>, CancellationToken, Task<Organization?>>>()
).Returns(organization);
var context = await sutProvider.Sut.BuildContextAsync(eventMessage, _templateWithOrganization);
await cache.Received(1).GetOrSetAsync(
key: Arg.Any<string>(),
factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<Organization?>, CancellationToken, Task<Organization?>>>()
);
Assert.Equal(organization, context.Organization);
}
[Theory, BitAutoData]
public async Task BuildContextAsync_OrganizationIdNull_SkipsCache(EventMessage eventMessage)
{
var sutProvider = GetSutProvider(OneConfiguration(_templateWithOrganization));
var cache = sutProvider.GetDependency<IFusionCache>();
eventMessage.OrganizationId = null;
var context = await sutProvider.Sut.BuildContextAsync(eventMessage, _templateWithOrganization);
await cache.DidNotReceive().GetOrSetAsync(
key: Arg.Any<string>(),
factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<Organization?>, CancellationToken, Task<Organization?>>>()
);
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<IFusionCache>();
eventMessage.OrganizationId ??= Guid.NewGuid();
eventMessage.UserId ??= Guid.NewGuid();
cache.GetOrSetAsync(
key: Arg.Any<string>(),
factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<OrganizationUserUserDetails?>, CancellationToken, Task<OrganizationUserUserDetails?>>>()
).Returns(userDetails);
var context = await sutProvider.Sut.BuildContextAsync(eventMessage, _templateWithUser);
await cache.Received(1).GetOrSetAsync(
key: Arg.Any<string>(),
factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<OrganizationUserUserDetails?>, CancellationToken, Task<OrganizationUserUserDetails?>>>()
);
Assert.Equal(userDetails, context.User);
}
[Theory, BitAutoData]
public async Task BuildContextAsync_UserIdNull_SkipsCache(EventMessage eventMessage)
{
var sutProvider = GetSutProvider(OneConfiguration(_templateWithUser));
var cache = sutProvider.GetDependency<IFusionCache>();
eventMessage.OrganizationId = null;
eventMessage.UserId ??= Guid.NewGuid();
var context = await sutProvider.Sut.BuildContextAsync(eventMessage, _templateWithUser);
await cache.DidNotReceive().GetOrSetAsync(
key: Arg.Any<string>(),
factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<OrganizationUserUserDetails?>, CancellationToken, Task<OrganizationUserUserDetails?>>>()
);
Assert.Null(context.User);
}
[Theory, BitAutoData]
public async Task BuildContextAsync_OrganizationUserIdNull_SkipsCache(EventMessage eventMessage)
{
var sutProvider = GetSutProvider(OneConfiguration(_templateWithUser));
var cache = sutProvider.GetDependency<IFusionCache>();
eventMessage.OrganizationId ??= Guid.NewGuid();
eventMessage.UserId = null;
var context = await sutProvider.Sut.BuildContextAsync(eventMessage, _templateWithUser);
await cache.DidNotReceive().GetOrSetAsync(
key: Arg.Any<string>(),
factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<OrganizationUserUserDetails?>, CancellationToken, Task<OrganizationUserUserDetails?>>>()
);
Assert.Null(context.User);
}
[Theory, BitAutoData]
public async Task BuildContextAsync_NoSpecialTokens_DoesNotCallAnyCache(EventMessage eventMessage)
{
var sutProvider = GetSutProvider(OneConfiguration(_templateWithUser));
var cache = sutProvider.GetDependency<IFusionCache>();
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<string>(),
factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<Group?>, CancellationToken, Task<Group?>>>()
);
await cache.DidNotReceive().GetOrSetAsync(
key: Arg.Any<string>(),
factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<Organization?>, CancellationToken, Task<Organization?>>>()
);
await cache.DidNotReceive().GetOrSetAsync(
key: Arg.Any<string>(),
factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<OrganizationUserUserDetails?>, CancellationToken, Task<OrganizationUserUserDetails?>>>()
);
}
[Theory, BitAutoData]
public async Task HandleEventAsync_BaseTemplateNoConfigurations_DoesNothing(EventMessage eventMessage)
@@ -176,99 +402,6 @@ public class EventIntegrationHandlerTests
await sutProvider.GetDependency<IOrganizationUserRepository>().DidNotReceiveWithAnyArgs().GetDetailsByOrganizationIdUserIdAsync(Arg.Any<Guid>(), Arg.Any<Guid>());
}
[Theory, BitAutoData]
public async Task HandleEventAsync_ActingUserTemplate_LoadsUserFromRepository(EventMessage eventMessage)
{
var sutProvider = GetSutProvider(OneConfiguration(_templateWithActingUser));
var user = Substitute.For<OrganizationUserUserDetails>();
user.Email = "test@example.com";
user.Name = "Test";
eventMessage.OrganizationId = _organizationId;
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetDetailsByOrganizationIdUserIdAsync(Arg.Any<Guid>(), Arg.Any<Guid>()).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<IGroupRepository>().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any<Guid>());
await sutProvider.GetDependency<IOrganizationRepository>().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any<Guid>());
await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1).GetDetailsByOrganizationIdUserIdAsync(Arg.Any<Guid>(), eventMessage.ActingUserId ?? Guid.Empty);
}
[Theory, BitAutoData]
public async Task HandleEventAsync_GroupTemplate_LoadsGroupFromRepository(EventMessage eventMessage)
{
var sutProvider = GetSutProvider(OneConfiguration(_templateWithGroup));
var group = Substitute.For<Group>();
group.Name = "Test";
eventMessage.GroupId = _groupId;
eventMessage.OrganizationId = _organizationId;
sutProvider.GetDependency<IGroupRepository>().GetByIdAsync(Arg.Any<Guid>()).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<IGroupRepository>().Received(1).GetByIdAsync(eventMessage.GroupId ?? Guid.Empty);
await sutProvider.GetDependency<IOrganizationRepository>().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any<Guid>());
await sutProvider.GetDependency<IOrganizationUserRepository>().DidNotReceiveWithAnyArgs().GetDetailsByOrganizationIdUserIdAsync(Arg.Any<Guid>(), Arg.Any<Guid>());
}
[Theory, BitAutoData]
public async Task HandleEventAsync_OrganizationTemplate_LoadsOrganizationFromRepository(EventMessage eventMessage)
{
var sutProvider = GetSutProvider(OneConfiguration(_templateWithOrganization));
var organization = Substitute.For<Organization>();
organization.Name = "Test";
eventMessage.OrganizationId = _organizationId;
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(Arg.Any<Guid>()).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<IGroupRepository>().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any<Guid>());
await sutProvider.GetDependency<IOrganizationRepository>().Received(1).GetByIdAsync(eventMessage.OrganizationId ?? Guid.Empty);
await sutProvider.GetDependency<IOrganizationUserRepository>().DidNotReceiveWithAnyArgs().GetDetailsByOrganizationIdUserIdAsync(Arg.Any<Guid>(), Arg.Any<Guid>());
}
[Theory, BitAutoData]
public async Task HandleEventAsync_UserTemplate_LoadsUserFromRepository(EventMessage eventMessage)
{
var sutProvider = GetSutProvider(OneConfiguration(_templateWithUser));
var user = Substitute.For<OrganizationUserUserDetails>();
user.Email = "test@example.com";
user.Name = "Test";
eventMessage.OrganizationId = _organizationId;
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetDetailsByOrganizationIdUserIdAsync(Arg.Any<Guid>(), Arg.Any<Guid>()).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<IGroupRepository>().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any<Guid>());
await sutProvider.GetDependency<IOrganizationRepository>().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any<Guid>());
await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1).GetDetailsByOrganizationIdUserIdAsync(Arg.Any<Guid>(), eventMessage.UserId ?? Guid.Empty);
}
[Theory, BitAutoData]
public async Task HandleEventAsync_FilterReturnsFalse_DoesNothing(EventMessage eventMessage)
{

View File

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