From 196e555116aba4666ae8c9cc9080291c42c1fe46 Mon Sep 17 00:00:00 2001
From: Brant DeBow <125889545+brant-livefront@users.noreply.github.com>
Date: Fri, 12 Dec 2025 16:17:43 -0500
Subject: [PATCH] Refactor event integration service collection extensions into
their own extension (#6714)
* Add CQRS and caching support for OrganizationIntegrationConfigurations
* Refactor event integration service collection extensions into their own extension
---
...IntegrationsServiceCollectionExtensions.cs | 505 ++++++++++++-
src/Core/Core.csproj | 1 +
src/Core/Utilities/CACHING.md | 2 +-
src/Events/Startup.cs | 2 +
src/EventsProcessor/Startup.cs | 3 +-
.../Utilities/ServiceCollectionExtensions.cs | 297 --------
...grationServiceCollectionExtensionsTests.cs | 667 ++++++++++++++++++
.../TestListenerConfiguration.cs | 1 +
8 files changed, 1178 insertions(+), 300 deletions(-)
diff --git a/src/Core/AdminConsole/EventIntegrations/EventIntegrationsServiceCollectionExtensions.cs b/src/Core/AdminConsole/EventIntegrations/EventIntegrationsServiceCollectionExtensions.cs
index 6b848be11f..bd8ebf6483 100644
--- a/src/Core/AdminConsole/EventIntegrations/EventIntegrationsServiceCollectionExtensions.cs
+++ b/src/Core/AdminConsole/EventIntegrations/EventIntegrationsServiceCollectionExtensions.cs
@@ -1,11 +1,24 @@
-using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations;
+using Azure.Messaging.ServiceBus;
+using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations;
using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations.Interfaces;
using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrations;
using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrations.Interfaces;
+using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
+using Bit.Core.AdminConsole.Models.Teams;
+using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services;
+using Bit.Core.AdminConsole.Services.NoopImplementations;
+using Bit.Core.Repositories;
+using Bit.Core.Services;
using Bit.Core.Settings;
using Bit.Core.Utilities;
+using Microsoft.Bot.Builder;
+using Microsoft.Bot.Builder.Integration.AspNet.Core;
using Microsoft.Extensions.DependencyInjection.Extensions;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+using ZiggyCreatures.Caching.Fusion;
+using TableStorageRepos = Bit.Core.Repositories.TableStorage;
namespace Microsoft.Extensions.DependencyInjection;
@@ -33,6 +46,460 @@ public static class EventIntegrationsServiceCollectionExtensions
return services;
}
+ ///
+ /// Registers event write services based on available configuration.
+ ///
+ /// The service collection to add services to.
+ /// The global settings containing event logging configuration.
+ /// The service collection for chaining.
+ ///
+ ///
+ /// This method registers the appropriate IEventWriteService implementation based on the available
+ /// configuration, checking in the following priority order:
+ ///
+ ///
+ /// 1. Azure Service Bus - If all Azure Service Bus settings are present, registers
+ /// EventIntegrationEventWriteService with AzureServiceBusService as the publisher
+ ///
+ ///
+ /// 2. RabbitMQ - If all RabbitMQ settings are present, registers EventIntegrationEventWriteService with
+ /// RabbitMqService as the publisher
+ ///
+ ///
+ /// 3. Azure Queue Storage - If Events.ConnectionString is present, registers AzureQueueEventWriteService
+ ///
+ ///
+ /// 4. Repository (Self-Hosted) - If SelfHosted is true, registers RepositoryEventWriteService
+ ///
+ ///
+ /// 5. Noop - If none of the above are configured, registers NoopEventWriteService (no-op implementation)
+ ///
+ ///
+ public static IServiceCollection AddEventWriteServices(this IServiceCollection services, GlobalSettings globalSettings)
+ {
+ if (IsAzureServiceBusEnabled(globalSettings))
+ {
+ services.TryAddSingleton();
+ services.TryAddSingleton();
+ return services;
+ }
+
+ if (IsRabbitMqEnabled(globalSettings))
+ {
+ services.TryAddSingleton();
+ services.TryAddSingleton();
+ return services;
+ }
+
+ if (CoreHelpers.SettingHasValue(globalSettings.Events.ConnectionString))
+ {
+ services.TryAddSingleton();
+ return services;
+ }
+
+ if (globalSettings.SelfHosted)
+ {
+ services.TryAddSingleton();
+ return services;
+ }
+
+ services.TryAddSingleton();
+ return services;
+ }
+
+ ///
+ /// Registers Azure Service Bus-based event integration listeners and supporting infrastructure.
+ ///
+ /// The service collection to add services to.
+ /// The global settings containing Azure Service Bus configuration.
+ /// The service collection for chaining.
+ ///
+ ///
+ /// If Azure Service Bus is not enabled (missing required settings), this method returns immediately
+ /// without registering any services.
+ ///
+ ///
+ /// When Azure Service Bus is enabled, this method registers:
+ /// - IAzureServiceBusService and IEventIntegrationPublisher implementations
+ /// - Table Storage event repository
+ /// - Azure Table Storage event handler
+ /// - All event integration services via AddEventIntegrationServices
+ ///
+ ///
+ /// PREREQUISITE: Callers must ensure AddDistributedCache has been called before this method,
+ /// as it is required to create the event integrations extended cache.
+ ///
+ ///
+ public static IServiceCollection AddAzureServiceBusListeners(this IServiceCollection services, GlobalSettings globalSettings)
+ {
+ if (!IsAzureServiceBusEnabled(globalSettings))
+ {
+ return services;
+ }
+
+ services.TryAddSingleton();
+ services.TryAddSingleton();
+ services.TryAddSingleton();
+ services.TryAddKeyedSingleton("persistent");
+ services.TryAddSingleton();
+
+ services.AddEventIntegrationServices(globalSettings);
+
+ return services;
+ }
+
+ ///
+ /// Registers RabbitMQ-based event integration listeners and supporting infrastructure.
+ ///
+ /// The service collection to add services to.
+ /// The global settings containing RabbitMQ configuration.
+ /// The service collection for chaining.
+ ///
+ ///
+ /// If RabbitMQ is not enabled (missing required settings), this method returns immediately
+ /// without registering any services.
+ ///
+ ///
+ /// When RabbitMQ is enabled, this method registers:
+ /// - IRabbitMqService and IEventIntegrationPublisher implementations
+ /// - Event repository handler
+ /// - All event integration services via AddEventIntegrationServices
+ ///
+ ///
+ /// PREREQUISITE: Callers must ensure AddDistributedCache has been called before this method,
+ /// as it is required to create the event integrations extended cache.
+ ///
+ ///
+ public static IServiceCollection AddRabbitMqListeners(this IServiceCollection services, GlobalSettings globalSettings)
+ {
+ if (!IsRabbitMqEnabled(globalSettings))
+ {
+ return services;
+ }
+
+ services.TryAddSingleton();
+ services.TryAddSingleton();
+ services.TryAddSingleton();
+
+ services.AddEventIntegrationServices(globalSettings);
+
+ return services;
+ }
+
+ ///
+ /// Registers Slack integration services based on configuration settings.
+ ///
+ /// The service collection to add services to.
+ /// The global settings containing Slack configuration.
+ /// The service collection for chaining.
+ ///
+ /// If all required Slack settings are configured (ClientId, ClientSecret, Scopes), registers the full SlackService,
+ /// including an HttpClient for Slack API calls. Otherwise, registers a NoopSlackService that performs no operations.
+ ///
+ public static IServiceCollection AddSlackService(this IServiceCollection services, GlobalSettings globalSettings)
+ {
+ if (CoreHelpers.SettingHasValue(globalSettings.Slack.ClientId) &&
+ CoreHelpers.SettingHasValue(globalSettings.Slack.ClientSecret) &&
+ CoreHelpers.SettingHasValue(globalSettings.Slack.Scopes))
+ {
+ services.AddHttpClient(SlackService.HttpClientName);
+ services.TryAddSingleton();
+ }
+ else
+ {
+ services.TryAddSingleton();
+ }
+
+ return services;
+ }
+
+ ///
+ /// Registers Microsoft Teams integration services based on configuration settings.
+ ///
+ /// The service collection to add services to.
+ /// The global settings containing Teams configuration.
+ /// The service collection for chaining.
+ ///
+ /// If all required Teams settings are configured (ClientId, ClientSecret, Scopes), registers:
+ /// - TeamsService and its interfaces (IBot, ITeamsService)
+ /// - IBotFrameworkHttpAdapter with Teams credentials
+ /// - HttpClient for Teams API calls
+ /// Otherwise, registers a NoopTeamsService that performs no operations.
+ ///
+ public static IServiceCollection AddTeamsService(this IServiceCollection services, GlobalSettings globalSettings)
+ {
+ if (CoreHelpers.SettingHasValue(globalSettings.Teams.ClientId) &&
+ CoreHelpers.SettingHasValue(globalSettings.Teams.ClientSecret) &&
+ CoreHelpers.SettingHasValue(globalSettings.Teams.Scopes))
+ {
+ services.AddHttpClient(TeamsService.HttpClientName);
+ services.TryAddSingleton();
+ services.TryAddSingleton(sp => sp.GetRequiredService());
+ services.TryAddSingleton(sp => sp.GetRequiredService());
+ services.TryAddSingleton(_ =>
+ new BotFrameworkHttpAdapter(
+ new TeamsBotCredentialProvider(
+ clientId: globalSettings.Teams.ClientId,
+ clientSecret: globalSettings.Teams.ClientSecret
+ )
+ )
+ );
+ }
+ else
+ {
+ services.TryAddSingleton();
+ }
+
+ return services;
+ }
+
+ ///
+ /// Registers event integration services including handlers, listeners, and supporting infrastructure.
+ ///
+ /// The service collection to add services to.
+ /// The global settings containing integration configuration.
+ /// The service collection for chaining.
+ ///
+ ///
+ /// This method orchestrates the registration of all event integration components based on the enabled
+ /// message broker (Azure Service Bus or RabbitMQ). It is an internal method called by the public
+ /// entry points AddAzureServiceBusListeners and AddRabbitMqListeners.
+ ///
+ ///
+ /// NOTE: If both Azure Service Bus and RabbitMQ are configured, Azure Service Bus takes precedence. This means that
+ /// Azure Service Bus listeners will be registered (and RabbitMQ listeners will NOT) even if this event is called
+ /// from AddRabbitMqListeners when Azure Service Bus settings are configured.
+ ///
+ ///
+ /// PREREQUISITE: Callers must ensure AddDistributedCache has been called before invoking this method.
+ /// This method depends on distributed cache infrastructure being available for the keyed extended
+ /// cache registration.
+ ///
+ ///
+ /// Registered Services:
+ /// - Keyed ExtendedCache for event integrations
+ /// - Integration filter service
+ /// - Integration handlers for Slack, Webhook, Hec, Datadog, and Teams
+ /// - Hosted services for event and integration listeners (based on enabled message broker)
+ ///
+ ///
+ internal static IServiceCollection AddEventIntegrationServices(this IServiceCollection services,
+ GlobalSettings globalSettings)
+ {
+ // Add common services
+ // NOTE: AddDistributedCache must be called by the caller before this method
+ services.AddExtendedCache(EventIntegrationsCacheConstants.CacheName, globalSettings);
+ services.TryAddSingleton();
+ services.TryAddKeyedSingleton("persistent");
+
+ // Add services in support of handlers
+ services.AddSlackService(globalSettings);
+ services.AddTeamsService(globalSettings);
+ services.TryAddSingleton(TimeProvider.System);
+ services.AddHttpClient(WebhookIntegrationHandler.HttpClientName);
+ services.AddHttpClient(DatadogIntegrationHandler.HttpClientName);
+
+ // Add integration handlers
+ services.TryAddSingleton, SlackIntegrationHandler>();
+ services.TryAddSingleton, WebhookIntegrationHandler>();
+ services.TryAddSingleton, DatadogIntegrationHandler>();
+ services.TryAddSingleton, TeamsIntegrationHandler>();
+
+ var repositoryConfiguration = new RepositoryListenerConfiguration(globalSettings);
+ var slackConfiguration = new SlackListenerConfiguration(globalSettings);
+ var webhookConfiguration = new WebhookListenerConfiguration(globalSettings);
+ var hecConfiguration = new HecListenerConfiguration(globalSettings);
+ var datadogConfiguration = new DatadogListenerConfiguration(globalSettings);
+ var teamsConfiguration = new TeamsListenerConfiguration(globalSettings);
+
+ if (IsAzureServiceBusEnabled(globalSettings))
+ {
+ services.TryAddEnumerable(ServiceDescriptor.Singleton>(provider =>
+ new AzureServiceBusEventListenerService(
+ configuration: repositoryConfiguration,
+ handler: provider.GetRequiredService(),
+ serviceBusService: provider.GetRequiredService(),
+ serviceBusOptions: new ServiceBusProcessorOptions()
+ {
+ PrefetchCount = repositoryConfiguration.EventPrefetchCount,
+ MaxConcurrentCalls = repositoryConfiguration.EventMaxConcurrentCalls
+ },
+ loggerFactory: provider.GetRequiredService()
+ )
+ )
+ );
+ services.AddAzureServiceBusIntegration(slackConfiguration);
+ services.AddAzureServiceBusIntegration(webhookConfiguration);
+ services.AddAzureServiceBusIntegration(hecConfiguration);
+ services.AddAzureServiceBusIntegration(datadogConfiguration);
+ services.AddAzureServiceBusIntegration(teamsConfiguration);
+
+ return services;
+ }
+
+ if (IsRabbitMqEnabled(globalSettings))
+ {
+ services.TryAddEnumerable(ServiceDescriptor.Singleton>(provider =>
+ new RabbitMqEventListenerService(
+ handler: provider.GetRequiredService(),
+ configuration: repositoryConfiguration,
+ rabbitMqService: provider.GetRequiredService(),
+ loggerFactory: provider.GetRequiredService()
+ )
+ )
+ );
+ services.AddRabbitMqIntegration(slackConfiguration);
+ services.AddRabbitMqIntegration(webhookConfiguration);
+ services.AddRabbitMqIntegration(hecConfiguration);
+ services.AddRabbitMqIntegration(datadogConfiguration);
+ services.AddRabbitMqIntegration(teamsConfiguration);
+ }
+
+ return services;
+ }
+
+ ///
+ /// Registers Azure Service Bus-based event integration listeners for a specific integration type.
+ ///
+ /// The integration configuration details type (e.g., SlackIntegrationConfigurationDetails).
+ /// The listener configuration type implementing IIntegrationListenerConfiguration.
+ /// The service collection to add services to.
+ /// The listener configuration containing routing keys and message processing settings.
+ /// The service collection for chaining.
+ ///
+ ///
+ /// This method registers three key components:
+ /// 1. EventIntegrationHandler - Keyed singleton for processing integration events
+ /// 2. AzureServiceBusEventListenerService - Hosted service for listening to event messages from Azure Service Bus
+ /// for this integration type
+ /// 3. AzureServiceBusIntegrationListenerService - Hosted service for listening to integration messages from
+ /// Azure Service Bus for this integration type
+ ///
+ ///
+ /// The handler uses the listener configuration's routing key as its service key, allowing multiple
+ /// handlers to be registered for different integration types.
+ ///
+ ///
+ /// Service Bus processor options (PrefetchCount and MaxConcurrentCalls) are configured from the listener
+ /// configuration to optimize message throughput and concurrency.
+ ///
+ ///
+ internal static IServiceCollection AddAzureServiceBusIntegration(this IServiceCollection services,
+ TListenerConfig listenerConfiguration)
+ where TConfig : class
+ where TListenerConfig : IIntegrationListenerConfiguration
+ {
+ services.TryAddKeyedSingleton(serviceKey: listenerConfiguration.RoutingKey, implementationFactory: (provider, _) =>
+ new EventIntegrationHandler(
+ integrationType: listenerConfiguration.IntegrationType,
+ eventIntegrationPublisher: provider.GetRequiredService(),
+ integrationFilterService: provider.GetRequiredService(),
+ cache: provider.GetRequiredKeyedService(EventIntegrationsCacheConstants.CacheName),
+ configurationRepository: provider.GetRequiredService(),
+ groupRepository: provider.GetRequiredService(),
+ organizationRepository: provider.GetRequiredService(),
+ organizationUserRepository: provider.GetRequiredService(), logger: provider.GetRequiredService>>())
+ );
+ services.TryAddEnumerable(ServiceDescriptor.Singleton>(provider =>
+ new AzureServiceBusEventListenerService(
+ configuration: listenerConfiguration,
+ handler: provider.GetRequiredKeyedService(serviceKey: listenerConfiguration.RoutingKey),
+ serviceBusService: provider.GetRequiredService(),
+ serviceBusOptions: new ServiceBusProcessorOptions()
+ {
+ PrefetchCount = listenerConfiguration.EventPrefetchCount,
+ MaxConcurrentCalls = listenerConfiguration.EventMaxConcurrentCalls
+ },
+ loggerFactory: provider.GetRequiredService()
+ )
+ )
+ );
+ services.TryAddEnumerable(ServiceDescriptor.Singleton>(provider =>
+ new AzureServiceBusIntegrationListenerService(
+ configuration: listenerConfiguration,
+ handler: provider.GetRequiredService>(),
+ serviceBusService: provider.GetRequiredService(),
+ serviceBusOptions: new ServiceBusProcessorOptions()
+ {
+ PrefetchCount = listenerConfiguration.IntegrationPrefetchCount,
+ MaxConcurrentCalls = listenerConfiguration.IntegrationMaxConcurrentCalls
+ },
+ loggerFactory: provider.GetRequiredService()
+ )
+ )
+ );
+
+ return services;
+ }
+
+ ///
+ /// Registers RabbitMQ-based event integration listeners for a specific integration type.
+ ///
+ /// The integration configuration details type (e.g., SlackIntegrationConfigurationDetails).
+ /// The listener configuration type implementing IIntegrationListenerConfiguration.
+ /// The service collection to add services to.
+ /// The listener configuration containing routing keys and message processing settings.
+ /// The service collection for chaining.
+ ///
+ ///
+ /// This method registers three key components:
+ /// 1. EventIntegrationHandler - Keyed singleton for processing integration events
+ /// 2. RabbitMqEventListenerService - Hosted service for listening to event messages from RabbitMQ for
+ /// this integration type
+ /// 3. RabbitMqIntegrationListenerService - Hosted service for listening to integration messages from RabbitMQ for
+ /// this integration type
+ ///
+ ///
+ ///
+ /// The handler uses the listener configuration's routing key as its service key, allowing multiple
+ /// handlers to be registered for different integration types.
+ ///
+ ///
+ internal static IServiceCollection AddRabbitMqIntegration(this IServiceCollection services,
+ TListenerConfig listenerConfiguration)
+ where TConfig : class
+ where TListenerConfig : IIntegrationListenerConfiguration
+ {
+ services.TryAddKeyedSingleton(serviceKey: listenerConfiguration.RoutingKey, implementationFactory: (provider, _) =>
+ new EventIntegrationHandler(
+ integrationType: listenerConfiguration.IntegrationType,
+ eventIntegrationPublisher: provider.GetRequiredService(),
+ integrationFilterService: provider.GetRequiredService(),
+ cache: provider.GetRequiredKeyedService(EventIntegrationsCacheConstants.CacheName),
+ configurationRepository: provider.GetRequiredService(),
+ groupRepository: provider.GetRequiredService(),
+ organizationRepository: provider.GetRequiredService(),
+ organizationUserRepository: provider.GetRequiredService(), logger: provider.GetRequiredService>>())
+ );
+ services.TryAddEnumerable(ServiceDescriptor.Singleton>(provider =>
+ new RabbitMqEventListenerService(
+ handler: provider.GetRequiredKeyedService(serviceKey: listenerConfiguration.RoutingKey),
+ configuration: listenerConfiguration,
+ rabbitMqService: provider.GetRequiredService(),
+ loggerFactory: provider.GetRequiredService()
+ )
+ )
+ );
+ services.TryAddEnumerable(ServiceDescriptor.Singleton>(provider =>
+ new RabbitMqIntegrationListenerService(
+ handler: provider.GetRequiredService>(),
+ configuration: listenerConfiguration,
+ rabbitMqService: provider.GetRequiredService(),
+ loggerFactory: provider.GetRequiredService(),
+ timeProvider: provider.GetRequiredService()
+ )
+ )
+ );
+
+ return services;
+ }
+
internal static IServiceCollection AddOrganizationIntegrationCommandsQueries(this IServiceCollection services)
{
services.TryAddScoped();
@@ -52,4 +519,40 @@ public static class EventIntegrationsServiceCollectionExtensions
return services;
}
+
+ ///
+ /// Determines if RabbitMQ is enabled for event integrations based on configuration settings.
+ ///
+ /// The global settings containing RabbitMQ configuration.
+ /// True if all required RabbitMQ settings are present; otherwise, false.
+ ///
+ /// Requires all the following settings to be configured:
+ /// - EventLogging.RabbitMq.HostName
+ /// - EventLogging.RabbitMq.Username
+ /// - EventLogging.RabbitMq.Password
+ /// - EventLogging.RabbitMq.EventExchangeName
+ ///
+ internal static bool IsRabbitMqEnabled(GlobalSettings settings)
+ {
+ return CoreHelpers.SettingHasValue(settings.EventLogging.RabbitMq.HostName) &&
+ CoreHelpers.SettingHasValue(settings.EventLogging.RabbitMq.Username) &&
+ CoreHelpers.SettingHasValue(settings.EventLogging.RabbitMq.Password) &&
+ CoreHelpers.SettingHasValue(settings.EventLogging.RabbitMq.EventExchangeName);
+ }
+
+ ///
+ /// Determines if Azure Service Bus is enabled for event integrations based on configuration settings.
+ ///
+ /// The global settings containing Azure Service Bus configuration.
+ /// True if all required Azure Service Bus settings are present; otherwise, false.
+ ///
+ /// Requires both of the following settings to be configured:
+ /// - EventLogging.AzureServiceBus.ConnectionString
+ /// - EventLogging.AzureServiceBus.EventTopicName
+ ///
+ internal static bool IsAzureServiceBusEnabled(GlobalSettings settings)
+ {
+ return CoreHelpers.SettingHasValue(settings.EventLogging.AzureServiceBus.ConnectionString) &&
+ CoreHelpers.SettingHasValue(settings.EventLogging.AzureServiceBus.EventTopicName);
+ }
}
diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj
index 4902d5bdbe..52c0a641ab 100644
--- a/src/Core/Core.csproj
+++ b/src/Core/Core.csproj
@@ -41,6 +41,7 @@
+
diff --git a/src/Core/Utilities/CACHING.md b/src/Core/Utilities/CACHING.md
index d80e629bdd..c29a14d751 100644
--- a/src/Core/Utilities/CACHING.md
+++ b/src/Core/Utilities/CACHING.md
@@ -381,7 +381,7 @@ public class OrganizationAbilityService
### Example Usage: Default (Ephemeral Data)
-#### 1. Registration (already done in Api, Admin, Billing, Identity, and Notifications Startup.cs files, plus Events and EventsProcessor service collection extensions):
+#### 1. Registration (already done in Api, Admin, Billing, Events, EventsProcessor, Identity, and Notifications Startup.cs files):
```csharp
services.AddDistributedCache(globalSettings);
diff --git a/src/Events/Startup.cs b/src/Events/Startup.cs
index f67debd092..75301cf08c 100644
--- a/src/Events/Startup.cs
+++ b/src/Events/Startup.cs
@@ -84,6 +84,8 @@ public class Startup
services.AddHostedService();
}
+ // Add event integration services
+ services.AddDistributedCache(globalSettings);
services.AddRabbitMqListeners(globalSettings);
}
diff --git a/src/EventsProcessor/Startup.cs b/src/EventsProcessor/Startup.cs
index 260c501e01..888dda43a1 100644
--- a/src/EventsProcessor/Startup.cs
+++ b/src/EventsProcessor/Startup.cs
@@ -31,7 +31,8 @@ public class Startup
// Repositories
services.AddDatabaseRepositories(globalSettings);
- // Hosted Services
+ // Add event integration services
+ services.AddDistributedCache(globalSettings);
services.AddAzureServiceBusListeners(globalSettings);
services.AddHostedService();
}
diff --git a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs
index d1fb0b8ac4..8dd629e63a 100644
--- a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs
+++ b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs
@@ -6,14 +6,10 @@ using System.Reflection;
using System.Security.Claims;
using System.Security.Cryptography.X509Certificates;
using AspNetCoreRateLimit;
-using Azure.Messaging.ServiceBus;
using Bit.Core;
using Bit.Core.AdminConsole.AbilitiesCache;
using Bit.Core.AdminConsole.Models.Business.Tokenables;
-using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
-using Bit.Core.AdminConsole.Models.Teams;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
-using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services;
using Bit.Core.AdminConsole.Services.Implementations;
using Bit.Core.AdminConsole.Services.NoopImplementations;
@@ -74,8 +70,6 @@ using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc.Localization;
using Microsoft.Azure.Cosmos.Fluent;
-using Microsoft.Bot.Builder;
-using Microsoft.Bot.Builder.Integration.AspNet.Core;
using Microsoft.Extensions.Caching.Cosmos;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Configuration;
@@ -87,7 +81,6 @@ using Microsoft.Extensions.Options;
using Microsoft.OpenApi.Models;
using StackExchange.Redis;
using Swashbuckle.AspNetCore.SwaggerGen;
-using ZiggyCreatures.Caching.Fusion;
using NoopRepos = Bit.Core.Repositories.Noop;
using Role = Bit.Core.Entities.Role;
using TableStorageRepos = Bit.Core.Repositories.TableStorage;
@@ -525,116 +518,6 @@ public static class ServiceCollectionExtensions
return globalSettings;
}
- public static IServiceCollection AddEventWriteServices(this IServiceCollection services, GlobalSettings globalSettings)
- {
- if (IsAzureServiceBusEnabled(globalSettings))
- {
- services.TryAddSingleton();
- services.TryAddSingleton();
- return services;
- }
-
- if (IsRabbitMqEnabled(globalSettings))
- {
- services.TryAddSingleton();
- services.TryAddSingleton();
- return services;
- }
-
- if (CoreHelpers.SettingHasValue(globalSettings.Events.ConnectionString))
- {
- services.TryAddSingleton();
- return services;
- }
-
- if (globalSettings.SelfHosted)
- {
- services.TryAddSingleton();
- return services;
- }
-
- services.TryAddSingleton();
- return services;
- }
-
- public static IServiceCollection AddAzureServiceBusListeners(this IServiceCollection services, GlobalSettings globalSettings)
- {
- if (!IsAzureServiceBusEnabled(globalSettings))
- {
- return services;
- }
-
- services.TryAddSingleton();
- services.TryAddSingleton();
- services.TryAddSingleton();
- services.TryAddKeyedSingleton("persistent");
- services.TryAddSingleton();
-
- services.AddEventIntegrationServices(globalSettings);
-
- return services;
- }
-
- public static IServiceCollection AddRabbitMqListeners(this IServiceCollection services, GlobalSettings globalSettings)
- {
- if (!IsRabbitMqEnabled(globalSettings))
- {
- return services;
- }
-
- services.TryAddSingleton();
- services.TryAddSingleton();
- services.TryAddSingleton();
-
- services.AddEventIntegrationServices(globalSettings);
-
- return services;
- }
-
- public static IServiceCollection AddSlackService(this IServiceCollection services, GlobalSettings globalSettings)
- {
- if (CoreHelpers.SettingHasValue(globalSettings.Slack.ClientId) &&
- CoreHelpers.SettingHasValue(globalSettings.Slack.ClientSecret) &&
- CoreHelpers.SettingHasValue(globalSettings.Slack.Scopes))
- {
- services.AddHttpClient(SlackService.HttpClientName);
- services.TryAddSingleton();
- }
- else
- {
- services.TryAddSingleton();
- }
-
- return services;
- }
-
- public static IServiceCollection AddTeamsService(this IServiceCollection services, GlobalSettings globalSettings)
- {
- if (CoreHelpers.SettingHasValue(globalSettings.Teams.ClientId) &&
- CoreHelpers.SettingHasValue(globalSettings.Teams.ClientSecret) &&
- CoreHelpers.SettingHasValue(globalSettings.Teams.Scopes))
- {
- services.AddHttpClient(TeamsService.HttpClientName);
- services.TryAddSingleton();
- services.TryAddSingleton(sp => sp.GetRequiredService());
- services.TryAddSingleton(sp => sp.GetRequiredService());
- services.TryAddSingleton(sp =>
- new BotFrameworkHttpAdapter(
- new TeamsBotCredentialProvider(
- clientId: globalSettings.Teams.ClientId,
- clientSecret: globalSettings.Teams.ClientSecret
- )
- )
- );
- }
- else
- {
- services.TryAddSingleton();
- }
-
- return services;
- }
-
public static void UseDefaultMiddleware(this IApplicationBuilder app,
IWebHostEnvironment env, GlobalSettings globalSettings)
{
@@ -881,186 +764,6 @@ public static class ServiceCollectionExtensions
return (provider, connectionString);
}
- private static IServiceCollection AddAzureServiceBusIntegration(this IServiceCollection services,
- TListenerConfig listenerConfiguration)
- where TConfig : class
- where TListenerConfig : IIntegrationListenerConfiguration
- {
- services.TryAddKeyedSingleton(serviceKey: listenerConfiguration.RoutingKey, implementationFactory: (provider, _) =>
- new EventIntegrationHandler(
- integrationType: listenerConfiguration.IntegrationType,
- eventIntegrationPublisher: provider.GetRequiredService(),
- integrationFilterService: provider.GetRequiredService(),
- cache: provider.GetRequiredKeyedService(EventIntegrationsCacheConstants.CacheName),
- configurationRepository: provider.GetRequiredService(),
- groupRepository: provider.GetRequiredService(),
- organizationRepository: provider.GetRequiredService(),
- organizationUserRepository: provider.GetRequiredService(), logger: provider.GetRequiredService>>())
- );
- services.TryAddEnumerable(ServiceDescriptor.Singleton>(provider =>
- new AzureServiceBusEventListenerService(
- configuration: listenerConfiguration,
- handler: provider.GetRequiredKeyedService(serviceKey: listenerConfiguration.RoutingKey),
- serviceBusService: provider.GetRequiredService(),
- serviceBusOptions: new ServiceBusProcessorOptions()
- {
- PrefetchCount = listenerConfiguration.EventPrefetchCount,
- MaxConcurrentCalls = listenerConfiguration.EventMaxConcurrentCalls
- },
- loggerFactory: provider.GetRequiredService()
- )
- )
- );
- services.TryAddEnumerable(ServiceDescriptor.Singleton>(provider =>
- new AzureServiceBusIntegrationListenerService(
- configuration: listenerConfiguration,
- handler: provider.GetRequiredService>(),
- serviceBusService: provider.GetRequiredService(),
- serviceBusOptions: new ServiceBusProcessorOptions()
- {
- PrefetchCount = listenerConfiguration.IntegrationPrefetchCount,
- MaxConcurrentCalls = listenerConfiguration.IntegrationMaxConcurrentCalls
- },
- loggerFactory: provider.GetRequiredService()
- )
- )
- );
-
- return services;
- }
-
- private static IServiceCollection AddEventIntegrationServices(this IServiceCollection services,
- GlobalSettings globalSettings)
- {
- // Add common services
- services.AddDistributedCache(globalSettings);
- services.AddExtendedCache(EventIntegrationsCacheConstants.CacheName, globalSettings);
- services.TryAddSingleton();
- services.TryAddKeyedSingleton("persistent");
-
- // Add services in support of handlers
- services.AddSlackService(globalSettings);
- services.AddTeamsService(globalSettings);
- services.TryAddSingleton(TimeProvider.System);
- services.AddHttpClient(WebhookIntegrationHandler.HttpClientName);
- services.AddHttpClient(DatadogIntegrationHandler.HttpClientName);
-
- // Add integration handlers
- services.TryAddSingleton, SlackIntegrationHandler>();
- services.TryAddSingleton, WebhookIntegrationHandler>();
- services.TryAddSingleton, DatadogIntegrationHandler>();
- services.TryAddSingleton, TeamsIntegrationHandler>();
-
- var repositoryConfiguration = new RepositoryListenerConfiguration(globalSettings);
- var slackConfiguration = new SlackListenerConfiguration(globalSettings);
- var webhookConfiguration = new WebhookListenerConfiguration(globalSettings);
- var hecConfiguration = new HecListenerConfiguration(globalSettings);
- var datadogConfiguration = new DatadogListenerConfiguration(globalSettings);
- var teamsConfiguration = new TeamsListenerConfiguration(globalSettings);
-
- if (IsRabbitMqEnabled(globalSettings))
- {
- services.TryAddEnumerable(ServiceDescriptor.Singleton>(provider =>
- new RabbitMqEventListenerService(
- handler: provider.GetRequiredService(),
- configuration: repositoryConfiguration,
- rabbitMqService: provider.GetRequiredService(),
- loggerFactory: provider.GetRequiredService()
- )
- )
- );
- services.AddRabbitMqIntegration(slackConfiguration);
- services.AddRabbitMqIntegration(webhookConfiguration);
- services.AddRabbitMqIntegration(hecConfiguration);
- services.AddRabbitMqIntegration(datadogConfiguration);
- services.AddRabbitMqIntegration(teamsConfiguration);
- }
-
- if (IsAzureServiceBusEnabled(globalSettings))
- {
- services.TryAddEnumerable(ServiceDescriptor.Singleton>(provider =>
- new AzureServiceBusEventListenerService(
- configuration: repositoryConfiguration,
- handler: provider.GetRequiredService(),
- serviceBusService: provider.GetRequiredService(),
- serviceBusOptions: new ServiceBusProcessorOptions()
- {
- PrefetchCount = repositoryConfiguration.EventPrefetchCount,
- MaxConcurrentCalls = repositoryConfiguration.EventMaxConcurrentCalls
- },
- loggerFactory: provider.GetRequiredService()
- )
- )
- );
- services.AddAzureServiceBusIntegration(slackConfiguration);
- services.AddAzureServiceBusIntegration(webhookConfiguration);
- services.AddAzureServiceBusIntegration(hecConfiguration);
- services.AddAzureServiceBusIntegration(datadogConfiguration);
- services.AddAzureServiceBusIntegration(teamsConfiguration);
- }
-
- return services;
- }
-
- private static IServiceCollection AddRabbitMqIntegration(this IServiceCollection services,
- TListenerConfig listenerConfiguration)
- where TConfig : class
- where TListenerConfig : IIntegrationListenerConfiguration
- {
- services.TryAddKeyedSingleton(serviceKey: listenerConfiguration.RoutingKey, implementationFactory: (provider, _) =>
- new EventIntegrationHandler(
- integrationType: listenerConfiguration.IntegrationType,
- eventIntegrationPublisher: provider.GetRequiredService(),
- integrationFilterService: provider.GetRequiredService(),
- cache: provider.GetRequiredKeyedService(EventIntegrationsCacheConstants.CacheName),
- configurationRepository: provider.GetRequiredService(),
- groupRepository: provider.GetRequiredService(),
- organizationRepository: provider.GetRequiredService(),
- organizationUserRepository: provider.GetRequiredService(), logger: provider.GetRequiredService>>())
- );
- services.TryAddEnumerable(ServiceDescriptor.Singleton>(provider =>
- new RabbitMqEventListenerService(
- handler: provider.GetRequiredKeyedService(serviceKey: listenerConfiguration.RoutingKey),
- configuration: listenerConfiguration,
- rabbitMqService: provider.GetRequiredService(),
- loggerFactory: provider.GetRequiredService()
- )
- )
- );
- services.TryAddEnumerable(ServiceDescriptor.Singleton>(provider =>
- new RabbitMqIntegrationListenerService(
- handler: provider.GetRequiredService>(),
- configuration: listenerConfiguration,
- rabbitMqService: provider.GetRequiredService(),
- loggerFactory: provider.GetRequiredService(),
- timeProvider: provider.GetRequiredService()
- )
- )
- );
-
- return services;
- }
-
- private static bool IsAzureServiceBusEnabled(GlobalSettings settings)
- {
- return CoreHelpers.SettingHasValue(settings.EventLogging.AzureServiceBus.ConnectionString) &&
- CoreHelpers.SettingHasValue(settings.EventLogging.AzureServiceBus.EventTopicName);
- }
-
- private static bool IsRabbitMqEnabled(GlobalSettings settings)
- {
- return CoreHelpers.SettingHasValue(settings.EventLogging.RabbitMq.HostName) &&
- CoreHelpers.SettingHasValue(settings.EventLogging.RabbitMq.Username) &&
- CoreHelpers.SettingHasValue(settings.EventLogging.RabbitMq.Password) &&
- CoreHelpers.SettingHasValue(settings.EventLogging.RabbitMq.EventExchangeName);
- }
-
///
/// Adds a server with its corresponding OAuth2 client credentials security definition and requirement.
///
diff --git a/test/Core.Test/AdminConsole/EventIntegrations/EventIntegrationServiceCollectionExtensionsTests.cs b/test/Core.Test/AdminConsole/EventIntegrations/EventIntegrationServiceCollectionExtensionsTests.cs
index 4711426677..aca783ade2 100644
--- a/test/Core.Test/AdminConsole/EventIntegrations/EventIntegrationServiceCollectionExtensionsTests.cs
+++ b/test/Core.Test/AdminConsole/EventIntegrations/EventIntegrationServiceCollectionExtensionsTests.cs
@@ -1,12 +1,19 @@
using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations.Interfaces;
using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrations.Interfaces;
+using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
+using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services;
+using Bit.Core.AdminConsole.Services.NoopImplementations;
using Bit.Core.Repositories;
+using Bit.Core.Services;
using Bit.Core.Settings;
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;
@@ -185,6 +192,666 @@ public class EventIntegrationServiceCollectionExtensionsTests
Assert.Single(createCmdDescriptors);
}
+ [Fact]
+ public void IsRabbitMqEnabled_AllSettingsPresent_ReturnsTrue()
+ {
+ var globalSettings = CreateGlobalSettings(new Dictionary
+ {
+ ["GlobalSettings:EventLogging:RabbitMq:HostName"] = "localhost",
+ ["GlobalSettings:EventLogging:RabbitMq:Username"] = "user",
+ ["GlobalSettings:EventLogging:RabbitMq:Password"] = "pass",
+ ["GlobalSettings:EventLogging:RabbitMq:EventExchangeName"] = "exchange"
+ });
+
+ Assert.True(EventIntegrationsServiceCollectionExtensions.IsRabbitMqEnabled(globalSettings));
+ }
+
+ [Fact]
+ public void IsRabbitMqEnabled_MissingHostName_ReturnsFalse()
+ {
+ var globalSettings = CreateGlobalSettings(new Dictionary
+ {
+ ["GlobalSettings:EventLogging:RabbitMq:HostName"] = null,
+ ["GlobalSettings:EventLogging:RabbitMq:Username"] = "user",
+ ["GlobalSettings:EventLogging:RabbitMq:Password"] = "pass",
+ ["GlobalSettings:EventLogging:RabbitMq:EventExchangeName"] = "exchange"
+ });
+
+ Assert.False(EventIntegrationsServiceCollectionExtensions.IsRabbitMqEnabled(globalSettings));
+ }
+
+ [Fact]
+ public void IsRabbitMqEnabled_MissingUsername_ReturnsFalse()
+ {
+ var globalSettings = CreateGlobalSettings(new Dictionary
+ {
+ ["GlobalSettings:EventLogging:RabbitMq:HostName"] = "localhost",
+ ["GlobalSettings:EventLogging:RabbitMq:Username"] = null,
+ ["GlobalSettings:EventLogging:RabbitMq:Password"] = "pass",
+ ["GlobalSettings:EventLogging:RabbitMq:EventExchangeName"] = "exchange"
+ });
+
+ Assert.False(EventIntegrationsServiceCollectionExtensions.IsRabbitMqEnabled(globalSettings));
+ }
+
+ [Fact]
+ public void IsRabbitMqEnabled_MissingPassword_ReturnsFalse()
+ {
+ var globalSettings = CreateGlobalSettings(new Dictionary
+ {
+ ["GlobalSettings:EventLogging:RabbitMq:HostName"] = "localhost",
+ ["GlobalSettings:EventLogging:RabbitMq:Username"] = "user",
+ ["GlobalSettings:EventLogging:RabbitMq:Password"] = null,
+ ["GlobalSettings:EventLogging:RabbitMq:EventExchangeName"] = "exchange"
+ });
+
+ Assert.False(EventIntegrationsServiceCollectionExtensions.IsRabbitMqEnabled(globalSettings));
+ }
+
+ [Fact]
+ public void IsRabbitMqEnabled_MissingExchangeName_ReturnsFalse()
+ {
+ var globalSettings = CreateGlobalSettings(new Dictionary
+ {
+ ["GlobalSettings:EventLogging:RabbitMq:HostName"] = "localhost",
+ ["GlobalSettings:EventLogging:RabbitMq:Username"] = "user",
+ ["GlobalSettings:EventLogging:RabbitMq:Password"] = "pass",
+ ["GlobalSettings:EventLogging:RabbitMq:EventExchangeName"] = null
+ });
+
+ Assert.False(EventIntegrationsServiceCollectionExtensions.IsRabbitMqEnabled(globalSettings));
+ }
+
+ [Fact]
+ public void IsAzureServiceBusEnabled_AllSettingsPresent_ReturnsTrue()
+ {
+ var globalSettings = CreateGlobalSettings(new Dictionary
+ {
+ ["GlobalSettings:EventLogging:AzureServiceBus:ConnectionString"] = "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=test;SharedAccessKey=test",
+ ["GlobalSettings:EventLogging:AzureServiceBus:EventTopicName"] = "events"
+ });
+
+ Assert.True(EventIntegrationsServiceCollectionExtensions.IsAzureServiceBusEnabled(globalSettings));
+ }
+
+ [Fact]
+ public void IsAzureServiceBusEnabled_MissingConnectionString_ReturnsFalse()
+ {
+ var globalSettings = CreateGlobalSettings(new Dictionary
+ {
+ ["GlobalSettings:EventLogging:AzureServiceBus:ConnectionString"] = null,
+ ["GlobalSettings:EventLogging:AzureServiceBus:EventTopicName"] = "events"
+ });
+
+ Assert.False(EventIntegrationsServiceCollectionExtensions.IsAzureServiceBusEnabled(globalSettings));
+ }
+
+ [Fact]
+ public void IsAzureServiceBusEnabled_MissingTopicName_ReturnsFalse()
+ {
+ var globalSettings = CreateGlobalSettings(new Dictionary
+ {
+ ["GlobalSettings:EventLogging:AzureServiceBus:ConnectionString"] = "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=test;SharedAccessKey=test",
+ ["GlobalSettings:EventLogging:AzureServiceBus:EventTopicName"] = null
+ });
+
+ Assert.False(EventIntegrationsServiceCollectionExtensions.IsAzureServiceBusEnabled(globalSettings));
+ }
+
+ [Fact]
+ public void AddSlackService_AllSettingsPresent_RegistersSlackService()
+ {
+ var services = new ServiceCollection();
+ var globalSettings = CreateGlobalSettings(new Dictionary
+ {
+ ["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();
+
+ Assert.NotNull(slackService);
+ Assert.IsType(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
+ {
+ ["GlobalSettings:Slack:ClientId"] = null,
+ ["GlobalSettings:Slack:ClientSecret"] = null,
+ ["GlobalSettings:Slack:Scopes"] = null
+ });
+
+ services.AddSlackService(globalSettings);
+
+ var provider = services.BuildServiceProvider();
+ var slackService = provider.GetService();
+
+ Assert.NotNull(slackService);
+ Assert.IsType(slackService);
+ }
+
+ [Fact]
+ public void AddTeamsService_AllSettingsPresent_RegistersTeamsServices()
+ {
+ var services = new ServiceCollection();
+ var globalSettings = CreateGlobalSettings(new Dictionary
+ {
+ ["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());
+ services.AddTeamsService(globalSettings);
+
+ var provider = services.BuildServiceProvider();
+
+ var teamsService = provider.GetService();
+ Assert.NotNull(teamsService);
+ Assert.IsType(teamsService);
+
+ var bot = provider.GetService();
+ Assert.NotNull(bot);
+ Assert.IsType(bot);
+
+ var adapter = provider.GetService();
+ Assert.NotNull(adapter);
+ Assert.IsType(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
+ {
+ ["GlobalSettings:Teams:ClientId"] = null,
+ ["GlobalSettings:Teams:ClientSecret"] = null,
+ ["GlobalSettings:Teams:Scopes"] = null
+ });
+
+ services.AddTeamsService(globalSettings);
+
+ var provider = services.BuildServiceProvider();
+ var teamsService = provider.GetService();
+
+ Assert.NotNull(teamsService);
+ Assert.IsType(teamsService);
+ }
+
+ [Fact]
+ public void AddRabbitMqIntegration_RegistersEventIntegrationHandler()
+ {
+ var services = new ServiceCollection();
+ var listenerConfig = new TestListenerConfiguration();
+
+ // Add required dependencies
+ services.TryAddSingleton(Substitute.For());
+ services.TryAddSingleton(Substitute.For());
+ services.TryAddKeyedSingleton(EventIntegrationsCacheConstants.CacheName, Substitute.For());
+ services.TryAddSingleton(Substitute.For());
+ services.TryAddSingleton(Substitute.For());
+ services.TryAddSingleton(Substitute.For());
+ services.TryAddSingleton(Substitute.For());
+ services.AddLogging();
+
+ services.AddRabbitMqIntegration(listenerConfig);
+
+ var provider = services.BuildServiceProvider();
+ var handler = provider.GetRequiredKeyedService(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());
+ services.TryAddSingleton(Substitute.For());
+ services.TryAddKeyedSingleton(EventIntegrationsCacheConstants.CacheName, Substitute.For());
+ services.TryAddSingleton(Substitute.For());
+ services.TryAddSingleton(Substitute.For());
+ services.TryAddSingleton(Substitute.For());
+ services.TryAddSingleton(Substitute.For());
+ services.TryAddSingleton(Substitute.For());
+ services.AddLogging();
+
+ var beforeCount = services.Count(s => s.ServiceType == typeof(IHostedService));
+ services.AddRabbitMqIntegration(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());
+ services.TryAddSingleton(Substitute.For());
+ services.TryAddKeyedSingleton(EventIntegrationsCacheConstants.CacheName, Substitute.For());
+ services.TryAddSingleton(Substitute.For());
+ services.TryAddSingleton(Substitute.For());
+ services.TryAddSingleton(Substitute.For());
+ services.TryAddSingleton(Substitute.For());
+ services.TryAddSingleton(Substitute.For());
+ services.TryAddSingleton(Substitute.For>());
+ services.TryAddSingleton(TimeProvider.System);
+ services.AddLogging();
+
+ var beforeCount = services.Count(s => s.ServiceType == typeof(IHostedService));
+ services.AddRabbitMqIntegration(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());
+ services.TryAddSingleton(Substitute.For());
+ services.TryAddKeyedSingleton(EventIntegrationsCacheConstants.CacheName, Substitute.For());
+ services.TryAddSingleton(Substitute.For());
+ services.TryAddSingleton(Substitute.For());
+ services.TryAddSingleton(Substitute.For());
+ services.TryAddSingleton(Substitute.For());
+ services.AddLogging();
+
+ services.AddAzureServiceBusIntegration(listenerConfig);
+
+ var provider = services.BuildServiceProvider();
+ var handler = provider.GetRequiredKeyedService(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());
+ services.TryAddSingleton(Substitute.For());
+ services.TryAddKeyedSingleton(EventIntegrationsCacheConstants.CacheName, Substitute.For());
+ services.TryAddSingleton(Substitute.For