#nullable enable using System.Text.Json; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Models.Data.EventIntegrations; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Enums; using Bit.Core.Models.Data; using Bit.Core.Models.Data.Organizations; using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Utilities; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.Helpers; using Microsoft.Extensions.Logging; using NSubstitute; using Xunit; using ZiggyCreatures.Caching.Fusion; namespace Bit.Core.Test.Services; [SutProviderCustomize] public class EventIntegrationHandlerTests { private const string _templateBase = "Date: #Date#, Type: #Type#, UserId: #UserId#"; private const string _templateWithGroup = "Group: #GroupName#"; private const string _templateWithOrganization = "Org: #OrganizationName#"; private const string _templateWithUser = "#UserName#, #UserEmail#, #UserType#"; private const string _templateWithActingUser = "#ActingUserName#, #ActingUserEmail#, #ActingUserType#"; 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"); private readonly IEventIntegrationPublisher _eventIntegrationPublisher = Substitute.For(); private readonly ILogger> _logger = Substitute.For>>(); private SutProvider> GetSutProvider( List configurations) { var cache = Substitute.For(); cache.GetOrSetAsync( key: Arg.Any(), factory: Arg.Any>>>(), options: Arg.Any(), tags: Arg.Any>() ).Returns(configurations); return new SutProvider>() .SetDependency(cache) .SetDependency(_eventIntegrationPublisher) .SetDependency(IntegrationType.Webhook) .SetDependency(_logger) .Create(); } private static IntegrationMessage ExpectedMessage(string template) { return new IntegrationMessage() { IntegrationType = IntegrationType.Webhook, MessageId = "TestMessageId", OrganizationId = _organizationId.ToString(), Configuration = new WebhookIntegrationConfigurationDetails(_uri), RenderedTemplate = template, RetryCount = 0, DelayUntilDate = null }; } private static List NoConfigurations() { return []; } private static List OneConfiguration(string template) { var config = Substitute.For(); config.Configuration = null; config.IntegrationConfiguration = JsonSerializer.Serialize(new { Uri = _uri }); config.Template = template; return [config]; } private static List TwoConfigurations(string template) { var config = Substitute.For(); config.Configuration = null; config.IntegrationConfiguration = JsonSerializer.Serialize(new { Uri = _uri }); config.Template = template; var config2 = Substitute.For(); config2.Configuration = null; config2.IntegrationConfiguration = JsonSerializer.Serialize(new { Uri = _uri2 }); config2.Template = template; return [config, config2]; } private static List InvalidFilterConfiguration() { var config = Substitute.For(); config.Configuration = null; config.IntegrationConfiguration = JsonSerializer.Serialize(new { Uri = _uri }); config.Template = _templateBase; config.Filters = "Invalid Configuration!"; return [config]; } private static List ValidFilterConfiguration() { var config = Substitute.For(); config.Configuration = null; config.IntegrationConfiguration = JsonSerializer.Serialize(new { Uri = _uri }); config.Template = _templateBase; config.Filters = JsonSerializer.Serialize(new IntegrationFilterGroup()); 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_ActingUserFactory_CallsOrganizationUserRepository(EventMessage eventMessage, OrganizationUserUserDetails actingUser) { var sutProvider = GetSutProvider(OneConfiguration(_templateWithActingUser)); var cache = sutProvider.GetDependency(); var organizationUserRepository = sutProvider.GetDependency(); eventMessage.OrganizationId ??= Guid.NewGuid(); eventMessage.ActingUserId ??= Guid.NewGuid(); organizationUserRepository.GetDetailsByOrganizationIdUserIdAsync( eventMessage.OrganizationId.Value, eventMessage.ActingUserId.Value).Returns(actingUser); // Capture the factory function passed to the cache Func, CancellationToken, Task>? capturedFactory = null; cache.GetOrSetAsync( key: Arg.Any(), factory: Arg.Do, CancellationToken, Task>>(f => capturedFactory = f) ).Returns(actingUser); await sutProvider.Sut.BuildContextAsync(eventMessage, _templateWithActingUser); Assert.NotNull(capturedFactory); var result = await capturedFactory(null!, CancellationToken.None); await organizationUserRepository.Received(1).GetDetailsByOrganizationIdUserIdAsync( eventMessage.OrganizationId.Value, eventMessage.ActingUserId.Value); Assert.Equal(actingUser, result); } [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_GroupFactory_CallsGroupRepository(EventMessage eventMessage, Group group) { var sutProvider = GetSutProvider(OneConfiguration(_templateWithGroup)); var cache = sutProvider.GetDependency(); var groupRepository = sutProvider.GetDependency(); eventMessage.GroupId ??= Guid.NewGuid(); groupRepository.GetByIdAsync(eventMessage.GroupId.Value).Returns(group); // Capture the factory function passed to the cache Func, CancellationToken, Task>? capturedFactory = null; cache.GetOrSetAsync( key: Arg.Any(), factory: Arg.Do, CancellationToken, Task>>(f => capturedFactory = f) ).Returns(group); await sutProvider.Sut.BuildContextAsync(eventMessage, _templateWithGroup); Assert.NotNull(capturedFactory); var result = await capturedFactory(null!, CancellationToken.None); await groupRepository.Received(1).GetByIdAsync(eventMessage.GroupId.Value); Assert.Equal(group, result); } [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_OrganizationFactory_CallsOrganizationRepository(EventMessage eventMessage, Organization organization) { var sutProvider = GetSutProvider(OneConfiguration(_templateWithOrganization)); var cache = sutProvider.GetDependency(); var organizationRepository = sutProvider.GetDependency(); eventMessage.OrganizationId ??= Guid.NewGuid(); organizationRepository.GetByIdAsync(eventMessage.OrganizationId.Value).Returns(organization); // Capture the factory function passed to the cache Func, CancellationToken, Task>? capturedFactory = null; cache.GetOrSetAsync( key: Arg.Any(), factory: Arg.Do, CancellationToken, Task>>(f => capturedFactory = f) ).Returns(organization); await sutProvider.Sut.BuildContextAsync(eventMessage, _templateWithOrganization); Assert.NotNull(capturedFactory); var result = await capturedFactory(null!, CancellationToken.None); await organizationRepository.Received(1).GetByIdAsync(eventMessage.OrganizationId.Value); Assert.Equal(organization, result); } [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_UserFactory_CallsOrganizationUserRepository(EventMessage eventMessage, OrganizationUserUserDetails userDetails) { var sutProvider = GetSutProvider(OneConfiguration(_templateWithUser)); var cache = sutProvider.GetDependency(); var organizationUserRepository = sutProvider.GetDependency(); eventMessage.OrganizationId ??= Guid.NewGuid(); eventMessage.UserId ??= Guid.NewGuid(); organizationUserRepository.GetDetailsByOrganizationIdUserIdAsync( eventMessage.OrganizationId.Value, eventMessage.UserId.Value).Returns(userDetails); // Capture the factory function passed to the cache Func, CancellationToken, Task>? capturedFactory = null; cache.GetOrSetAsync( key: Arg.Any(), factory: Arg.Do, CancellationToken, Task>>(f => capturedFactory = f) ).Returns(userDetails); await sutProvider.Sut.BuildContextAsync(eventMessage, _templateWithUser); Assert.NotNull(capturedFactory); var result = await capturedFactory(null!, CancellationToken.None); await organizationUserRepository.Received(1).GetDetailsByOrganizationIdUserIdAsync( eventMessage.OrganizationId.Value, eventMessage.UserId.Value); Assert.Equal(userDetails, result); } [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) { var sutProvider = GetSutProvider(NoConfigurations()); var cache = sutProvider.GetDependency(); cache.GetOrSetAsync>( Arg.Any(), Arg.Any>>>(), Arg.Any() ).Returns(NoConfigurations()); await sutProvider.Sut.HandleEventAsync(eventMessage); Assert.Empty(_eventIntegrationPublisher.ReceivedCalls()); } [Theory, BitAutoData] public async Task HandleEventAsync_NoOrganizationId_DoesNothing(EventMessage eventMessage) { var sutProvider = GetSutProvider(OneConfiguration(_templateBase)); eventMessage.OrganizationId = null; await sutProvider.Sut.HandleEventAsync(eventMessage); Assert.Empty(_eventIntegrationPublisher.ReceivedCalls()); } [Theory, BitAutoData] public async Task HandleEventAsync_BaseTemplateOneConfiguration_PublishesIntegrationMessage(EventMessage eventMessage) { eventMessage.OrganizationId = _organizationId; var sutProvider = GetSutProvider(OneConfiguration(_templateBase)); await sutProvider.Sut.HandleEventAsync(eventMessage); var expectedMessage = EventIntegrationHandlerTests.ExpectedMessage( $"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}" ); 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().DidNotReceiveWithAnyArgs().GetDetailsByOrganizationIdUserIdAsync(Arg.Any(), Arg.Any()); } [Theory, BitAutoData] public async Task HandleEventAsync_BaseTemplateTwoConfigurations_PublishesIntegrationMessages(EventMessage eventMessage) { eventMessage.OrganizationId = _organizationId; var sutProvider = GetSutProvider(TwoConfigurations(_templateBase)); await sutProvider.Sut.HandleEventAsync(eventMessage); var expectedMessage = EventIntegrationHandlerTests.ExpectedMessage( $"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}" ); await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is( AssertHelper.AssertPropertyEqual(expectedMessage, new[] { "MessageId" }))); expectedMessage.Configuration = new WebhookIntegrationConfigurationDetails(_uri2); 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().DidNotReceiveWithAnyArgs().GetDetailsByOrganizationIdUserIdAsync(Arg.Any(), Arg.Any()); } [Theory, BitAutoData] public async Task HandleEventAsync_FilterReturnsFalse_DoesNothing(EventMessage eventMessage) { eventMessage.OrganizationId = _organizationId; var sutProvider = GetSutProvider(ValidFilterConfiguration()); sutProvider.GetDependency().EvaluateFilterGroup( Arg.Any(), Arg.Any()).Returns(false); await sutProvider.Sut.HandleEventAsync(eventMessage); Assert.Empty(_eventIntegrationPublisher.ReceivedCalls()); } [Theory, BitAutoData] public async Task HandleEventAsync_FilterReturnsTrue_PublishesIntegrationMessage(EventMessage eventMessage) { eventMessage.OrganizationId = _organizationId; var sutProvider = GetSutProvider(ValidFilterConfiguration()); sutProvider.GetDependency().EvaluateFilterGroup( Arg.Any(), Arg.Any()).Returns(true); await sutProvider.Sut.HandleEventAsync(eventMessage); var expectedMessage = EventIntegrationHandlerTests.ExpectedMessage( $"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}" ); Assert.Single(_eventIntegrationPublisher.ReceivedCalls()); await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is( AssertHelper.AssertPropertyEqual(expectedMessage, new[] { "MessageId" }))); } [Theory, BitAutoData] public async Task HandleEventAsync_InvalidFilter_LogsErrorDoesNothing(EventMessage eventMessage) { eventMessage.OrganizationId = _organizationId; var sutProvider = GetSutProvider(InvalidFilterConfiguration()); await sutProvider.Sut.HandleEventAsync(eventMessage); Assert.Empty(_eventIntegrationPublisher.ReceivedCalls()); _logger.Received(1).Log( LogLevel.Error, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any>()); } [Theory, BitAutoData] public async Task HandleManyEventsAsync_BaseTemplateNoConfigurations_DoesNothing(List eventMessages) { eventMessages.ForEach(e => e.OrganizationId = _organizationId); var sutProvider = GetSutProvider(NoConfigurations()); await sutProvider.Sut.HandleManyEventsAsync(eventMessages); Assert.Empty(_eventIntegrationPublisher.ReceivedCalls()); } [Theory, BitAutoData] public async Task HandleManyEventsAsync_BaseTemplateOneConfiguration_PublishesIntegrationMessages(List eventMessages) { eventMessages.ForEach(e => e.OrganizationId = _organizationId); var sutProvider = GetSutProvider(OneConfiguration(_templateBase)); await sutProvider.Sut.HandleManyEventsAsync(eventMessages); foreach (var eventMessage in eventMessages) { var expectedMessage = ExpectedMessage( $"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}" ); await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is( AssertHelper.AssertPropertyEqual(expectedMessage, new[] { "MessageId", "OrganizationId" }))); } } [Theory, BitAutoData] public async Task HandleManyEventsAsync_BaseTemplateTwoConfigurations_PublishesIntegrationMessages( List eventMessages) { eventMessages.ForEach(e => e.OrganizationId = _organizationId); var sutProvider = GetSutProvider(TwoConfigurations(_templateBase)); await sutProvider.Sut.HandleManyEventsAsync(eventMessages); foreach (var eventMessage in eventMessages) { var expectedMessage = ExpectedMessage( $"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}" ); await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is(AssertHelper.AssertPropertyEqual( expectedMessage, new[] { "MessageId", "OrganizationId" }))); expectedMessage.Configuration = new WebhookIntegrationConfigurationDetails(_uri2); await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is(AssertHelper.AssertPropertyEqual( expectedMessage, new[] { "MessageId", "OrganizationId" }))); } } [Theory, BitAutoData] public async Task HandleEventAsync_CapturedFactories_CallConfigurationRepository(EventMessage eventMessage) { eventMessage.OrganizationId = _organizationId; var sutProvider = GetSutProvider(NoConfigurations()); var cache = sutProvider.GetDependency(); var configurationRepository = sutProvider.GetDependency(); var configs = OneConfiguration(_templateBase); configurationRepository.GetManyByEventTypeOrganizationIdIntegrationType(eventType: eventMessage.Type, organizationId: _organizationId, integrationType: IntegrationType.Webhook).Returns(configs); // Capture the factory function - there will be 1 call that returns both specific and wildcard matches Func>, CancellationToken, Task>>? capturedFactory = null; cache.GetOrSetAsync( key: Arg.Any(), factory: Arg.Do>, CancellationToken, Task>>>(f => capturedFactory = f), options: Arg.Any(), tags: Arg.Any>() ).Returns(new List()); await sutProvider.Sut.HandleEventAsync(eventMessage); // Verify factory was captured Assert.NotNull(capturedFactory); // Execute the captured factory to trigger repository call await capturedFactory(null!, CancellationToken.None); await configurationRepository.Received(1).GetManyByEventTypeOrganizationIdIntegrationType(eventType: eventMessage.Type, organizationId: _organizationId, integrationType: IntegrationType.Webhook); } [Theory, BitAutoData] public async Task HandleEventAsync_ConfigurationCacheOptions_SetsDurationToConstant(EventMessage eventMessage) { eventMessage.OrganizationId = _organizationId; var sutProvider = GetSutProvider(NoConfigurations()); var cache = sutProvider.GetDependency(); FusionCacheEntryOptions? capturedOption = null; cache.GetOrSetAsync( key: Arg.Any(), factory: Arg.Any>, CancellationToken, Task>>>(), options: Arg.Do(opt => capturedOption = opt), tags: Arg.Any?>() ).Returns(new List()); await sutProvider.Sut.HandleEventAsync(eventMessage); Assert.NotNull(capturedOption); Assert.Equal(EventIntegrationsCacheConstants.DurationForOrganizationIntegrationConfigurationDetails, capturedOption.Duration); } [Theory, BitAutoData] public async Task HandleEventAsync_ConfigurationCache_AddsOrganizationIntegrationTag(EventMessage eventMessage) { eventMessage.OrganizationId = _organizationId; var sutProvider = GetSutProvider(NoConfigurations()); var cache = sutProvider.GetDependency(); IEnumerable? capturedTags = null; cache.GetOrSetAsync( key: Arg.Any(), factory: Arg.Any>, CancellationToken, Task>>>(), options: Arg.Any(), tags: Arg.Do>(t => capturedTags = t) ).Returns(new List()); await sutProvider.Sut.HandleEventAsync(eventMessage); var expectedTag = EventIntegrationsCacheConstants.BuildCacheTagForOrganizationIntegration( _organizationId, IntegrationType.Webhook ); Assert.NotNull(capturedTags); Assert.Contains(expectedTag, capturedTags); } }