1
0
mirror of https://github.com/bitwarden/server synced 2026-02-24 16:42:52 +00:00
Files
server/test/Core.Test/Dirt/Services/EventIntegrationHandlerTests.cs

813 lines
38 KiB
C#

#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<IEventIntegrationPublisher>();
private readonly ILogger<EventIntegrationHandler<WebhookIntegrationConfigurationDetails>> _logger =
Substitute.For<ILogger<EventIntegrationHandler<WebhookIntegrationConfigurationDetails>>>();
private SutProvider<EventIntegrationHandler<WebhookIntegrationConfigurationDetails>> GetSutProvider(
List<OrganizationIntegrationConfigurationDetails> configurations)
{
var cache = Substitute.For<IFusionCache>();
cache.GetOrSetAsync(
key: Arg.Any<string>(),
factory: Arg.Any<Func<object, CancellationToken, Task<List<OrganizationIntegrationConfigurationDetails>>>>(),
options: Arg.Any<FusionCacheEntryOptions>(),
tags: Arg.Any<IEnumerable<string>>()
).Returns(configurations);
return new SutProvider<EventIntegrationHandler<WebhookIntegrationConfigurationDetails>>()
.SetDependency(cache)
.SetDependency(_eventIntegrationPublisher)
.SetDependency(IntegrationType.Webhook)
.SetDependency(_logger)
.Create();
}
private static IntegrationMessage<WebhookIntegrationConfigurationDetails> ExpectedMessage(string template)
{
return new IntegrationMessage<WebhookIntegrationConfigurationDetails>()
{
IntegrationType = IntegrationType.Webhook,
MessageId = "TestMessageId",
OrganizationId = _organizationId.ToString(),
Configuration = new WebhookIntegrationConfigurationDetails(_uri),
RenderedTemplate = template,
RetryCount = 0,
DelayUntilDate = null
};
}
private static List<OrganizationIntegrationConfigurationDetails> NoConfigurations()
{
return [];
}
private static List<OrganizationIntegrationConfigurationDetails> OneConfiguration(string template)
{
var config = Substitute.For<OrganizationIntegrationConfigurationDetails>();
config.Configuration = null;
config.IntegrationConfiguration = JsonSerializer.Serialize(new { Uri = _uri });
config.Template = template;
return [config];
}
private static List<OrganizationIntegrationConfigurationDetails> TwoConfigurations(string template)
{
var config = Substitute.For<OrganizationIntegrationConfigurationDetails>();
config.Configuration = null;
config.IntegrationConfiguration = JsonSerializer.Serialize(new { Uri = _uri });
config.Template = template;
var config2 = Substitute.For<OrganizationIntegrationConfigurationDetails>();
config2.Configuration = null;
config2.IntegrationConfiguration = JsonSerializer.Serialize(new { Uri = _uri2 });
config2.Template = template;
return [config, config2];
}
private static List<OrganizationIntegrationConfigurationDetails> InvalidFilterConfiguration()
{
var config = Substitute.For<OrganizationIntegrationConfigurationDetails>();
config.Configuration = null;
config.IntegrationConfiguration = JsonSerializer.Serialize(new { Uri = _uri });
config.Template = _templateBase;
config.Filters = "Invalid Configuration!";
return [config];
}
private static List<OrganizationIntegrationConfigurationDetails> ValidFilterConfiguration()
{
var config = Substitute.For<OrganizationIntegrationConfigurationDetails>();
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<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_ActingUserFactory_CallsOrganizationUserRepository(EventMessage eventMessage, OrganizationUserUserDetails actingUser)
{
var sutProvider = GetSutProvider(OneConfiguration(_templateWithActingUser));
var cache = sutProvider.GetDependency<IFusionCache>();
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
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<FusionCacheFactoryExecutionContext<OrganizationUserUserDetails?>, CancellationToken, Task<OrganizationUserUserDetails?>>? capturedFactory = null;
cache.GetOrSetAsync(
key: Arg.Any<string>(),
factory: Arg.Do<Func<FusionCacheFactoryExecutionContext<OrganizationUserUserDetails?>, CancellationToken, Task<OrganizationUserUserDetails?>>>(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<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_GroupFactory_CallsGroupRepository(EventMessage eventMessage, Group group)
{
var sutProvider = GetSutProvider(OneConfiguration(_templateWithGroup));
var cache = sutProvider.GetDependency<IFusionCache>();
var groupRepository = sutProvider.GetDependency<IGroupRepository>();
eventMessage.GroupId ??= Guid.NewGuid();
groupRepository.GetByIdAsync(eventMessage.GroupId.Value).Returns(group);
// Capture the factory function passed to the cache
Func<FusionCacheFactoryExecutionContext<Group?>, CancellationToken, Task<Group?>>? capturedFactory = null;
cache.GetOrSetAsync(
key: Arg.Any<string>(),
factory: Arg.Do<Func<FusionCacheFactoryExecutionContext<Group?>, CancellationToken, Task<Group?>>>(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<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_OrganizationFactory_CallsOrganizationRepository(EventMessage eventMessage, Organization organization)
{
var sutProvider = GetSutProvider(OneConfiguration(_templateWithOrganization));
var cache = sutProvider.GetDependency<IFusionCache>();
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
eventMessage.OrganizationId ??= Guid.NewGuid();
organizationRepository.GetByIdAsync(eventMessage.OrganizationId.Value).Returns(organization);
// Capture the factory function passed to the cache
Func<FusionCacheFactoryExecutionContext<Organization?>, CancellationToken, Task<Organization?>>? capturedFactory = null;
cache.GetOrSetAsync(
key: Arg.Any<string>(),
factory: Arg.Do<Func<FusionCacheFactoryExecutionContext<Organization?>, CancellationToken, Task<Organization?>>>(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<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_UserFactory_CallsOrganizationUserRepository(EventMessage eventMessage, OrganizationUserUserDetails userDetails)
{
var sutProvider = GetSutProvider(OneConfiguration(_templateWithUser));
var cache = sutProvider.GetDependency<IFusionCache>();
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
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<FusionCacheFactoryExecutionContext<OrganizationUserUserDetails?>, CancellationToken, Task<OrganizationUserUserDetails?>>? capturedFactory = null;
cache.GetOrSetAsync(
key: Arg.Any<string>(),
factory: Arg.Do<Func<FusionCacheFactoryExecutionContext<OrganizationUserUserDetails?>, CancellationToken, Task<OrganizationUserUserDetails?>>>(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<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)
{
var sutProvider = GetSutProvider(NoConfigurations());
var cache = sutProvider.GetDependency<IFusionCache>();
cache.GetOrSetAsync<List<OrganizationIntegrationConfigurationDetails>>(
Arg.Any<string>(),
Arg.Any<Func<object, CancellationToken, Task<List<OrganizationIntegrationConfigurationDetails>>>>(),
Arg.Any<FusionCacheEntryOptions>()
).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<IGroupRepository>().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any<Guid>());
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_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<IGroupRepository>().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any<Guid>());
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_FilterReturnsFalse_DoesNothing(EventMessage eventMessage)
{
eventMessage.OrganizationId = _organizationId;
var sutProvider = GetSutProvider(ValidFilterConfiguration());
sutProvider.GetDependency<IIntegrationFilterService>().EvaluateFilterGroup(
Arg.Any<IntegrationFilterGroup>(), Arg.Any<EventMessage>()).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<IIntegrationFilterService>().EvaluateFilterGroup(
Arg.Any<IntegrationFilterGroup>(), Arg.Any<EventMessage>()).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<EventId>(),
Arg.Any<object>(),
Arg.Any<JsonException>(),
Arg.Any<Func<object, Exception?, string>>());
}
[Theory, BitAutoData]
public async Task HandleManyEventsAsync_BaseTemplateNoConfigurations_DoesNothing(List<EventMessage> 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<EventMessage> 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<EventMessage> 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<IFusionCache>();
var configurationRepository = sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>();
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<FusionCacheFactoryExecutionContext<List<OrganizationIntegrationConfigurationDetails>>, CancellationToken, Task<List<OrganizationIntegrationConfigurationDetails>>>? capturedFactory = null;
cache.GetOrSetAsync(
key: Arg.Any<string>(),
factory: Arg.Do<Func<FusionCacheFactoryExecutionContext<List<OrganizationIntegrationConfigurationDetails>>, CancellationToken, Task<List<OrganizationIntegrationConfigurationDetails>>>>(f
=> capturedFactory = f),
options: Arg.Any<FusionCacheEntryOptions>(),
tags: Arg.Any<IEnumerable<string>>()
).Returns(new List<OrganizationIntegrationConfigurationDetails>());
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<IFusionCache>();
FusionCacheEntryOptions? capturedOption = null;
cache.GetOrSetAsync(
key: Arg.Any<string>(),
factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<List<OrganizationIntegrationConfigurationDetails>>, CancellationToken, Task<List<OrganizationIntegrationConfigurationDetails>>>>(),
options: Arg.Do<FusionCacheEntryOptions>(opt => capturedOption = opt),
tags: Arg.Any<IEnumerable<string>?>()
).Returns(new List<OrganizationIntegrationConfigurationDetails>());
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<IFusionCache>();
IEnumerable<string>? capturedTags = null;
cache.GetOrSetAsync(
key: Arg.Any<string>(),
factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<List<OrganizationIntegrationConfigurationDetails>>, CancellationToken, Task<List<OrganizationIntegrationConfigurationDetails>>>>(),
options: Arg.Any<FusionCacheEntryOptions>(),
tags: Arg.Do<IEnumerable<string>>(t => capturedTags = t)
).Returns(new List<OrganizationIntegrationConfigurationDetails>());
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<WebhookIntegrationConfigurationDetails>;
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);
}
}