1
0
mirror of https://github.com/bitwarden/server synced 2026-01-14 14:33:51 +00:00

Move all event integration code to Dirt (#6757)

* Move all event integration code to Dirt

* Format to fix lint
This commit is contained in:
Brant DeBow
2025-12-30 10:59:19 -05:00
committed by GitHub
parent 9a340c0fdd
commit 86a68ab637
158 changed files with 487 additions and 472 deletions

View File

@@ -0,0 +1,915 @@
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Dirt.EventIntegrations.OrganizationIntegrationConfigurations.Interfaces;
using Bit.Core.Dirt.EventIntegrations.OrganizationIntegrations.Interfaces;
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.Dirt.Services.NoopImplementations;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;
using Bit.Core.Test.Dirt.Models.Data.EventIntegrations;
using Bit.Core.Utilities;
using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Integration.AspNet.Core;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Hosting;
using NSubstitute;
using StackExchange.Redis;
using Xunit;
using ZiggyCreatures.Caching.Fusion;
namespace Bit.Core.Test.Dirt.EventIntegrations;
public class EventIntegrationServiceCollectionExtensionsTests
{
private readonly IServiceCollection _services;
private readonly GlobalSettings _globalSettings;
public EventIntegrationServiceCollectionExtensionsTests()
{
_services = new ServiceCollection();
_globalSettings = CreateGlobalSettings([]);
// Add required infrastructure services
_services.TryAddSingleton(_globalSettings);
_services.TryAddSingleton<IGlobalSettings>(_globalSettings);
_services.AddLogging();
// Mock Redis connection for cache
_services.AddSingleton(Substitute.For<IConnectionMultiplexer>());
// Mock required repository dependencies for commands
_services.TryAddScoped(_ => Substitute.For<IOrganizationIntegrationRepository>());
_services.TryAddScoped(_ => Substitute.For<IOrganizationIntegrationConfigurationRepository>());
_services.TryAddScoped(_ => Substitute.For<IOrganizationRepository>());
}
[Fact]
public void AddEventIntegrationsCommandsQueries_RegistersAllServices()
{
_services.AddEventIntegrationsCommandsQueries(_globalSettings);
using var provider = _services.BuildServiceProvider();
var cache = provider.GetRequiredKeyedService<IFusionCache>(EventIntegrationsCacheConstants.CacheName);
Assert.NotNull(cache);
var validator = provider.GetRequiredService<IOrganizationIntegrationConfigurationValidator>();
Assert.NotNull(validator);
using var scope = provider.CreateScope();
var sp = scope.ServiceProvider;
Assert.NotNull(sp.GetService<ICreateOrganizationIntegrationCommand>());
Assert.NotNull(sp.GetService<IUpdateOrganizationIntegrationCommand>());
Assert.NotNull(sp.GetService<IDeleteOrganizationIntegrationCommand>());
Assert.NotNull(sp.GetService<IGetOrganizationIntegrationsQuery>());
Assert.NotNull(sp.GetService<ICreateOrganizationIntegrationConfigurationCommand>());
Assert.NotNull(sp.GetService<IUpdateOrganizationIntegrationConfigurationCommand>());
Assert.NotNull(sp.GetService<IDeleteOrganizationIntegrationConfigurationCommand>());
Assert.NotNull(sp.GetService<IGetOrganizationIntegrationConfigurationsQuery>());
}
[Fact]
public void AddEventIntegrationsCommandsQueries_CommandsQueries_AreRegisteredAsScoped()
{
_services.AddEventIntegrationsCommandsQueries(_globalSettings);
var createIntegrationDescriptor = _services.First(s =>
s.ServiceType == typeof(ICreateOrganizationIntegrationCommand));
var createConfigDescriptor = _services.First(s =>
s.ServiceType == typeof(ICreateOrganizationIntegrationConfigurationCommand));
Assert.Equal(ServiceLifetime.Scoped, createIntegrationDescriptor.Lifetime);
Assert.Equal(ServiceLifetime.Scoped, createConfigDescriptor.Lifetime);
}
[Fact]
public void AddEventIntegrationsCommandsQueries_CommandsQueries_DifferentInstancesPerScope()
{
_services.AddEventIntegrationsCommandsQueries(_globalSettings);
var provider = _services.BuildServiceProvider();
ICreateOrganizationIntegrationCommand? instance1, instance2, instance3;
using (var scope1 = provider.CreateScope())
{
instance1 = scope1.ServiceProvider.GetService<ICreateOrganizationIntegrationCommand>();
}
using (var scope2 = provider.CreateScope())
{
instance2 = scope2.ServiceProvider.GetService<ICreateOrganizationIntegrationCommand>();
}
using (var scope3 = provider.CreateScope())
{
instance3 = scope3.ServiceProvider.GetService<ICreateOrganizationIntegrationCommand>();
}
Assert.NotNull(instance1);
Assert.NotNull(instance2);
Assert.NotNull(instance3);
Assert.NotSame(instance1, instance2);
Assert.NotSame(instance2, instance3);
Assert.NotSame(instance1, instance3);
}
[Fact]
public void AddEventIntegrationsCommandsQueries_CommandsQueries__SameInstanceWithinScope()
{
_services.AddEventIntegrationsCommandsQueries(_globalSettings);
var provider = _services.BuildServiceProvider();
using var scope = provider.CreateScope();
var instance1 = scope.ServiceProvider.GetService<ICreateOrganizationIntegrationCommand>();
var instance2 = scope.ServiceProvider.GetService<ICreateOrganizationIntegrationCommand>();
Assert.NotNull(instance1);
Assert.NotNull(instance2);
Assert.Same(instance1, instance2);
}
[Fact]
public void AddEventIntegrationsCommandsQueries_MultipleCalls_IsIdempotent()
{
_services.AddEventIntegrationsCommandsQueries(_globalSettings);
_services.AddEventIntegrationsCommandsQueries(_globalSettings);
_services.AddEventIntegrationsCommandsQueries(_globalSettings);
var createConfigCmdDescriptors = _services.Where(s =>
s.ServiceType == typeof(ICreateOrganizationIntegrationConfigurationCommand)).ToList();
Assert.Single(createConfigCmdDescriptors);
var updateIntegrationCmdDescriptors = _services.Where(s =>
s.ServiceType == typeof(IUpdateOrganizationIntegrationCommand)).ToList();
Assert.Single(updateIntegrationCmdDescriptors);
}
[Fact]
public void AddOrganizationIntegrationCommandsQueries_RegistersAllIntegrationServices()
{
_services.AddOrganizationIntegrationCommandsQueries();
Assert.Contains(_services, s => s.ServiceType == typeof(ICreateOrganizationIntegrationCommand));
Assert.Contains(_services, s => s.ServiceType == typeof(IUpdateOrganizationIntegrationCommand));
Assert.Contains(_services, s => s.ServiceType == typeof(IDeleteOrganizationIntegrationCommand));
Assert.Contains(_services, s => s.ServiceType == typeof(IGetOrganizationIntegrationsQuery));
}
[Fact]
public void AddOrganizationIntegrationCommandsQueries_MultipleCalls_IsIdempotent()
{
_services.AddOrganizationIntegrationCommandsQueries();
_services.AddOrganizationIntegrationCommandsQueries();
_services.AddOrganizationIntegrationCommandsQueries();
var createCmdDescriptors = _services.Where(s =>
s.ServiceType == typeof(ICreateOrganizationIntegrationCommand)).ToList();
Assert.Single(createCmdDescriptors);
}
[Fact]
public void AddOrganizationIntegrationConfigurationCommandsQueries_RegistersAllConfigurationServices()
{
_services.AddOrganizationIntegrationConfigurationCommandsQueries();
Assert.Contains(_services, s => s.ServiceType == typeof(ICreateOrganizationIntegrationConfigurationCommand));
Assert.Contains(_services, s => s.ServiceType == typeof(IUpdateOrganizationIntegrationConfigurationCommand));
Assert.Contains(_services, s => s.ServiceType == typeof(IDeleteOrganizationIntegrationConfigurationCommand));
Assert.Contains(_services, s => s.ServiceType == typeof(IGetOrganizationIntegrationConfigurationsQuery));
}
[Fact]
public void AddOrganizationIntegrationConfigurationCommandsQueries_MultipleCalls_IsIdempotent()
{
_services.AddOrganizationIntegrationConfigurationCommandsQueries();
_services.AddOrganizationIntegrationConfigurationCommandsQueries();
_services.AddOrganizationIntegrationConfigurationCommandsQueries();
var createCmdDescriptors = _services.Where(s =>
s.ServiceType == typeof(ICreateOrganizationIntegrationConfigurationCommand)).ToList();
Assert.Single(createCmdDescriptors);
}
[Fact]
public void IsRabbitMqEnabled_AllSettingsPresent_ReturnsTrue()
{
var globalSettings = CreateGlobalSettings(new Dictionary<string, string?>
{
["GlobalSettings:EventLogging:RabbitMq:HostName"] = "localhost",
["GlobalSettings:EventLogging:RabbitMq:Username"] = "user",
["GlobalSettings:EventLogging:RabbitMq:Password"] = "pass",
["GlobalSettings:EventLogging:RabbitMq:EventExchangeName"] = "exchange",
["GlobalSettings:EventLogging:RabbitMq:IntegrationExchangeName"] = "integration"
});
Assert.True(EventIntegrationsServiceCollectionExtensions.IsRabbitMqEnabled(globalSettings));
}
[Fact]
public void IsRabbitMqEnabled_MissingHostName_ReturnsFalse()
{
var globalSettings = CreateGlobalSettings(new Dictionary<string, string?>
{
["GlobalSettings:EventLogging:RabbitMq:HostName"] = null,
["GlobalSettings:EventLogging:RabbitMq:Username"] = "user",
["GlobalSettings:EventLogging:RabbitMq:Password"] = "pass",
["GlobalSettings:EventLogging:RabbitMq:EventExchangeName"] = "exchange",
["GlobalSettings:EventLogging:RabbitMq:IntegrationExchangeName"] = "integration"
});
Assert.False(EventIntegrationsServiceCollectionExtensions.IsRabbitMqEnabled(globalSettings));
}
[Fact]
public void IsRabbitMqEnabled_MissingUsername_ReturnsFalse()
{
var globalSettings = CreateGlobalSettings(new Dictionary<string, string?>
{
["GlobalSettings:EventLogging:RabbitMq:HostName"] = "localhost",
["GlobalSettings:EventLogging:RabbitMq:Username"] = null,
["GlobalSettings:EventLogging:RabbitMq:Password"] = "pass",
["GlobalSettings:EventLogging:RabbitMq:EventExchangeName"] = "exchange",
["GlobalSettings:EventLogging:RabbitMq:IntegrationExchangeName"] = "integration"
});
Assert.False(EventIntegrationsServiceCollectionExtensions.IsRabbitMqEnabled(globalSettings));
}
[Fact]
public void IsRabbitMqEnabled_MissingPassword_ReturnsFalse()
{
var globalSettings = CreateGlobalSettings(new Dictionary<string, string?>
{
["GlobalSettings:EventLogging:RabbitMq:HostName"] = "localhost",
["GlobalSettings:EventLogging:RabbitMq:Username"] = "user",
["GlobalSettings:EventLogging:RabbitMq:Password"] = null,
["GlobalSettings:EventLogging:RabbitMq:EventExchangeName"] = "exchange",
["GlobalSettings:EventLogging:RabbitMq:IntegrationExchangeName"] = "integration"
});
Assert.False(EventIntegrationsServiceCollectionExtensions.IsRabbitMqEnabled(globalSettings));
}
[Fact]
public void IsRabbitMqEnabled_MissingEventExchangeName_ReturnsFalse()
{
var globalSettings = CreateGlobalSettings(new Dictionary<string, string?>
{
["GlobalSettings:EventLogging:RabbitMq:HostName"] = "localhost",
["GlobalSettings:EventLogging:RabbitMq:Username"] = "user",
["GlobalSettings:EventLogging:RabbitMq:Password"] = "pass",
["GlobalSettings:EventLogging:RabbitMq:EventExchangeName"] = null,
["GlobalSettings:EventLogging:RabbitMq:IntegrationExchangeName"] = "integration"
});
Assert.False(EventIntegrationsServiceCollectionExtensions.IsRabbitMqEnabled(globalSettings));
}
[Fact]
public void IsRabbitMqEnabled_MissingIntegrationExchangeName_ReturnsFalse()
{
var globalSettings = CreateGlobalSettings(new Dictionary<string, string?>
{
["GlobalSettings:EventLogging:RabbitMq:HostName"] = "localhost",
["GlobalSettings:EventLogging:RabbitMq:Username"] = "user",
["GlobalSettings:EventLogging:RabbitMq:Password"] = "pass",
["GlobalSettings:EventLogging:RabbitMq:EventExchangeName"] = "exchange",
["GlobalSettings:EventLogging:RabbitMq:IntegrationExchangeName"] = null
});
Assert.False(EventIntegrationsServiceCollectionExtensions.IsRabbitMqEnabled(globalSettings));
}
[Fact]
public void IsAzureServiceBusEnabled_AllSettingsPresent_ReturnsTrue()
{
var globalSettings = CreateGlobalSettings(new Dictionary<string, string?>
{
["GlobalSettings:EventLogging:AzureServiceBus:ConnectionString"] = "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=test;SharedAccessKey=test",
["GlobalSettings:EventLogging:AzureServiceBus:EventTopicName"] = "events",
["GlobalSettings:EventLogging:AzureServiceBus:IntegrationTopicName"] = "integration"
});
Assert.True(EventIntegrationsServiceCollectionExtensions.IsAzureServiceBusEnabled(globalSettings));
}
[Fact]
public void IsAzureServiceBusEnabled_MissingConnectionString_ReturnsFalse()
{
var globalSettings = CreateGlobalSettings(new Dictionary<string, string?>
{
["GlobalSettings:EventLogging:AzureServiceBus:ConnectionString"] = null,
["GlobalSettings:EventLogging:AzureServiceBus:EventTopicName"] = "events",
["GlobalSettings:EventLogging:AzureServiceBus:IntegrationTopicName"] = "integration"
});
Assert.False(EventIntegrationsServiceCollectionExtensions.IsAzureServiceBusEnabled(globalSettings));
}
[Fact]
public void IsAzureServiceBusEnabled_MissingEventTopicName_ReturnsFalse()
{
var globalSettings = CreateGlobalSettings(new Dictionary<string, string?>
{
["GlobalSettings:EventLogging:AzureServiceBus:ConnectionString"] = "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=test;SharedAccessKey=test",
["GlobalSettings:EventLogging:AzureServiceBus:EventTopicName"] = null,
["GlobalSettings:EventLogging:AzureServiceBus:IntegrationTopicName"] = "integration"
});
Assert.False(EventIntegrationsServiceCollectionExtensions.IsAzureServiceBusEnabled(globalSettings));
}
[Fact]
public void IsAzureServiceBusEnabled_MissingIntegrationTopicName_ReturnsFalse()
{
var globalSettings = CreateGlobalSettings(new Dictionary<string, string?>
{
["GlobalSettings:EventLogging:AzureServiceBus:ConnectionString"] = "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=test;SharedAccessKey=test",
["GlobalSettings:EventLogging:AzureServiceBus:EventTopicName"] = "events",
["GlobalSettings:EventLogging:AzureServiceBus:IntegrationTopicName"] = null
});
Assert.False(EventIntegrationsServiceCollectionExtensions.IsAzureServiceBusEnabled(globalSettings));
}
[Fact]
public void AddSlackService_AllSettingsPresent_RegistersSlackService()
{
var services = new ServiceCollection();
var globalSettings = CreateGlobalSettings(new Dictionary<string, string?>
{
["GlobalSettings:Slack:ClientId"] = "test-client-id",
["GlobalSettings:Slack:ClientSecret"] = "test-client-secret",
["GlobalSettings:Slack:Scopes"] = "test-scopes"
});
services.TryAddSingleton(globalSettings);
services.AddLogging();
services.AddSlackService(globalSettings);
var provider = services.BuildServiceProvider();
var slackService = provider.GetService<ISlackService>();
Assert.NotNull(slackService);
Assert.IsType<SlackService>(slackService);
var httpClientDescriptor = services.FirstOrDefault(s =>
s.ServiceType == typeof(IHttpClientFactory));
Assert.NotNull(httpClientDescriptor);
}
[Fact]
public void AddSlackService_SettingsMissing_RegistersNoopService()
{
var services = new ServiceCollection();
var globalSettings = CreateGlobalSettings(new Dictionary<string, string?>
{
["GlobalSettings:Slack:ClientId"] = null,
["GlobalSettings:Slack:ClientSecret"] = null,
["GlobalSettings:Slack:Scopes"] = null
});
services.AddSlackService(globalSettings);
var provider = services.BuildServiceProvider();
var slackService = provider.GetService<ISlackService>();
Assert.NotNull(slackService);
Assert.IsType<NoopSlackService>(slackService);
}
[Fact]
public void AddTeamsService_AllSettingsPresent_RegistersTeamsServices()
{
var services = new ServiceCollection();
var globalSettings = CreateGlobalSettings(new Dictionary<string, string?>
{
["GlobalSettings:Teams:ClientId"] = "test-client-id",
["GlobalSettings:Teams:ClientSecret"] = "test-client-secret",
["GlobalSettings:Teams:Scopes"] = "test-scopes"
});
services.TryAddSingleton(globalSettings);
services.AddLogging();
services.TryAddScoped(_ => Substitute.For<IOrganizationIntegrationRepository>());
services.AddTeamsService(globalSettings);
var provider = services.BuildServiceProvider();
var teamsService = provider.GetService<ITeamsService>();
Assert.NotNull(teamsService);
Assert.IsType<TeamsService>(teamsService);
var bot = provider.GetService<IBot>();
Assert.NotNull(bot);
Assert.IsType<TeamsService>(bot);
var adapter = provider.GetService<IBotFrameworkHttpAdapter>();
Assert.NotNull(adapter);
Assert.IsType<BotFrameworkHttpAdapter>(adapter);
var httpClientDescriptor = services.FirstOrDefault(s =>
s.ServiceType == typeof(IHttpClientFactory));
Assert.NotNull(httpClientDescriptor);
}
[Fact]
public void AddTeamsService_SettingsMissing_RegistersNoopService()
{
var services = new ServiceCollection();
var globalSettings = CreateGlobalSettings(new Dictionary<string, string?>
{
["GlobalSettings:Teams:ClientId"] = null,
["GlobalSettings:Teams:ClientSecret"] = null,
["GlobalSettings:Teams:Scopes"] = null
});
services.AddTeamsService(globalSettings);
var provider = services.BuildServiceProvider();
var teamsService = provider.GetService<ITeamsService>();
Assert.NotNull(teamsService);
Assert.IsType<NoopTeamsService>(teamsService);
}
[Fact]
public void AddRabbitMqIntegration_RegistersEventIntegrationHandler()
{
var services = new ServiceCollection();
var listenerConfig = new TestListenerConfiguration();
// Add required dependencies
services.TryAddSingleton(Substitute.For<IEventIntegrationPublisher>());
services.TryAddSingleton(Substitute.For<IIntegrationFilterService>());
services.TryAddKeyedSingleton(EventIntegrationsCacheConstants.CacheName, Substitute.For<IFusionCache>());
services.TryAddSingleton(Substitute.For<IOrganizationIntegrationConfigurationRepository>());
services.TryAddSingleton(Substitute.For<IGroupRepository>());
services.TryAddSingleton(Substitute.For<IOrganizationRepository>());
services.TryAddSingleton(Substitute.For<IOrganizationUserRepository>());
services.AddLogging();
services.AddRabbitMqIntegration<WebhookIntegrationConfigurationDetails, TestListenerConfiguration>(listenerConfig);
var provider = services.BuildServiceProvider();
var handler = provider.GetRequiredKeyedService<IEventMessageHandler>(listenerConfig.RoutingKey);
Assert.NotNull(handler);
}
[Fact]
public void AddRabbitMqIntegration_RegistersEventListenerService()
{
var services = new ServiceCollection();
var listenerConfig = new TestListenerConfiguration();
// Add required dependencies
services.TryAddSingleton(Substitute.For<IEventIntegrationPublisher>());
services.TryAddSingleton(Substitute.For<IIntegrationFilterService>());
services.TryAddKeyedSingleton(EventIntegrationsCacheConstants.CacheName, Substitute.For<IFusionCache>());
services.TryAddSingleton(Substitute.For<IOrganizationIntegrationConfigurationRepository>());
services.TryAddSingleton(Substitute.For<IGroupRepository>());
services.TryAddSingleton(Substitute.For<IOrganizationRepository>());
services.TryAddSingleton(Substitute.For<IOrganizationUserRepository>());
services.TryAddSingleton(Substitute.For<IRabbitMqService>());
services.AddLogging();
var beforeCount = services.Count(s => s.ServiceType == typeof(IHostedService));
services.AddRabbitMqIntegration<WebhookIntegrationConfigurationDetails, TestListenerConfiguration>(listenerConfig);
var afterCount = services.Count(s => s.ServiceType == typeof(IHostedService));
// AddRabbitMqIntegration should register 2 hosted services (Event + Integration listeners)
Assert.Equal(2, afterCount - beforeCount);
}
[Fact]
public void AddRabbitMqIntegration_RegistersIntegrationListenerService()
{
var services = new ServiceCollection();
var listenerConfig = new TestListenerConfiguration();
// Add required dependencies
services.TryAddSingleton(Substitute.For<IEventIntegrationPublisher>());
services.TryAddSingleton(Substitute.For<IIntegrationFilterService>());
services.TryAddKeyedSingleton(EventIntegrationsCacheConstants.CacheName, Substitute.For<IFusionCache>());
services.TryAddSingleton(Substitute.For<IOrganizationIntegrationConfigurationRepository>());
services.TryAddSingleton(Substitute.For<IGroupRepository>());
services.TryAddSingleton(Substitute.For<IOrganizationRepository>());
services.TryAddSingleton(Substitute.For<IOrganizationUserRepository>());
services.TryAddSingleton(Substitute.For<IRabbitMqService>());
services.TryAddSingleton(Substitute.For<IIntegrationHandler<WebhookIntegrationConfigurationDetails>>());
services.TryAddSingleton(TimeProvider.System);
services.AddLogging();
var beforeCount = services.Count(s => s.ServiceType == typeof(IHostedService));
services.AddRabbitMqIntegration<WebhookIntegrationConfigurationDetails, TestListenerConfiguration>(listenerConfig);
var afterCount = services.Count(s => s.ServiceType == typeof(IHostedService));
// AddRabbitMqIntegration should register 2 hosted services (Event + Integration listeners)
Assert.Equal(2, afterCount - beforeCount);
}
[Fact]
public void AddAzureServiceBusIntegration_RegistersEventIntegrationHandler()
{
var services = new ServiceCollection();
var listenerConfig = new TestListenerConfiguration();
// Add required dependencies
services.TryAddSingleton(Substitute.For<IEventIntegrationPublisher>());
services.TryAddSingleton(Substitute.For<IIntegrationFilterService>());
services.TryAddKeyedSingleton(EventIntegrationsCacheConstants.CacheName, Substitute.For<IFusionCache>());
services.TryAddSingleton(Substitute.For<IOrganizationIntegrationConfigurationRepository>());
services.TryAddSingleton(Substitute.For<IGroupRepository>());
services.TryAddSingleton(Substitute.For<IOrganizationRepository>());
services.TryAddSingleton(Substitute.For<IOrganizationUserRepository>());
services.AddLogging();
services.AddAzureServiceBusIntegration<WebhookIntegrationConfigurationDetails, TestListenerConfiguration>(listenerConfig);
var provider = services.BuildServiceProvider();
var handler = provider.GetRequiredKeyedService<IEventMessageHandler>(listenerConfig.RoutingKey);
Assert.NotNull(handler);
}
[Fact]
public void AddAzureServiceBusIntegration_RegistersEventListenerService()
{
var services = new ServiceCollection();
var listenerConfig = new TestListenerConfiguration();
// Add required dependencies
services.TryAddSingleton(Substitute.For<IEventIntegrationPublisher>());
services.TryAddSingleton(Substitute.For<IIntegrationFilterService>());
services.TryAddKeyedSingleton(EventIntegrationsCacheConstants.CacheName, Substitute.For<IFusionCache>());
services.TryAddSingleton(Substitute.For<IOrganizationIntegrationConfigurationRepository>());
services.TryAddSingleton(Substitute.For<IGroupRepository>());
services.TryAddSingleton(Substitute.For<IOrganizationRepository>());
services.TryAddSingleton(Substitute.For<IOrganizationUserRepository>());
services.TryAddSingleton(Substitute.For<IAzureServiceBusService>());
services.AddLogging();
var beforeCount = services.Count(s => s.ServiceType == typeof(IHostedService));
services.AddAzureServiceBusIntegration<WebhookIntegrationConfigurationDetails, TestListenerConfiguration>(listenerConfig);
var afterCount = services.Count(s => s.ServiceType == typeof(IHostedService));
// AddAzureServiceBusIntegration should register 2 hosted services (Event + Integration listeners)
Assert.Equal(2, afterCount - beforeCount);
}
[Fact]
public void AddAzureServiceBusIntegration_RegistersIntegrationListenerService()
{
var services = new ServiceCollection();
var listenerConfig = new TestListenerConfiguration();
// Add required dependencies
services.TryAddSingleton(Substitute.For<IEventIntegrationPublisher>());
services.TryAddSingleton(Substitute.For<IIntegrationFilterService>());
services.TryAddKeyedSingleton(EventIntegrationsCacheConstants.CacheName, Substitute.For<IFusionCache>());
services.TryAddSingleton(Substitute.For<IOrganizationIntegrationConfigurationRepository>());
services.TryAddSingleton(Substitute.For<IGroupRepository>());
services.TryAddSingleton(Substitute.For<IOrganizationRepository>());
services.TryAddSingleton(Substitute.For<IOrganizationUserRepository>());
services.TryAddSingleton(Substitute.For<IAzureServiceBusService>());
services.TryAddSingleton(Substitute.For<IIntegrationHandler<WebhookIntegrationConfigurationDetails>>());
services.AddLogging();
var beforeCount = services.Count(s => s.ServiceType == typeof(IHostedService));
services.AddAzureServiceBusIntegration<WebhookIntegrationConfigurationDetails, TestListenerConfiguration>(listenerConfig);
var afterCount = services.Count(s => s.ServiceType == typeof(IHostedService));
// AddAzureServiceBusIntegration should register 2 hosted services (Event + Integration listeners)
Assert.Equal(2, afterCount - beforeCount);
}
[Fact]
public void AddEventIntegrationServices_RegistersCommonServices()
{
var services = new ServiceCollection();
var globalSettings = CreateGlobalSettings([]);
// Add prerequisites
services.TryAddSingleton(globalSettings);
services.TryAddSingleton(Substitute.For<IConnectionMultiplexer>());
services.AddLogging();
services.AddEventIntegrationServices(globalSettings);
// Verify common services are registered
Assert.Contains(services, s => s.ServiceType == typeof(IIntegrationFilterService));
Assert.Contains(services, s => s.ServiceType == typeof(TimeProvider));
// Verify HttpClients for handlers are registered
var httpClientDescriptors = services.Where(s => s.ServiceType == typeof(IHttpClientFactory)).ToList();
Assert.NotEmpty(httpClientDescriptors);
}
[Fact]
public void AddEventIntegrationServices_RegistersIntegrationHandlers()
{
var services = new ServiceCollection();
var globalSettings = CreateGlobalSettings([]);
// Add prerequisites
services.TryAddSingleton(globalSettings);
services.TryAddSingleton(Substitute.For<IConnectionMultiplexer>());
services.AddLogging();
services.AddEventIntegrationServices(globalSettings);
// Verify integration handlers are registered
Assert.Contains(services, s => s.ServiceType == typeof(IIntegrationHandler<SlackIntegrationConfigurationDetails>));
Assert.Contains(services, s => s.ServiceType == typeof(IIntegrationHandler<WebhookIntegrationConfigurationDetails>));
Assert.Contains(services, s => s.ServiceType == typeof(IIntegrationHandler<DatadogIntegrationConfigurationDetails>));
Assert.Contains(services, s => s.ServiceType == typeof(IIntegrationHandler<TeamsIntegrationConfigurationDetails>));
}
[Fact]
public void AddEventIntegrationServices_RabbitMqEnabled_RegistersRabbitMqListeners()
{
var services = new ServiceCollection();
var globalSettings = CreateGlobalSettings(new Dictionary<string, string?>
{
["GlobalSettings:EventLogging:RabbitMq:HostName"] = "localhost",
["GlobalSettings:EventLogging:RabbitMq:Username"] = "user",
["GlobalSettings:EventLogging:RabbitMq:Password"] = "pass",
["GlobalSettings:EventLogging:RabbitMq:EventExchangeName"] = "exchange",
["GlobalSettings:EventLogging:RabbitMq:IntegrationExchangeName"] = "integration"
});
// Add prerequisites
services.TryAddSingleton(globalSettings);
services.TryAddSingleton(Substitute.For<IConnectionMultiplexer>());
services.AddLogging();
var beforeCount = services.Count(s => s.ServiceType == typeof(IHostedService));
services.AddEventIntegrationServices(globalSettings);
var afterCount = services.Count(s => s.ServiceType == typeof(IHostedService));
// Should register 11 hosted services for RabbitMQ: 1 repository + 5*2 integration listeners (event+integration)
Assert.Equal(11, afterCount - beforeCount);
}
[Fact]
public void AddEventIntegrationServices_AzureServiceBusEnabled_RegistersAzureListeners()
{
var services = new ServiceCollection();
var globalSettings = CreateGlobalSettings(new Dictionary<string, string?>
{
["GlobalSettings:EventLogging:AzureServiceBus:ConnectionString"] = "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=test;SharedAccessKey=test",
["GlobalSettings:EventLogging:AzureServiceBus:EventTopicName"] = "events",
["GlobalSettings:EventLogging:AzureServiceBus:IntegrationTopicName"] = "integration"
});
// Add prerequisites
services.TryAddSingleton(globalSettings);
services.TryAddSingleton(Substitute.For<IConnectionMultiplexer>());
services.AddLogging();
var beforeCount = services.Count(s => s.ServiceType == typeof(IHostedService));
services.AddEventIntegrationServices(globalSettings);
var afterCount = services.Count(s => s.ServiceType == typeof(IHostedService));
// Should register 11 hosted services for Azure Service Bus: 1 repository + 5*2 integration listeners (event+integration)
Assert.Equal(11, afterCount - beforeCount);
}
[Fact]
public void AddEventIntegrationServices_BothEnabled_AzureServiceBusTakesPrecedence()
{
var services = new ServiceCollection();
var globalSettings = CreateGlobalSettings(new Dictionary<string, string?>
{
["GlobalSettings:EventLogging:RabbitMq:HostName"] = "localhost",
["GlobalSettings:EventLogging:RabbitMq:Username"] = "user",
["GlobalSettings:EventLogging:RabbitMq:Password"] = "pass",
["GlobalSettings:EventLogging:RabbitMq:EventExchangeName"] = "exchange",
["GlobalSettings:EventLogging:RabbitMq:IntegrationExchangeName"] = "integration",
["GlobalSettings:EventLogging:AzureServiceBus:ConnectionString"] = "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=test;SharedAccessKey=test",
["GlobalSettings:EventLogging:AzureServiceBus:EventTopicName"] = "events",
["GlobalSettings:EventLogging:AzureServiceBus:IntegrationTopicName"] = "integration"
});
// Add prerequisites
services.TryAddSingleton(globalSettings);
services.TryAddSingleton(Substitute.For<IConnectionMultiplexer>());
services.AddLogging();
var beforeCount = services.Count(s => s.ServiceType == typeof(IHostedService));
services.AddEventIntegrationServices(globalSettings);
var afterCount = services.Count(s => s.ServiceType == typeof(IHostedService));
// Should register 11 hosted services for Azure Service Bus: 1 repository + 5*2 integration listeners (event+integration)
// NO RabbitMQ services should be enabled because ASB takes precedence
Assert.Equal(11, afterCount - beforeCount);
}
[Fact]
public void AddEventIntegrationServices_NeitherEnabled_RegistersNoListeners()
{
var services = new ServiceCollection();
var globalSettings = CreateGlobalSettings([]);
// Add prerequisites
services.TryAddSingleton(globalSettings);
services.TryAddSingleton(Substitute.For<IConnectionMultiplexer>());
services.AddLogging();
var beforeCount = services.Count(s => s.ServiceType == typeof(IHostedService));
services.AddEventIntegrationServices(globalSettings);
var afterCount = services.Count(s => s.ServiceType == typeof(IHostedService));
// Should register no hosted services when neither RabbitMQ nor Azure Service Bus is enabled
Assert.Equal(0, afterCount - beforeCount);
}
[Fact]
public void AddEventWriteServices_AzureServiceBusEnabled_RegistersAzureServices()
{
var services = new ServiceCollection();
var globalSettings = CreateGlobalSettings(new Dictionary<string, string?>
{
["GlobalSettings:EventLogging:AzureServiceBus:ConnectionString"] = "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=test;SharedAccessKey=test",
["GlobalSettings:EventLogging:AzureServiceBus:EventTopicName"] = "events",
["GlobalSettings:EventLogging:AzureServiceBus:IntegrationTopicName"] = "integration"
});
services.AddEventWriteServices(globalSettings);
Assert.Contains(services, s => s.ServiceType == typeof(IEventIntegrationPublisher) && s.ImplementationType == typeof(AzureServiceBusService));
Assert.Contains(services, s => s.ServiceType == typeof(IEventWriteService) && s.ImplementationType == typeof(EventIntegrationEventWriteService));
}
[Fact]
public void AddEventWriteServices_RabbitMqEnabled_RegistersRabbitMqServices()
{
var services = new ServiceCollection();
var globalSettings = CreateGlobalSettings(new Dictionary<string, string?>
{
["GlobalSettings:EventLogging:RabbitMq:HostName"] = "localhost",
["GlobalSettings:EventLogging:RabbitMq:Username"] = "user",
["GlobalSettings:EventLogging:RabbitMq:Password"] = "pass",
["GlobalSettings:EventLogging:RabbitMq:EventExchangeName"] = "exchange",
["GlobalSettings:EventLogging:RabbitMq:IntegrationExchangeName"] = "integration"
});
services.AddEventWriteServices(globalSettings);
Assert.Contains(services, s => s.ServiceType == typeof(IEventIntegrationPublisher) && s.ImplementationType == typeof(RabbitMqService));
Assert.Contains(services, s => s.ServiceType == typeof(IEventWriteService) && s.ImplementationType == typeof(EventIntegrationEventWriteService));
}
[Fact]
public void AddEventWriteServices_EventsConnectionStringPresent_RegistersAzureQueueService()
{
var services = new ServiceCollection();
var globalSettings = CreateGlobalSettings(new Dictionary<string, string?>
{
["GlobalSettings:Events:ConnectionString"] = "DefaultEndpointsProtocol=https;AccountName=test;AccountKey=test;EndpointSuffix=core.windows.net",
["GlobalSettings:Events:QueueName"] = "event"
});
services.AddEventWriteServices(globalSettings);
Assert.Contains(services, s => s.ServiceType == typeof(IEventWriteService) && s.ImplementationType == typeof(AzureQueueEventWriteService));
}
[Fact]
public void AddEventWriteServices_SelfHosted_RegistersRepositoryService()
{
var services = new ServiceCollection();
var globalSettings = CreateGlobalSettings(new Dictionary<string, string?>
{
["GlobalSettings:SelfHosted"] = "true"
});
services.AddEventWriteServices(globalSettings);
Assert.Contains(services, s => s.ServiceType == typeof(IEventWriteService) && s.ImplementationType == typeof(RepositoryEventWriteService));
}
[Fact]
public void AddEventWriteServices_NothingEnabled_RegistersNoopService()
{
var services = new ServiceCollection();
var globalSettings = CreateGlobalSettings([]);
services.AddEventWriteServices(globalSettings);
Assert.Contains(services, s => s.ServiceType == typeof(IEventWriteService) && s.ImplementationType == typeof(NoopEventWriteService));
}
[Fact]
public void AddEventWriteServices_AzureTakesPrecedenceOverRabbitMq()
{
var services = new ServiceCollection();
var globalSettings = CreateGlobalSettings(new Dictionary<string, string?>
{
["GlobalSettings:EventLogging:AzureServiceBus:ConnectionString"] = "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=test;SharedAccessKey=test",
["GlobalSettings:EventLogging:AzureServiceBus:EventTopicName"] = "events",
["GlobalSettings:EventLogging:AzureServiceBus:IntegrationTopicName"] = "integration",
["GlobalSettings:EventLogging:RabbitMq:HostName"] = "localhost",
["GlobalSettings:EventLogging:RabbitMq:Username"] = "user",
["GlobalSettings:EventLogging:RabbitMq:Password"] = "pass",
["GlobalSettings:EventLogging:RabbitMq:EventExchangeName"] = "exchange",
["GlobalSettings:EventLogging:RabbitMq:IntegrationExchangeName"] = "integration"
});
services.AddEventWriteServices(globalSettings);
// Should use Azure Service Bus, not RabbitMQ
Assert.Contains(services, s => s.ServiceType == typeof(IEventIntegrationPublisher) && s.ImplementationType == typeof(AzureServiceBusService));
Assert.DoesNotContain(services, s => s.ServiceType == typeof(IEventIntegrationPublisher) && s.ImplementationType == typeof(RabbitMqService));
}
[Fact]
public void AddAzureServiceBusListeners_AzureServiceBusEnabled_RegistersAzureServiceBusServices()
{
var services = new ServiceCollection();
var globalSettings = CreateGlobalSettings(new Dictionary<string, string?>
{
["GlobalSettings:EventLogging:AzureServiceBus:ConnectionString"] = "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=test;SharedAccessKey=test",
["GlobalSettings:EventLogging:AzureServiceBus:EventTopicName"] = "events",
["GlobalSettings:EventLogging:AzureServiceBus:IntegrationTopicName"] = "integration"
});
// Add prerequisites
services.TryAddSingleton(globalSettings);
services.TryAddSingleton(Substitute.For<IConnectionMultiplexer>());
services.AddLogging();
services.AddAzureServiceBusListeners(globalSettings);
Assert.Contains(services, s => s.ServiceType == typeof(IAzureServiceBusService));
Assert.Contains(services, s => s.ServiceType == typeof(IEventRepository));
Assert.Contains(services, s => s.ServiceType == typeof(AzureTableStorageEventHandler));
}
[Fact]
public void AddAzureServiceBusListeners_AzureServiceBusDisabled_ReturnsEarly()
{
var services = new ServiceCollection();
var globalSettings = CreateGlobalSettings([]);
var initialCount = services.Count;
services.AddAzureServiceBusListeners(globalSettings);
var finalCount = services.Count;
Assert.Equal(initialCount, finalCount);
}
[Fact]
public void AddRabbitMqListeners_RabbitMqEnabled_RegistersRabbitMqServices()
{
var services = new ServiceCollection();
var globalSettings = CreateGlobalSettings(new Dictionary<string, string?>
{
["GlobalSettings:EventLogging:RabbitMq:HostName"] = "localhost",
["GlobalSettings:EventLogging:RabbitMq:Username"] = "user",
["GlobalSettings:EventLogging:RabbitMq:Password"] = "pass",
["GlobalSettings:EventLogging:RabbitMq:EventExchangeName"] = "exchange",
["GlobalSettings:EventLogging:RabbitMq:IntegrationExchangeName"] = "integration"
});
// Add prerequisites
services.TryAddSingleton(globalSettings);
services.TryAddSingleton(Substitute.For<IConnectionMultiplexer>());
services.AddLogging();
services.AddRabbitMqListeners(globalSettings);
Assert.Contains(services, s => s.ServiceType == typeof(IRabbitMqService));
Assert.Contains(services, s => s.ServiceType == typeof(EventRepositoryHandler));
}
[Fact]
public void AddRabbitMqListeners_RabbitMqDisabled_ReturnsEarly()
{
var services = new ServiceCollection();
var globalSettings = CreateGlobalSettings([]);
var initialCount = services.Count;
services.AddRabbitMqListeners(globalSettings);
var finalCount = services.Count;
Assert.Equal(initialCount, finalCount);
}
private static GlobalSettings CreateGlobalSettings(Dictionary<string, string?> data)
{
var config = new ConfigurationBuilder()
.AddInMemoryCollection(data)
.Build();
var settings = new GlobalSettings();
config.GetSection("GlobalSettings").Bind(settings);
return settings;
}
}

View File

@@ -0,0 +1,180 @@
using Bit.Core.Dirt.Entities;
using Bit.Core.Dirt.Enums;
using Bit.Core.Dirt.EventIntegrations.OrganizationIntegrationConfigurations;
using Bit.Core.Dirt.Repositories;
using Bit.Core.Dirt.Services;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Utilities;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
using ZiggyCreatures.Caching.Fusion;
namespace Bit.Core.Test.Dirt.EventIntegrations.OrganizationIntegrationConfigurations;
[SutProviderCustomize]
public class CreateOrganizationIntegrationConfigurationCommandTests
{
[Theory, BitAutoData]
public async Task CreateAsync_Success_CreatesConfigurationAndInvalidatesCache(
SutProvider<CreateOrganizationIntegrationConfigurationCommand> sutProvider,
Guid organizationId,
Guid integrationId,
OrganizationIntegration integration,
OrganizationIntegrationConfiguration configuration)
{
integration.Id = integrationId;
integration.OrganizationId = organizationId;
integration.Type = IntegrationType.Webhook;
configuration.OrganizationIntegrationId = integrationId;
configuration.EventType = EventType.User_LoggedIn;
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(integrationId)
.Returns(integration);
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
.CreateAsync(configuration)
.Returns(configuration);
sutProvider.GetDependency<IOrganizationIntegrationConfigurationValidator>()
.ValidateConfiguration(Arg.Any<IntegrationType>(), Arg.Any<OrganizationIntegrationConfiguration>())
.Returns(true);
var result = await sutProvider.Sut.CreateAsync(organizationId, integrationId, configuration);
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)
.GetByIdAsync(integrationId);
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)
.CreateAsync(configuration);
await sutProvider.GetDependency<IFusionCache>().Received(1)
.RemoveAsync(EventIntegrationsCacheConstants.BuildCacheKeyForOrganizationIntegrationConfigurationDetails(
organizationId,
integration.Type,
configuration.EventType.Value));
// Also verify RemoveByTagAsync was NOT called
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveByTagAsync(Arg.Any<string>());
Assert.Equal(configuration, result);
}
[Theory, BitAutoData]
public async Task CreateAsync_WildcardSuccess_CreatesConfigurationAndInvalidatesCache(
SutProvider<CreateOrganizationIntegrationConfigurationCommand> sutProvider,
Guid organizationId,
Guid integrationId,
OrganizationIntegration integration,
OrganizationIntegrationConfiguration configuration)
{
integration.Id = integrationId;
integration.OrganizationId = organizationId;
integration.Type = IntegrationType.Webhook;
configuration.OrganizationIntegrationId = integrationId;
configuration.EventType = null;
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(integrationId)
.Returns(integration);
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
.CreateAsync(configuration)
.Returns(configuration);
sutProvider.GetDependency<IOrganizationIntegrationConfigurationValidator>()
.ValidateConfiguration(Arg.Any<IntegrationType>(), Arg.Any<OrganizationIntegrationConfiguration>())
.Returns(true);
var result = await sutProvider.Sut.CreateAsync(organizationId, integrationId, configuration);
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)
.GetByIdAsync(integrationId);
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)
.CreateAsync(configuration);
await sutProvider.GetDependency<IFusionCache>().Received(1)
.RemoveByTagAsync(EventIntegrationsCacheConstants.BuildCacheTagForOrganizationIntegration(
organizationId,
integration.Type));
// Also verify RemoveAsync was NOT called
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveAsync(Arg.Any<string>());
Assert.Equal(configuration, result);
}
[Theory, BitAutoData]
public async Task CreateAsync_IntegrationDoesNotExist_ThrowsNotFound(
SutProvider<CreateOrganizationIntegrationConfigurationCommand> sutProvider,
Guid organizationId,
Guid integrationId,
OrganizationIntegrationConfiguration configuration)
{
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(integrationId)
.Returns((OrganizationIntegration)null);
await Assert.ThrowsAsync<NotFoundException>(
() => sutProvider.Sut.CreateAsync(organizationId, integrationId, configuration));
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().DidNotReceive()
.CreateAsync(Arg.Any<OrganizationIntegrationConfiguration>());
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveAsync(Arg.Any<string>());
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveByTagAsync(Arg.Any<string>());
}
[Theory, BitAutoData]
public async Task CreateAsync_IntegrationDoesNotBelongToOrganization_ThrowsNotFound(
SutProvider<CreateOrganizationIntegrationConfigurationCommand> sutProvider,
Guid organizationId,
Guid integrationId,
OrganizationIntegration integration,
OrganizationIntegrationConfiguration configuration)
{
integration.Id = integrationId;
integration.OrganizationId = Guid.NewGuid(); // Different organization
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(integrationId)
.Returns(integration);
await Assert.ThrowsAsync<NotFoundException>(
() => sutProvider.Sut.CreateAsync(organizationId, integrationId, configuration));
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().DidNotReceive()
.CreateAsync(Arg.Any<OrganizationIntegrationConfiguration>());
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveAsync(Arg.Any<string>());
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveByTagAsync(Arg.Any<string>());
}
[Theory, BitAutoData]
public async Task CreateAsync_ValidationFails_ThrowsBadRequest(
SutProvider<CreateOrganizationIntegrationConfigurationCommand> sutProvider,
Guid organizationId,
Guid integrationId,
OrganizationIntegration integration,
OrganizationIntegrationConfiguration configuration)
{
sutProvider.GetDependency<IOrganizationIntegrationConfigurationValidator>()
.ValidateConfiguration(Arg.Any<IntegrationType>(), Arg.Any<OrganizationIntegrationConfiguration>())
.Returns(false);
integration.Id = integrationId;
integration.OrganizationId = organizationId;
configuration.OrganizationIntegrationId = integrationId;
configuration.Template = "template";
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(integrationId)
.Returns(integration);
await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.CreateAsync(organizationId, integrationId, configuration));
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().DidNotReceive()
.CreateAsync(Arg.Any<OrganizationIntegrationConfiguration>());
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveAsync(Arg.Any<string>());
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveByTagAsync(Arg.Any<string>());
}
}

View File

@@ -0,0 +1,212 @@
using Bit.Core.Dirt.Entities;
using Bit.Core.Dirt.Enums;
using Bit.Core.Dirt.EventIntegrations.OrganizationIntegrationConfigurations;
using Bit.Core.Dirt.Repositories;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Utilities;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
using ZiggyCreatures.Caching.Fusion;
namespace Bit.Core.Test.Dirt.EventIntegrations.OrganizationIntegrationConfigurations;
[SutProviderCustomize]
public class DeleteOrganizationIntegrationConfigurationCommandTests
{
[Theory, BitAutoData]
public async Task DeleteAsync_Success_DeletesConfigurationAndInvalidatesCache(
SutProvider<DeleteOrganizationIntegrationConfigurationCommand> sutProvider,
Guid organizationId,
Guid integrationId,
Guid configurationId,
OrganizationIntegration integration,
OrganizationIntegrationConfiguration configuration)
{
integration.Id = integrationId;
integration.OrganizationId = organizationId;
integration.Type = IntegrationType.Webhook;
configuration.Id = configurationId;
configuration.OrganizationIntegrationId = integrationId;
configuration.EventType = EventType.User_LoggedIn;
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(integrationId)
.Returns(integration);
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
.GetByIdAsync(configurationId)
.Returns(configuration);
await sutProvider.Sut.DeleteAsync(organizationId, integrationId, configurationId);
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)
.GetByIdAsync(integrationId);
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)
.GetByIdAsync(configurationId);
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)
.DeleteAsync(configuration);
await sutProvider.GetDependency<IFusionCache>().Received(1)
.RemoveAsync(EventIntegrationsCacheConstants.BuildCacheKeyForOrganizationIntegrationConfigurationDetails(
organizationId,
integration.Type,
configuration.EventType.Value));
// Also verify RemoveByTagAsync was NOT called
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveByTagAsync(Arg.Any<string>());
}
[Theory, BitAutoData]
public async Task DeleteAsync_WildcardSuccess_DeletesConfigurationAndInvalidatesCache(
SutProvider<DeleteOrganizationIntegrationConfigurationCommand> sutProvider,
Guid organizationId,
Guid integrationId,
Guid configurationId,
OrganizationIntegration integration,
OrganizationIntegrationConfiguration configuration)
{
integration.Id = integrationId;
integration.OrganizationId = organizationId;
integration.Type = IntegrationType.Webhook;
configuration.Id = configurationId;
configuration.OrganizationIntegrationId = integrationId;
configuration.EventType = null;
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(integrationId)
.Returns(integration);
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
.GetByIdAsync(configurationId)
.Returns(configuration);
await sutProvider.Sut.DeleteAsync(organizationId, integrationId, configurationId);
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)
.GetByIdAsync(integrationId);
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)
.GetByIdAsync(configurationId);
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)
.DeleteAsync(configuration);
await sutProvider.GetDependency<IFusionCache>().Received(1)
.RemoveByTagAsync(EventIntegrationsCacheConstants.BuildCacheTagForOrganizationIntegration(
organizationId,
integration.Type));
// Also verify RemoveAsync was NOT called
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveAsync(Arg.Any<string>());
}
[Theory, BitAutoData]
public async Task DeleteAsync_IntegrationDoesNotExist_ThrowsNotFound(
SutProvider<DeleteOrganizationIntegrationConfigurationCommand> sutProvider,
Guid organizationId,
Guid integrationId,
Guid configurationId)
{
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(integrationId)
.Returns((OrganizationIntegration)null);
await Assert.ThrowsAsync<NotFoundException>(
() => sutProvider.Sut.DeleteAsync(organizationId, integrationId, configurationId));
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().DidNotReceive()
.GetByIdAsync(Arg.Any<Guid>());
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().DidNotReceive()
.DeleteAsync(Arg.Any<OrganizationIntegrationConfiguration>());
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveAsync(Arg.Any<string>());
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveByTagAsync(Arg.Any<string>());
}
[Theory, BitAutoData]
public async Task DeleteAsync_IntegrationDoesNotBelongToOrganization_ThrowsNotFound(
SutProvider<DeleteOrganizationIntegrationConfigurationCommand> sutProvider,
Guid organizationId,
Guid integrationId,
Guid configurationId,
OrganizationIntegration integration)
{
integration.Id = integrationId;
integration.OrganizationId = Guid.NewGuid(); // Different organization
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(integrationId)
.Returns(integration);
await Assert.ThrowsAsync<NotFoundException>(
() => sutProvider.Sut.DeleteAsync(organizationId, integrationId, configurationId));
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().DidNotReceive()
.GetByIdAsync(Arg.Any<Guid>());
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().DidNotReceive()
.DeleteAsync(Arg.Any<OrganizationIntegrationConfiguration>());
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveAsync(Arg.Any<string>());
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveByTagAsync(Arg.Any<string>());
}
[Theory, BitAutoData]
public async Task DeleteAsync_ConfigurationDoesNotExist_ThrowsNotFound(
SutProvider<DeleteOrganizationIntegrationConfigurationCommand> sutProvider,
Guid organizationId,
Guid integrationId,
Guid configurationId,
OrganizationIntegration integration)
{
integration.Id = integrationId;
integration.OrganizationId = organizationId;
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(integrationId)
.Returns(integration);
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
.GetByIdAsync(configurationId)
.Returns((OrganizationIntegrationConfiguration)null);
await Assert.ThrowsAsync<NotFoundException>(
() => sutProvider.Sut.DeleteAsync(organizationId, integrationId, configurationId));
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().DidNotReceive()
.DeleteAsync(Arg.Any<OrganizationIntegrationConfiguration>());
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveAsync(Arg.Any<string>());
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveByTagAsync(Arg.Any<string>());
}
[Theory, BitAutoData]
public async Task DeleteAsync_ConfigurationDoesNotBelongToIntegration_ThrowsNotFound(
SutProvider<DeleteOrganizationIntegrationConfigurationCommand> sutProvider,
Guid organizationId,
Guid integrationId,
Guid configurationId,
OrganizationIntegration integration,
OrganizationIntegrationConfiguration configuration)
{
integration.Id = integrationId;
integration.OrganizationId = organizationId;
configuration.Id = configurationId;
configuration.OrganizationIntegrationId = Guid.NewGuid(); // Different integration
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(integrationId)
.Returns(integration);
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
.GetByIdAsync(configurationId)
.Returns(configuration);
await Assert.ThrowsAsync<NotFoundException>(
() => sutProvider.Sut.DeleteAsync(organizationId, integrationId, configurationId));
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().DidNotReceive()
.DeleteAsync(Arg.Any<OrganizationIntegrationConfiguration>());
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveAsync(Arg.Any<string>());
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveByTagAsync(Arg.Any<string>());
}
}

View File

@@ -0,0 +1,101 @@
using Bit.Core.Dirt.Entities;
using Bit.Core.Dirt.EventIntegrations.OrganizationIntegrationConfigurations;
using Bit.Core.Dirt.Repositories;
using Bit.Core.Exceptions;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.Dirt.EventIntegrations.OrganizationIntegrationConfigurations;
[SutProviderCustomize]
public class GetOrganizationIntegrationConfigurationsQueryTests
{
[Theory, BitAutoData]
public async Task GetManyByIntegrationAsync_Success_ReturnsConfigurations(
SutProvider<GetOrganizationIntegrationConfigurationsQuery> sutProvider,
Guid organizationId,
Guid integrationId,
OrganizationIntegration integration,
List<OrganizationIntegrationConfiguration> configurations)
{
integration.Id = integrationId;
integration.OrganizationId = organizationId;
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(integrationId)
.Returns(integration);
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
.GetManyByIntegrationAsync(integrationId)
.Returns(configurations);
var result = await sutProvider.Sut.GetManyByIntegrationAsync(organizationId, integrationId);
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)
.GetByIdAsync(integrationId);
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)
.GetManyByIntegrationAsync(integrationId);
Assert.Equal(configurations.Count, result.Count);
}
[Theory, BitAutoData]
public async Task GetManyByIntegrationAsync_NoConfigurations_ReturnsEmptyList(
SutProvider<GetOrganizationIntegrationConfigurationsQuery> sutProvider,
Guid organizationId,
Guid integrationId,
OrganizationIntegration integration)
{
integration.Id = integrationId;
integration.OrganizationId = organizationId;
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(integrationId)
.Returns(integration);
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
.GetManyByIntegrationAsync(integrationId)
.Returns([]);
var result = await sutProvider.Sut.GetManyByIntegrationAsync(organizationId, integrationId);
Assert.Empty(result);
}
[Theory, BitAutoData]
public async Task GetManyByIntegrationAsync_IntegrationDoesNotExist_ThrowsNotFound(
SutProvider<GetOrganizationIntegrationConfigurationsQuery> sutProvider,
Guid organizationId,
Guid integrationId)
{
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(integrationId)
.Returns((OrganizationIntegration)null);
await Assert.ThrowsAsync<NotFoundException>(
() => sutProvider.Sut.GetManyByIntegrationAsync(organizationId, integrationId));
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().DidNotReceive()
.GetManyByIntegrationAsync(Arg.Any<Guid>());
}
[Theory, BitAutoData]
public async Task GetManyByIntegrationAsync_IntegrationDoesNotBelongToOrganization_ThrowsNotFound(
SutProvider<GetOrganizationIntegrationConfigurationsQuery> sutProvider,
Guid organizationId,
Guid integrationId,
OrganizationIntegration integration)
{
integration.Id = integrationId;
integration.OrganizationId = Guid.NewGuid(); // Different organization
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(integrationId)
.Returns(integration);
await Assert.ThrowsAsync<NotFoundException>(
() => sutProvider.Sut.GetManyByIntegrationAsync(organizationId, integrationId));
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().DidNotReceive()
.GetManyByIntegrationAsync(Arg.Any<Guid>());
}
}

View File

@@ -0,0 +1,391 @@
using Bit.Core.Dirt.Entities;
using Bit.Core.Dirt.Enums;
using Bit.Core.Dirt.EventIntegrations.OrganizationIntegrationConfigurations;
using Bit.Core.Dirt.Repositories;
using Bit.Core.Dirt.Services;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Utilities;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
using ZiggyCreatures.Caching.Fusion;
namespace Bit.Core.Test.Dirt.EventIntegrations.OrganizationIntegrationConfigurations;
[SutProviderCustomize]
public class UpdateOrganizationIntegrationConfigurationCommandTests
{
[Theory, BitAutoData]
public async Task UpdateAsync_Success_UpdatesConfigurationAndInvalidatesCache(
SutProvider<UpdateOrganizationIntegrationConfigurationCommand> sutProvider,
Guid organizationId,
Guid integrationId,
Guid configurationId,
OrganizationIntegration integration,
OrganizationIntegrationConfiguration existingConfiguration,
OrganizationIntegrationConfiguration updatedConfiguration)
{
integration.Id = integrationId;
integration.OrganizationId = organizationId;
integration.Type = IntegrationType.Webhook;
existingConfiguration.Id = configurationId;
existingConfiguration.OrganizationIntegrationId = integrationId;
existingConfiguration.EventType = EventType.User_LoggedIn;
updatedConfiguration.Id = configurationId;
updatedConfiguration.OrganizationIntegrationId = integrationId;
existingConfiguration.EventType = EventType.User_LoggedIn;
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(integrationId)
.Returns(integration);
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
.GetByIdAsync(configurationId)
.Returns(existingConfiguration);
sutProvider.GetDependency<IOrganizationIntegrationConfigurationValidator>()
.ValidateConfiguration(Arg.Any<IntegrationType>(), Arg.Any<OrganizationIntegrationConfiguration>())
.Returns(true);
var result = await sutProvider.Sut.UpdateAsync(organizationId, integrationId, configurationId, updatedConfiguration);
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)
.GetByIdAsync(integrationId);
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)
.GetByIdAsync(configurationId);
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)
.ReplaceAsync(updatedConfiguration);
await sutProvider.GetDependency<IFusionCache>().Received(1)
.RemoveAsync(EventIntegrationsCacheConstants.BuildCacheKeyForOrganizationIntegrationConfigurationDetails(
organizationId,
integration.Type,
existingConfiguration.EventType.Value));
// Also verify RemoveByTagAsync was NOT called
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveByTagAsync(Arg.Any<string>());
Assert.Equal(updatedConfiguration, result);
}
[Theory, BitAutoData]
public async Task UpdateAsync_WildcardSuccess_UpdatesConfigurationAndInvalidatesCache(
SutProvider<UpdateOrganizationIntegrationConfigurationCommand> sutProvider,
Guid organizationId,
Guid integrationId,
Guid configurationId,
OrganizationIntegration integration,
OrganizationIntegrationConfiguration existingConfiguration,
OrganizationIntegrationConfiguration updatedConfiguration)
{
integration.Id = integrationId;
integration.OrganizationId = organizationId;
integration.Type = IntegrationType.Webhook;
existingConfiguration.Id = configurationId;
existingConfiguration.OrganizationIntegrationId = integrationId;
existingConfiguration.EventType = null;
updatedConfiguration.Id = configurationId;
updatedConfiguration.OrganizationIntegrationId = integrationId;
updatedConfiguration.EventType = null;
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(integrationId)
.Returns(integration);
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
.GetByIdAsync(configurationId)
.Returns(existingConfiguration);
sutProvider.GetDependency<IOrganizationIntegrationConfigurationValidator>()
.ValidateConfiguration(Arg.Any<IntegrationType>(), Arg.Any<OrganizationIntegrationConfiguration>())
.Returns(true);
var result = await sutProvider.Sut.UpdateAsync(organizationId, integrationId, configurationId, updatedConfiguration);
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)
.GetByIdAsync(integrationId);
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)
.GetByIdAsync(configurationId);
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)
.ReplaceAsync(updatedConfiguration);
await sutProvider.GetDependency<IFusionCache>().Received(1)
.RemoveByTagAsync(EventIntegrationsCacheConstants.BuildCacheTagForOrganizationIntegration(
organizationId,
integration.Type));
// Also verify RemoveAsync was NOT called
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveAsync(Arg.Any<string>());
Assert.Equal(updatedConfiguration, result);
}
[Theory, BitAutoData]
public async Task UpdateAsync_ChangedEventType_UpdatesConfigurationAndInvalidatesCacheForBothTypes(
SutProvider<UpdateOrganizationIntegrationConfigurationCommand> sutProvider,
Guid organizationId,
Guid integrationId,
Guid configurationId,
OrganizationIntegration integration,
OrganizationIntegrationConfiguration existingConfiguration,
OrganizationIntegrationConfiguration updatedConfiguration)
{
integration.Id = integrationId;
integration.OrganizationId = organizationId;
integration.Type = IntegrationType.Webhook;
existingConfiguration.Id = configurationId;
existingConfiguration.OrganizationIntegrationId = integrationId;
existingConfiguration.EventType = EventType.User_LoggedIn;
updatedConfiguration.Id = configurationId;
updatedConfiguration.OrganizationIntegrationId = integrationId;
updatedConfiguration.EventType = EventType.Cipher_Created;
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(integrationId)
.Returns(integration);
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
.GetByIdAsync(configurationId)
.Returns(existingConfiguration);
sutProvider.GetDependency<IOrganizationIntegrationConfigurationValidator>()
.ValidateConfiguration(Arg.Any<IntegrationType>(), Arg.Any<OrganizationIntegrationConfiguration>())
.Returns(true);
var result = await sutProvider.Sut.UpdateAsync(organizationId, integrationId, configurationId, updatedConfiguration);
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)
.GetByIdAsync(integrationId);
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)
.GetByIdAsync(configurationId);
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)
.ReplaceAsync(updatedConfiguration);
await sutProvider.GetDependency<IFusionCache>().Received(1)
.RemoveAsync(EventIntegrationsCacheConstants.BuildCacheKeyForOrganizationIntegrationConfigurationDetails(
organizationId,
integration.Type,
existingConfiguration.EventType.Value));
await sutProvider.GetDependency<IFusionCache>().Received(1)
.RemoveAsync(EventIntegrationsCacheConstants.BuildCacheKeyForOrganizationIntegrationConfigurationDetails(
organizationId,
integration.Type,
updatedConfiguration.EventType.Value));
// Verify RemoveByTagAsync was NOT called since both are specific event types
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveByTagAsync(Arg.Any<string>());
Assert.Equal(updatedConfiguration, result);
}
[Theory, BitAutoData]
public async Task UpdateAsync_IntegrationDoesNotExist_ThrowsNotFound(
SutProvider<UpdateOrganizationIntegrationConfigurationCommand> sutProvider,
Guid organizationId,
Guid integrationId,
Guid configurationId,
OrganizationIntegrationConfiguration updatedConfiguration)
{
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(integrationId)
.Returns((OrganizationIntegration)null);
await Assert.ThrowsAsync<NotFoundException>(
() => sutProvider.Sut.UpdateAsync(organizationId, integrationId, configurationId, updatedConfiguration));
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().DidNotReceive()
.GetByIdAsync(Arg.Any<Guid>());
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().DidNotReceive()
.ReplaceAsync(Arg.Any<OrganizationIntegrationConfiguration>());
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveAsync(Arg.Any<string>());
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveByTagAsync(Arg.Any<string>());
}
[Theory, BitAutoData]
public async Task UpdateAsync_IntegrationDoesNotBelongToOrganization_ThrowsNotFound(
SutProvider<UpdateOrganizationIntegrationConfigurationCommand> sutProvider,
Guid organizationId,
Guid integrationId,
Guid configurationId,
OrganizationIntegration integration,
OrganizationIntegrationConfiguration updatedConfiguration)
{
integration.Id = integrationId;
integration.OrganizationId = Guid.NewGuid(); // Different organization
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(integrationId)
.Returns(integration);
await Assert.ThrowsAsync<NotFoundException>(
() => sutProvider.Sut.UpdateAsync(organizationId, integrationId, configurationId, updatedConfiguration));
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().DidNotReceive()
.GetByIdAsync(Arg.Any<Guid>());
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().DidNotReceive()
.ReplaceAsync(Arg.Any<OrganizationIntegrationConfiguration>());
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveAsync(Arg.Any<string>());
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveByTagAsync(Arg.Any<string>());
}
[Theory, BitAutoData]
public async Task UpdateAsync_ConfigurationDoesNotExist_ThrowsNotFound(
SutProvider<UpdateOrganizationIntegrationConfigurationCommand> sutProvider,
Guid organizationId,
Guid integrationId,
Guid configurationId,
OrganizationIntegration integration,
OrganizationIntegrationConfiguration updatedConfiguration)
{
integration.Id = integrationId;
integration.OrganizationId = organizationId;
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(integrationId)
.Returns(integration);
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
.GetByIdAsync(configurationId)
.Returns((OrganizationIntegrationConfiguration)null);
await Assert.ThrowsAsync<NotFoundException>(
() => sutProvider.Sut.UpdateAsync(organizationId, integrationId, configurationId, updatedConfiguration));
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().DidNotReceive()
.ReplaceAsync(Arg.Any<OrganizationIntegrationConfiguration>());
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveAsync(Arg.Any<string>());
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveByTagAsync(Arg.Any<string>());
}
[Theory, BitAutoData]
public async Task UpdateAsync_ConfigurationDoesNotBelongToIntegration_ThrowsNotFound(
SutProvider<UpdateOrganizationIntegrationConfigurationCommand> sutProvider,
Guid organizationId,
Guid integrationId,
Guid configurationId,
OrganizationIntegration integration,
OrganizationIntegrationConfiguration existingConfiguration,
OrganizationIntegrationConfiguration updatedConfiguration)
{
integration.Id = integrationId;
integration.OrganizationId = organizationId;
existingConfiguration.Id = configurationId;
existingConfiguration.OrganizationIntegrationId = Guid.NewGuid(); // Different integration
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(integrationId)
.Returns(integration);
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
.GetByIdAsync(configurationId)
.Returns(existingConfiguration);
await Assert.ThrowsAsync<NotFoundException>(
() => sutProvider.Sut.UpdateAsync(organizationId, integrationId, configurationId, updatedConfiguration));
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().DidNotReceive()
.ReplaceAsync(Arg.Any<OrganizationIntegrationConfiguration>());
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveAsync(Arg.Any<string>());
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveByTagAsync(Arg.Any<string>());
}
[Theory, BitAutoData]
public async Task UpdateAsync_ValidationFails_ThrowsBadRequest(
SutProvider<UpdateOrganizationIntegrationConfigurationCommand> sutProvider,
Guid organizationId,
Guid integrationId,
Guid configurationId,
OrganizationIntegration integration,
OrganizationIntegrationConfiguration existingConfiguration,
OrganizationIntegrationConfiguration updatedConfiguration)
{
sutProvider.GetDependency<IOrganizationIntegrationConfigurationValidator>()
.ValidateConfiguration(Arg.Any<IntegrationType>(), Arg.Any<OrganizationIntegrationConfiguration>())
.Returns(false);
integration.Id = integrationId;
integration.OrganizationId = organizationId;
existingConfiguration.Id = configurationId;
existingConfiguration.OrganizationIntegrationId = integrationId;
updatedConfiguration.Id = configurationId;
updatedConfiguration.OrganizationIntegrationId = integrationId;
updatedConfiguration.Template = "template";
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(integrationId)
.Returns(integration);
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
.GetByIdAsync(configurationId)
.Returns(existingConfiguration);
await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.UpdateAsync(organizationId, integrationId, configurationId, updatedConfiguration));
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().DidNotReceive()
.ReplaceAsync(Arg.Any<OrganizationIntegrationConfiguration>());
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveAsync(Arg.Any<string>());
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveByTagAsync(Arg.Any<string>());
}
[Theory, BitAutoData]
public async Task UpdateAsync_ChangedFromWildcardToSpecific_InvalidatesAllCaches(
Guid organizationId,
Guid integrationId,
OrganizationIntegration integration,
OrganizationIntegrationConfiguration existingConfiguration,
OrganizationIntegrationConfiguration updatedConfiguration,
SutProvider<UpdateOrganizationIntegrationConfigurationCommand> sutProvider)
{
integration.OrganizationId = organizationId;
existingConfiguration.OrganizationIntegrationId = integrationId;
existingConfiguration.EventType = null; // Wildcard
updatedConfiguration.EventType = EventType.User_LoggedIn; // Specific
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(integrationId).Returns(integration);
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
.GetByIdAsync(existingConfiguration.Id).Returns(existingConfiguration);
sutProvider.GetDependency<IOrganizationIntegrationConfigurationValidator>()
.ValidateConfiguration(Arg.Any<IntegrationType>(), Arg.Any<OrganizationIntegrationConfiguration>())
.Returns(true);
await sutProvider.Sut.UpdateAsync(organizationId, integrationId, existingConfiguration.Id, updatedConfiguration);
await sutProvider.GetDependency<IFusionCache>().Received(1)
.RemoveByTagAsync(EventIntegrationsCacheConstants.BuildCacheTagForOrganizationIntegration(
organizationId,
integration.Type));
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveAsync(Arg.Any<string>());
}
[Theory, BitAutoData]
public async Task UpdateAsync_ChangedFromSpecificToWildcard_InvalidatesAllCaches(
Guid organizationId,
Guid integrationId,
OrganizationIntegration integration,
OrganizationIntegrationConfiguration existingConfiguration,
OrganizationIntegrationConfiguration updatedConfiguration,
SutProvider<UpdateOrganizationIntegrationConfigurationCommand> sutProvider)
{
integration.OrganizationId = organizationId;
existingConfiguration.OrganizationIntegrationId = integrationId;
existingConfiguration.EventType = EventType.User_LoggedIn; // Specific
updatedConfiguration.EventType = null; // Wildcard
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(integrationId).Returns(integration);
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
.GetByIdAsync(existingConfiguration.Id).Returns(existingConfiguration);
sutProvider.GetDependency<IOrganizationIntegrationConfigurationValidator>()
.ValidateConfiguration(Arg.Any<IntegrationType>(), Arg.Any<OrganizationIntegrationConfiguration>())
.Returns(true);
await sutProvider.Sut.UpdateAsync(organizationId, integrationId, existingConfiguration.Id, updatedConfiguration);
await sutProvider.GetDependency<IFusionCache>().Received(1)
.RemoveByTagAsync(EventIntegrationsCacheConstants.BuildCacheTagForOrganizationIntegration(
organizationId,
integration.Type));
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveAsync(Arg.Any<string>());
}
}

View File

@@ -0,0 +1,92 @@
using Bit.Core.Dirt.Entities;
using Bit.Core.Dirt.Enums;
using Bit.Core.Dirt.EventIntegrations.OrganizationIntegrations;
using Bit.Core.Dirt.Repositories;
using Bit.Core.Exceptions;
using Bit.Core.Utilities;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
using ZiggyCreatures.Caching.Fusion;
namespace Bit.Core.Test.Dirt.EventIntegrations.OrganizationIntegrations;
[SutProviderCustomize]
public class CreateOrganizationIntegrationCommandTests
{
[Theory, BitAutoData]
public async Task CreateAsync_Success_CreatesIntegrationAndInvalidatesCache(
SutProvider<CreateOrganizationIntegrationCommand> sutProvider,
OrganizationIntegration integration)
{
integration.Type = IntegrationType.Webhook;
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetManyByOrganizationAsync(integration.OrganizationId)
.Returns([]);
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.CreateAsync(integration)
.Returns(integration);
var result = await sutProvider.Sut.CreateAsync(integration);
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)
.GetManyByOrganizationAsync(integration.OrganizationId);
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)
.CreateAsync(integration);
await sutProvider.GetDependency<IFusionCache>().Received(1)
.RemoveByTagAsync(EventIntegrationsCacheConstants.BuildCacheTagForOrganizationIntegration(
integration.OrganizationId,
integration.Type));
Assert.Equal(integration, result);
}
[Theory, BitAutoData]
public async Task CreateAsync_DuplicateType_ThrowsBadRequest(
SutProvider<CreateOrganizationIntegrationCommand> sutProvider,
OrganizationIntegration integration,
OrganizationIntegration existingIntegration)
{
integration.Type = IntegrationType.Webhook;
existingIntegration.Type = IntegrationType.Webhook;
existingIntegration.OrganizationId = integration.OrganizationId;
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetManyByOrganizationAsync(integration.OrganizationId)
.Returns([existingIntegration]);
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.CreateAsync(integration));
Assert.Contains("An integration of this type already exists", exception.Message);
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().DidNotReceive()
.CreateAsync(Arg.Any<OrganizationIntegration>());
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveByTagAsync(Arg.Any<string>());
}
[Theory, BitAutoData]
public async Task CreateAsync_DifferentType_Success(
SutProvider<CreateOrganizationIntegrationCommand> sutProvider,
OrganizationIntegration integration,
OrganizationIntegration existingIntegration)
{
integration.Type = IntegrationType.Webhook;
existingIntegration.Type = IntegrationType.Slack;
existingIntegration.OrganizationId = integration.OrganizationId;
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetManyByOrganizationAsync(integration.OrganizationId)
.Returns([existingIntegration]);
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.CreateAsync(integration)
.Returns(integration);
var result = await sutProvider.Sut.CreateAsync(integration);
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)
.CreateAsync(integration);
Assert.Equal(integration, result);
}
}

View File

@@ -0,0 +1,86 @@
using Bit.Core.Dirt.Entities;
using Bit.Core.Dirt.Enums;
using Bit.Core.Dirt.EventIntegrations.OrganizationIntegrations;
using Bit.Core.Dirt.Repositories;
using Bit.Core.Exceptions;
using Bit.Core.Utilities;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
using ZiggyCreatures.Caching.Fusion;
namespace Bit.Core.Test.Dirt.EventIntegrations.OrganizationIntegrations;
[SutProviderCustomize]
public class DeleteOrganizationIntegrationCommandTests
{
[Theory, BitAutoData]
public async Task DeleteAsync_Success_DeletesIntegrationAndInvalidatesCache(
SutProvider<DeleteOrganizationIntegrationCommand> sutProvider,
Guid organizationId,
Guid integrationId,
OrganizationIntegration integration)
{
integration.Id = integrationId;
integration.OrganizationId = organizationId;
integration.Type = IntegrationType.Webhook;
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(integrationId)
.Returns(integration);
await sutProvider.Sut.DeleteAsync(organizationId, integrationId);
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)
.GetByIdAsync(integrationId);
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)
.DeleteAsync(integration);
await sutProvider.GetDependency<IFusionCache>().Received(1)
.RemoveByTagAsync(EventIntegrationsCacheConstants.BuildCacheTagForOrganizationIntegration(
organizationId,
integration.Type));
}
[Theory, BitAutoData]
public async Task DeleteAsync_IntegrationDoesNotExist_ThrowsNotFound(
SutProvider<DeleteOrganizationIntegrationCommand> sutProvider,
Guid organizationId,
Guid integrationId)
{
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(integrationId)
.Returns((OrganizationIntegration)null);
await Assert.ThrowsAsync<NotFoundException>(
() => sutProvider.Sut.DeleteAsync(organizationId, integrationId));
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().DidNotReceive()
.DeleteAsync(Arg.Any<OrganizationIntegration>());
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveByTagAsync(Arg.Any<string>());
}
[Theory, BitAutoData]
public async Task DeleteAsync_IntegrationDoesNotBelongToOrganization_ThrowsNotFound(
SutProvider<DeleteOrganizationIntegrationCommand> sutProvider,
Guid organizationId,
Guid integrationId,
OrganizationIntegration integration)
{
integration.Id = integrationId;
integration.OrganizationId = Guid.NewGuid(); // Different organization
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(integrationId)
.Returns(integration);
await Assert.ThrowsAsync<NotFoundException>(
() => sutProvider.Sut.DeleteAsync(organizationId, integrationId));
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().DidNotReceive()
.DeleteAsync(Arg.Any<OrganizationIntegration>());
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveByTagAsync(Arg.Any<string>());
}
}

View File

@@ -0,0 +1,44 @@
using Bit.Core.Dirt.Entities;
using Bit.Core.Dirt.EventIntegrations.OrganizationIntegrations;
using Bit.Core.Dirt.Repositories;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.Dirt.EventIntegrations.OrganizationIntegrations;
[SutProviderCustomize]
public class GetOrganizationIntegrationsQueryTests
{
[Theory, BitAutoData]
public async Task GetManyByOrganizationAsync_CallsRepository(
SutProvider<GetOrganizationIntegrationsQuery> sutProvider,
Guid organizationId,
List<OrganizationIntegration> integrations)
{
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetManyByOrganizationAsync(organizationId)
.Returns(integrations);
var result = await sutProvider.Sut.GetManyByOrganizationAsync(organizationId);
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)
.GetManyByOrganizationAsync(organizationId);
Assert.Equal(integrations.Count, result.Count);
}
[Theory, BitAutoData]
public async Task GetManyByOrganizationAsync_NoIntegrations_ReturnsEmptyList(
SutProvider<GetOrganizationIntegrationsQuery> sutProvider,
Guid organizationId)
{
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetManyByOrganizationAsync(organizationId)
.Returns([]);
var result = await sutProvider.Sut.GetManyByOrganizationAsync(organizationId);
Assert.Empty(result);
}
}

View File

@@ -0,0 +1,121 @@
using Bit.Core.Dirt.Entities;
using Bit.Core.Dirt.Enums;
using Bit.Core.Dirt.EventIntegrations.OrganizationIntegrations;
using Bit.Core.Dirt.Repositories;
using Bit.Core.Exceptions;
using Bit.Core.Utilities;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
using ZiggyCreatures.Caching.Fusion;
namespace Bit.Core.Test.Dirt.EventIntegrations.OrganizationIntegrations;
[SutProviderCustomize]
public class UpdateOrganizationIntegrationCommandTests
{
[Theory, BitAutoData]
public async Task UpdateAsync_Success_UpdatesIntegrationAndInvalidatesCache(
SutProvider<UpdateOrganizationIntegrationCommand> sutProvider,
Guid organizationId,
Guid integrationId,
OrganizationIntegration existingIntegration,
OrganizationIntegration updatedIntegration)
{
existingIntegration.Id = integrationId;
existingIntegration.OrganizationId = organizationId;
existingIntegration.Type = IntegrationType.Webhook;
updatedIntegration.Id = integrationId;
updatedIntegration.OrganizationId = organizationId;
updatedIntegration.Type = IntegrationType.Webhook;
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(integrationId)
.Returns(existingIntegration);
var result = await sutProvider.Sut.UpdateAsync(organizationId, integrationId, updatedIntegration);
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)
.GetByIdAsync(integrationId);
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)
.ReplaceAsync(updatedIntegration);
await sutProvider.GetDependency<IFusionCache>().Received(1)
.RemoveByTagAsync(EventIntegrationsCacheConstants.BuildCacheTagForOrganizationIntegration(
organizationId,
existingIntegration.Type));
Assert.Equal(updatedIntegration, result);
}
[Theory, BitAutoData]
public async Task UpdateAsync_IntegrationDoesNotExist_ThrowsNotFound(
SutProvider<UpdateOrganizationIntegrationCommand> sutProvider,
Guid organizationId,
Guid integrationId,
OrganizationIntegration updatedIntegration)
{
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(integrationId)
.Returns((OrganizationIntegration)null);
await Assert.ThrowsAsync<NotFoundException>(
() => sutProvider.Sut.UpdateAsync(organizationId, integrationId, updatedIntegration));
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().DidNotReceive()
.ReplaceAsync(Arg.Any<OrganizationIntegration>());
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveByTagAsync(Arg.Any<string>());
}
[Theory, BitAutoData]
public async Task UpdateAsync_IntegrationDoesNotBelongToOrganization_ThrowsNotFound(
SutProvider<UpdateOrganizationIntegrationCommand> sutProvider,
Guid organizationId,
Guid integrationId,
OrganizationIntegration existingIntegration,
OrganizationIntegration updatedIntegration)
{
existingIntegration.Id = integrationId;
existingIntegration.OrganizationId = Guid.NewGuid(); // Different organization
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(integrationId)
.Returns(existingIntegration);
await Assert.ThrowsAsync<NotFoundException>(
() => sutProvider.Sut.UpdateAsync(organizationId, integrationId, updatedIntegration));
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().DidNotReceive()
.ReplaceAsync(Arg.Any<OrganizationIntegration>());
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveByTagAsync(Arg.Any<string>());
}
[Theory, BitAutoData]
public async Task UpdateAsync_IntegrationIsDifferentType_ThrowsNotFound(
SutProvider<UpdateOrganizationIntegrationCommand> sutProvider,
Guid organizationId,
Guid integrationId,
OrganizationIntegration existingIntegration,
OrganizationIntegration updatedIntegration)
{
existingIntegration.Id = integrationId;
existingIntegration.OrganizationId = organizationId;
existingIntegration.Type = IntegrationType.Webhook;
updatedIntegration.Id = integrationId;
updatedIntegration.OrganizationId = organizationId;
updatedIntegration.Type = IntegrationType.Hec; // Different Type
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(integrationId)
.Returns(existingIntegration);
await Assert.ThrowsAsync<NotFoundException>(
() => sutProvider.Sut.UpdateAsync(organizationId, integrationId, updatedIntegration));
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().DidNotReceive()
.ReplaceAsync(Arg.Any<OrganizationIntegration>());
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveByTagAsync(Arg.Any<string>());
}
}