#nullable enable using System.Text.Json; using System.Text.Json.Nodes; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Dirt.Enums; using Bit.Core.Dirt.Models.Data.EventIntegrations; using Bit.Core.Dirt.Repositories; using Bit.Core.Dirt.Services; using Bit.Core.Dirt.Services.Implementations; using Bit.Core.Models.Data; using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Repositories; 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.Dirt.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); } [Theory, BitAutoData] public async Task HandleEventAsync_SubstituteTemplateTags(EventMessage eventMessage) { eventMessage.OrganizationId = _organizationId; var templateJson = @"{ ""bw_serviceName"": ""bitwarden"", ""ddsource"": ""bitwarden"", ""service"": ""event-logs"", ""event"": { ""object"": ""event"", ""type"": ""#TypeId#"", ""typeName"": ""#Type#"", ""userId"": ""#UserId#"", ""organizationId"": ""#OrganizationId#"", ""providerId"": ""#ProviderId#"", ""cipherId"": ""#CipherId#"", ""collectionId"": ""#CollectionId#"", ""groupId"": ""#GroupId#"", ""policyId"": ""#PolicyId#"", ""organizationUserId"": ""#OrganizationUserId#"", ""providerUserId"": ""#ProviderUserId#"", ""providerOrganizationId"": ""#ProviderOrganizationId#"", ""actingUserId"": ""#ActingUserId#"", ""installationId"": ""#InstallationId#"", ""date"": ""#DateIso8601#"", ""deviceType"": ""#DeviceType#"", ""deviceTypeId"": ""#DeviceTypeId#"", ""ipAddress"": ""#IpAddress#"", ""systemUser"": ""#SystemUser#"", ""domainName"": ""#DomainName#"", ""secretId"": ""#SecretId#"", ""projectId"": ""#ProjectId#"", ""serviceAccountId"": ""#ServiceAccountId#"" } }"; var sutProvider = GetSutProvider(OneConfiguration(templateJson)); await sutProvider.Sut.HandleEventAsync(eventMessage); var deviceTypeId = eventMessage.DeviceType is not null ? (int)eventMessage.DeviceType : (int?)null; var systemUser = eventMessage.SystemUser is not null ? (int)eventMessage.SystemUser : (int?)null; var parsedJson = $@"{{ ""bw_serviceName"": ""bitwarden"", ""ddsource"": ""bitwarden"", ""service"": ""event-logs"", ""event"": {{ ""object"": ""event"", ""type"": ""{(int)eventMessage.Type}"", ""typeName"": ""{eventMessage.Type}"", ""userId"": ""{eventMessage.UserId}"", ""organizationId"": ""{eventMessage.OrganizationId}"", ""providerId"": ""{eventMessage.ProviderId}"", ""cipherId"": ""{eventMessage.CipherId}"", ""collectionId"": ""{eventMessage.CollectionId}"", ""groupId"": ""{eventMessage.GroupId}"", ""policyId"": ""{eventMessage.PolicyId}"", ""organizationUserId"": ""{eventMessage.OrganizationUserId}"", ""providerUserId"": ""{eventMessage.ProviderUserId}"", ""providerOrganizationId"": ""{eventMessage.ProviderOrganizationId}"", ""actingUserId"": ""{eventMessage.ActingUserId}"", ""installationId"": ""{eventMessage.InstallationId}"", ""date"": ""{eventMessage.Date.ToString("o")}"", ""deviceType"": ""{eventMessage.DeviceType}"", ""deviceTypeId"": ""{deviceTypeId}"", ""ipAddress"": ""{eventMessage.IpAddress}"", ""systemUser"": ""{systemUser}"", ""domainName"": ""{eventMessage.DomainName}"", ""secretId"": ""{eventMessage.SecretId}"", ""projectId"": ""{eventMessage.ProjectId}"", ""serviceAccountId"": ""{eventMessage.ServiceAccountId}"" }} }}"; var expectedMessage = EventIntegrationHandlerTests.ExpectedMessage( parsedJson ); Assert.Single(_eventIntegrationPublisher.ReceivedCalls()); await _eventIntegrationPublisher.Received(1) .PublishAsync(Arg.Is(AssertHelper.AssertPropertyEqual(expectedMessage, new[] { "MessageId", "RenderedTemplate" }))); // compare renderedTemplate var receivedCalls = _eventIntegrationPublisher.ReceivedCalls().ToList(); Assert.Single(receivedCalls); var publishCall = receivedCalls.First(); var actualMessage = publishCall.GetArguments()[0] as IntegrationMessage; Assert.NotNull(actualMessage); Assert.True(JsonStringsAreEqual(expectedMessage.RenderedTemplate!, actualMessage.RenderedTemplate!), $"Expected: {expectedMessage.RenderedTemplate}\nActual: {actualMessage.RenderedTemplate}"); } private bool JsonStringsAreEqual(string expectedJson, string actualJson) { var expectedDoc = JsonNode.Parse(expectedJson); var actualDoc = JsonNode.Parse(actualJson); return JsonNode.DeepEquals(expectedDoc, actualDoc); } }