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()); + 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.AddAzureServiceBusIntegration(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()); + 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.AddLogging(); + + var beforeCount = services.Count(s => s.ServiceType == typeof(IHostedService)); + services.AddAzureServiceBusIntegration(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()); + 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()); + services.AddLogging(); + + services.AddEventIntegrationServices(globalSettings); + + // Verify integration handlers are registered + Assert.Contains(services, s => s.ServiceType == typeof(IIntegrationHandler)); + Assert.Contains(services, s => s.ServiceType == typeof(IIntegrationHandler)); + Assert.Contains(services, s => s.ServiceType == typeof(IIntegrationHandler)); + Assert.Contains(services, s => s.ServiceType == typeof(IIntegrationHandler)); + } + + [Fact] + public void AddEventIntegrationServices_RabbitMqEnabled_RegistersRabbitMqListeners() + { + var services = new ServiceCollection(); + 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" + }); + + // Add prerequisites + services.TryAddSingleton(globalSettings); + services.TryAddSingleton(Substitute.For()); + 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 + { + ["GlobalSettings:EventLogging:AzureServiceBus:ConnectionString"] = "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=test;SharedAccessKey=test", + ["GlobalSettings:EventLogging:AzureServiceBus:EventTopicName"] = "events" + }); + + // Add prerequisites + services.TryAddSingleton(globalSettings); + services.TryAddSingleton(Substitute.For()); + 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 + { + ["GlobalSettings:EventLogging:RabbitMq:HostName"] = "localhost", + ["GlobalSettings:EventLogging:RabbitMq:Username"] = "user", + ["GlobalSettings:EventLogging:RabbitMq:Password"] = "pass", + ["GlobalSettings:EventLogging:RabbitMq:EventExchangeName"] = "exchange", + ["GlobalSettings:EventLogging:AzureServiceBus:ConnectionString"] = "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=test;SharedAccessKey=test", + ["GlobalSettings:EventLogging:AzureServiceBus:EventTopicName"] = "events" + }); + + // Add prerequisites + services.TryAddSingleton(globalSettings); + services.TryAddSingleton(Substitute.For()); + 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()); + 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 + { + ["GlobalSettings:EventLogging:AzureServiceBus:ConnectionString"] = "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=test;SharedAccessKey=test", + ["GlobalSettings:EventLogging:AzureServiceBus:EventTopicName"] = "events" + }); + + 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 + { + ["GlobalSettings:EventLogging:RabbitMq:HostName"] = "localhost", + ["GlobalSettings:EventLogging:RabbitMq:Username"] = "user", + ["GlobalSettings:EventLogging:RabbitMq:Password"] = "pass", + ["GlobalSettings:EventLogging:RabbitMq:EventExchangeName"] = "exchange" + }); + + 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 + { + ["GlobalSettings:Events:ConnectionString"] = "DefaultEndpointsProtocol=https;AccountName=test;AccountKey=test;EndpointSuffix=core.windows.net" + }); + + 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 + { + ["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 + { + ["GlobalSettings:EventLogging:AzureServiceBus:ConnectionString"] = "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=test;SharedAccessKey=test", + ["GlobalSettings:EventLogging:AzureServiceBus:EventTopicName"] = "events", + ["GlobalSettings:EventLogging:RabbitMq:HostName"] = "localhost", + ["GlobalSettings:EventLogging:RabbitMq:Username"] = "user", + ["GlobalSettings:EventLogging:RabbitMq:Password"] = "pass", + ["GlobalSettings:EventLogging:RabbitMq:EventExchangeName"] = "exchange" + }); + + 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 + { + ["GlobalSettings:EventLogging:AzureServiceBus:ConnectionString"] = "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=test;SharedAccessKey=test", + ["GlobalSettings:EventLogging:AzureServiceBus:EventTopicName"] = "events" + }); + + // Add prerequisites + services.TryAddSingleton(globalSettings); + services.TryAddSingleton(Substitute.For()); + 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 + { + ["GlobalSettings:EventLogging:RabbitMq:HostName"] = "localhost", + ["GlobalSettings:EventLogging:RabbitMq:Username"] = "user", + ["GlobalSettings:EventLogging:RabbitMq:Password"] = "pass", + ["GlobalSettings:EventLogging:RabbitMq:EventExchangeName"] = "exchange" + }); + + // Add prerequisites + services.TryAddSingleton(globalSettings); + services.TryAddSingleton(Substitute.For()); + 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 data) { var config = new ConfigurationBuilder() diff --git a/test/Core.Test/AdminConsole/Models/Data/EventIntegrations/TestListenerConfiguration.cs b/test/Core.Test/AdminConsole/Models/Data/EventIntegrations/TestListenerConfiguration.cs index 916fe981de..50442dd463 100644 --- a/test/Core.Test/AdminConsole/Models/Data/EventIntegrations/TestListenerConfiguration.cs +++ b/test/Core.Test/AdminConsole/Models/Data/EventIntegrations/TestListenerConfiguration.cs @@ -17,4 +17,5 @@ public class TestListenerConfiguration : IIntegrationListenerConfiguration public int EventPrefetchCount => 0; public int IntegrationMaxConcurrentCalls => 1; public int IntegrationPrefetchCount => 0; + public string RoutingKey => IntegrationType.ToRoutingKey(); }