diff --git a/dev/servicebusemulator_config.json b/dev/servicebusemulator_config.json index 073a44618f..b107bc6190 100644 --- a/dev/servicebusemulator_config.json +++ b/dev/servicebusemulator_config.json @@ -33,6 +33,39 @@ "Name": "events-webhook-subscription" } ] + }, + { + "Name": "event-integrations", + "Subscriptions": [ + { + "Name": "integration-slack-subscription", + "Rules": [ + { + "Name": "slack-integration-filter", + "Properties": { + "FilterType": "Correlation", + "CorrelationFilter": { + "Label": "slack" + } + } + } + ] + }, + { + "Name": "integration-webhook-subscription", + "Rules": [ + { + "Name": "webhook-integration-filter", + "Properties": { + "FilterType": "Correlation", + "CorrelationFilter": { + "Label": "webhook" + } + } + } + ] + } + ] } ] } diff --git a/src/Core/AdminConsole/Services/Implementations/AzureServiceBusEventListenerService.cs b/src/Core/AdminConsole/Services/Implementations/AzureServiceBusEventListenerService.cs index 4cd71ae77e..2ab10418a3 100644 --- a/src/Core/AdminConsole/Services/Implementations/AzureServiceBusEventListenerService.cs +++ b/src/Core/AdminConsole/Services/Implementations/AzureServiceBusEventListenerService.cs @@ -20,7 +20,7 @@ public class AzureServiceBusEventListenerService : EventLoggingListenerService string subscriptionName) : base(handler) { _client = new ServiceBusClient(globalSettings.EventLogging.AzureServiceBus.ConnectionString); - _processor = _client.CreateProcessor(globalSettings.EventLogging.AzureServiceBus.TopicName, subscriptionName, new ServiceBusProcessorOptions()); + _processor = _client.CreateProcessor(globalSettings.EventLogging.AzureServiceBus.EventTopicName, subscriptionName, new ServiceBusProcessorOptions()); _logger = logger; } diff --git a/src/Core/AdminConsole/Services/Implementations/AzureServiceBusEventWriteService.cs b/src/Core/AdminConsole/Services/Implementations/AzureServiceBusEventWriteService.cs index fc865b327c..224f86a802 100644 --- a/src/Core/AdminConsole/Services/Implementations/AzureServiceBusEventWriteService.cs +++ b/src/Core/AdminConsole/Services/Implementations/AzureServiceBusEventWriteService.cs @@ -14,7 +14,7 @@ public class AzureServiceBusEventWriteService : IEventWriteService, IAsyncDispos public AzureServiceBusEventWriteService(GlobalSettings globalSettings) { _client = new ServiceBusClient(globalSettings.EventLogging.AzureServiceBus.ConnectionString); - _sender = _client.CreateSender(globalSettings.EventLogging.AzureServiceBus.TopicName); + _sender = _client.CreateSender(globalSettings.EventLogging.AzureServiceBus.EventTopicName); } public async Task CreateAsync(IEvent e) diff --git a/src/Core/AdminConsole/Services/Implementations/AzureServiceBusIntegrationListenerService.cs b/src/Core/AdminConsole/Services/Implementations/AzureServiceBusIntegrationListenerService.cs new file mode 100644 index 0000000000..8244f39c09 --- /dev/null +++ b/src/Core/AdminConsole/Services/Implementations/AzureServiceBusIntegrationListenerService.cs @@ -0,0 +1,101 @@ +#nullable enable + +using Azure.Messaging.ServiceBus; +using Bit.Core.Settings; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace Bit.Core.Services; + +public class AzureServiceBusIntegrationListenerService : BackgroundService +{ + private readonly int _maxRetries; + private readonly string _subscriptionName; + private readonly string _topicName; + private readonly IIntegrationHandler _handler; + private readonly ServiceBusClient _client; + private readonly ServiceBusProcessor _processor; + private readonly ServiceBusSender _sender; + private readonly ILogger _logger; + + public AzureServiceBusIntegrationListenerService( + IIntegrationHandler handler, + string subscriptionName, + GlobalSettings globalSettings, + ILogger logger) + { + _handler = handler; + _logger = logger; + _maxRetries = globalSettings.EventLogging.AzureServiceBus.MaxRetries; + _topicName = globalSettings.EventLogging.AzureServiceBus.IntegrationTopicName; + _subscriptionName = subscriptionName; + + _client = new ServiceBusClient(globalSettings.EventLogging.AzureServiceBus.ConnectionString); + _processor = _client.CreateProcessor(_topicName, _subscriptionName, new ServiceBusProcessorOptions()); + _sender = _client.CreateSender(_topicName); + } + + protected override async Task ExecuteAsync(CancellationToken cancellationToken) + { + _processor.ProcessMessageAsync += HandleMessageAsync; + _processor.ProcessErrorAsync += args => + { + _logger.LogError(args.Exception, "Azure Service Bus error"); + return Task.CompletedTask; + }; + + await _processor.StartProcessingAsync(cancellationToken); + } + + public override async Task StopAsync(CancellationToken cancellationToken) + { + await _processor.StopProcessingAsync(cancellationToken); + await _processor.DisposeAsync(); + await _sender.DisposeAsync(); + await _client.DisposeAsync(); + await base.StopAsync(cancellationToken); + } + + private async Task HandleMessageAsync(ProcessMessageEventArgs args) + { + var json = args.Message.Body.ToString(); + + try + { + var result = await _handler.HandleAsync(json); + var message = result.Message; + + if (result.Success) + { + await args.CompleteMessageAsync(args.Message); + return; + } + + message.ApplyRetry(result.DelayUntilDate); + + if (result.Retryable && message.RetryCount < _maxRetries) + { + var scheduledTime = (DateTime)message.DelayUntilDate!; + var retryMsg = new ServiceBusMessage(message.ToJson()) + { + Subject = args.Message.Subject, + ScheduledEnqueueTime = scheduledTime + }; + + await _sender.SendMessageAsync(retryMsg); + } + else + { + await args.DeadLetterMessageAsync(args.Message, "Retry limit exceeded or non-retryable"); + return; + } + + await args.CompleteMessageAsync(args.Message); + } + catch (Exception ex) + { + _logger.LogError(ex, "Unhandled error processing ASB message"); + await args.CompleteMessageAsync(args.Message); + } + } +} diff --git a/src/Core/AdminConsole/Services/Implementations/AzureServiceBusIntegrationPublisher.cs b/src/Core/AdminConsole/Services/Implementations/AzureServiceBusIntegrationPublisher.cs new file mode 100644 index 0000000000..4a906e719f --- /dev/null +++ b/src/Core/AdminConsole/Services/Implementations/AzureServiceBusIntegrationPublisher.cs @@ -0,0 +1,36 @@ +using Azure.Messaging.ServiceBus; +using Bit.Core.AdminConsole.Models.Data.Integrations; +using Bit.Core.Enums; +using Bit.Core.Settings; + +namespace Bit.Core.Services; + +public class AzureServiceBusIntegrationPublisher : IIntegrationPublisher, IAsyncDisposable +{ + private readonly ServiceBusClient _client; + private readonly ServiceBusSender _sender; + + public AzureServiceBusIntegrationPublisher(GlobalSettings globalSettings) + { + _client = new ServiceBusClient(globalSettings.EventLogging.AzureServiceBus.ConnectionString); + _sender = _client.CreateSender(globalSettings.EventLogging.AzureServiceBus.IntegrationTopicName); + } + + public async Task PublishAsync(IIntegrationMessage message) + { + var json = message.ToJson(); + + var serviceBusMessage = new ServiceBusMessage(json) + { + Subject = message.IntegrationType.ToRoutingKey(), + }; + + await _sender.SendMessageAsync(serviceBusMessage); + } + + public async ValueTask DisposeAsync() + { + await _sender.DisposeAsync(); + await _client.DisposeAsync(); + } +} diff --git a/src/Core/AdminConsole/Services/Implementations/IntegrationEventHandlerBase.cs b/src/Core/AdminConsole/Services/Implementations/IntegrationEventHandlerBase.cs deleted file mode 100644 index 4df2d25b1b..0000000000 --- a/src/Core/AdminConsole/Services/Implementations/IntegrationEventHandlerBase.cs +++ /dev/null @@ -1,66 +0,0 @@ -using System.Text.Json.Nodes; -using Bit.Core.AdminConsole.Models.Data.Integrations; -using Bit.Core.AdminConsole.Utilities; -using Bit.Core.Enums; -using Bit.Core.Models.Data; -using Bit.Core.Repositories; - -namespace Bit.Core.Services; - -public abstract class IntegrationEventHandlerBase( - IUserRepository userRepository, - IOrganizationRepository organizationRepository, - IOrganizationIntegrationConfigurationRepository configurationRepository) - : IEventMessageHandler -{ - public async Task HandleEventAsync(EventMessage eventMessage) - { - var organizationId = eventMessage.OrganizationId ?? Guid.Empty; - var configurations = await configurationRepository.GetConfigurationDetailsAsync( - organizationId, - GetIntegrationType(), - eventMessage.Type); - - foreach (var configuration in configurations) - { - var context = await BuildContextAsync(eventMessage, configuration.Template); - var renderedTemplate = IntegrationTemplateProcessor.ReplaceTokens(configuration.Template, context); - - await ProcessEventIntegrationAsync(configuration.MergedConfiguration, renderedTemplate); - } - } - - public async Task HandleManyEventsAsync(IEnumerable eventMessages) - { - foreach (var eventMessage in eventMessages) - { - await HandleEventAsync(eventMessage); - } - } - - private async Task BuildContextAsync(EventMessage eventMessage, string template) - { - var context = new IntegrationTemplateContext(eventMessage); - - if (IntegrationTemplateProcessor.TemplateRequiresUser(template) && eventMessage.UserId.HasValue) - { - context.User = await userRepository.GetByIdAsync(eventMessage.UserId.Value); - } - - if (IntegrationTemplateProcessor.TemplateRequiresActingUser(template) && eventMessage.ActingUserId.HasValue) - { - context.ActingUser = await userRepository.GetByIdAsync(eventMessage.ActingUserId.Value); - } - - if (IntegrationTemplateProcessor.TemplateRequiresOrganization(template) && eventMessage.OrganizationId.HasValue) - { - context.Organization = await organizationRepository.GetByIdAsync(eventMessage.OrganizationId.Value); - } - - return context; - } - - protected abstract IntegrationType GetIntegrationType(); - - protected abstract Task ProcessEventIntegrationAsync(JsonObject mergedConfiguration, string renderedTemplate); -} diff --git a/src/Core/AdminConsole/Services/Implementations/SlackEventHandler.cs b/src/Core/AdminConsole/Services/Implementations/SlackEventHandler.cs deleted file mode 100644 index a767776c36..0000000000 --- a/src/Core/AdminConsole/Services/Implementations/SlackEventHandler.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Text.Json; -using System.Text.Json.Nodes; -using Bit.Core.AdminConsole.Models.Data.Integrations; -using Bit.Core.Enums; -using Bit.Core.Repositories; - -#nullable enable - -namespace Bit.Core.Services; - -public class SlackEventHandler( - IUserRepository userRepository, - IOrganizationRepository organizationRepository, - IOrganizationIntegrationConfigurationRepository configurationRepository, - ISlackService slackService) - : IntegrationEventHandlerBase(userRepository, organizationRepository, configurationRepository) -{ - protected override IntegrationType GetIntegrationType() => IntegrationType.Slack; - - protected override async Task ProcessEventIntegrationAsync(JsonObject mergedConfiguration, - string renderedTemplate) - { - var config = mergedConfiguration.Deserialize(); - if (config is null) - { - return; - } - - await slackService.SendSlackMessageByChannelIdAsync( - config.token, - renderedTemplate, - config.channelId - ); - } -} diff --git a/src/Core/AdminConsole/Services/Implementations/WebhookEventHandler.cs b/src/Core/AdminConsole/Services/Implementations/WebhookEventHandler.cs deleted file mode 100644 index 97453497bc..0000000000 --- a/src/Core/AdminConsole/Services/Implementations/WebhookEventHandler.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System.Text; -using System.Text.Json; -using System.Text.Json.Nodes; -using Bit.Core.AdminConsole.Models.Data.Integrations; -using Bit.Core.Enums; -using Bit.Core.Repositories; - -#nullable enable - -namespace Bit.Core.Services; - -public class WebhookEventHandler( - IHttpClientFactory httpClientFactory, - IUserRepository userRepository, - IOrganizationRepository organizationRepository, - IOrganizationIntegrationConfigurationRepository configurationRepository) - : IntegrationEventHandlerBase(userRepository, organizationRepository, configurationRepository) -{ - private readonly HttpClient _httpClient = httpClientFactory.CreateClient(HttpClientName); - - public const string HttpClientName = "WebhookEventHandlerHttpClient"; - - protected override IntegrationType GetIntegrationType() => IntegrationType.Webhook; - - protected override async Task ProcessEventIntegrationAsync(JsonObject mergedConfiguration, - string renderedTemplate) - { - var config = mergedConfiguration.Deserialize(); - if (config is null || string.IsNullOrEmpty(config.url)) - { - return; - } - - var content = new StringContent(renderedTemplate, Encoding.UTF8, "application/json"); - var response = await _httpClient.PostAsync(config.url, content); - response.EnsureSuccessStatusCode(); - } -} diff --git a/src/Core/Settings/GlobalSettings.cs b/src/Core/Settings/GlobalSettings.cs index d3f4253908..e228218a29 100644 --- a/src/Core/Settings/GlobalSettings.cs +++ b/src/Core/Settings/GlobalSettings.cs @@ -288,11 +288,15 @@ public class GlobalSettings : IGlobalSettings public class AzureServiceBusSettings { private string _connectionString; - private string _topicName; + private string _eventTopicName; + private string _integrationTopicName; + public int MaxRetries { get; set; } = 3; public virtual string EventRepositorySubscriptionName { get; set; } = "events-write-subscription"; - public virtual string SlackSubscriptionName { get; set; } = "events-slack-subscription"; - public virtual string WebhookSubscriptionName { get; set; } = "events-webhook-subscription"; + public virtual string SlackEventSubscriptionName { get; set; } = "events-slack-subscription"; + public virtual string SlackIntegrationSubscriptionName { get; set; } = "integration-slack-subscription"; + public virtual string WebhookEventSubscriptionName { get; set; } = "events-webhook-subscription"; + public virtual string WebhookIntegrationSubscriptionName { get; set; } = "integration-webhook-subscription"; public string ConnectionString { @@ -300,10 +304,16 @@ public class GlobalSettings : IGlobalSettings set => _connectionString = value.Trim('"'); } - public string TopicName + public string EventTopicName { - get => _topicName; - set => _topicName = value.Trim('"'); + get => _eventTopicName; + set => _eventTopicName = value.Trim('"'); + } + + public string IntegrationTopicName + { + get => _integrationTopicName; + set => _integrationTopicName = value.Trim('"'); } } diff --git a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs index e425cf7254..247d4c5d43 100644 --- a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs +++ b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs @@ -557,7 +557,7 @@ public static class ServiceCollectionExtensions services.AddKeyedSingleton("storage"); if (CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.ConnectionString) && - CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.TopicName)) + CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.EventTopicName)) { services.AddKeyedSingleton("broadcast"); } @@ -589,86 +589,83 @@ public static class ServiceCollectionExtensions return services; } + private static IServiceCollection AddAzureServiceBusEventRepositoryListener(this IServiceCollection services, GlobalSettings globalSettings) + { + services.AddSingleton(); + services.AddSingleton(); + services.AddKeyedSingleton("persistent"); + + services.AddSingleton(provider => + new AzureServiceBusEventListenerService( + handler: provider.GetRequiredService(), + logger: provider.GetRequiredService>(), + globalSettings: globalSettings, + subscriptionName: globalSettings.EventLogging.AzureServiceBus.EventRepositorySubscriptionName)); + + return services; + } + + private static IServiceCollection AddAzureServiceBusIntegration( + this IServiceCollection services, + string eventSubscriptionName, + string integrationSubscriptionName, + IntegrationType integrationType, + GlobalSettings globalSettings) + where TConfig : class + where THandler : class, IIntegrationHandler + { + var routingKey = integrationType.ToRoutingKey(); + + services.AddSingleton(); + + services.AddKeyedSingleton(routingKey, (provider, _) => + new EventIntegrationHandler( + integrationType, + provider.GetRequiredService(), + provider.GetRequiredService(), + provider.GetRequiredService(), + provider.GetRequiredService())); + + services.AddSingleton(provider => + new AzureServiceBusEventListenerService( + handler: provider.GetRequiredKeyedService(routingKey), + logger: provider.GetRequiredService>(), + globalSettings: globalSettings, + subscriptionName: eventSubscriptionName)); + + services.AddSingleton, THandler>(); + + services.AddSingleton(provider => + new AzureServiceBusIntegrationListenerService( + handler: provider.GetRequiredService>(), + subscriptionName: integrationSubscriptionName, + logger: provider.GetRequiredService>(), + globalSettings: globalSettings)); + + return services; + } + public static IServiceCollection AddAzureServiceBusListeners(this IServiceCollection services, GlobalSettings globalSettings) { - if (CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.ConnectionString) && - CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.TopicName)) - { - services.AddSingleton(); - services.AddSingleton(); - services.AddKeyedSingleton("persistent"); - services.AddSingleton(provider => - new AzureServiceBusEventListenerService( - provider.GetRequiredService(), - provider.GetRequiredService>(), - globalSettings, - globalSettings.EventLogging.AzureServiceBus.EventRepositorySubscriptionName)); + if (!CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.ConnectionString) || + !CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.EventTopicName)) + return services; + services.AddAzureServiceBusEventRepositoryListener(globalSettings); - services.AddSlackService(globalSettings); - services.AddSingleton(); - services.AddSingleton(provider => - new AzureServiceBusEventListenerService( - provider.GetRequiredService(), - provider.GetRequiredService>(), - globalSettings, - globalSettings.EventLogging.AzureServiceBus.SlackSubscriptionName)); - - services.AddSingleton(); - services.AddHttpClient(WebhookEventHandler.HttpClientName); - services.AddSingleton(provider => - new AzureServiceBusEventListenerService( - provider.GetRequiredService(), - provider.GetRequiredService>(), - globalSettings, - globalSettings.EventLogging.AzureServiceBus.WebhookSubscriptionName)); - } - - return services; - } - - public static IServiceCollection AddRabbitMqListeners(this IServiceCollection services, GlobalSettings globalSettings) - { - if (IsRabbitMqEnabled(globalSettings)) - { - services.AddRabbitMqEventRepositoryListener(globalSettings); - - services.AddSlackService(globalSettings); - services.AddRabbitMqIntegration( - globalSettings.EventLogging.RabbitMq.SlackEventsQueueName, - globalSettings.EventLogging.RabbitMq.SlackIntegrationQueueName, - globalSettings.EventLogging.RabbitMq.SlackIntegrationRetryQueueName, - globalSettings.EventLogging.RabbitMq.IntegrationDeadLetterQueueName, - IntegrationType.Slack, - globalSettings); - - services.AddHttpClient(WebhookIntegrationHandler.HttpClientName); - services.AddRabbitMqIntegration( - globalSettings.EventLogging.RabbitMq.WebhookEventsQueueName, - globalSettings.EventLogging.RabbitMq.WebhookIntegrationQueueName, - globalSettings.EventLogging.RabbitMq.WebhookIntegrationRetryQueueName, - globalSettings.EventLogging.RabbitMq.IntegrationDeadLetterQueueName, - IntegrationType.Webhook, - 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.AddSingleton(); - } - else - { - services.AddSingleton(); - } + services.AddSlackService(globalSettings); + services.AddAzureServiceBusIntegration( + eventSubscriptionName: globalSettings.EventLogging.AzureServiceBus.SlackEventSubscriptionName, + integrationSubscriptionName: globalSettings.EventLogging.AzureServiceBus.SlackIntegrationSubscriptionName, + integrationType: IntegrationType.Slack, + globalSettings: globalSettings); + services.AddHttpClient(WebhookIntegrationHandler.HttpClientName); + services.AddAzureServiceBusIntegration( + eventSubscriptionName: globalSettings.EventLogging.AzureServiceBus.WebhookEventSubscriptionName, + integrationSubscriptionName: globalSettings.EventLogging.AzureServiceBus.WebhookIntegrationSubscriptionName, + integrationType: IntegrationType.Webhook, + globalSettings: globalSettings); return services; } @@ -729,6 +726,36 @@ public static class ServiceCollectionExtensions return services; } + public static IServiceCollection AddRabbitMqListeners(this IServiceCollection services, GlobalSettings globalSettings) + { + if (!IsRabbitMqEnabled(globalSettings)) + { + return services; + } + + services.AddRabbitMqEventRepositoryListener(globalSettings); + + services.AddSlackService(globalSettings); + services.AddRabbitMqIntegration( + globalSettings.EventLogging.RabbitMq.SlackEventsQueueName, + globalSettings.EventLogging.RabbitMq.SlackIntegrationQueueName, + globalSettings.EventLogging.RabbitMq.SlackIntegrationRetryQueueName, + globalSettings.EventLogging.RabbitMq.IntegrationDeadLetterQueueName, + IntegrationType.Slack, + globalSettings); + + services.AddHttpClient(WebhookIntegrationHandler.HttpClientName); + services.AddRabbitMqIntegration( + globalSettings.EventLogging.RabbitMq.WebhookEventsQueueName, + globalSettings.EventLogging.RabbitMq.WebhookIntegrationQueueName, + globalSettings.EventLogging.RabbitMq.WebhookIntegrationRetryQueueName, + globalSettings.EventLogging.RabbitMq.IntegrationDeadLetterQueueName, + IntegrationType.Webhook, + globalSettings); + + return services; + } + private static bool IsRabbitMqEnabled(GlobalSettings settings) { return CoreHelpers.SettingHasValue(settings.EventLogging.RabbitMq.HostName) && @@ -737,6 +764,23 @@ public static class ServiceCollectionExtensions CoreHelpers.SettingHasValue(settings.EventLogging.RabbitMq.EventExchangeName); } + 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.AddSingleton(); + } + else + { + services.AddSingleton(); + } + + return services; + } + public static void UseDefaultMiddleware(this IApplicationBuilder app, IWebHostEnvironment env, GlobalSettings globalSettings) { diff --git a/test/Core.Test/AdminConsole/Models/Data/Integrations/IntegrationMessageTests.cs b/test/Core.Test/AdminConsole/Models/Data/Integrations/IntegrationMessageTests.cs index 44774449c1..0946841347 100644 --- a/test/Core.Test/AdminConsole/Models/Data/Integrations/IntegrationMessageTests.cs +++ b/test/Core.Test/AdminConsole/Models/Data/Integrations/IntegrationMessageTests.cs @@ -20,6 +20,7 @@ public class IntegrationMessageTests message.ApplyRetry(baseline); Assert.Equal(3, message.RetryCount); + Assert.NotNull(message.DelayUntilDate); Assert.True(message.DelayUntilDate > baseline); } diff --git a/test/Core.Test/AdminConsole/Services/IntegrationEventHandlerBaseTests.cs b/test/Core.Test/AdminConsole/Services/IntegrationEventHandlerBaseTests.cs deleted file mode 100644 index e1a2fbff68..0000000000 --- a/test/Core.Test/AdminConsole/Services/IntegrationEventHandlerBaseTests.cs +++ /dev/null @@ -1,219 +0,0 @@ -using System.Text.Json; -using System.Text.Json.Nodes; -using Bit.Core.AdminConsole.Entities; -using Bit.Core.Entities; -using Bit.Core.Enums; -using Bit.Core.Models.Data; -using Bit.Core.Models.Data.Organizations; -using Bit.Core.Repositories; -using Bit.Core.Services; -using Bit.Test.Common.AutoFixture; -using Bit.Test.Common.AutoFixture.Attributes; -using NSubstitute; -using Xunit; - -namespace Bit.Core.Test.Services; - -[SutProviderCustomize] -public class IntegrationEventHandlerBaseEventHandlerTests -{ - private const string _templateBase = "Date: #Date#, Type: #Type#, UserId: #UserId#"; - private const string _templateWithOrganization = "Org: #OrganizationName#"; - private const string _templateWithUser = "#UserName#, #UserEmail#"; - private const string _templateWithActingUser = "#ActingUserName#, #ActingUserEmail#"; - private const string _url = "https://localhost"; - - private SutProvider GetSutProvider( - List configurations) - { - var configurationRepository = Substitute.For(); - configurationRepository.GetConfigurationDetailsAsync(Arg.Any(), - IntegrationType.Webhook, Arg.Any()).Returns(configurations); - - return new SutProvider() - .SetDependency(configurationRepository) - .Create(); - } - - private static List NoConfigurations() - { - return []; - } - - private static List OneConfiguration(string template) - { - var config = Substitute.For(); - config.Configuration = null; - config.IntegrationConfiguration = JsonSerializer.Serialize(new { url = _url }); - config.Template = template; - - return [config]; - } - - private static List TwoConfigurations(string template) - { - var config = Substitute.For(); - config.Configuration = null; - config.IntegrationConfiguration = JsonSerializer.Serialize(new { url = _url }); - config.Template = template; - var config2 = Substitute.For(); - config2.Configuration = null; - config2.IntegrationConfiguration = JsonSerializer.Serialize(new { url = _url }); - config2.Template = template; - - return [config, config2]; - } - - [Theory, BitAutoData] - public async Task HandleEventAsync_BaseTemplateNoConfigurations_DoesNothing(EventMessage eventMessage) - { - var sutProvider = GetSutProvider(NoConfigurations()); - - await sutProvider.Sut.HandleEventAsync(eventMessage); - Assert.Empty(sutProvider.Sut.CapturedCalls); - } - - [Theory, BitAutoData] - public async Task HandleEventAsync_BaseTemplateOneConfiguration_CallsProcessEventIntegrationAsync(EventMessage eventMessage) - { - var sutProvider = GetSutProvider(OneConfiguration(_templateBase)); - - await sutProvider.Sut.HandleEventAsync(eventMessage); - - Assert.Single(sutProvider.Sut.CapturedCalls); - - var expectedTemplate = $"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}"; - - Assert.Equal(expectedTemplate, sutProvider.Sut.CapturedCalls.Single().RenderedTemplate); - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any()); - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any()); - } - - [Theory, BitAutoData] - public async Task HandleEventAsync_ActingUserTemplate_LoadsUserFromRepository(EventMessage eventMessage) - { - var sutProvider = GetSutProvider(OneConfiguration(_templateWithActingUser)); - var user = Substitute.For(); - user.Email = "test@example.com"; - user.Name = "Test"; - - sutProvider.GetDependency().GetByIdAsync(Arg.Any()).Returns(user); - await sutProvider.Sut.HandleEventAsync(eventMessage); - - Assert.Single(sutProvider.Sut.CapturedCalls); - - var expectedTemplate = $"{user.Name}, {user.Email}"; - Assert.Equal(expectedTemplate, sutProvider.Sut.CapturedCalls.Single().RenderedTemplate); - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any()); - await sutProvider.GetDependency().Received(1).GetByIdAsync(eventMessage.ActingUserId ?? Guid.Empty); - } - - [Theory, BitAutoData] - public async Task HandleEventAsync_OrganizationTemplate_LoadsOrganizationFromRepository(EventMessage eventMessage) - { - var sutProvider = GetSutProvider(OneConfiguration(_templateWithOrganization)); - var organization = Substitute.For(); - organization.Name = "Test"; - - sutProvider.GetDependency().GetByIdAsync(Arg.Any()).Returns(organization); - await sutProvider.Sut.HandleEventAsync(eventMessage); - - Assert.Single(sutProvider.Sut.CapturedCalls); - - var expectedTemplate = $"Org: {organization.Name}"; - Assert.Equal(expectedTemplate, sutProvider.Sut.CapturedCalls.Single().RenderedTemplate); - await sutProvider.GetDependency().Received(1).GetByIdAsync(eventMessage.OrganizationId ?? Guid.Empty); - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any()); - } - - [Theory, BitAutoData] - public async Task HandleEventAsync_UserTemplate_LoadsUserFromRepository(EventMessage eventMessage) - { - var sutProvider = GetSutProvider(OneConfiguration(_templateWithUser)); - var user = Substitute.For(); - user.Email = "test@example.com"; - user.Name = "Test"; - - sutProvider.GetDependency().GetByIdAsync(Arg.Any()).Returns(user); - await sutProvider.Sut.HandleEventAsync(eventMessage); - - Assert.Single(sutProvider.Sut.CapturedCalls); - - var expectedTemplate = $"{user.Name}, {user.Email}"; - Assert.Equal(expectedTemplate, sutProvider.Sut.CapturedCalls.Single().RenderedTemplate); - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any()); - await sutProvider.GetDependency().Received(1).GetByIdAsync(eventMessage.UserId ?? Guid.Empty); - } - - [Theory, BitAutoData] - public async Task HandleManyEventsAsync_BaseTemplateNoConfigurations_DoesNothing(List eventMessages) - { - var sutProvider = GetSutProvider(NoConfigurations()); - - await sutProvider.Sut.HandleManyEventsAsync(eventMessages); - Assert.Empty(sutProvider.Sut.CapturedCalls); - } - - [Theory, BitAutoData] - public async Task HandleManyEventsAsync_BaseTemplateOneConfiguration_CallsProcessEventIntegrationAsync(List eventMessages) - { - var sutProvider = GetSutProvider(OneConfiguration(_templateBase)); - - await sutProvider.Sut.HandleManyEventsAsync(eventMessages); - - Assert.Equal(eventMessages.Count, sutProvider.Sut.CapturedCalls.Count); - var index = 0; - foreach (var call in sutProvider.Sut.CapturedCalls) - { - var expected = eventMessages[index]; - var expectedTemplate = $"Date: {expected.Date}, Type: {expected.Type}, UserId: {expected.UserId}"; - - Assert.Equal(expectedTemplate, call.RenderedTemplate); - index++; - } - } - - [Theory, BitAutoData] - public async Task HandleManyEventsAsync_BaseTemplateTwoConfigurations_CallsProcessEventIntegrationAsyncMultipleTimes( - List eventMessages) - { - var sutProvider = GetSutProvider(TwoConfigurations(_templateBase)); - - await sutProvider.Sut.HandleManyEventsAsync(eventMessages); - - Assert.Equal(eventMessages.Count * 2, sutProvider.Sut.CapturedCalls.Count); - - var capturedCalls = sutProvider.Sut.CapturedCalls.GetEnumerator(); - foreach (var eventMessage in eventMessages) - { - var expectedTemplate = $"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}"; - - Assert.True(capturedCalls.MoveNext()); - var call = capturedCalls.Current; - Assert.Equal(expectedTemplate, call.RenderedTemplate); - - Assert.True(capturedCalls.MoveNext()); - call = capturedCalls.Current; - Assert.Equal(expectedTemplate, call.RenderedTemplate); - } - } - - private class TestIntegrationEventHandlerBase : IntegrationEventHandlerBase - { - public TestIntegrationEventHandlerBase(IUserRepository userRepository, - IOrganizationRepository organizationRepository, - IOrganizationIntegrationConfigurationRepository configurationRepository) - : base(userRepository, organizationRepository, configurationRepository) - { } - - public List<(JsonObject MergedConfiguration, string RenderedTemplate)> CapturedCalls { get; } = new(); - - protected override IntegrationType GetIntegrationType() => IntegrationType.Webhook; - - protected override Task ProcessEventIntegrationAsync(JsonObject mergedConfiguration, string renderedTemplate) - { - CapturedCalls.Add((mergedConfiguration, renderedTemplate)); - return Task.CompletedTask; - } - } -} diff --git a/test/Core.Test/AdminConsole/Services/SlackEventHandlerTests.cs b/test/Core.Test/AdminConsole/Services/SlackEventHandlerTests.cs deleted file mode 100644 index 558bded8b3..0000000000 --- a/test/Core.Test/AdminConsole/Services/SlackEventHandlerTests.cs +++ /dev/null @@ -1,181 +0,0 @@ -using System.Text.Json; -using Bit.Core.Enums; -using Bit.Core.Models.Data; -using Bit.Core.Models.Data.Organizations; -using Bit.Core.Repositories; -using Bit.Core.Services; -using Bit.Test.Common.AutoFixture; -using Bit.Test.Common.AutoFixture.Attributes; -using Bit.Test.Common.Helpers; -using NSubstitute; -using Xunit; - -namespace Bit.Core.Test.Services; - -[SutProviderCustomize] -public class SlackEventHandlerTests -{ - private readonly IOrganizationIntegrationConfigurationRepository _repository = Substitute.For(); - private readonly ISlackService _slackService = Substitute.For(); - private readonly string _channelId = "C12345"; - private readonly string _channelId2 = "C67890"; - private readonly string _token = "xoxb-test-token"; - private readonly string _token2 = "xoxb-another-test-token"; - - private SutProvider GetSutProvider( - List integrationConfigurations) - { - _repository.GetConfigurationDetailsAsync(Arg.Any(), - IntegrationType.Slack, Arg.Any()) - .Returns(integrationConfigurations); - - return new SutProvider() - .SetDependency(_repository) - .SetDependency(_slackService) - .Create(); - } - - private List NoConfigurations() - { - return []; - } - - private List OneConfiguration() - { - var config = Substitute.For(); - config.Configuration = JsonSerializer.Serialize(new { token = _token }); - config.IntegrationConfiguration = JsonSerializer.Serialize(new { channelId = _channelId }); - config.Template = "Date: #Date#, Type: #Type#, UserId: #UserId#"; - - return [config]; - } - - private List TwoConfigurations() - { - var config = Substitute.For(); - config.Configuration = JsonSerializer.Serialize(new { token = _token }); - config.IntegrationConfiguration = JsonSerializer.Serialize(new { channelId = _channelId }); - config.Template = "Date: #Date#, Type: #Type#, UserId: #UserId#"; - var config2 = Substitute.For(); - config2.Configuration = JsonSerializer.Serialize(new { token = _token2 }); - config2.IntegrationConfiguration = JsonSerializer.Serialize(new { channelId = _channelId2 }); - config2.Template = "Date: #Date#, Type: #Type#, UserId: #UserId#"; - - return [config, config2]; - } - - private List WrongConfiguration() - { - var config = Substitute.For(); - config.Configuration = JsonSerializer.Serialize(new { }); - config.IntegrationConfiguration = JsonSerializer.Serialize(new { }); - config.Template = "Date: #Date#, Type: #Type#, UserId: #UserId#"; - - return [config]; - } - - [Theory, BitAutoData] - public async Task HandleEventAsync_NoConfigurations_DoesNothing(EventMessage eventMessage) - { - var sutProvider = GetSutProvider(NoConfigurations()); - - await sutProvider.Sut.HandleEventAsync(eventMessage); - sutProvider.GetDependency().DidNotReceiveWithAnyArgs(); - } - - [Theory, BitAutoData] - public async Task HandleEventAsync_OneConfiguration_SendsEventViaSlackService(EventMessage eventMessage) - { - var sutProvider = GetSutProvider(OneConfiguration()); - - await sutProvider.Sut.HandleEventAsync(eventMessage); - await sutProvider.GetDependency().Received(1).SendSlackMessageByChannelIdAsync( - Arg.Is(AssertHelper.AssertPropertyEqual(_token)), - Arg.Is(AssertHelper.AssertPropertyEqual( - $"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}")), - Arg.Is(AssertHelper.AssertPropertyEqual(_channelId)) - ); - } - - [Theory, BitAutoData] - public async Task HandleEventAsync_TwoConfigurations_SendsMultipleEvents(EventMessage eventMessage) - { - var sutProvider = GetSutProvider(TwoConfigurations()); - - await sutProvider.Sut.HandleEventAsync(eventMessage); - await sutProvider.GetDependency().Received(1).SendSlackMessageByChannelIdAsync( - Arg.Is(AssertHelper.AssertPropertyEqual(_token)), - Arg.Is(AssertHelper.AssertPropertyEqual( - $"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}")), - Arg.Is(AssertHelper.AssertPropertyEqual(_channelId)) - ); - await sutProvider.GetDependency().Received(1).SendSlackMessageByChannelIdAsync( - Arg.Is(AssertHelper.AssertPropertyEqual(_token2)), - Arg.Is(AssertHelper.AssertPropertyEqual( - $"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}")), - Arg.Is(AssertHelper.AssertPropertyEqual(_channelId2)) - ); - } - - [Theory, BitAutoData] - public async Task HandleEventAsync_WrongConfiguration_DoesNothing(EventMessage eventMessage) - { - var sutProvider = GetSutProvider(WrongConfiguration()); - - await sutProvider.Sut.HandleEventAsync(eventMessage); - sutProvider.GetDependency().DidNotReceiveWithAnyArgs(); - } - - [Theory, BitAutoData] - public async Task HandleManyEventsAsync_OneConfiguration_SendsEventsViaSlackService(List eventMessages) - { - var sutProvider = GetSutProvider(OneConfiguration()); - - await sutProvider.Sut.HandleManyEventsAsync(eventMessages); - - var received = sutProvider.GetDependency().ReceivedCalls(); - using var calls = received.GetEnumerator(); - - Assert.Equal(eventMessages.Count, received.Count()); - - foreach (var eventMessage in eventMessages) - { - Assert.True(calls.MoveNext()); - var arguments = calls.Current.GetArguments(); - Assert.Equal(_token, arguments[0] as string); - Assert.Equal($"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}", - arguments[1] as string); - Assert.Equal(_channelId, arguments[2] as string); - } - } - - [Theory, BitAutoData] - public async Task HandleManyEventsAsync_TwoConfigurations_SendsMultipleEvents(List eventMessages) - { - var sutProvider = GetSutProvider(TwoConfigurations()); - - await sutProvider.Sut.HandleManyEventsAsync(eventMessages); - - var received = sutProvider.GetDependency().ReceivedCalls(); - using var calls = received.GetEnumerator(); - - Assert.Equal(eventMessages.Count * 2, received.Count()); - - foreach (var eventMessage in eventMessages) - { - Assert.True(calls.MoveNext()); - var arguments = calls.Current.GetArguments(); - Assert.Equal(_token, arguments[0] as string); - Assert.Equal($"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}", - arguments[1] as string); - Assert.Equal(_channelId, arguments[2] as string); - - Assert.True(calls.MoveNext()); - var arguments2 = calls.Current.GetArguments(); - Assert.Equal(_token2, arguments2[0] as string); - Assert.Equal($"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}", - arguments2[1] as string); - Assert.Equal(_channelId2, arguments2[2] as string); - } - } -} diff --git a/test/Core.Test/AdminConsole/Services/WebhookEventHandlerTests.cs b/test/Core.Test/AdminConsole/Services/WebhookEventHandlerTests.cs deleted file mode 100644 index c426f8eaad..0000000000 --- a/test/Core.Test/AdminConsole/Services/WebhookEventHandlerTests.cs +++ /dev/null @@ -1,235 +0,0 @@ -using System.Net; -using System.Net.Http.Json; -using System.Text.Json; -using Bit.Core.Enums; -using Bit.Core.Models.Data; -using Bit.Core.Models.Data.Organizations; -using Bit.Core.Repositories; -using Bit.Core.Services; -using Bit.Test.Common.AutoFixture; -using Bit.Test.Common.AutoFixture.Attributes; -using Bit.Test.Common.Helpers; -using Bit.Test.Common.MockedHttpClient; -using NSubstitute; -using Xunit; - -namespace Bit.Core.Test.Services; - -[SutProviderCustomize] -public class WebhookEventHandlerTests -{ - private readonly MockedHttpMessageHandler _handler; - private readonly HttpClient _httpClient; - - private const string _template = - """ - { - "Date": "#Date#", - "Type": "#Type#", - "UserId": "#UserId#" - } - """; - private const string _webhookUrl = "http://localhost/test/event"; - private const string _webhookUrl2 = "http://localhost/another/event"; - - public WebhookEventHandlerTests() - { - _handler = new MockedHttpMessageHandler(); - _handler.Fallback - .WithStatusCode(HttpStatusCode.OK) - .WithContent(new StringContent("testtest")); - _httpClient = _handler.ToHttpClient(); - } - - private SutProvider GetSutProvider( - List configurations) - { - var clientFactory = Substitute.For(); - clientFactory.CreateClient(WebhookEventHandler.HttpClientName).Returns(_httpClient); - - var repository = Substitute.For(); - repository.GetConfigurationDetailsAsync(Arg.Any(), - IntegrationType.Webhook, Arg.Any()).Returns(configurations); - - return new SutProvider() - .SetDependency(repository) - .SetDependency(clientFactory) - .Create(); - } - - private static List NoConfigurations() - { - return []; - } - - private static List OneConfiguration() - { - var config = Substitute.For(); - config.Configuration = null; - config.IntegrationConfiguration = JsonSerializer.Serialize(new { url = _webhookUrl }); - config.Template = _template; - - return [config]; - } - - private static List TwoConfigurations() - { - var config = Substitute.For(); - config.Configuration = null; - config.IntegrationConfiguration = JsonSerializer.Serialize(new { url = _webhookUrl }); - config.Template = _template; - var config2 = Substitute.For(); - config2.Configuration = null; - config2.IntegrationConfiguration = JsonSerializer.Serialize(new { url = _webhookUrl2 }); - config2.Template = _template; - - return [config, config2]; - } - - private static List WrongConfiguration() - { - var config = Substitute.For(); - config.Configuration = null; - config.IntegrationConfiguration = JsonSerializer.Serialize(new { error = string.Empty }); - config.Template = _template; - - return [config]; - } - - [Theory, BitAutoData] - public async Task HandleEventAsync_NoConfigurations_DoesNothing(EventMessage eventMessage) - { - var sutProvider = GetSutProvider(NoConfigurations()); - - await sutProvider.Sut.HandleEventAsync(eventMessage); - sutProvider.GetDependency().Received(1).CreateClient( - Arg.Is(AssertHelper.AssertPropertyEqual(WebhookEventHandler.HttpClientName)) - ); - - Assert.Empty(_handler.CapturedRequests); - } - - [Theory, BitAutoData] - public async Task HandleEventAsync_OneConfiguration_PostsEventToUrl(EventMessage eventMessage) - { - var sutProvider = GetSutProvider(OneConfiguration()); - - await sutProvider.Sut.HandleEventAsync(eventMessage); - sutProvider.GetDependency().Received(1).CreateClient( - Arg.Is(AssertHelper.AssertPropertyEqual(WebhookEventHandler.HttpClientName)) - ); - - Assert.Single(_handler.CapturedRequests); - var request = _handler.CapturedRequests[0]; - Assert.NotNull(request); - var returned = await request.Content.ReadFromJsonAsync(); - var expected = MockEvent.From(eventMessage); - - Assert.Equal(HttpMethod.Post, request.Method); - Assert.Equal(_webhookUrl, request.RequestUri.ToString()); - AssertHelper.AssertPropertyEqual(expected, returned); - } - - [Theory, BitAutoData] - public async Task HandleEventAsync_WrongConfigurations_DoesNothing(EventMessage eventMessage) - { - var sutProvider = GetSutProvider(WrongConfiguration()); - - await sutProvider.Sut.HandleEventAsync(eventMessage); - sutProvider.GetDependency().Received(1).CreateClient( - Arg.Is(AssertHelper.AssertPropertyEqual(WebhookEventHandler.HttpClientName)) - ); - - Assert.Empty(_handler.CapturedRequests); - } - - [Theory, BitAutoData] - public async Task HandleManyEventsAsync_NoConfigurations_DoesNothing(List eventMessages) - { - var sutProvider = GetSutProvider(NoConfigurations()); - - await sutProvider.Sut.HandleManyEventsAsync(eventMessages); - sutProvider.GetDependency().Received(1).CreateClient( - Arg.Is(AssertHelper.AssertPropertyEqual(WebhookEventHandler.HttpClientName)) - ); - - Assert.Empty(_handler.CapturedRequests); - } - - - [Theory, BitAutoData] - public async Task HandleManyEventsAsync_OneConfiguration_PostsEventsToUrl(List eventMessages) - { - var sutProvider = GetSutProvider(OneConfiguration()); - - await sutProvider.Sut.HandleManyEventsAsync(eventMessages); - sutProvider.GetDependency().Received(1).CreateClient( - Arg.Is(AssertHelper.AssertPropertyEqual(WebhookEventHandler.HttpClientName)) - ); - - Assert.Equal(eventMessages.Count, _handler.CapturedRequests.Count); - var index = 0; - foreach (var request in _handler.CapturedRequests) - { - Assert.NotNull(request); - var returned = await request.Content.ReadFromJsonAsync(); - var expected = MockEvent.From(eventMessages[index]); - - Assert.Equal(HttpMethod.Post, request.Method); - Assert.Equal(_webhookUrl, request.RequestUri.ToString()); - AssertHelper.AssertPropertyEqual(expected, returned); - index++; - } - } - - [Theory, BitAutoData] - public async Task HandleManyEventsAsync_TwoConfigurations_PostsEventsToMultipleUrls(List eventMessages) - { - var sutProvider = GetSutProvider(TwoConfigurations()); - - await sutProvider.Sut.HandleManyEventsAsync(eventMessages); - sutProvider.GetDependency().Received(1).CreateClient( - Arg.Is(AssertHelper.AssertPropertyEqual(WebhookEventHandler.HttpClientName)) - ); - - using var capturedRequests = _handler.CapturedRequests.GetEnumerator(); - Assert.Equal(eventMessages.Count * 2, _handler.CapturedRequests.Count); - - foreach (var eventMessage in eventMessages) - { - var expected = MockEvent.From(eventMessage); - - Assert.True(capturedRequests.MoveNext()); - var request = capturedRequests.Current; - Assert.NotNull(request); - Assert.Equal(HttpMethod.Post, request.Method); - Assert.Equal(_webhookUrl, request.RequestUri.ToString()); - var returned = await request.Content.ReadFromJsonAsync(); - AssertHelper.AssertPropertyEqual(expected, returned); - - Assert.True(capturedRequests.MoveNext()); - request = capturedRequests.Current; - Assert.NotNull(request); - Assert.Equal(HttpMethod.Post, request.Method); - Assert.Equal(_webhookUrl2, request.RequestUri.ToString()); - returned = await request.Content.ReadFromJsonAsync(); - AssertHelper.AssertPropertyEqual(expected, returned); - } - } -} - -public class MockEvent(string date, string type, string userId) -{ - public string Date { get; set; } = date; - public string Type { get; set; } = type; - public string UserId { get; set; } = userId; - - public static MockEvent From(EventMessage eventMessage) - { - return new MockEvent( - eventMessage.Date.ToString(), - eventMessage.Type.ToString(), - eventMessage.UserId.ToString() - ); - } -}