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

Move all event integration code to Dirt (#6757)

* Move all event integration code to Dirt

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

View File

@@ -1,161 +0,0 @@
#nullable enable
using System.Text.Json;
using Azure.Messaging.ServiceBus;
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
using Bit.Core.Models.Data;
using Bit.Core.Services;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Bit.Test.Common.Helpers;
using Microsoft.Extensions.Logging;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.Services;
[SutProviderCustomize]
public class AzureServiceBusEventListenerServiceTests
{
private const string _messageId = "messageId";
private readonly TestListenerConfiguration _config = new();
private readonly ILogger _logger = Substitute.For<ILogger>();
private SutProvider<AzureServiceBusEventListenerService<TestListenerConfiguration>> GetSutProvider()
{
var loggerFactory = Substitute.For<ILoggerFactory>();
loggerFactory.CreateLogger<object>().ReturnsForAnyArgs(_logger);
return new SutProvider<AzureServiceBusEventListenerService<TestListenerConfiguration>>()
.SetDependency(_config)
.SetDependency(loggerFactory)
.Create();
}
[Fact]
public void Constructor_CreatesLogWithCorrectCategory()
{
var sutProvider = GetSutProvider();
var fullName = typeof(AzureServiceBusEventListenerService<>).FullName ?? "";
var tickIndex = fullName.IndexOf('`');
var cleanedName = tickIndex >= 0 ? fullName.Substring(0, tickIndex) : fullName;
var categoryName = cleanedName + '.' + _config.EventSubscriptionName;
sutProvider.GetDependency<ILoggerFactory>().Received(1).CreateLogger(categoryName);
}
[Fact]
public void Constructor_CreatesProcessor()
{
var sutProvider = GetSutProvider();
sutProvider.GetDependency<IAzureServiceBusService>().Received(1).CreateProcessor(
Arg.Is(_config.EventTopicName),
Arg.Is(_config.EventSubscriptionName),
Arg.Any<ServiceBusProcessorOptions>()
);
}
[Theory, BitAutoData]
public async Task ProcessErrorAsync_LogsError(ProcessErrorEventArgs args)
{
var sutProvider = GetSutProvider();
await sutProvider.Sut.ProcessErrorAsync(args);
_logger.Received(1).Log(
LogLevel.Error,
Arg.Any<EventId>(),
Arg.Any<object>(),
Arg.Any<Exception>(),
Arg.Any<Func<object, Exception?, string>>());
}
[Fact]
public async Task ProcessReceivedMessageAsync_EmptyJson_LogsError()
{
var sutProvider = GetSutProvider();
await sutProvider.Sut.ProcessReceivedMessageAsync(string.Empty, _messageId);
_logger.Received(1).Log(
LogLevel.Error,
Arg.Any<EventId>(),
Arg.Any<object>(),
Arg.Any<JsonException>(),
Arg.Any<Func<object, Exception?, string>>());
}
[Fact]
public async Task ProcessReceivedMessageAsync_InvalidJson_LogsError()
{
var sutProvider = GetSutProvider();
await sutProvider.Sut.ProcessReceivedMessageAsync("{ Invalid JSON }", _messageId);
_logger.Received(1).Log(
LogLevel.Error,
Arg.Any<EventId>(),
Arg.Is<object>(o => (o.ToString() ?? "").Contains("Invalid JSON")),
Arg.Any<Exception>(),
Arg.Any<Func<object, Exception?, string>>());
}
[Fact]
public async Task ProcessReceivedMessageAsync_InvalidJsonArray_LogsError()
{
var sutProvider = GetSutProvider();
await sutProvider.Sut.ProcessReceivedMessageAsync(
"{ \"not a valid\", \"list of event messages\" }",
_messageId
);
_logger.Received(1).Log(
LogLevel.Error,
Arg.Any<EventId>(),
Arg.Any<object>(),
Arg.Any<JsonException>(),
Arg.Any<Func<object, Exception?, string>>());
}
[Fact]
public async Task ProcessReceivedMessageAsync_InvalidJsonObject_LogsError()
{
var sutProvider = GetSutProvider();
await sutProvider.Sut.ProcessReceivedMessageAsync(
JsonSerializer.Serialize(DateTime.UtcNow), // wrong object - not EventMessage
_messageId
);
_logger.Received(1).Log(
LogLevel.Error,
Arg.Any<EventId>(),
Arg.Any<object>(),
Arg.Any<JsonException>(),
Arg.Any<Func<object, Exception?, string>>());
}
[Theory, BitAutoData]
public async Task ProcessReceivedMessageAsync_SingleEvent_DelegatesToHandler(EventMessage message)
{
var sutProvider = GetSutProvider();
await sutProvider.Sut.ProcessReceivedMessageAsync(
JsonSerializer.Serialize(message),
_messageId
);
await sutProvider.GetDependency<IEventMessageHandler>().Received(1).HandleEventAsync(
Arg.Is(AssertHelper.AssertPropertyEqual(message, new[] { "IdempotencyId" })));
}
[Theory, BitAutoData]
public async Task ProcessReceivedMessageAsync_ManyEvents_DelegatesToHandler(IEnumerable<EventMessage> messages)
{
var sutProvider = GetSutProvider();
await sutProvider.Sut.ProcessReceivedMessageAsync(
JsonSerializer.Serialize(messages),
_messageId
);
await sutProvider.GetDependency<IEventMessageHandler>().Received(1).HandleManyEventsAsync(
Arg.Is(AssertHelper.AssertPropertyEqual(messages, new[] { "IdempotencyId" })));
}
}

View File

@@ -1,182 +0,0 @@
#nullable enable
using System.Text.Json;
using Azure.Messaging.ServiceBus;
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
using Bit.Core.Services;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Microsoft.Extensions.Logging;
using NSubstitute;
using NSubstitute.ExceptionExtensions;
using Xunit;
namespace Bit.Core.Test.Services;
[SutProviderCustomize]
public class AzureServiceBusIntegrationListenerServiceTests
{
private readonly IIntegrationHandler _handler = Substitute.For<IIntegrationHandler>();
private readonly ILogger _logger = Substitute.For<ILogger>();
private readonly IAzureServiceBusService _serviceBusService = Substitute.For<IAzureServiceBusService>();
private readonly TestListenerConfiguration _config = new();
private SutProvider<AzureServiceBusIntegrationListenerService<TestListenerConfiguration>> GetSutProvider()
{
var loggerFactory = Substitute.For<ILoggerFactory>();
loggerFactory.CreateLogger<object>().ReturnsForAnyArgs(_logger);
return new SutProvider<AzureServiceBusIntegrationListenerService<TestListenerConfiguration>>()
.SetDependency(_config)
.SetDependency(loggerFactory)
.SetDependency(_handler)
.SetDependency(_serviceBusService)
.Create();
}
[Fact]
public void Constructor_CreatesLogWithCorrectCategory()
{
var sutProvider = GetSutProvider();
var fullName = typeof(AzureServiceBusIntegrationListenerService<>).FullName ?? "";
var tickIndex = fullName.IndexOf('`');
var cleanedName = tickIndex >= 0 ? fullName.Substring(0, tickIndex) : fullName;
var categoryName = cleanedName + '.' + _config.IntegrationSubscriptionName;
sutProvider.GetDependency<ILoggerFactory>().Received(1).CreateLogger(categoryName);
}
[Fact]
public void Constructor_CreatesProcessor()
{
var sutProvider = GetSutProvider();
sutProvider.GetDependency<IAzureServiceBusService>().Received(1).CreateProcessor(
Arg.Is(_config.IntegrationTopicName),
Arg.Is(_config.IntegrationSubscriptionName),
Arg.Any<ServiceBusProcessorOptions>()
);
}
[Theory, BitAutoData]
public async Task ProcessErrorAsync_LogsError(ProcessErrorEventArgs args)
{
var sutProvider = GetSutProvider();
await sutProvider.Sut.ProcessErrorAsync(args);
_logger.Received(1).Log(
LogLevel.Error,
Arg.Any<EventId>(),
Arg.Any<object>(),
Arg.Any<Exception>(),
Arg.Any<Func<object, Exception?, string>>());
}
[Theory, BitAutoData]
public async Task HandleMessageAsync_FailureNotRetryable_PublishesToDeadLetterQueue(IntegrationMessage<WebhookIntegrationConfiguration> message)
{
var sutProvider = GetSutProvider();
message.RetryCount = 0;
var result = IntegrationHandlerResult.Fail(
message: message,
category: IntegrationFailureCategory.AuthenticationFailed, // NOT retryable
failureReason: "403");
_handler.HandleAsync(Arg.Any<string>()).Returns(result);
var expected = IntegrationMessage<WebhookIntegrationConfiguration>.FromJson(message.ToJson());
Assert.NotNull(expected);
Assert.False(await sutProvider.Sut.HandleMessageAsync(message.ToJson()));
await _handler.Received(1).HandleAsync(Arg.Is(expected.ToJson()));
await _serviceBusService.DidNotReceiveWithAnyArgs().PublishToRetryAsync(Arg.Any<IIntegrationMessage>());
_logger.Received().Log(
LogLevel.Warning,
Arg.Any<EventId>(),
Arg.Is<object>(o => (o.ToString() ?? "").Contains("Integration failure - non-recoverable error or max retries exceeded.")),
Arg.Any<Exception?>(),
Arg.Any<Func<object, Exception?, string>>());
}
[Theory, BitAutoData]
public async Task HandleMessageAsync_FailureRetryableButTooManyRetries_PublishesToDeadLetterQueue(IntegrationMessage<WebhookIntegrationConfiguration> message)
{
var sutProvider = GetSutProvider();
message.RetryCount = _config.MaxRetries;
var result = IntegrationHandlerResult.Fail(
message: message,
category: IntegrationFailureCategory.TransientError, // Retryable
failureReason: "403");
_handler.HandleAsync(Arg.Any<string>()).Returns(result);
var expected = IntegrationMessage<WebhookIntegrationConfiguration>.FromJson(message.ToJson());
Assert.NotNull(expected);
Assert.False(await sutProvider.Sut.HandleMessageAsync(message.ToJson()));
await _handler.Received(1).HandleAsync(Arg.Is(expected.ToJson()));
await _serviceBusService.DidNotReceiveWithAnyArgs().PublishToRetryAsync(Arg.Any<IIntegrationMessage>());
_logger.Received().Log(
LogLevel.Warning,
Arg.Any<EventId>(),
Arg.Is<object>(o => (o.ToString() ?? "").Contains("Integration failure - non-recoverable error or max retries exceeded.")),
Arg.Any<Exception?>(),
Arg.Any<Func<object, Exception?, string>>());
}
[Theory, BitAutoData]
public async Task HandleMessageAsync_FailureRetryable_PublishesToRetryQueue(IntegrationMessage<WebhookIntegrationConfiguration> message)
{
var sutProvider = GetSutProvider();
message.RetryCount = 0;
var result = IntegrationHandlerResult.Fail(
message: message,
category: IntegrationFailureCategory.TransientError, // Retryable
failureReason: "403");
_handler.HandleAsync(Arg.Any<string>()).Returns(result);
var expected = IntegrationMessage<WebhookIntegrationConfiguration>.FromJson(message.ToJson());
Assert.NotNull(expected);
Assert.True(await sutProvider.Sut.HandleMessageAsync(message.ToJson()));
await _handler.Received(1).HandleAsync(Arg.Is(expected.ToJson()));
await _serviceBusService.Received(1).PublishToRetryAsync(message);
}
[Theory, BitAutoData]
public async Task HandleMessageAsync_SuccessfulResult_Succeeds(IntegrationMessage<WebhookIntegrationConfiguration> message)
{
var sutProvider = GetSutProvider();
var result = IntegrationHandlerResult.Succeed(message);
_handler.HandleAsync(Arg.Any<string>()).Returns(result);
var expected = IntegrationMessage<WebhookIntegrationConfiguration>.FromJson(message.ToJson());
Assert.NotNull(expected);
Assert.True(await sutProvider.Sut.HandleMessageAsync(message.ToJson()));
await _handler.Received(1).HandleAsync(Arg.Is(expected.ToJson()));
await _serviceBusService.DidNotReceiveWithAnyArgs().PublishToRetryAsync(Arg.Any<IIntegrationMessage>());
}
[Fact]
public async Task HandleMessageAsync_UnknownError_LogsError()
{
var sutProvider = GetSutProvider();
_handler.HandleAsync(Arg.Any<string>()).ThrowsAsync<JsonException>();
Assert.True(await sutProvider.Sut.HandleMessageAsync("Bad JSON"));
_logger.Received(1).Log(
LogLevel.Error,
Arg.Any<EventId>(),
Arg.Is<object>(o => (o.ToString() ?? "").Contains("Unhandled error processing ASB message")),
Arg.Any<Exception>(),
Arg.Any<Func<object, Exception?, string>>());
await _serviceBusService.DidNotReceiveWithAnyArgs().PublishToRetryAsync(Arg.Any<IIntegrationMessage>());
}
}

View File

@@ -1,157 +0,0 @@
#nullable enable
using System.Net;
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
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 Microsoft.Extensions.Time.Testing;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.Services;
[SutProviderCustomize]
public class DatadogIntegrationHandlerTests
{
private readonly MockedHttpMessageHandler _handler;
private readonly HttpClient _httpClient;
private const string _apiKey = "AUTH_TOKEN";
private static readonly Uri _datadogUri = new Uri("https://localhost");
public DatadogIntegrationHandlerTests()
{
_handler = new MockedHttpMessageHandler();
_handler.Fallback
.WithStatusCode(HttpStatusCode.OK)
.WithContent(new StringContent("<html><head><title>test</title></head><body>test</body></html>"));
_httpClient = _handler.ToHttpClient();
}
private SutProvider<DatadogIntegrationHandler> GetSutProvider()
{
var clientFactory = Substitute.For<IHttpClientFactory>();
clientFactory.CreateClient(DatadogIntegrationHandler.HttpClientName).Returns(_httpClient);
return new SutProvider<DatadogIntegrationHandler>()
.SetDependency(clientFactory)
.WithFakeTimeProvider()
.Create();
}
[Theory, BitAutoData]
public async Task HandleAsync_SuccessfulRequest_ReturnsSuccess(IntegrationMessage<DatadogIntegrationConfigurationDetails> message)
{
var sutProvider = GetSutProvider();
message.Configuration = new DatadogIntegrationConfigurationDetails(ApiKey: _apiKey, Uri: _datadogUri);
var result = await sutProvider.Sut.HandleAsync(message);
Assert.True(result.Success);
Assert.Equal(result.Message, message);
Assert.Null(result.FailureReason);
sutProvider.GetDependency<IHttpClientFactory>().Received(1).CreateClient(
Arg.Is(AssertHelper.AssertPropertyEqual(DatadogIntegrationHandler.HttpClientName))
);
Assert.Single(_handler.CapturedRequests);
var request = _handler.CapturedRequests[0];
Assert.NotNull(request);
Assert.NotNull(request.Content);
var returned = await request.Content.ReadAsStringAsync();
Assert.Equal(HttpMethod.Post, request.Method);
Assert.Equal(_apiKey, request.Headers.GetValues("DD-API-KEY").Single());
Assert.Equal(_datadogUri, request.RequestUri);
AssertHelper.AssertPropertyEqual(message.RenderedTemplate, returned);
}
[Theory, BitAutoData]
public async Task HandleAsync_TooManyRequests_ReturnsFailureSetsDelayUntilDate(IntegrationMessage<DatadogIntegrationConfigurationDetails> message)
{
var sutProvider = GetSutProvider();
var now = new DateTime(2014, 3, 2, 1, 0, 0, DateTimeKind.Utc);
var retryAfter = now.AddSeconds(60);
sutProvider.GetDependency<FakeTimeProvider>().SetUtcNow(now);
message.Configuration = new DatadogIntegrationConfigurationDetails(ApiKey: _apiKey, Uri: _datadogUri);
_handler.Fallback
.WithStatusCode(HttpStatusCode.TooManyRequests)
.WithHeader("Retry-After", "60")
.WithContent(new StringContent("<html><head><title>test</title></head><body>test</body></html>"));
var result = await sutProvider.Sut.HandleAsync(message);
Assert.False(result.Success);
Assert.True(result.Retryable);
Assert.Equal(result.Message, message);
Assert.True(result.DelayUntilDate.HasValue);
Assert.Equal(retryAfter, result.DelayUntilDate.Value);
Assert.Equal("Too Many Requests", result.FailureReason);
}
[Theory, BitAutoData]
public async Task HandleAsync_TooManyRequestsWithDate_ReturnsFailureSetsDelayUntilDate(IntegrationMessage<DatadogIntegrationConfigurationDetails> message)
{
var sutProvider = GetSutProvider();
var now = new DateTime(2014, 3, 2, 1, 0, 0, DateTimeKind.Utc);
var retryAfter = now.AddSeconds(60);
message.Configuration = new DatadogIntegrationConfigurationDetails(ApiKey: _apiKey, Uri: _datadogUri);
_handler.Fallback
.WithStatusCode(HttpStatusCode.TooManyRequests)
.WithHeader("Retry-After", retryAfter.ToString("r"))
.WithContent(new StringContent("<html><head><title>test</title></head><body>test</body></html>"));
var result = await sutProvider.Sut.HandleAsync(message);
Assert.False(result.Success);
Assert.True(result.Retryable);
Assert.Equal(result.Message, message);
Assert.True(result.DelayUntilDate.HasValue);
Assert.Equal(retryAfter, result.DelayUntilDate.Value);
Assert.Equal("Too Many Requests", result.FailureReason);
}
[Theory, BitAutoData]
public async Task HandleAsync_InternalServerError_ReturnsFailureSetsRetryable(IntegrationMessage<DatadogIntegrationConfigurationDetails> message)
{
var sutProvider = GetSutProvider();
message.Configuration = new DatadogIntegrationConfigurationDetails(ApiKey: _apiKey, Uri: _datadogUri);
_handler.Fallback
.WithStatusCode(HttpStatusCode.InternalServerError)
.WithContent(new StringContent("<html><head><title>test</title></head><body>test</body></html>"));
var result = await sutProvider.Sut.HandleAsync(message);
Assert.False(result.Success);
Assert.True(result.Retryable);
Assert.Equal(result.Message, message);
Assert.False(result.DelayUntilDate.HasValue);
Assert.Equal("Internal Server Error", result.FailureReason);
}
[Theory, BitAutoData]
public async Task HandleAsync_UnexpectedRedirect_ReturnsFailureNotRetryable(IntegrationMessage<DatadogIntegrationConfigurationDetails> message)
{
var sutProvider = GetSutProvider();
message.Configuration = new DatadogIntegrationConfigurationDetails(ApiKey: _apiKey, Uri: _datadogUri);
_handler.Fallback
.WithStatusCode(HttpStatusCode.TemporaryRedirect)
.WithContent(new StringContent("<html><head><title>test</title></head><body>test</body></html>"));
var result = await sutProvider.Sut.HandleAsync(message);
Assert.False(result.Success);
Assert.False(result.Retryable);
Assert.Equal(result.Message, message);
Assert.Null(result.DelayUntilDate);
Assert.Equal("Temporary Redirect", result.FailureReason);
}
}

View File

@@ -1,73 +0,0 @@
using System.Text.Json;
using Bit.Core.Models.Data;
using Bit.Core.Services;
using Bit.Test.Common.AutoFixture.Attributes;
using Bit.Test.Common.Helpers;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.Services;
[SutProviderCustomize]
public class EventIntegrationEventWriteServiceTests
{
private readonly IEventIntegrationPublisher _eventIntegrationPublisher = Substitute.For<IEventIntegrationPublisher>();
private readonly EventIntegrationEventWriteService Subject;
public EventIntegrationEventWriteServiceTests()
{
Subject = new EventIntegrationEventWriteService(_eventIntegrationPublisher);
}
[Theory, BitAutoData]
public async Task CreateAsync_EventPublishedToEventQueue(EventMessage eventMessage)
{
await Subject.CreateAsync(eventMessage);
await _eventIntegrationPublisher.Received(1).PublishEventAsync(
body: Arg.Is<string>(body => AssertJsonStringsMatch(eventMessage, body)),
organizationId: Arg.Is<string>(orgId => eventMessage.OrganizationId.ToString().Equals(orgId)));
}
[Theory, BitAutoData]
public async Task CreateManyAsync_EventsPublishedToEventQueue(IEnumerable<EventMessage> eventMessages)
{
var eventMessage = eventMessages.First();
await Subject.CreateManyAsync(eventMessages);
await _eventIntegrationPublisher.Received(1).PublishEventAsync(
body: Arg.Is<string>(body => AssertJsonStringsMatch(eventMessages, body)),
organizationId: Arg.Is<string>(orgId => eventMessage.OrganizationId.ToString().Equals(orgId)));
}
[Fact]
public async Task CreateManyAsync_EmptyList_DoesNothing()
{
await Subject.CreateManyAsync([]);
await _eventIntegrationPublisher.DidNotReceiveWithAnyArgs().PublishEventAsync(Arg.Any<string>(), Arg.Any<string>());
}
[Fact]
public async Task DisposeAsync_DisposesEventIntegrationPublisher()
{
await Subject.DisposeAsync();
await _eventIntegrationPublisher.Received(1).DisposeAsync();
}
private static bool AssertJsonStringsMatch(EventMessage expected, string body)
{
var actual = JsonSerializer.Deserialize<EventMessage>(body);
AssertHelper.AssertPropertyEqual(expected, actual, new[] { "IdempotencyId" });
return true;
}
private static bool AssertJsonStringsMatch(IEnumerable<EventMessage> expected, string body)
{
using var actual = JsonSerializer.Deserialize<IEnumerable<EventMessage>>(body).GetEnumerator();
foreach (var expectedMessage in expected)
{
actual.MoveNext();
AssertHelper.AssertPropertyEqual(expectedMessage, actual.Current, new[] { "IdempotencyId" });
}
return true;
}
}

View File

@@ -1,710 +0,0 @@
#nullable enable
using System.Text.Json;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Enums;
using Bit.Core.Models.Data;
using Bit.Core.Models.Data.Organizations;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Utilities;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Bit.Test.Common.Helpers;
using Microsoft.Extensions.Logging;
using NSubstitute;
using Xunit;
using ZiggyCreatures.Caching.Fusion;
namespace Bit.Core.Test.Services;
[SutProviderCustomize]
public class EventIntegrationHandlerTests
{
private const string _templateBase = "Date: #Date#, Type: #Type#, UserId: #UserId#";
private const string _templateWithGroup = "Group: #GroupName#";
private const string _templateWithOrganization = "Org: #OrganizationName#";
private const string _templateWithUser = "#UserName#, #UserEmail#, #UserType#";
private const string _templateWithActingUser = "#ActingUserName#, #ActingUserEmail#, #ActingUserType#";
private static readonly Guid _organizationId = Guid.NewGuid();
private static readonly Uri _uri = new Uri("https://localhost");
private static readonly Uri _uri2 = new Uri("https://example.com");
private readonly IEventIntegrationPublisher _eventIntegrationPublisher = Substitute.For<IEventIntegrationPublisher>();
private readonly ILogger<EventIntegrationHandler<WebhookIntegrationConfigurationDetails>> _logger =
Substitute.For<ILogger<EventIntegrationHandler<WebhookIntegrationConfigurationDetails>>>();
private SutProvider<EventIntegrationHandler<WebhookIntegrationConfigurationDetails>> GetSutProvider(
List<OrganizationIntegrationConfigurationDetails> configurations)
{
var cache = Substitute.For<IFusionCache>();
cache.GetOrSetAsync(
key: Arg.Any<string>(),
factory: Arg.Any<Func<object, CancellationToken, Task<List<OrganizationIntegrationConfigurationDetails>>>>(),
options: Arg.Any<FusionCacheEntryOptions>(),
tags: Arg.Any<IEnumerable<string>>()
).Returns(configurations);
return new SutProvider<EventIntegrationHandler<WebhookIntegrationConfigurationDetails>>()
.SetDependency(cache)
.SetDependency(_eventIntegrationPublisher)
.SetDependency(IntegrationType.Webhook)
.SetDependency(_logger)
.Create();
}
private static IntegrationMessage<WebhookIntegrationConfigurationDetails> ExpectedMessage(string template)
{
return new IntegrationMessage<WebhookIntegrationConfigurationDetails>()
{
IntegrationType = IntegrationType.Webhook,
MessageId = "TestMessageId",
OrganizationId = _organizationId.ToString(),
Configuration = new WebhookIntegrationConfigurationDetails(_uri),
RenderedTemplate = template,
RetryCount = 0,
DelayUntilDate = null
};
}
private static List<OrganizationIntegrationConfigurationDetails> NoConfigurations()
{
return [];
}
private static List<OrganizationIntegrationConfigurationDetails> OneConfiguration(string template)
{
var config = Substitute.For<OrganizationIntegrationConfigurationDetails>();
config.Configuration = null;
config.IntegrationConfiguration = JsonSerializer.Serialize(new { Uri = _uri });
config.Template = template;
return [config];
}
private static List<OrganizationIntegrationConfigurationDetails> TwoConfigurations(string template)
{
var config = Substitute.For<OrganizationIntegrationConfigurationDetails>();
config.Configuration = null;
config.IntegrationConfiguration = JsonSerializer.Serialize(new { Uri = _uri });
config.Template = template;
var config2 = Substitute.For<OrganizationIntegrationConfigurationDetails>();
config2.Configuration = null;
config2.IntegrationConfiguration = JsonSerializer.Serialize(new { Uri = _uri2 });
config2.Template = template;
return [config, config2];
}
private static List<OrganizationIntegrationConfigurationDetails> InvalidFilterConfiguration()
{
var config = Substitute.For<OrganizationIntegrationConfigurationDetails>();
config.Configuration = null;
config.IntegrationConfiguration = JsonSerializer.Serialize(new { Uri = _uri });
config.Template = _templateBase;
config.Filters = "Invalid Configuration!";
return [config];
}
private static List<OrganizationIntegrationConfigurationDetails> ValidFilterConfiguration()
{
var config = Substitute.For<OrganizationIntegrationConfigurationDetails>();
config.Configuration = null;
config.IntegrationConfiguration = JsonSerializer.Serialize(new { Uri = _uri });
config.Template = _templateBase;
config.Filters = JsonSerializer.Serialize(new IntegrationFilterGroup());
return [config];
}
[Theory, BitAutoData]
public async Task BuildContextAsync_ActingUserIdPresent_UsesCache(EventMessage eventMessage, OrganizationUserUserDetails actingUser)
{
var sutProvider = GetSutProvider(OneConfiguration(_templateWithActingUser));
var cache = sutProvider.GetDependency<IFusionCache>();
eventMessage.OrganizationId ??= Guid.NewGuid();
eventMessage.ActingUserId ??= Guid.NewGuid();
cache.GetOrSetAsync(
key: Arg.Any<string>(),
factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<OrganizationUserUserDetails?>, CancellationToken, Task<OrganizationUserUserDetails?>>>()
).Returns(actingUser);
var context = await sutProvider.Sut.BuildContextAsync(eventMessage, _templateWithActingUser);
await cache.Received(1).GetOrSetAsync(
key: Arg.Any<string>(),
factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<OrganizationUserUserDetails?>, CancellationToken, Task<OrganizationUserUserDetails?>>>()
);
Assert.Equal(actingUser, context.ActingUser);
}
[Theory, BitAutoData]
public async Task BuildContextAsync_ActingUserIdNull_SkipsCache(EventMessage eventMessage)
{
var sutProvider = GetSutProvider(OneConfiguration(_templateWithActingUser));
var cache = sutProvider.GetDependency<IFusionCache>();
eventMessage.OrganizationId ??= Guid.NewGuid();
eventMessage.ActingUserId = null;
var context = await sutProvider.Sut.BuildContextAsync(eventMessage, _templateWithActingUser);
await cache.DidNotReceive().GetOrSetAsync(
key: Arg.Any<string>(),
factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<OrganizationUserUserDetails?>, CancellationToken, Task<OrganizationUserUserDetails?>>>()
);
Assert.Null(context.ActingUser);
}
[Theory, BitAutoData]
public async Task BuildContextAsync_ActingUserOrganizationIdNull_SkipsCache(EventMessage eventMessage)
{
var sutProvider = GetSutProvider(OneConfiguration(_templateWithActingUser));
var cache = sutProvider.GetDependency<IFusionCache>();
eventMessage.OrganizationId = null;
eventMessage.ActingUserId ??= Guid.NewGuid();
var context = await sutProvider.Sut.BuildContextAsync(eventMessage, _templateWithActingUser);
await cache.DidNotReceive().GetOrSetAsync(
key: Arg.Any<string>(),
factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<OrganizationUserUserDetails?>, CancellationToken, Task<OrganizationUserUserDetails?>>>()
);
Assert.Null(context.ActingUser);
}
[Theory, BitAutoData]
public async Task BuildContextAsync_ActingUserFactory_CallsOrganizationUserRepository(EventMessage eventMessage, OrganizationUserUserDetails actingUser)
{
var sutProvider = GetSutProvider(OneConfiguration(_templateWithActingUser));
var cache = sutProvider.GetDependency<IFusionCache>();
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
eventMessage.OrganizationId ??= Guid.NewGuid();
eventMessage.ActingUserId ??= Guid.NewGuid();
organizationUserRepository.GetDetailsByOrganizationIdUserIdAsync(
eventMessage.OrganizationId.Value,
eventMessage.ActingUserId.Value).Returns(actingUser);
// Capture the factory function passed to the cache
Func<FusionCacheFactoryExecutionContext<OrganizationUserUserDetails?>, CancellationToken, Task<OrganizationUserUserDetails?>>? capturedFactory = null;
cache.GetOrSetAsync(
key: Arg.Any<string>(),
factory: Arg.Do<Func<FusionCacheFactoryExecutionContext<OrganizationUserUserDetails?>, CancellationToken, Task<OrganizationUserUserDetails?>>>(f => capturedFactory = f)
).Returns(actingUser);
await sutProvider.Sut.BuildContextAsync(eventMessage, _templateWithActingUser);
Assert.NotNull(capturedFactory);
var result = await capturedFactory(null!, CancellationToken.None);
await organizationUserRepository.Received(1).GetDetailsByOrganizationIdUserIdAsync(
eventMessage.OrganizationId.Value,
eventMessage.ActingUserId.Value);
Assert.Equal(actingUser, result);
}
[Theory, BitAutoData]
public async Task BuildContextAsync_GroupIdPresent_UsesCache(EventMessage eventMessage, Group group)
{
var sutProvider = GetSutProvider(OneConfiguration(_templateWithGroup));
var cache = sutProvider.GetDependency<IFusionCache>();
eventMessage.GroupId ??= Guid.NewGuid();
cache.GetOrSetAsync(
key: Arg.Any<string>(),
factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<Group?>, CancellationToken, Task<Group?>>>()
).Returns(group);
var context = await sutProvider.Sut.BuildContextAsync(eventMessage, _templateWithGroup);
await cache.Received(1).GetOrSetAsync(
key: Arg.Any<string>(),
factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<Group?>, CancellationToken, Task<Group?>>>()
);
Assert.Equal(group, context.Group);
}
[Theory, BitAutoData]
public async Task BuildContextAsync_GroupIdNull_SkipsCache(EventMessage eventMessage)
{
var sutProvider = GetSutProvider(OneConfiguration(_templateWithGroup));
var cache = sutProvider.GetDependency<IFusionCache>();
eventMessage.GroupId = null;
var context = await sutProvider.Sut.BuildContextAsync(eventMessage, _templateWithGroup);
await cache.DidNotReceive().GetOrSetAsync(
key: Arg.Any<string>(),
factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<Group?>, CancellationToken, Task<Group?>>>()
);
Assert.Null(context.Group);
}
[Theory, BitAutoData]
public async Task BuildContextAsync_GroupFactory_CallsGroupRepository(EventMessage eventMessage, Group group)
{
var sutProvider = GetSutProvider(OneConfiguration(_templateWithGroup));
var cache = sutProvider.GetDependency<IFusionCache>();
var groupRepository = sutProvider.GetDependency<IGroupRepository>();
eventMessage.GroupId ??= Guid.NewGuid();
groupRepository.GetByIdAsync(eventMessage.GroupId.Value).Returns(group);
// Capture the factory function passed to the cache
Func<FusionCacheFactoryExecutionContext<Group?>, CancellationToken, Task<Group?>>? capturedFactory = null;
cache.GetOrSetAsync(
key: Arg.Any<string>(),
factory: Arg.Do<Func<FusionCacheFactoryExecutionContext<Group?>, CancellationToken, Task<Group?>>>(f => capturedFactory = f)
).Returns(group);
await sutProvider.Sut.BuildContextAsync(eventMessage, _templateWithGroup);
Assert.NotNull(capturedFactory);
var result = await capturedFactory(null!, CancellationToken.None);
await groupRepository.Received(1).GetByIdAsync(eventMessage.GroupId.Value);
Assert.Equal(group, result);
}
[Theory, BitAutoData]
public async Task BuildContextAsync_OrganizationIdPresent_UsesCache(EventMessage eventMessage, Organization organization)
{
var sutProvider = GetSutProvider(OneConfiguration(_templateWithOrganization));
var cache = sutProvider.GetDependency<IFusionCache>();
eventMessage.OrganizationId ??= Guid.NewGuid();
cache.GetOrSetAsync(
key: Arg.Any<string>(),
factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<Organization?>, CancellationToken, Task<Organization?>>>()
).Returns(organization);
var context = await sutProvider.Sut.BuildContextAsync(eventMessage, _templateWithOrganization);
await cache.Received(1).GetOrSetAsync(
key: Arg.Any<string>(),
factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<Organization?>, CancellationToken, Task<Organization?>>>()
);
Assert.Equal(organization, context.Organization);
}
[Theory, BitAutoData]
public async Task BuildContextAsync_OrganizationIdNull_SkipsCache(EventMessage eventMessage)
{
var sutProvider = GetSutProvider(OneConfiguration(_templateWithOrganization));
var cache = sutProvider.GetDependency<IFusionCache>();
eventMessage.OrganizationId = null;
var context = await sutProvider.Sut.BuildContextAsync(eventMessage, _templateWithOrganization);
await cache.DidNotReceive().GetOrSetAsync(
key: Arg.Any<string>(),
factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<Organization?>, CancellationToken, Task<Organization?>>>()
);
Assert.Null(context.Organization);
}
[Theory, BitAutoData]
public async Task BuildContextAsync_OrganizationFactory_CallsOrganizationRepository(EventMessage eventMessage, Organization organization)
{
var sutProvider = GetSutProvider(OneConfiguration(_templateWithOrganization));
var cache = sutProvider.GetDependency<IFusionCache>();
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
eventMessage.OrganizationId ??= Guid.NewGuid();
organizationRepository.GetByIdAsync(eventMessage.OrganizationId.Value).Returns(organization);
// Capture the factory function passed to the cache
Func<FusionCacheFactoryExecutionContext<Organization?>, CancellationToken, Task<Organization?>>? capturedFactory = null;
cache.GetOrSetAsync(
key: Arg.Any<string>(),
factory: Arg.Do<Func<FusionCacheFactoryExecutionContext<Organization?>, CancellationToken, Task<Organization?>>>(f => capturedFactory = f)
).Returns(organization);
await sutProvider.Sut.BuildContextAsync(eventMessage, _templateWithOrganization);
Assert.NotNull(capturedFactory);
var result = await capturedFactory(null!, CancellationToken.None);
await organizationRepository.Received(1).GetByIdAsync(eventMessage.OrganizationId.Value);
Assert.Equal(organization, result);
}
[Theory, BitAutoData]
public async Task BuildContextAsync_UserIdPresent_UsesCache(EventMessage eventMessage, OrganizationUserUserDetails userDetails)
{
var sutProvider = GetSutProvider(OneConfiguration(_templateWithUser));
var cache = sutProvider.GetDependency<IFusionCache>();
eventMessage.OrganizationId ??= Guid.NewGuid();
eventMessage.UserId ??= Guid.NewGuid();
cache.GetOrSetAsync(
key: Arg.Any<string>(),
factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<OrganizationUserUserDetails?>, CancellationToken, Task<OrganizationUserUserDetails?>>>()
).Returns(userDetails);
var context = await sutProvider.Sut.BuildContextAsync(eventMessage, _templateWithUser);
await cache.Received(1).GetOrSetAsync(
key: Arg.Any<string>(),
factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<OrganizationUserUserDetails?>, CancellationToken, Task<OrganizationUserUserDetails?>>>()
);
Assert.Equal(userDetails, context.User);
}
[Theory, BitAutoData]
public async Task BuildContextAsync_UserIdNull_SkipsCache(EventMessage eventMessage)
{
var sutProvider = GetSutProvider(OneConfiguration(_templateWithUser));
var cache = sutProvider.GetDependency<IFusionCache>();
eventMessage.OrganizationId = null;
eventMessage.UserId ??= Guid.NewGuid();
var context = await sutProvider.Sut.BuildContextAsync(eventMessage, _templateWithUser);
await cache.DidNotReceive().GetOrSetAsync(
key: Arg.Any<string>(),
factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<OrganizationUserUserDetails?>, CancellationToken, Task<OrganizationUserUserDetails?>>>()
);
Assert.Null(context.User);
}
[Theory, BitAutoData]
public async Task BuildContextAsync_OrganizationUserIdNull_SkipsCache(EventMessage eventMessage)
{
var sutProvider = GetSutProvider(OneConfiguration(_templateWithUser));
var cache = sutProvider.GetDependency<IFusionCache>();
eventMessage.OrganizationId ??= Guid.NewGuid();
eventMessage.UserId = null;
var context = await sutProvider.Sut.BuildContextAsync(eventMessage, _templateWithUser);
await cache.DidNotReceive().GetOrSetAsync(
key: Arg.Any<string>(),
factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<OrganizationUserUserDetails?>, CancellationToken, Task<OrganizationUserUserDetails?>>>()
);
Assert.Null(context.User);
}
[Theory, BitAutoData]
public async Task BuildContextAsync_UserFactory_CallsOrganizationUserRepository(EventMessage eventMessage, OrganizationUserUserDetails userDetails)
{
var sutProvider = GetSutProvider(OneConfiguration(_templateWithUser));
var cache = sutProvider.GetDependency<IFusionCache>();
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
eventMessage.OrganizationId ??= Guid.NewGuid();
eventMessage.UserId ??= Guid.NewGuid();
organizationUserRepository.GetDetailsByOrganizationIdUserIdAsync(
eventMessage.OrganizationId.Value,
eventMessage.UserId.Value).Returns(userDetails);
// Capture the factory function passed to the cache
Func<FusionCacheFactoryExecutionContext<OrganizationUserUserDetails?>, CancellationToken, Task<OrganizationUserUserDetails?>>? capturedFactory = null;
cache.GetOrSetAsync(
key: Arg.Any<string>(),
factory: Arg.Do<Func<FusionCacheFactoryExecutionContext<OrganizationUserUserDetails?>, CancellationToken, Task<OrganizationUserUserDetails?>>>(f => capturedFactory = f)
).Returns(userDetails);
await sutProvider.Sut.BuildContextAsync(eventMessage, _templateWithUser);
Assert.NotNull(capturedFactory);
var result = await capturedFactory(null!, CancellationToken.None);
await organizationUserRepository.Received(1).GetDetailsByOrganizationIdUserIdAsync(
eventMessage.OrganizationId.Value,
eventMessage.UserId.Value);
Assert.Equal(userDetails, result);
}
[Theory, BitAutoData]
public async Task BuildContextAsync_NoSpecialTokens_DoesNotCallAnyCache(EventMessage eventMessage)
{
var sutProvider = GetSutProvider(OneConfiguration(_templateWithUser));
var cache = sutProvider.GetDependency<IFusionCache>();
eventMessage.ActingUserId ??= Guid.NewGuid();
eventMessage.GroupId ??= Guid.NewGuid();
eventMessage.OrganizationId ??= Guid.NewGuid();
eventMessage.UserId ??= Guid.NewGuid();
await sutProvider.Sut.BuildContextAsync(eventMessage, _templateBase);
await cache.DidNotReceive().GetOrSetAsync(
key: Arg.Any<string>(),
factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<Group?>, CancellationToken, Task<Group?>>>()
);
await cache.DidNotReceive().GetOrSetAsync(
key: Arg.Any<string>(),
factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<Organization?>, CancellationToken, Task<Organization?>>>()
);
await cache.DidNotReceive().GetOrSetAsync(
key: Arg.Any<string>(),
factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<OrganizationUserUserDetails?>, CancellationToken, Task<OrganizationUserUserDetails?>>>()
);
}
[Theory, BitAutoData]
public async Task HandleEventAsync_BaseTemplateNoConfigurations_DoesNothing(EventMessage eventMessage)
{
var sutProvider = GetSutProvider(NoConfigurations());
var cache = sutProvider.GetDependency<IFusionCache>();
cache.GetOrSetAsync<List<OrganizationIntegrationConfigurationDetails>>(
Arg.Any<string>(),
Arg.Any<Func<object, CancellationToken, Task<List<OrganizationIntegrationConfigurationDetails>>>>(),
Arg.Any<FusionCacheEntryOptions>()
).Returns(NoConfigurations());
await sutProvider.Sut.HandleEventAsync(eventMessage);
Assert.Empty(_eventIntegrationPublisher.ReceivedCalls());
}
[Theory, BitAutoData]
public async Task HandleEventAsync_NoOrganizationId_DoesNothing(EventMessage eventMessage)
{
var sutProvider = GetSutProvider(OneConfiguration(_templateBase));
eventMessage.OrganizationId = null;
await sutProvider.Sut.HandleEventAsync(eventMessage);
Assert.Empty(_eventIntegrationPublisher.ReceivedCalls());
}
[Theory, BitAutoData]
public async Task HandleEventAsync_BaseTemplateOneConfiguration_PublishesIntegrationMessage(EventMessage eventMessage)
{
eventMessage.OrganizationId = _organizationId;
var sutProvider = GetSutProvider(OneConfiguration(_templateBase));
await sutProvider.Sut.HandleEventAsync(eventMessage);
var expectedMessage = EventIntegrationHandlerTests.ExpectedMessage(
$"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}"
);
Assert.Single(_eventIntegrationPublisher.ReceivedCalls());
await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is(
AssertHelper.AssertPropertyEqual(expectedMessage, new[] { "MessageId" })));
await sutProvider.GetDependency<IGroupRepository>().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any<Guid>());
await sutProvider.GetDependency<IOrganizationRepository>().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any<Guid>());
await sutProvider.GetDependency<IOrganizationUserRepository>().DidNotReceiveWithAnyArgs().GetDetailsByOrganizationIdUserIdAsync(Arg.Any<Guid>(), Arg.Any<Guid>());
}
[Theory, BitAutoData]
public async Task HandleEventAsync_BaseTemplateTwoConfigurations_PublishesIntegrationMessages(EventMessage eventMessage)
{
eventMessage.OrganizationId = _organizationId;
var sutProvider = GetSutProvider(TwoConfigurations(_templateBase));
await sutProvider.Sut.HandleEventAsync(eventMessage);
var expectedMessage = EventIntegrationHandlerTests.ExpectedMessage(
$"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}"
);
await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is(
AssertHelper.AssertPropertyEqual(expectedMessage, new[] { "MessageId" })));
expectedMessage.Configuration = new WebhookIntegrationConfigurationDetails(_uri2);
await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is(
AssertHelper.AssertPropertyEqual(expectedMessage, new[] { "MessageId" })));
await sutProvider.GetDependency<IGroupRepository>().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any<Guid>());
await sutProvider.GetDependency<IOrganizationRepository>().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any<Guid>());
await sutProvider.GetDependency<IOrganizationUserRepository>().DidNotReceiveWithAnyArgs().GetDetailsByOrganizationIdUserIdAsync(Arg.Any<Guid>(), Arg.Any<Guid>());
}
[Theory, BitAutoData]
public async Task HandleEventAsync_FilterReturnsFalse_DoesNothing(EventMessage eventMessage)
{
eventMessage.OrganizationId = _organizationId;
var sutProvider = GetSutProvider(ValidFilterConfiguration());
sutProvider.GetDependency<IIntegrationFilterService>().EvaluateFilterGroup(
Arg.Any<IntegrationFilterGroup>(), Arg.Any<EventMessage>()).Returns(false);
await sutProvider.Sut.HandleEventAsync(eventMessage);
Assert.Empty(_eventIntegrationPublisher.ReceivedCalls());
}
[Theory, BitAutoData]
public async Task HandleEventAsync_FilterReturnsTrue_PublishesIntegrationMessage(EventMessage eventMessage)
{
eventMessage.OrganizationId = _organizationId;
var sutProvider = GetSutProvider(ValidFilterConfiguration());
sutProvider.GetDependency<IIntegrationFilterService>().EvaluateFilterGroup(
Arg.Any<IntegrationFilterGroup>(), Arg.Any<EventMessage>()).Returns(true);
await sutProvider.Sut.HandleEventAsync(eventMessage);
var expectedMessage = EventIntegrationHandlerTests.ExpectedMessage(
$"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}"
);
Assert.Single(_eventIntegrationPublisher.ReceivedCalls());
await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is(
AssertHelper.AssertPropertyEqual(expectedMessage, new[] { "MessageId" })));
}
[Theory, BitAutoData]
public async Task HandleEventAsync_InvalidFilter_LogsErrorDoesNothing(EventMessage eventMessage)
{
eventMessage.OrganizationId = _organizationId;
var sutProvider = GetSutProvider(InvalidFilterConfiguration());
await sutProvider.Sut.HandleEventAsync(eventMessage);
Assert.Empty(_eventIntegrationPublisher.ReceivedCalls());
_logger.Received(1).Log(
LogLevel.Error,
Arg.Any<EventId>(),
Arg.Any<object>(),
Arg.Any<JsonException>(),
Arg.Any<Func<object, Exception?, string>>());
}
[Theory, BitAutoData]
public async Task HandleManyEventsAsync_BaseTemplateNoConfigurations_DoesNothing(List<EventMessage> eventMessages)
{
eventMessages.ForEach(e => e.OrganizationId = _organizationId);
var sutProvider = GetSutProvider(NoConfigurations());
await sutProvider.Sut.HandleManyEventsAsync(eventMessages);
Assert.Empty(_eventIntegrationPublisher.ReceivedCalls());
}
[Theory, BitAutoData]
public async Task HandleManyEventsAsync_BaseTemplateOneConfiguration_PublishesIntegrationMessages(List<EventMessage> eventMessages)
{
eventMessages.ForEach(e => e.OrganizationId = _organizationId);
var sutProvider = GetSutProvider(OneConfiguration(_templateBase));
await sutProvider.Sut.HandleManyEventsAsync(eventMessages);
foreach (var eventMessage in eventMessages)
{
var expectedMessage = ExpectedMessage(
$"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}"
);
await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is(
AssertHelper.AssertPropertyEqual(expectedMessage, new[] { "MessageId", "OrganizationId" })));
}
}
[Theory, BitAutoData]
public async Task HandleManyEventsAsync_BaseTemplateTwoConfigurations_PublishesIntegrationMessages(
List<EventMessage> eventMessages)
{
eventMessages.ForEach(e => e.OrganizationId = _organizationId);
var sutProvider = GetSutProvider(TwoConfigurations(_templateBase));
await sutProvider.Sut.HandleManyEventsAsync(eventMessages);
foreach (var eventMessage in eventMessages)
{
var expectedMessage = ExpectedMessage(
$"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}"
);
await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is(AssertHelper.AssertPropertyEqual(
expectedMessage, new[] { "MessageId", "OrganizationId" })));
expectedMessage.Configuration = new WebhookIntegrationConfigurationDetails(_uri2);
await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is(AssertHelper.AssertPropertyEqual(
expectedMessage, new[] { "MessageId", "OrganizationId" })));
}
}
[Theory, BitAutoData]
public async Task HandleEventAsync_CapturedFactories_CallConfigurationRepository(EventMessage eventMessage)
{
eventMessage.OrganizationId = _organizationId;
var sutProvider = GetSutProvider(NoConfigurations());
var cache = sutProvider.GetDependency<IFusionCache>();
var configurationRepository = sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>();
var configs = OneConfiguration(_templateBase);
configurationRepository.GetManyByEventTypeOrganizationIdIntegrationType(eventType: eventMessage.Type, organizationId: _organizationId, integrationType: IntegrationType.Webhook).Returns(configs);
// Capture the factory function - there will be 1 call that returns both specific and wildcard matches
Func<FusionCacheFactoryExecutionContext<List<OrganizationIntegrationConfigurationDetails>>, CancellationToken, Task<List<OrganizationIntegrationConfigurationDetails>>>? capturedFactory = null;
cache.GetOrSetAsync(
key: Arg.Any<string>(),
factory: Arg.Do<Func<FusionCacheFactoryExecutionContext<List<OrganizationIntegrationConfigurationDetails>>, CancellationToken, Task<List<OrganizationIntegrationConfigurationDetails>>>>(f
=> capturedFactory = f),
options: Arg.Any<FusionCacheEntryOptions>(),
tags: Arg.Any<IEnumerable<string>>()
).Returns(new List<OrganizationIntegrationConfigurationDetails>());
await sutProvider.Sut.HandleEventAsync(eventMessage);
// Verify factory was captured
Assert.NotNull(capturedFactory);
// Execute the captured factory to trigger repository call
await capturedFactory(null!, CancellationToken.None);
await configurationRepository.Received(1).GetManyByEventTypeOrganizationIdIntegrationType(eventType: eventMessage.Type, organizationId: _organizationId, integrationType: IntegrationType.Webhook);
}
[Theory, BitAutoData]
public async Task HandleEventAsync_ConfigurationCacheOptions_SetsDurationToConstant(EventMessage eventMessage)
{
eventMessage.OrganizationId = _organizationId;
var sutProvider = GetSutProvider(NoConfigurations());
var cache = sutProvider.GetDependency<IFusionCache>();
FusionCacheEntryOptions? capturedOption = null;
cache.GetOrSetAsync(
key: Arg.Any<string>(),
factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<List<OrganizationIntegrationConfigurationDetails>>, CancellationToken, Task<List<OrganizationIntegrationConfigurationDetails>>>>(),
options: Arg.Do<FusionCacheEntryOptions>(opt => capturedOption = opt),
tags: Arg.Any<IEnumerable<string>?>()
).Returns(new List<OrganizationIntegrationConfigurationDetails>());
await sutProvider.Sut.HandleEventAsync(eventMessage);
Assert.NotNull(capturedOption);
Assert.Equal(EventIntegrationsCacheConstants.DurationForOrganizationIntegrationConfigurationDetails,
capturedOption.Duration);
}
[Theory, BitAutoData]
public async Task HandleEventAsync_ConfigurationCache_AddsOrganizationIntegrationTag(EventMessage eventMessage)
{
eventMessage.OrganizationId = _organizationId;
var sutProvider = GetSutProvider(NoConfigurations());
var cache = sutProvider.GetDependency<IFusionCache>();
IEnumerable<string>? capturedTags = null;
cache.GetOrSetAsync(
key: Arg.Any<string>(),
factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<List<OrganizationIntegrationConfigurationDetails>>, CancellationToken, Task<List<OrganizationIntegrationConfigurationDetails>>>>(),
options: Arg.Any<FusionCacheEntryOptions>(),
tags: Arg.Do<IEnumerable<string>>(t => capturedTags = t)
).Returns(new List<OrganizationIntegrationConfigurationDetails>());
await sutProvider.Sut.HandleEventAsync(eventMessage);
var expectedTag = EventIntegrationsCacheConstants.BuildCacheTagForOrganizationIntegration(
_organizationId,
IntegrationType.Webhook
);
Assert.NotNull(capturedTags);
Assert.Contains(expectedTag, capturedTags);
}
}

View File

@@ -1,35 +0,0 @@
using Bit.Core.Models.Data;
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 EventRepositoryHandlerTests
{
[Theory, BitAutoData]
public async Task HandleEventAsync_WritesEventToIEventWriteService(
EventMessage eventMessage,
SutProvider<EventRepositoryHandler> sutProvider)
{
await sutProvider.Sut.HandleEventAsync(eventMessage);
await sutProvider.GetDependency<IEventWriteService>().Received(1).CreateAsync(
Arg.Is(AssertHelper.AssertPropertyEqual<IEvent>(eventMessage))
);
}
[Theory, BitAutoData]
public async Task HandleManyEventAsync_WritesEventsToIEventWriteService(
IEnumerable<EventMessage> eventMessages,
SutProvider<EventRepositoryHandler> sutProvider)
{
await sutProvider.Sut.HandleManyEventsAsync(eventMessages);
await sutProvider.GetDependency<IEventWriteService>().Received(1).CreateManyAsync(
Arg.Is(AssertHelper.AssertPropertyEqual<IEvent>(eventMessages))
);
}
}

View File

@@ -1,46 +0,0 @@
using Bit.Core.Models.Data;
using Bit.Core.Services;
using Bit.Test.Common.AutoFixture.Attributes;
using Xunit;
namespace Bit.Core.Test.Services;
public class IntegrationFilterFactoryTests
{
[Theory, BitAutoData]
public void BuildEqualityFilter_ReturnsCorrectMatch(EventMessage message)
{
var different = Guid.NewGuid();
var expected = Guid.NewGuid();
message.UserId = expected;
var filter = IntegrationFilterFactory.BuildEqualityFilter<Guid?>("UserId");
Assert.True(filter(message, expected));
Assert.False(filter(message, different));
}
[Theory, BitAutoData]
public void BuildEqualityFilter_UserIdIsNull_ReturnsFalse(EventMessage message)
{
message.UserId = null;
var filter = IntegrationFilterFactory.BuildEqualityFilter<Guid?>("UserId");
Assert.False(filter(message, Guid.NewGuid()));
}
[Theory, BitAutoData]
public void BuildInFilter_ReturnsCorrectMatch(EventMessage message)
{
var match = Guid.NewGuid();
message.UserId = match;
var inList = new List<Guid?> { Guid.NewGuid(), match, Guid.NewGuid() };
var outList = new List<Guid?> { Guid.NewGuid(), Guid.NewGuid() };
var filter = IntegrationFilterFactory.BuildInFilter<Guid?>("UserId");
Assert.True(filter(message, inList));
Assert.False(filter(message, outList));
}
}

View File

@@ -1,467 +0,0 @@
#nullable enable
using System.Text.Json;
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
using Bit.Core.Models.Data;
using Bit.Core.Services;
using Bit.Test.Common.AutoFixture.Attributes;
using Xunit;
namespace Bit.Core.Test.Services;
public class IntegrationFilterServiceTests
{
private readonly IntegrationFilterService _service = new();
[Theory, BitAutoData]
public void EvaluateFilterGroup_EqualsUserId_Matches(EventMessage eventMessage)
{
var userId = Guid.NewGuid();
eventMessage.UserId = userId;
var group = new IntegrationFilterGroup
{
AndOperator = true,
Rules =
[
new()
{
Property = "UserId",
Operation = IntegrationFilterOperation.Equals,
Value = userId
}
]
};
var result = _service.EvaluateFilterGroup(group, eventMessage);
Assert.True(result);
var jsonGroup = JsonSerializer.Serialize(group);
var roundtrippedGroup = JsonSerializer.Deserialize<IntegrationFilterGroup>(jsonGroup);
Assert.NotNull(roundtrippedGroup);
Assert.True(_service.EvaluateFilterGroup(roundtrippedGroup, eventMessage));
}
[Theory, BitAutoData]
public void EvaluateFilterGroup_EqualsUserIdString_Matches(EventMessage eventMessage)
{
var userId = Guid.NewGuid();
eventMessage.UserId = userId;
var group = new IntegrationFilterGroup
{
AndOperator = true,
Rules =
[
new()
{
Property = "UserId",
Operation = IntegrationFilterOperation.Equals,
Value = userId.ToString()
}
]
};
var result = _service.EvaluateFilterGroup(group, eventMessage);
Assert.True(result);
var jsonGroup = JsonSerializer.Serialize(group);
var roundtrippedGroup = JsonSerializer.Deserialize<IntegrationFilterGroup>(jsonGroup);
Assert.NotNull(roundtrippedGroup);
Assert.True(_service.EvaluateFilterGroup(roundtrippedGroup, eventMessage));
}
[Theory, BitAutoData]
public void EvaluateFilterGroup_EqualsUserId_DoesNotMatch(EventMessage eventMessage)
{
eventMessage.UserId = Guid.NewGuid();
var otherUserId = Guid.NewGuid();
var group = new IntegrationFilterGroup
{
AndOperator = true,
Rules =
[
new()
{
Property = "UserId",
Operation = IntegrationFilterOperation.Equals,
Value = otherUserId
}
]
};
var result = _service.EvaluateFilterGroup(group, eventMessage);
Assert.False(result);
var jsonGroup = JsonSerializer.Serialize(group);
var roundtrippedGroup = JsonSerializer.Deserialize<IntegrationFilterGroup>(jsonGroup);
Assert.NotNull(roundtrippedGroup);
Assert.False(_service.EvaluateFilterGroup(roundtrippedGroup, eventMessage));
}
[Theory, BitAutoData]
public void EvaluateFilterGroup_NotEqualsUniqueUserId_ReturnsTrue(EventMessage eventMessage)
{
var otherId = Guid.NewGuid();
eventMessage.UserId = otherId;
var group = new IntegrationFilterGroup
{
AndOperator = true,
Rules =
[
new()
{
Property = "UserId",
Operation = IntegrationFilterOperation.NotEquals,
Value = Guid.NewGuid()
}
]
};
var result = _service.EvaluateFilterGroup(group, eventMessage);
Assert.True(result);
var jsonGroup = JsonSerializer.Serialize(group);
var roundtrippedGroup = JsonSerializer.Deserialize<IntegrationFilterGroup>(jsonGroup);
Assert.NotNull(roundtrippedGroup);
Assert.True(_service.EvaluateFilterGroup(roundtrippedGroup, eventMessage));
}
[Theory, BitAutoData]
public void EvaluateFilterGroup_NotEqualsMatchingUserId_ReturnsFalse(EventMessage eventMessage)
{
var id = Guid.NewGuid();
eventMessage.UserId = id;
var group = new IntegrationFilterGroup
{
AndOperator = true,
Rules =
[
new()
{
Property = "UserId",
Operation = IntegrationFilterOperation.NotEquals,
Value = id
}
]
};
var result = _service.EvaluateFilterGroup(group, eventMessage);
Assert.False(result);
var jsonGroup = JsonSerializer.Serialize(group);
var roundtrippedGroup = JsonSerializer.Deserialize<IntegrationFilterGroup>(jsonGroup);
Assert.NotNull(roundtrippedGroup);
Assert.False(_service.EvaluateFilterGroup(roundtrippedGroup, eventMessage));
}
[Theory, BitAutoData]
public void EvaluateFilterGroup_InCollectionId_Matches(EventMessage eventMessage)
{
var id = Guid.NewGuid();
eventMessage.CollectionId = id;
var group = new IntegrationFilterGroup
{
AndOperator = true,
Rules =
[
new()
{
Property = "CollectionId",
Operation = IntegrationFilterOperation.In,
Value = new Guid?[] { Guid.NewGuid(), id }
}
]
};
var result = _service.EvaluateFilterGroup(group, eventMessage);
Assert.True(result);
var jsonGroup = JsonSerializer.Serialize(group);
var roundtrippedGroup = JsonSerializer.Deserialize<IntegrationFilterGroup>(jsonGroup);
Assert.NotNull(roundtrippedGroup);
Assert.True(_service.EvaluateFilterGroup(roundtrippedGroup, eventMessage));
}
[Theory, BitAutoData]
public void EvaluateFilterGroup_InCollectionId_DoesNotMatch(EventMessage eventMessage)
{
eventMessage.CollectionId = Guid.NewGuid();
var group = new IntegrationFilterGroup
{
AndOperator = true,
Rules =
[
new()
{
Property = "CollectionId",
Operation = IntegrationFilterOperation.In,
Value = new Guid?[] { Guid.NewGuid(), Guid.NewGuid() }
}
]
};
var result = _service.EvaluateFilterGroup(group, eventMessage);
Assert.False(result);
var jsonGroup = JsonSerializer.Serialize(group);
var roundtrippedGroup = JsonSerializer.Deserialize<IntegrationFilterGroup>(jsonGroup);
Assert.NotNull(roundtrippedGroup);
Assert.False(_service.EvaluateFilterGroup(roundtrippedGroup, eventMessage));
}
[Theory, BitAutoData]
public void EvaluateFilterGroup_NotInCollectionIdUniqueId_ReturnsTrue(EventMessage eventMessage)
{
eventMessage.CollectionId = Guid.NewGuid();
var group = new IntegrationFilterGroup
{
AndOperator = true,
Rules =
[
new()
{
Property = "CollectionId",
Operation = IntegrationFilterOperation.NotIn,
Value = new Guid?[] { Guid.NewGuid(), Guid.NewGuid() }
}
]
};
var result = _service.EvaluateFilterGroup(group, eventMessage);
Assert.True(result);
var jsonGroup = JsonSerializer.Serialize(group);
var roundtrippedGroup = JsonSerializer.Deserialize<IntegrationFilterGroup>(jsonGroup);
Assert.NotNull(roundtrippedGroup);
Assert.True(_service.EvaluateFilterGroup(roundtrippedGroup, eventMessage));
}
[Theory, BitAutoData]
public void EvaluateFilterGroup_NotInCollectionIdPresent_ReturnsFalse(EventMessage eventMessage)
{
var matchId = Guid.NewGuid();
eventMessage.CollectionId = matchId;
var group = new IntegrationFilterGroup
{
AndOperator = true,
Rules =
[
new()
{
Property = "CollectionId",
Operation = IntegrationFilterOperation.NotIn,
Value = new Guid?[] { Guid.NewGuid(), matchId }
}
]
};
var result = _service.EvaluateFilterGroup(group, eventMessage);
Assert.False(result);
var jsonGroup = JsonSerializer.Serialize(group);
var roundtrippedGroup = JsonSerializer.Deserialize<IntegrationFilterGroup>(jsonGroup);
Assert.NotNull(roundtrippedGroup);
Assert.False(_service.EvaluateFilterGroup(roundtrippedGroup, eventMessage));
}
[Theory, BitAutoData]
public void EvaluateFilterGroup_NestedGroups_AllMatch(EventMessage eventMessage)
{
var id = Guid.NewGuid();
var collectionId = Guid.NewGuid();
eventMessage.UserId = id;
eventMessage.CollectionId = collectionId;
var nestedGroup = new IntegrationFilterGroup
{
AndOperator = true,
Rules =
[
new() { Property = "UserId", Operation = IntegrationFilterOperation.Equals, Value = id },
new()
{
Property = "CollectionId",
Operation = IntegrationFilterOperation.In,
Value = new Guid?[] { collectionId, Guid.NewGuid() }
}
]
};
var topGroup = new IntegrationFilterGroup
{
AndOperator = true,
Groups = [nestedGroup]
};
var result = _service.EvaluateFilterGroup(topGroup, eventMessage);
Assert.True(result);
var jsonGroup = JsonSerializer.Serialize(topGroup);
var roundtrippedGroup = JsonSerializer.Deserialize<IntegrationFilterGroup>(jsonGroup);
Assert.NotNull(roundtrippedGroup);
Assert.True(_service.EvaluateFilterGroup(roundtrippedGroup, eventMessage));
}
[Theory, BitAutoData]
public void EvaluateFilterGroup_NestedGroups_AnyMatch(EventMessage eventMessage)
{
var id = Guid.NewGuid();
var collectionId = Guid.NewGuid();
eventMessage.UserId = id;
eventMessage.CollectionId = collectionId;
var nestedGroup = new IntegrationFilterGroup
{
AndOperator = false,
Rules =
[
new() { Property = "UserId", Operation = IntegrationFilterOperation.Equals, Value = id },
new()
{
Property = "CollectionId",
Operation = IntegrationFilterOperation.In,
Value = new Guid?[] { Guid.NewGuid() }
}
]
};
var topGroup = new IntegrationFilterGroup
{
AndOperator = false,
Groups = [nestedGroup]
};
var result = _service.EvaluateFilterGroup(topGroup, eventMessage);
Assert.True(result);
var jsonGroup = JsonSerializer.Serialize(topGroup);
var roundtrippedGroup = JsonSerializer.Deserialize<IntegrationFilterGroup>(jsonGroup);
Assert.NotNull(roundtrippedGroup);
Assert.True(_service.EvaluateFilterGroup(roundtrippedGroup, eventMessage));
}
[Theory, BitAutoData]
public void EvaluateFilterGroup_UnknownProperty_ReturnsFalse(EventMessage eventMessage)
{
var group = new IntegrationFilterGroup
{
Rules =
[
new() { Property = "NotARealProperty", Operation = IntegrationFilterOperation.Equals, Value = "test" }
]
};
var result = _service.EvaluateFilterGroup(group, eventMessage);
Assert.False(result);
}
[Theory, BitAutoData]
public void EvaluateFilterGroup_UnsupportedOperation_ReturnsFalse(EventMessage eventMessage)
{
var group = new IntegrationFilterGroup
{
Rules =
[
new()
{
Property = "UserId",
Operation = (IntegrationFilterOperation)999, // Unknown operation
Value = eventMessage.UserId
}
]
};
var result = _service.EvaluateFilterGroup(group, eventMessage);
Assert.False(result);
}
[Theory, BitAutoData]
public void EvaluateFilterGroup_WrongTypeForInList_ThrowsException(EventMessage eventMessage)
{
var group = new IntegrationFilterGroup
{
Rules =
[
new()
{
Property = "CollectionId",
Operation = IntegrationFilterOperation.In,
Value = "not an array" // Should be Guid[]
}
]
};
Assert.Throws<InvalidCastException>(() =>
_service.EvaluateFilterGroup(group, eventMessage));
}
[Theory, BitAutoData]
public void EvaluateFilterGroup_NullValue_ThrowsException(EventMessage eventMessage)
{
var group = new IntegrationFilterGroup
{
Rules =
[
new()
{
Property = "UserId",
Operation = IntegrationFilterOperation.Equals,
Value = null
}
]
};
Assert.Throws<InvalidCastException>(() =>
_service.EvaluateFilterGroup(group, eventMessage));
}
[Theory, BitAutoData]
public void EvaluateFilterGroup_EmptyRuleList_ReturnsTrue(EventMessage eventMessage)
{
var group = new IntegrationFilterGroup
{
Rules = [],
Groups = [],
AndOperator = true
};
var result = _service.EvaluateFilterGroup(group, eventMessage);
Assert.True(result); // Nothing to fail, returns true by design
}
[Theory, BitAutoData]
public void EvaluateFilterGroup_InvalidNestedGroup_ReturnsFalse(EventMessage eventMessage)
{
var group = new IntegrationFilterGroup
{
Groups =
[
new()
{
Rules =
[
new()
{
Property = "Nope",
Operation = IntegrationFilterOperation.Equals,
Value = "bad"
}
]
}
],
AndOperator = true
};
var result = _service.EvaluateFilterGroup(group, eventMessage);
Assert.False(result);
}
}

View File

@@ -1,145 +0,0 @@
using System.Net;
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
using Bit.Core.Enums;
using Bit.Core.Services;
using Xunit;
namespace Bit.Core.Test.Services;
public class IntegrationHandlerTests
{
[Fact]
public async Task HandleAsync_ConvertsJsonToTypedIntegrationMessage()
{
var sut = new TestIntegrationHandler();
var expected = new IntegrationMessage<WebhookIntegrationConfigurationDetails>()
{
Configuration = new WebhookIntegrationConfigurationDetails(new Uri("https://localhost"), "Bearer", "AUTH-TOKEN"),
MessageId = "TestMessageId",
OrganizationId = "TestOrganizationId",
IntegrationType = IntegrationType.Webhook,
RenderedTemplate = "Template",
DelayUntilDate = null,
RetryCount = 0
};
var result = await sut.HandleAsync(expected.ToJson());
var typedResult = Assert.IsType<IntegrationMessage<WebhookIntegrationConfigurationDetails>>(result.Message);
Assert.Equal(expected.MessageId, typedResult.MessageId);
Assert.Equal(expected.OrganizationId, typedResult.OrganizationId);
Assert.Equal(expected.Configuration, typedResult.Configuration);
Assert.Equal(expected.RenderedTemplate, typedResult.RenderedTemplate);
Assert.Equal(expected.IntegrationType, typedResult.IntegrationType);
}
[Theory]
[InlineData(HttpStatusCode.Unauthorized)]
[InlineData(HttpStatusCode.Forbidden)]
public void ClassifyHttpStatusCode_AuthenticationFailed(HttpStatusCode code)
{
Assert.Equal(
IntegrationFailureCategory.AuthenticationFailed,
TestIntegrationHandler.Classify(code));
}
[Theory]
[InlineData(HttpStatusCode.NotFound)]
[InlineData(HttpStatusCode.Gone)]
[InlineData(HttpStatusCode.MovedPermanently)]
[InlineData(HttpStatusCode.TemporaryRedirect)]
[InlineData(HttpStatusCode.PermanentRedirect)]
public void ClassifyHttpStatusCode_ConfigurationError(HttpStatusCode code)
{
Assert.Equal(
IntegrationFailureCategory.ConfigurationError,
TestIntegrationHandler.Classify(code));
}
[Fact]
public void ClassifyHttpStatusCode_TooManyRequests_IsRateLimited()
{
Assert.Equal(
IntegrationFailureCategory.RateLimited,
TestIntegrationHandler.Classify(HttpStatusCode.TooManyRequests));
}
[Fact]
public void ClassifyHttpStatusCode_RequestTimeout_IsTransient()
{
Assert.Equal(
IntegrationFailureCategory.TransientError,
TestIntegrationHandler.Classify(HttpStatusCode.RequestTimeout));
}
[Theory]
[InlineData(HttpStatusCode.InternalServerError)]
[InlineData(HttpStatusCode.BadGateway)]
[InlineData(HttpStatusCode.GatewayTimeout)]
public void ClassifyHttpStatusCode_Common5xx_AreTransient(HttpStatusCode code)
{
Assert.Equal(
IntegrationFailureCategory.TransientError,
TestIntegrationHandler.Classify(code));
}
[Fact]
public void ClassifyHttpStatusCode_ServiceUnavailable_IsServiceUnavailable()
{
Assert.Equal(
IntegrationFailureCategory.ServiceUnavailable,
TestIntegrationHandler.Classify(HttpStatusCode.ServiceUnavailable));
}
[Fact]
public void ClassifyHttpStatusCode_NotImplemented_IsPermanentFailure()
{
Assert.Equal(
IntegrationFailureCategory.PermanentFailure,
TestIntegrationHandler.Classify(HttpStatusCode.NotImplemented));
}
[Fact]
public void FClassifyHttpStatusCode_Unhandled3xx_IsConfigurationError()
{
Assert.Equal(
IntegrationFailureCategory.ConfigurationError,
TestIntegrationHandler.Classify(HttpStatusCode.Found));
}
[Fact]
public void ClassifyHttpStatusCode_Unhandled4xx_IsConfigurationError()
{
Assert.Equal(
IntegrationFailureCategory.ConfigurationError,
TestIntegrationHandler.Classify(HttpStatusCode.BadRequest));
}
[Fact]
public void ClassifyHttpStatusCode_Unhandled5xx_IsServiceUnavailable()
{
Assert.Equal(
IntegrationFailureCategory.ServiceUnavailable,
TestIntegrationHandler.Classify(HttpStatusCode.HttpVersionNotSupported));
}
[Fact]
public void ClassifyHttpStatusCode_UnknownCode_DefaultsToServiceUnavailable()
{
// cast an out-of-range value to ensure default path is stable
Assert.Equal(
IntegrationFailureCategory.ServiceUnavailable,
TestIntegrationHandler.Classify((HttpStatusCode)799));
}
private class TestIntegrationHandler : IntegrationHandlerBase<WebhookIntegrationConfigurationDetails>
{
public override Task<IntegrationHandlerResult> HandleAsync(
IntegrationMessage<WebhookIntegrationConfigurationDetails> message)
{
return Task.FromResult(IntegrationHandlerResult.Succeed(message: message));
}
public static IntegrationFailureCategory Classify(HttpStatusCode code) => ClassifyHttpStatusCode(code);
}
}

View File

@@ -1,4 +1,4 @@
using Bit.Core.Enums;
using Bit.Core.Dirt.Enums;
using Xunit;
namespace Bit.Core.Test.Services;

View File

@@ -1,244 +0,0 @@
using System.Text.Json;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Enums;
using Xunit;
namespace Bit.Core.Test.AdminConsole.Services;
public class OrganizationIntegrationConfigurationValidatorTests
{
private readonly OrganizationIntegrationConfigurationValidator _sut = new();
[Fact]
public void ValidateConfiguration_CloudBillingSyncIntegration_ReturnsFalse()
{
var configuration = new OrganizationIntegrationConfiguration
{
Configuration = "{}",
Template = "template"
};
Assert.False(_sut.ValidateConfiguration(IntegrationType.CloudBillingSync, configuration));
}
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
public void ValidateConfiguration_EmptyTemplate_ReturnsFalse(string? template)
{
var config1 = new OrganizationIntegrationConfiguration
{
Configuration = JsonSerializer.Serialize(new SlackIntegrationConfiguration(ChannelId: "C12345")),
Template = template
};
Assert.False(_sut.ValidateConfiguration(IntegrationType.Slack, config1));
var config2 = new OrganizationIntegrationConfiguration
{
Configuration = JsonSerializer.Serialize(new WebhookIntegrationConfiguration(Uri: new Uri("https://example.com"))),
Template = template
};
Assert.False(_sut.ValidateConfiguration(IntegrationType.Webhook, config2));
}
[Theory]
[InlineData("")]
[InlineData(" ")]
public void ValidateConfiguration_EmptyNonNullConfiguration_ReturnsFalse(string? config)
{
var config1 = new OrganizationIntegrationConfiguration
{
Configuration = config,
Template = "template"
};
Assert.False(_sut.ValidateConfiguration(IntegrationType.Hec, config1));
var config2 = new OrganizationIntegrationConfiguration
{
Configuration = config,
Template = "template"
};
Assert.False(_sut.ValidateConfiguration(IntegrationType.Datadog, config2));
var config3 = new OrganizationIntegrationConfiguration
{
Configuration = config,
Template = "template"
};
Assert.False(_sut.ValidateConfiguration(IntegrationType.Teams, config3));
}
[Fact]
public void ValidateConfiguration_NullConfiguration_ReturnsTrue()
{
var config1 = new OrganizationIntegrationConfiguration
{
Configuration = null,
Template = "template"
};
Assert.True(_sut.ValidateConfiguration(IntegrationType.Hec, config1));
var config2 = new OrganizationIntegrationConfiguration
{
Configuration = null,
Template = "template"
};
Assert.True(_sut.ValidateConfiguration(IntegrationType.Datadog, config2));
var config3 = new OrganizationIntegrationConfiguration
{
Configuration = null,
Template = "template"
};
Assert.True(_sut.ValidateConfiguration(IntegrationType.Teams, config3));
}
[Fact]
public void ValidateConfiguration_InvalidJsonConfiguration_ReturnsFalse()
{
var config = new OrganizationIntegrationConfiguration
{
Configuration = "{not valid json}",
Template = "template"
};
Assert.False(_sut.ValidateConfiguration(IntegrationType.Slack, config));
Assert.False(_sut.ValidateConfiguration(IntegrationType.Webhook, config));
Assert.False(_sut.ValidateConfiguration(IntegrationType.Hec, config));
Assert.False(_sut.ValidateConfiguration(IntegrationType.Datadog, config));
Assert.False(_sut.ValidateConfiguration(IntegrationType.Teams, config));
}
[Fact]
public void ValidateConfiguration_InvalidJsonFilters_ReturnsFalse()
{
var configuration = new OrganizationIntegrationConfiguration
{
Configuration = JsonSerializer.Serialize(new WebhookIntegrationConfiguration(Uri: new Uri("https://example.com"))),
Template = "template",
Filters = "{Not valid json}"
};
Assert.False(_sut.ValidateConfiguration(IntegrationType.Webhook, configuration));
}
[Fact]
public void ValidateConfiguration_ScimIntegration_ReturnsFalse()
{
var configuration = new OrganizationIntegrationConfiguration
{
Configuration = "{}",
Template = "template"
};
Assert.False(_sut.ValidateConfiguration(IntegrationType.Scim, configuration));
}
[Fact]
public void ValidateConfiguration_ValidSlackConfiguration_ReturnsTrue()
{
var configuration = new OrganizationIntegrationConfiguration
{
Configuration = JsonSerializer.Serialize(new SlackIntegrationConfiguration(ChannelId: "C12345")),
Template = "template"
};
Assert.True(_sut.ValidateConfiguration(IntegrationType.Slack, configuration));
}
[Fact]
public void ValidateConfiguration_ValidSlackConfigurationWithFilters_ReturnsTrue()
{
var configuration = new OrganizationIntegrationConfiguration
{
Configuration = JsonSerializer.Serialize(new SlackIntegrationConfiguration("C12345")),
Template = "template",
Filters = JsonSerializer.Serialize(new IntegrationFilterGroup()
{
AndOperator = true,
Rules = [
new IntegrationFilterRule()
{
Operation = IntegrationFilterOperation.Equals,
Property = "CollectionId",
Value = Guid.NewGuid()
}
],
Groups = []
})
};
Assert.True(_sut.ValidateConfiguration(IntegrationType.Slack, configuration));
}
[Fact]
public void ValidateConfiguration_ValidNoAuthWebhookConfiguration_ReturnsTrue()
{
var configuration = new OrganizationIntegrationConfiguration
{
Configuration = JsonSerializer.Serialize(new WebhookIntegrationConfiguration(Uri: new Uri("https://localhost"))),
Template = "template"
};
Assert.True(_sut.ValidateConfiguration(IntegrationType.Webhook, configuration));
}
[Fact]
public void ValidateConfiguration_ValidWebhookConfiguration_ReturnsTrue()
{
var configuration = new OrganizationIntegrationConfiguration
{
Configuration = JsonSerializer.Serialize(new WebhookIntegrationConfiguration(
Uri: new Uri("https://localhost"),
Scheme: "Bearer",
Token: "AUTH-TOKEN")),
Template = "template"
};
Assert.True(_sut.ValidateConfiguration(IntegrationType.Webhook, configuration));
}
[Fact]
public void ValidateConfiguration_ValidWebhookConfigurationWithFilters_ReturnsTrue()
{
var configuration = new OrganizationIntegrationConfiguration
{
Configuration = JsonSerializer.Serialize(new WebhookIntegrationConfiguration(
Uri: new Uri("https://example.com"),
Scheme: "Bearer",
Token: "AUTH-TOKEN")),
Template = "template",
Filters = JsonSerializer.Serialize(new IntegrationFilterGroup()
{
AndOperator = true,
Rules = [
new IntegrationFilterRule()
{
Operation = IntegrationFilterOperation.Equals,
Property = "CollectionId",
Value = Guid.NewGuid()
}
],
Groups = []
})
};
Assert.True(_sut.ValidateConfiguration(IntegrationType.Webhook, configuration));
}
[Fact]
public void ValidateConfiguration_UnknownIntegrationType_ReturnsFalse()
{
var unknownType = (IntegrationType)999;
var configuration = new OrganizationIntegrationConfiguration
{
Configuration = "{}",
Template = "template"
};
Assert.False(_sut.ValidateConfiguration(unknownType, configuration));
}
}

View File

@@ -1,189 +0,0 @@
#nullable enable
using System.Text.Json;
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
using Bit.Core.Models.Data;
using Bit.Core.Services;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Bit.Test.Common.Helpers;
using Microsoft.Extensions.Logging;
using NSubstitute;
using RabbitMQ.Client;
using RabbitMQ.Client.Events;
using Xunit;
namespace Bit.Core.Test.Services;
[SutProviderCustomize]
public class RabbitMqEventListenerServiceTests
{
private readonly TestListenerConfiguration _config = new();
private readonly ILogger _logger = Substitute.For<ILogger>();
private SutProvider<RabbitMqEventListenerService<TestListenerConfiguration>> GetSutProvider()
{
var loggerFactory = Substitute.For<ILoggerFactory>();
loggerFactory.CreateLogger<object>().ReturnsForAnyArgs(_logger);
return new SutProvider<RabbitMqEventListenerService<TestListenerConfiguration>>()
.SetDependency(_config)
.SetDependency(loggerFactory)
.Create();
}
[Fact]
public void Constructor_CreatesLogWithCorrectCategory()
{
var sutProvider = GetSutProvider();
var fullName = typeof(RabbitMqEventListenerService<>).FullName ?? "";
var tickIndex = fullName.IndexOf('`');
var cleanedName = tickIndex >= 0 ? fullName.Substring(0, tickIndex) : fullName;
var categoryName = cleanedName + '.' + _config.EventQueueName;
sutProvider.GetDependency<ILoggerFactory>().Received(1).CreateLogger(categoryName);
}
[Fact]
public async Task StartAsync_CreatesQueue()
{
var sutProvider = GetSutProvider();
var cancellationToken = CancellationToken.None;
await sutProvider.Sut.StartAsync(cancellationToken);
await sutProvider.GetDependency<IRabbitMqService>().Received(1).CreateEventQueueAsync(
Arg.Is(_config.EventQueueName),
Arg.Is(cancellationToken)
);
}
[Fact]
public async Task ProcessReceivedMessageAsync_EmptyJson_LogsError()
{
var sutProvider = GetSutProvider();
var eventArgs = new BasicDeliverEventArgs(
consumerTag: string.Empty,
deliveryTag: 0,
redelivered: true,
exchange: string.Empty,
routingKey: string.Empty,
new BasicProperties(),
body: Array.Empty<byte>());
await sutProvider.Sut.ProcessReceivedMessageAsync(eventArgs);
_logger.Received(1).Log(
LogLevel.Error,
Arg.Any<EventId>(),
Arg.Any<object>(),
Arg.Any<JsonException>(),
Arg.Any<Func<object, Exception?, string>>());
}
[Fact]
public async Task ProcessReceivedMessageAsync_InvalidJson_LogsError()
{
var sutProvider = GetSutProvider();
var eventArgs = new BasicDeliverEventArgs(
consumerTag: string.Empty,
deliveryTag: 0,
redelivered: true,
exchange: string.Empty,
routingKey: string.Empty,
new BasicProperties(),
body: JsonSerializer.SerializeToUtf8Bytes("{ Invalid JSON"));
await sutProvider.Sut.ProcessReceivedMessageAsync(eventArgs);
_logger.Received(1).Log(
LogLevel.Error,
Arg.Any<EventId>(),
Arg.Is<object>(o => (o.ToString() ?? "").Contains("Invalid JSON")),
Arg.Any<Exception>(),
Arg.Any<Func<object, Exception?, string>>());
}
[Fact]
public async Task ProcessReceivedMessageAsync_InvalidJsonArray_LogsError()
{
var sutProvider = GetSutProvider();
var eventArgs = new BasicDeliverEventArgs(
consumerTag: string.Empty,
deliveryTag: 0,
redelivered: true,
exchange: string.Empty,
routingKey: string.Empty,
new BasicProperties(),
body: JsonSerializer.SerializeToUtf8Bytes(new[] { "not a valid", "list of event messages" }));
await sutProvider.Sut.ProcessReceivedMessageAsync(eventArgs);
_logger.Received(1).Log(
LogLevel.Error,
Arg.Any<EventId>(),
Arg.Any<object>(),
Arg.Any<JsonException>(),
Arg.Any<Func<object, Exception?, string>>());
}
[Fact]
public async Task ProcessReceivedMessageAsync_InvalidJsonObject_LogsError()
{
var sutProvider = GetSutProvider();
var eventArgs = new BasicDeliverEventArgs(
consumerTag: string.Empty,
deliveryTag: 0,
redelivered: true,
exchange: string.Empty,
routingKey: string.Empty,
new BasicProperties(),
body: JsonSerializer.SerializeToUtf8Bytes(DateTime.UtcNow)); // wrong object - not EventMessage
await sutProvider.Sut.ProcessReceivedMessageAsync(eventArgs);
_logger.Received(1).Log(
LogLevel.Error,
Arg.Any<EventId>(),
Arg.Any<object>(),
Arg.Any<JsonException>(),
Arg.Any<Func<object, Exception?, string>>());
}
[Theory, BitAutoData]
public async Task ProcessReceivedMessageAsync_SingleEvent_DelegatesToHandler(EventMessage message)
{
var sutProvider = GetSutProvider();
var eventArgs = new BasicDeliverEventArgs(
consumerTag: string.Empty,
deliveryTag: 0,
redelivered: true,
exchange: string.Empty,
routingKey: string.Empty,
new BasicProperties(),
body: JsonSerializer.SerializeToUtf8Bytes(message));
await sutProvider.Sut.ProcessReceivedMessageAsync(eventArgs);
await sutProvider.GetDependency<IEventMessageHandler>().Received(1).HandleEventAsync(
Arg.Is(AssertHelper.AssertPropertyEqual(message, new[] { "IdempotencyId" })));
}
[Theory, BitAutoData]
public async Task ProcessReceivedMessageAsync_ManyEvents_DelegatesToHandler(IEnumerable<EventMessage> messages)
{
var sutProvider = GetSutProvider();
var eventArgs = new BasicDeliverEventArgs(
consumerTag: string.Empty,
deliveryTag: 0,
redelivered: true,
exchange: string.Empty,
routingKey: string.Empty,
new BasicProperties(),
body: JsonSerializer.SerializeToUtf8Bytes(messages));
await sutProvider.Sut.ProcessReceivedMessageAsync(eventArgs);
await sutProvider.GetDependency<IEventMessageHandler>().Received(1).HandleManyEventsAsync(
Arg.Is(AssertHelper.AssertPropertyEqual(messages, new[] { "IdempotencyId" })));
}
}

View File

@@ -1,272 +0,0 @@
#nullable enable
using System.Text;
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
using Bit.Core.Services;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Bit.Test.Common.Helpers;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Time.Testing;
using NSubstitute;
using RabbitMQ.Client;
using RabbitMQ.Client.Events;
using Xunit;
namespace Bit.Core.Test.Services;
[SutProviderCustomize]
public class RabbitMqIntegrationListenerServiceTests
{
private readonly DateTime _now = new DateTime(2014, 3, 2, 1, 0, 0, DateTimeKind.Utc);
private readonly IIntegrationHandler _handler = Substitute.For<IIntegrationHandler>();
private readonly ILogger _logger = Substitute.For<ILogger>();
private readonly IRabbitMqService _rabbitMqService = Substitute.For<IRabbitMqService>();
private readonly TestListenerConfiguration _config = new();
private SutProvider<RabbitMqIntegrationListenerService<TestListenerConfiguration>> GetSutProvider()
{
var loggerFactory = Substitute.For<ILoggerFactory>();
loggerFactory.CreateLogger<object>().ReturnsForAnyArgs(_logger);
var sutProvider = new SutProvider<RabbitMqIntegrationListenerService<TestListenerConfiguration>>()
.SetDependency(_config)
.SetDependency(_handler)
.SetDependency(loggerFactory)
.SetDependency(_rabbitMqService)
.WithFakeTimeProvider()
.Create();
sutProvider.GetDependency<FakeTimeProvider>().SetUtcNow(_now);
return sutProvider;
}
[Fact]
public void Constructor_CreatesLogWithCorrectCategory()
{
var sutProvider = GetSutProvider();
var fullName = typeof(RabbitMqIntegrationListenerService<>).FullName ?? "";
var tickIndex = fullName.IndexOf('`');
var cleanedName = tickIndex >= 0 ? fullName.Substring(0, tickIndex) : fullName;
var categoryName = cleanedName + '.' + _config.IntegrationQueueName;
sutProvider.GetDependency<ILoggerFactory>().Received(1).CreateLogger(categoryName);
}
[Fact]
public async Task StartAsync_CreatesQueues()
{
var sutProvider = GetSutProvider();
var cancellationToken = CancellationToken.None;
await sutProvider.Sut.StartAsync(cancellationToken);
await sutProvider.GetDependency<IRabbitMqService>().Received(1).CreateIntegrationQueuesAsync(
Arg.Is(_config.IntegrationQueueName),
Arg.Is(_config.IntegrationRetryQueueName),
Arg.Is(((IIntegrationListenerConfiguration)_config).RoutingKey),
Arg.Is(cancellationToken)
);
}
[Theory, BitAutoData]
public async Task ProcessReceivedMessageAsync_FailureNotRetryable_PublishesToDeadLetterQueue(IntegrationMessage<WebhookIntegrationConfiguration> message)
{
var sutProvider = GetSutProvider();
var cancellationToken = CancellationToken.None;
await sutProvider.Sut.StartAsync(cancellationToken);
message.DelayUntilDate = null;
message.RetryCount = 0;
var eventArgs = new BasicDeliverEventArgs(
consumerTag: string.Empty,
deliveryTag: 0,
redelivered: true,
exchange: string.Empty,
routingKey: string.Empty,
new BasicProperties(),
body: Encoding.UTF8.GetBytes(message.ToJson())
);
var result = IntegrationHandlerResult.Fail(
message: message,
category: IntegrationFailureCategory.AuthenticationFailed, // NOT retryable
failureReason: "403");
_handler.HandleAsync(Arg.Any<string>()).Returns(result);
var expected = IntegrationMessage<WebhookIntegrationConfiguration>.FromJson(message.ToJson());
Assert.NotNull(expected);
await sutProvider.Sut.ProcessReceivedMessageAsync(eventArgs, cancellationToken);
await _handler.Received(1).HandleAsync(Arg.Is(expected.ToJson()));
await _rabbitMqService.Received(1).PublishToDeadLetterAsync(
Arg.Any<IChannel>(),
Arg.Is(AssertHelper.AssertPropertyEqual(expected, new[] { "DelayUntilDate" })),
Arg.Any<CancellationToken>());
_logger.Received().Log(
LogLevel.Warning,
Arg.Any<EventId>(),
Arg.Is<object>(o => (o.ToString() ?? "").Contains("Integration failure - non-retryable.")),
Arg.Any<Exception?>(),
Arg.Any<Func<object, Exception?, string>>());
await _rabbitMqService.DidNotReceiveWithAnyArgs()
.RepublishToRetryQueueAsync(Arg.Any<IChannel>(), Arg.Any<BasicDeliverEventArgs>());
await _rabbitMqService.DidNotReceiveWithAnyArgs()
.PublishToRetryAsync(Arg.Any<IChannel>(), Arg.Any<IntegrationMessage>(), Arg.Any<CancellationToken>());
}
[Theory, BitAutoData]
public async Task ProcessReceivedMessageAsync_FailureRetryableButTooManyRetries_PublishesToDeadLetterQueue(IntegrationMessage<WebhookIntegrationConfiguration> message)
{
var sutProvider = GetSutProvider();
var cancellationToken = CancellationToken.None;
await sutProvider.Sut.StartAsync(cancellationToken);
message.DelayUntilDate = null;
message.RetryCount = _config.MaxRetries;
var eventArgs = new BasicDeliverEventArgs(
consumerTag: string.Empty,
deliveryTag: 0,
redelivered: true,
exchange: string.Empty,
routingKey: string.Empty,
new BasicProperties(),
body: Encoding.UTF8.GetBytes(message.ToJson())
);
var result = IntegrationHandlerResult.Fail(
message: message,
category: IntegrationFailureCategory.TransientError, // Retryable
failureReason: "403");
_handler.HandleAsync(Arg.Any<string>()).Returns(result);
var expected = IntegrationMessage<WebhookIntegrationConfiguration>.FromJson(message.ToJson());
Assert.NotNull(expected);
await sutProvider.Sut.ProcessReceivedMessageAsync(eventArgs, cancellationToken);
expected.ApplyRetry(result.DelayUntilDate);
await _rabbitMqService.Received(1).PublishToDeadLetterAsync(
Arg.Any<IChannel>(),
Arg.Is(AssertHelper.AssertPropertyEqual(expected, new[] { "DelayUntilDate" })),
Arg.Any<CancellationToken>());
_logger.Received().Log(
LogLevel.Warning,
Arg.Any<EventId>(),
Arg.Is<object>(o => (o.ToString() ?? "").Contains("Integration failure - max retries exceeded.")),
Arg.Any<Exception?>(),
Arg.Any<Func<object, Exception?, string>>());
await _rabbitMqService.DidNotReceiveWithAnyArgs()
.RepublishToRetryQueueAsync(Arg.Any<IChannel>(), Arg.Any<BasicDeliverEventArgs>());
await _rabbitMqService.DidNotReceiveWithAnyArgs()
.PublishToRetryAsync(Arg.Any<IChannel>(), Arg.Any<IntegrationMessage>(), Arg.Any<CancellationToken>());
}
[Theory, BitAutoData]
public async Task ProcessReceivedMessageAsync_FailureRetryable_PublishesToRetryQueue(IntegrationMessage<WebhookIntegrationConfiguration> message)
{
var sutProvider = GetSutProvider();
var cancellationToken = CancellationToken.None;
await sutProvider.Sut.StartAsync(cancellationToken);
message.DelayUntilDate = null;
message.RetryCount = 0;
var eventArgs = new BasicDeliverEventArgs(
consumerTag: string.Empty,
deliveryTag: 0,
redelivered: true,
exchange: string.Empty,
routingKey: string.Empty,
new BasicProperties(),
body: Encoding.UTF8.GetBytes(message.ToJson())
);
var result = IntegrationHandlerResult.Fail(
message: message,
category: IntegrationFailureCategory.TransientError, // Retryable
failureReason: "403");
_handler.HandleAsync(Arg.Any<string>()).Returns(result);
var expected = IntegrationMessage<WebhookIntegrationConfiguration>.FromJson(message.ToJson());
Assert.NotNull(expected);
await sutProvider.Sut.ProcessReceivedMessageAsync(eventArgs, cancellationToken);
await _handler.Received(1).HandleAsync(Arg.Is(expected.ToJson()));
expected.ApplyRetry(result.DelayUntilDate);
await _rabbitMqService.Received(1).PublishToRetryAsync(
Arg.Any<IChannel>(),
Arg.Is(AssertHelper.AssertPropertyEqual(expected, new[] { "DelayUntilDate" })),
Arg.Any<CancellationToken>());
await _rabbitMqService.DidNotReceiveWithAnyArgs()
.RepublishToRetryQueueAsync(Arg.Any<IChannel>(), Arg.Any<BasicDeliverEventArgs>());
await _rabbitMqService.DidNotReceiveWithAnyArgs()
.PublishToDeadLetterAsync(Arg.Any<IChannel>(), Arg.Any<IntegrationMessage>(), Arg.Any<CancellationToken>());
}
[Theory, BitAutoData]
public async Task ProcessReceivedMessageAsync_SuccessfulResult_Succeeds(IntegrationMessage<WebhookIntegrationConfiguration> message)
{
var sutProvider = GetSutProvider();
var cancellationToken = CancellationToken.None;
await sutProvider.Sut.StartAsync(cancellationToken);
message.DelayUntilDate = null;
var eventArgs = new BasicDeliverEventArgs(
consumerTag: string.Empty,
deliveryTag: 0,
redelivered: true,
exchange: string.Empty,
routingKey: string.Empty,
new BasicProperties(),
body: Encoding.UTF8.GetBytes(message.ToJson())
);
var result = IntegrationHandlerResult.Succeed(message);
_handler.HandleAsync(Arg.Any<string>()).Returns(result);
await sutProvider.Sut.ProcessReceivedMessageAsync(eventArgs, cancellationToken);
await _handler.Received(1).HandleAsync(Arg.Is(message.ToJson()));
await _rabbitMqService.DidNotReceiveWithAnyArgs()
.RepublishToRetryQueueAsync(Arg.Any<IChannel>(), Arg.Any<BasicDeliverEventArgs>());
await _rabbitMqService.DidNotReceiveWithAnyArgs()
.PublishToRetryAsync(Arg.Any<IChannel>(), Arg.Any<IntegrationMessage>(), Arg.Any<CancellationToken>());
await _rabbitMqService.DidNotReceiveWithAnyArgs()
.PublishToDeadLetterAsync(Arg.Any<IChannel>(), Arg.Any<IntegrationMessage>(), Arg.Any<CancellationToken>());
}
[Theory, BitAutoData]
public async Task ProcessReceivedMessageAsync_TooEarlyRetry_RepublishesToRetryQueue(IntegrationMessage<WebhookIntegrationConfiguration> message)
{
var sutProvider = GetSutProvider();
var cancellationToken = CancellationToken.None;
await sutProvider.Sut.StartAsync(cancellationToken);
message.DelayUntilDate = _now.AddMinutes(1);
var eventArgs = new BasicDeliverEventArgs(
consumerTag: string.Empty,
deliveryTag: 0,
redelivered: true,
exchange: string.Empty,
routingKey: string.Empty,
new BasicProperties(),
body: Encoding.UTF8.GetBytes(message.ToJson())
);
await sutProvider.Sut.ProcessReceivedMessageAsync(eventArgs, cancellationToken);
await _rabbitMqService.Received(1)
.RepublishToRetryQueueAsync(Arg.Any<IChannel>(), Arg.Any<BasicDeliverEventArgs>());
await _handler.DidNotReceiveWithAnyArgs().HandleAsync(Arg.Any<string>());
await _rabbitMqService.DidNotReceiveWithAnyArgs()
.PublishToRetryAsync(Arg.Any<IChannel>(), Arg.Any<IntegrationMessage>(), Arg.Any<CancellationToken>());
await _rabbitMqService.DidNotReceiveWithAnyArgs()
.PublishToDeadLetterAsync(Arg.Any<IChannel>(), Arg.Any<IntegrationMessage>(), Arg.Any<CancellationToken>());
}
}

View File

@@ -1,139 +0,0 @@
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
using Bit.Core.Models.Slack;
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 SlackIntegrationHandlerTests
{
private readonly ISlackService _slackService = Substitute.For<ISlackService>();
private readonly string _channelId = "C12345";
private readonly string _token = "xoxb-test-token";
private SutProvider<SlackIntegrationHandler> GetSutProvider()
{
return new SutProvider<SlackIntegrationHandler>()
.SetDependency(_slackService)
.Create();
}
[Theory, BitAutoData]
public async Task HandleAsync_SuccessfulRequest_ReturnsSuccess(IntegrationMessage<SlackIntegrationConfigurationDetails> message)
{
var sutProvider = GetSutProvider();
message.Configuration = new SlackIntegrationConfigurationDetails(_channelId, _token);
_slackService.SendSlackMessageByChannelIdAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>())
.Returns(new SlackSendMessageResponse() { Ok = true, Channel = _channelId });
var result = await sutProvider.Sut.HandleAsync(message);
Assert.True(result.Success);
Assert.Equal(result.Message, message);
await sutProvider.GetDependency<ISlackService>().Received(1).SendSlackMessageByChannelIdAsync(
Arg.Is(AssertHelper.AssertPropertyEqual(_token)),
Arg.Is(AssertHelper.AssertPropertyEqual(message.RenderedTemplate)),
Arg.Is(AssertHelper.AssertPropertyEqual(_channelId))
);
}
[Theory]
[InlineData("service_unavailable")]
[InlineData("ratelimited")]
[InlineData("rate_limited")]
[InlineData("internal_error")]
[InlineData("message_limit_exceeded")]
public async Task HandleAsync_FailedRetryableRequest_ReturnsFailureWithRetryable(string error)
{
var sutProvider = GetSutProvider();
var message = new IntegrationMessage<SlackIntegrationConfigurationDetails>()
{
Configuration = new SlackIntegrationConfigurationDetails(_channelId, _token),
MessageId = "MessageId",
RenderedTemplate = "Test Message"
};
_slackService.SendSlackMessageByChannelIdAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>())
.Returns(new SlackSendMessageResponse() { Ok = false, Channel = _channelId, Error = error });
var result = await sutProvider.Sut.HandleAsync(message);
Assert.False(result.Success);
Assert.True(result.Retryable);
Assert.NotNull(result.FailureReason);
Assert.Equal(result.Message, message);
await sutProvider.GetDependency<ISlackService>().Received(1).SendSlackMessageByChannelIdAsync(
Arg.Is(AssertHelper.AssertPropertyEqual(_token)),
Arg.Is(AssertHelper.AssertPropertyEqual(message.RenderedTemplate)),
Arg.Is(AssertHelper.AssertPropertyEqual(_channelId))
);
}
[Theory]
[InlineData("access_denied")]
[InlineData("channel_not_found")]
[InlineData("token_expired")]
[InlineData("token_revoked")]
public async Task HandleAsync_FailedNonRetryableRequest_ReturnsNonRetryableFailure(string error)
{
var sutProvider = GetSutProvider();
var message = new IntegrationMessage<SlackIntegrationConfigurationDetails>()
{
Configuration = new SlackIntegrationConfigurationDetails(_channelId, _token),
MessageId = "MessageId",
RenderedTemplate = "Test Message"
};
_slackService.SendSlackMessageByChannelIdAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>())
.Returns(new SlackSendMessageResponse() { Ok = false, Channel = _channelId, Error = error });
var result = await sutProvider.Sut.HandleAsync(message);
Assert.False(result.Success);
Assert.False(result.Retryable);
Assert.NotNull(result.FailureReason);
Assert.Equal(result.Message, message);
await sutProvider.GetDependency<ISlackService>().Received(1).SendSlackMessageByChannelIdAsync(
Arg.Is(AssertHelper.AssertPropertyEqual(_token)),
Arg.Is(AssertHelper.AssertPropertyEqual(message.RenderedTemplate)),
Arg.Is(AssertHelper.AssertPropertyEqual(_channelId))
);
}
[Fact]
public async Task HandleAsync_NullResponse_ReturnsRetryableFailure()
{
var sutProvider = GetSutProvider();
var message = new IntegrationMessage<SlackIntegrationConfigurationDetails>()
{
Configuration = new SlackIntegrationConfigurationDetails(_channelId, _token),
MessageId = "MessageId",
RenderedTemplate = "Test Message"
};
_slackService.SendSlackMessageByChannelIdAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>())
.Returns((SlackSendMessageResponse?)null);
var result = await sutProvider.Sut.HandleAsync(message);
Assert.False(result.Success);
Assert.True(result.Retryable); // Null response is classified as TransientError (retryable)
Assert.Equal("Slack response was null", result.FailureReason);
Assert.Equal(result.Message, message);
await sutProvider.GetDependency<ISlackService>().Received(1).SendSlackMessageByChannelIdAsync(
Arg.Is(AssertHelper.AssertPropertyEqual(_token)),
Arg.Is(AssertHelper.AssertPropertyEqual(message.RenderedTemplate)),
Arg.Is(AssertHelper.AssertPropertyEqual(_channelId))
);
}
}

View File

@@ -1,499 +0,0 @@
#nullable enable
using System.Net;
using System.Text.Json;
using System.Web;
using Bit.Core.Services;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Bit.Test.Common.MockedHttpClient;
using NSubstitute;
using Xunit;
using GlobalSettings = Bit.Core.Settings.GlobalSettings;
namespace Bit.Core.Test.Services;
[SutProviderCustomize]
public class SlackServiceTests
{
private readonly MockedHttpMessageHandler _handler;
private readonly HttpClient _httpClient;
private const string _token = "xoxb-test-token";
public SlackServiceTests()
{
_handler = new MockedHttpMessageHandler();
_httpClient = _handler.ToHttpClient();
}
private SutProvider<SlackService> GetSutProvider()
{
var clientFactory = Substitute.For<IHttpClientFactory>();
clientFactory.CreateClient(SlackService.HttpClientName).Returns(_httpClient);
var globalSettings = Substitute.For<GlobalSettings>();
globalSettings.Slack.ApiBaseUrl.Returns("https://slack.com/api");
return new SutProvider<SlackService>()
.SetDependency(clientFactory)
.SetDependency(globalSettings)
.Create();
}
[Fact]
public async Task GetChannelIdsAsync_ReturnsCorrectChannelIds()
{
var response = JsonSerializer.Serialize(
new
{
ok = true,
channels =
new[] {
new { id = "C12345", name = "general" },
new { id = "C67890", name = "random" }
},
response_metadata = new { next_cursor = "" }
}
);
_handler.When(HttpMethod.Get)
.RespondWith(HttpStatusCode.OK)
.WithContent(new StringContent(response));
var sutProvider = GetSutProvider();
var channelNames = new List<string> { "general", "random" };
var result = await sutProvider.Sut.GetChannelIdsAsync(_token, channelNames);
Assert.Equal(2, result.Count);
Assert.Contains("C12345", result);
Assert.Contains("C67890", result);
}
[Fact]
public async Task GetChannelIdsAsync_WithPagination_ReturnsCorrectChannelIds()
{
var firstPageResponse = JsonSerializer.Serialize(
new
{
ok = true,
channels = new[] { new { id = "C12345", name = "general" } },
response_metadata = new { next_cursor = "next_cursor_value" }
}
);
var secondPageResponse = JsonSerializer.Serialize(
new
{
ok = true,
channels = new[] { new { id = "C67890", name = "random" } },
response_metadata = new { next_cursor = "" }
}
);
_handler.When("https://slack.com/api/conversations.list?types=public_channel%2cprivate_channel&limit=1000")
.RespondWith(HttpStatusCode.OK)
.WithContent(new StringContent(firstPageResponse));
_handler.When("https://slack.com/api/conversations.list?types=public_channel%2cprivate_channel&limit=1000&cursor=next_cursor_value")
.RespondWith(HttpStatusCode.OK)
.WithContent(new StringContent(secondPageResponse));
var sutProvider = GetSutProvider();
var channelNames = new List<string> { "general", "random" };
var result = await sutProvider.Sut.GetChannelIdsAsync(_token, channelNames);
Assert.Equal(2, result.Count);
Assert.Contains("C12345", result);
Assert.Contains("C67890", result);
}
[Fact]
public async Task GetChannelIdsAsync_ApiError_ReturnsEmptyResult()
{
var errorResponse = JsonSerializer.Serialize(
new { ok = false, error = "rate_limited" }
);
_handler.When(HttpMethod.Get)
.RespondWith(HttpStatusCode.TooManyRequests)
.WithContent(new StringContent(errorResponse));
var sutProvider = GetSutProvider();
var channelNames = new List<string> { "general", "random" };
var result = await sutProvider.Sut.GetChannelIdsAsync(_token, channelNames);
Assert.Empty(result);
}
[Fact]
public async Task GetChannelIdsAsync_NoChannelsFound_ReturnsEmptyResult()
{
var emptyResponse = JsonSerializer.Serialize(
new
{
ok = true,
channels = Array.Empty<string>(),
response_metadata = new { next_cursor = "" }
});
_handler.When(HttpMethod.Get)
.RespondWith(HttpStatusCode.OK)
.WithContent(new StringContent(emptyResponse));
var sutProvider = GetSutProvider();
var channelNames = new List<string> { "general", "random" };
var result = await sutProvider.Sut.GetChannelIdsAsync(_token, channelNames);
Assert.Empty(result);
}
[Fact]
public async Task GetChannelIdAsync_NoChannelFound_ReturnsEmptyResult()
{
var emptyResponse = JsonSerializer.Serialize(
new
{
ok = true,
channels = Array.Empty<string>(),
response_metadata = new { next_cursor = "" }
});
_handler.When(HttpMethod.Get)
.RespondWith(HttpStatusCode.OK)
.WithContent(new StringContent(emptyResponse));
var sutProvider = GetSutProvider();
var result = await sutProvider.Sut.GetChannelIdAsync(_token, "general");
Assert.Empty(result);
}
[Fact]
public async Task GetChannelIdAsync_ReturnsCorrectChannelId()
{
var sutProvider = GetSutProvider();
var response = new
{
ok = true,
channels = new[]
{
new { id = "C12345", name = "general" },
new { id = "C67890", name = "random" }
},
response_metadata = new { next_cursor = "" }
};
_handler.When(HttpMethod.Get)
.RespondWith(HttpStatusCode.OK)
.WithContent(new StringContent(JsonSerializer.Serialize(response)));
var result = await sutProvider.Sut.GetChannelIdAsync(_token, "general");
Assert.Equal("C12345", result);
}
[Fact]
public async Task GetDmChannelByEmailAsync_ReturnsCorrectDmChannelId()
{
var sutProvider = GetSutProvider();
var email = "user@example.com";
var userId = "U12345";
var dmChannelId = "D67890";
var userResponse = new
{
ok = true,
user = new { id = userId }
};
var dmResponse = new
{
ok = true,
channel = new { id = dmChannelId }
};
_handler.When($"https://slack.com/api/users.lookupByEmail?email={email}")
.RespondWith(HttpStatusCode.OK)
.WithContent(new StringContent(JsonSerializer.Serialize(userResponse)));
_handler.When("https://slack.com/api/conversations.open")
.RespondWith(HttpStatusCode.OK)
.WithContent(new StringContent(JsonSerializer.Serialize(dmResponse)));
var result = await sutProvider.Sut.GetDmChannelByEmailAsync(_token, email);
Assert.Equal(dmChannelId, result);
}
[Fact]
public async Task GetDmChannelByEmailAsync_ApiErrorDmResponse_ReturnsEmptyString()
{
var sutProvider = GetSutProvider();
var email = "user@example.com";
var userId = "U12345";
var userResponse = new
{
ok = true,
user = new { id = userId }
};
var dmResponse = new
{
ok = false,
error = "An error occured"
};
_handler.When($"https://slack.com/api/users.lookupByEmail?email={email}")
.RespondWith(HttpStatusCode.OK)
.WithContent(new StringContent(JsonSerializer.Serialize(userResponse)));
_handler.When("https://slack.com/api/conversations.open")
.RespondWith(HttpStatusCode.OK)
.WithContent(new StringContent(JsonSerializer.Serialize(dmResponse)));
var result = await sutProvider.Sut.GetDmChannelByEmailAsync(_token, email);
Assert.Equal(string.Empty, result);
}
[Fact]
public async Task GetDmChannelByEmailAsync_ApiErrorUnparsableDmResponse_ReturnsEmptyString()
{
var sutProvider = GetSutProvider();
var email = "user@example.com";
var userId = "U12345";
var userResponse = new
{
ok = true,
user = new { id = userId }
};
_handler.When($"https://slack.com/api/users.lookupByEmail?email={email}")
.RespondWith(HttpStatusCode.OK)
.WithContent(new StringContent(JsonSerializer.Serialize(userResponse)));
_handler.When("https://slack.com/api/conversations.open")
.RespondWith(HttpStatusCode.OK)
.WithContent(new StringContent("NOT JSON"));
var result = await sutProvider.Sut.GetDmChannelByEmailAsync(_token, email);
Assert.Equal(string.Empty, result);
}
[Fact]
public async Task GetDmChannelByEmailAsync_ApiErrorUserResponse_ReturnsEmptyString()
{
var sutProvider = GetSutProvider();
var email = "user@example.com";
var userResponse = new
{
ok = false,
error = "An error occurred"
};
_handler.When($"https://slack.com/api/users.lookupByEmail?email={email}")
.RespondWith(HttpStatusCode.OK)
.WithContent(new StringContent(JsonSerializer.Serialize(userResponse)));
var result = await sutProvider.Sut.GetDmChannelByEmailAsync(_token, email);
Assert.Equal(string.Empty, result);
}
[Fact]
public async Task GetDmChannelByEmailAsync_ApiErrorUnparsableUserResponse_ReturnsEmptyString()
{
var sutProvider = GetSutProvider();
var email = "user@example.com";
_handler.When($"https://slack.com/api/users.lookupByEmail?email={email}")
.RespondWith(HttpStatusCode.OK)
.WithContent(new StringContent("Not JSON"));
var result = await sutProvider.Sut.GetDmChannelByEmailAsync(_token, email);
Assert.Equal(string.Empty, result);
}
[Fact]
public void GetRedirectUrl_ReturnsCorrectUrl()
{
var sutProvider = GetSutProvider();
var clientId = sutProvider.GetDependency<GlobalSettings>().Slack.ClientId;
var scopes = sutProvider.GetDependency<GlobalSettings>().Slack.Scopes;
var callbackUrl = "https://example.com/callback";
var state = Guid.NewGuid().ToString();
var result = sutProvider.Sut.GetRedirectUrl(callbackUrl, state);
var uri = new Uri(result);
var query = HttpUtility.ParseQueryString(uri.Query);
Assert.Equal(clientId, query["client_id"]);
Assert.Equal(scopes, query["scope"]);
Assert.Equal(callbackUrl, query["redirect_uri"]);
Assert.Equal(state, query["state"]);
Assert.Equal("slack.com", uri.Host);
Assert.Equal("/oauth/v2/authorize", uri.AbsolutePath);
}
[Fact]
public async Task ObtainTokenViaOAuth_ReturnsAccessToken_WhenSuccessful()
{
var sutProvider = GetSutProvider();
var jsonResponse = JsonSerializer.Serialize(new
{
ok = true,
access_token = "test-access-token"
});
_handler.When("https://slack.com/api/oauth.v2.access")
.RespondWith(HttpStatusCode.OK)
.WithContent(new StringContent(jsonResponse));
var result = await sutProvider.Sut.ObtainTokenViaOAuth("test-code", "https://example.com/callback");
Assert.Equal("test-access-token", result);
}
[Theory]
[InlineData("test-code", "")]
[InlineData("", "https://example.com/callback")]
[InlineData("", "")]
public async Task ObtainTokenViaOAuth_ReturnsEmptyString_WhenCodeOrRedirectUrlIsEmpty(string code, string redirectUrl)
{
var sutProvider = GetSutProvider();
var result = await sutProvider.Sut.ObtainTokenViaOAuth(code, redirectUrl);
Assert.Equal(string.Empty, result);
}
[Fact]
public async Task ObtainTokenViaOAuth_ReturnsEmptyString_WhenErrorResponse()
{
var sutProvider = GetSutProvider();
var jsonResponse = JsonSerializer.Serialize(new
{
ok = false,
error = "invalid_code"
});
_handler.When("https://slack.com/api/oauth.v2.access")
.RespondWith(HttpStatusCode.OK)
.WithContent(new StringContent(jsonResponse));
var result = await sutProvider.Sut.ObtainTokenViaOAuth("test-code", "https://example.com/callback");
Assert.Equal(string.Empty, result);
}
[Fact]
public async Task ObtainTokenViaOAuth_ReturnsEmptyString_WhenHttpCallFails()
{
var sutProvider = GetSutProvider();
_handler.When("https://slack.com/api/oauth.v2.access")
.RespondWith(HttpStatusCode.InternalServerError)
.WithContent(new StringContent(string.Empty));
var result = await sutProvider.Sut.ObtainTokenViaOAuth("test-code", "https://example.com/callback");
Assert.Equal(string.Empty, result);
}
[Fact]
public async Task SendSlackMessageByChannelId_Success_ReturnsSuccessfulResponse()
{
var sutProvider = GetSutProvider();
var channelId = "C12345";
var message = "Hello, Slack!";
var jsonResponse = JsonSerializer.Serialize(new
{
ok = true,
channel = channelId,
});
_handler.When(HttpMethod.Post)
.RespondWith(HttpStatusCode.OK)
.WithContent(new StringContent(jsonResponse));
var result = await sutProvider.Sut.SendSlackMessageByChannelIdAsync(_token, message, channelId);
// Response was parsed correctly
Assert.NotNull(result);
Assert.True(result.Ok);
// Request was sent correctly
Assert.Single(_handler.CapturedRequests);
var request = _handler.CapturedRequests[0];
Assert.NotNull(request);
Assert.Equal(HttpMethod.Post, request.Method);
Assert.NotNull(request.Headers.Authorization);
Assert.Equal($"Bearer {_token}", request.Headers.Authorization.ToString());
Assert.NotNull(request.Content);
var returned = (await request.Content.ReadAsStringAsync());
var json = JsonDocument.Parse(returned);
Assert.Equal(message, json.RootElement.GetProperty("text").GetString() ?? string.Empty);
Assert.Equal(channelId, json.RootElement.GetProperty("channel").GetString() ?? string.Empty);
}
[Fact]
public async Task SendSlackMessageByChannelId_Failure_ReturnsErrorResponse()
{
var sutProvider = GetSutProvider();
var channelId = "C12345";
var message = "Hello, Slack!";
var jsonResponse = JsonSerializer.Serialize(new
{
ok = false,
channel = channelId,
error = "error"
});
_handler.When(HttpMethod.Post)
.RespondWith(HttpStatusCode.OK)
.WithContent(new StringContent(jsonResponse));
var result = await sutProvider.Sut.SendSlackMessageByChannelIdAsync(_token, message, channelId);
// Response was parsed correctly
Assert.NotNull(result);
Assert.False(result.Ok);
Assert.NotNull(result.Error);
}
[Fact]
public async Task SendSlackMessageByChannelIdAsync_InvalidJson_ReturnsNull()
{
var sutProvider = GetSutProvider();
var channelId = "C12345";
var message = "Hello world!";
_handler.When(HttpMethod.Post)
.RespondWith(HttpStatusCode.OK)
.WithContent(new StringContent("Not JSON"));
var result = await sutProvider.Sut.SendSlackMessageByChannelIdAsync(_token, message, channelId);
Assert.Null(result);
}
[Fact]
public async Task SendSlackMessageByChannelIdAsync_HttpServerError_ReturnsNull()
{
var sutProvider = GetSutProvider();
var channelId = "C12345";
var message = "Hello world!";
_handler.When(HttpMethod.Post)
.RespondWith(HttpStatusCode.InternalServerError)
.WithContent(new StringContent(string.Empty));
var result = await sutProvider.Sut.SendSlackMessageByChannelIdAsync(_token, message, channelId);
Assert.Null(result);
}
}

View File

@@ -1,198 +0,0 @@
using System.Text.Json;
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
using Bit.Core.Services;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Bit.Test.Common.Helpers;
using Microsoft.Rest;
using NSubstitute;
using NSubstitute.ExceptionExtensions;
using Xunit;
namespace Bit.Core.Test.Services;
[SutProviderCustomize]
public class TeamsIntegrationHandlerTests
{
private readonly ITeamsService _teamsService = Substitute.For<ITeamsService>();
private readonly string _channelId = "C12345";
private readonly Uri _serviceUrl = new Uri("http://localhost");
private SutProvider<TeamsIntegrationHandler> GetSutProvider()
{
return new SutProvider<TeamsIntegrationHandler>()
.SetDependency(_teamsService)
.Create();
}
[Theory, BitAutoData]
public async Task HandleAsync_SuccessfulRequest_ReturnsSuccess(IntegrationMessage<TeamsIntegrationConfigurationDetails> message)
{
var sutProvider = GetSutProvider();
message.Configuration = new TeamsIntegrationConfigurationDetails(_channelId, _serviceUrl);
var result = await sutProvider.Sut.HandleAsync(message);
Assert.True(result.Success);
Assert.Equal(result.Message, message);
await sutProvider.GetDependency<ITeamsService>().Received(1).SendMessageToChannelAsync(
Arg.Is(AssertHelper.AssertPropertyEqual(_serviceUrl)),
Arg.Is(AssertHelper.AssertPropertyEqual(_channelId)),
Arg.Is(AssertHelper.AssertPropertyEqual(message.RenderedTemplate))
);
}
[Theory, BitAutoData]
public async Task HandleAsync_ArgumentException_ReturnsConfigurationError(IntegrationMessage<TeamsIntegrationConfigurationDetails> message)
{
var sutProvider = GetSutProvider();
message.Configuration = new TeamsIntegrationConfigurationDetails(_channelId, _serviceUrl);
sutProvider.GetDependency<ITeamsService>()
.SendMessageToChannelAsync(Arg.Any<Uri>(), Arg.Any<string>(), Arg.Any<string>())
.ThrowsAsync(new ArgumentException("argument error"));
var result = await sutProvider.Sut.HandleAsync(message);
Assert.False(result.Success);
Assert.Equal(IntegrationFailureCategory.ConfigurationError, result.Category);
Assert.False(result.Retryable);
Assert.Equal(result.Message, message);
await sutProvider.GetDependency<ITeamsService>().Received(1).SendMessageToChannelAsync(
Arg.Is(AssertHelper.AssertPropertyEqual(_serviceUrl)),
Arg.Is(AssertHelper.AssertPropertyEqual(_channelId)),
Arg.Is(AssertHelper.AssertPropertyEqual(message.RenderedTemplate))
);
}
[Theory, BitAutoData]
public async Task HandleAsync_JsonException_ReturnsPermanentFailure(IntegrationMessage<TeamsIntegrationConfigurationDetails> message)
{
var sutProvider = GetSutProvider();
message.Configuration = new TeamsIntegrationConfigurationDetails(_channelId, _serviceUrl);
sutProvider.GetDependency<ITeamsService>()
.SendMessageToChannelAsync(Arg.Any<Uri>(), Arg.Any<string>(), Arg.Any<string>())
.ThrowsAsync(new JsonException("JSON error"));
var result = await sutProvider.Sut.HandleAsync(message);
Assert.False(result.Success);
Assert.Equal(IntegrationFailureCategory.PermanentFailure, result.Category);
Assert.False(result.Retryable);
Assert.Equal(result.Message, message);
await sutProvider.GetDependency<ITeamsService>().Received(1).SendMessageToChannelAsync(
Arg.Is(AssertHelper.AssertPropertyEqual(_serviceUrl)),
Arg.Is(AssertHelper.AssertPropertyEqual(_channelId)),
Arg.Is(AssertHelper.AssertPropertyEqual(message.RenderedTemplate))
);
}
[Theory, BitAutoData]
public async Task HandleAsync_UriFormatException_ReturnsConfigurationError(IntegrationMessage<TeamsIntegrationConfigurationDetails> message)
{
var sutProvider = GetSutProvider();
message.Configuration = new TeamsIntegrationConfigurationDetails(_channelId, _serviceUrl);
sutProvider.GetDependency<ITeamsService>()
.SendMessageToChannelAsync(Arg.Any<Uri>(), Arg.Any<string>(), Arg.Any<string>())
.ThrowsAsync(new UriFormatException("Bad URI"));
var result = await sutProvider.Sut.HandleAsync(message);
Assert.False(result.Success);
Assert.Equal(IntegrationFailureCategory.ConfigurationError, result.Category);
Assert.False(result.Retryable);
Assert.Equal(result.Message, message);
await sutProvider.GetDependency<ITeamsService>().Received(1).SendMessageToChannelAsync(
Arg.Is(AssertHelper.AssertPropertyEqual(_serviceUrl)),
Arg.Is(AssertHelper.AssertPropertyEqual(_channelId)),
Arg.Is(AssertHelper.AssertPropertyEqual(message.RenderedTemplate))
);
}
[Theory, BitAutoData]
public async Task HandleAsync_HttpExceptionForbidden_ReturnsAuthenticationFailed(IntegrationMessage<TeamsIntegrationConfigurationDetails> message)
{
var sutProvider = GetSutProvider();
message.Configuration = new TeamsIntegrationConfigurationDetails(_channelId, _serviceUrl);
sutProvider.GetDependency<ITeamsService>()
.SendMessageToChannelAsync(Arg.Any<Uri>(), Arg.Any<string>(), Arg.Any<string>())
.ThrowsAsync(new HttpOperationException("Server error")
{
Response = new HttpResponseMessageWrapper(
new HttpResponseMessage(System.Net.HttpStatusCode.Forbidden),
"Forbidden"
)
}
);
var result = await sutProvider.Sut.HandleAsync(message);
Assert.False(result.Success);
Assert.Equal(IntegrationFailureCategory.AuthenticationFailed, result.Category);
Assert.False(result.Retryable);
Assert.Equal(result.Message, message);
await sutProvider.GetDependency<ITeamsService>().Received(1).SendMessageToChannelAsync(
Arg.Is(AssertHelper.AssertPropertyEqual(_serviceUrl)),
Arg.Is(AssertHelper.AssertPropertyEqual(_channelId)),
Arg.Is(AssertHelper.AssertPropertyEqual(message.RenderedTemplate))
);
}
[Theory, BitAutoData]
public async Task HandleAsync_HttpExceptionTooManyRequests_ReturnsRateLimited(IntegrationMessage<TeamsIntegrationConfigurationDetails> message)
{
var sutProvider = GetSutProvider();
message.Configuration = new TeamsIntegrationConfigurationDetails(_channelId, _serviceUrl);
sutProvider.GetDependency<ITeamsService>()
.SendMessageToChannelAsync(Arg.Any<Uri>(), Arg.Any<string>(), Arg.Any<string>())
.ThrowsAsync(new HttpOperationException("Server error")
{
Response = new HttpResponseMessageWrapper(
new HttpResponseMessage(System.Net.HttpStatusCode.TooManyRequests),
"Too Many Requests"
)
}
);
var result = await sutProvider.Sut.HandleAsync(message);
Assert.False(result.Success);
Assert.Equal(IntegrationFailureCategory.RateLimited, result.Category);
Assert.True(result.Retryable);
Assert.Equal(result.Message, message);
await sutProvider.GetDependency<ITeamsService>().Received(1).SendMessageToChannelAsync(
Arg.Is(AssertHelper.AssertPropertyEqual(_serviceUrl)),
Arg.Is(AssertHelper.AssertPropertyEqual(_channelId)),
Arg.Is(AssertHelper.AssertPropertyEqual(message.RenderedTemplate))
);
}
[Theory, BitAutoData]
public async Task HandleAsync_UnknownException_ReturnsTransientError(IntegrationMessage<TeamsIntegrationConfigurationDetails> message)
{
var sutProvider = GetSutProvider();
message.Configuration = new TeamsIntegrationConfigurationDetails(_channelId, _serviceUrl);
sutProvider.GetDependency<ITeamsService>()
.SendMessageToChannelAsync(Arg.Any<Uri>(), Arg.Any<string>(), Arg.Any<string>())
.ThrowsAsync(new Exception("Unknown error"));
var result = await sutProvider.Sut.HandleAsync(message);
Assert.False(result.Success);
Assert.Equal(IntegrationFailureCategory.TransientError, result.Category);
Assert.True(result.Retryable);
Assert.Equal(result.Message, message);
await sutProvider.GetDependency<ITeamsService>().Received(1).SendMessageToChannelAsync(
Arg.Is(AssertHelper.AssertPropertyEqual(_serviceUrl)),
Arg.Is(AssertHelper.AssertPropertyEqual(_channelId)),
Arg.Is(AssertHelper.AssertPropertyEqual(message.RenderedTemplate))
);
}
}

View File

@@ -1,289 +0,0 @@
#nullable enable
using System.Net;
using System.Text.Json;
using System.Web;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
using Bit.Core.Models.Teams;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Bit.Test.Common.MockedHttpClient;
using NSubstitute;
using Xunit;
using GlobalSettings = Bit.Core.Settings.GlobalSettings;
namespace Bit.Core.Test.Services;
[SutProviderCustomize]
public class TeamsServiceTests
{
private readonly MockedHttpMessageHandler _handler;
private readonly HttpClient _httpClient;
public TeamsServiceTests()
{
_handler = new MockedHttpMessageHandler();
_httpClient = _handler.ToHttpClient();
}
private SutProvider<TeamsService> GetSutProvider()
{
var clientFactory = Substitute.For<IHttpClientFactory>();
clientFactory.CreateClient(TeamsService.HttpClientName).Returns(_httpClient);
var globalSettings = Substitute.For<GlobalSettings>();
globalSettings.Teams.LoginBaseUrl.Returns("https://login.example.com");
globalSettings.Teams.GraphBaseUrl.Returns("https://graph.example.com");
return new SutProvider<TeamsService>()
.SetDependency(clientFactory)
.SetDependency(globalSettings)
.Create();
}
[Fact]
public void GetRedirectUrl_ReturnsCorrectUrl()
{
var sutProvider = GetSutProvider();
var clientId = sutProvider.GetDependency<GlobalSettings>().Teams.ClientId;
var scopes = sutProvider.GetDependency<GlobalSettings>().Teams.Scopes;
var callbackUrl = "https://example.com/callback";
var state = Guid.NewGuid().ToString();
var result = sutProvider.Sut.GetRedirectUrl(callbackUrl, state);
var uri = new Uri(result);
var query = HttpUtility.ParseQueryString(uri.Query);
Assert.Equal(clientId, query["client_id"]);
Assert.Equal(scopes, query["scope"]);
Assert.Equal(callbackUrl, query["redirect_uri"]);
Assert.Equal(state, query["state"]);
Assert.Equal("login.example.com", uri.Host);
Assert.Equal("/common/oauth2/v2.0/authorize", uri.AbsolutePath);
}
[Fact]
public async Task ObtainTokenViaOAuth_Success_ReturnsAccessToken()
{
var sutProvider = GetSutProvider();
var jsonResponse = JsonSerializer.Serialize(new
{
access_token = "test-access-token"
});
_handler.When("https://login.example.com/common/oauth2/v2.0/token")
.RespondWith(HttpStatusCode.OK)
.WithContent(new StringContent(jsonResponse));
var result = await sutProvider.Sut.ObtainTokenViaOAuth("test-code", "https://example.com/callback");
Assert.Equal("test-access-token", result);
}
[Theory]
[InlineData("test-code", "")]
[InlineData("", "https://example.com/callback")]
[InlineData("", "")]
public async Task ObtainTokenViaOAuth_CodeOrRedirectUrlIsEmpty_ReturnsEmptyString(string code, string redirectUrl)
{
var sutProvider = GetSutProvider();
var result = await sutProvider.Sut.ObtainTokenViaOAuth(code, redirectUrl);
Assert.Equal(string.Empty, result);
}
[Fact]
public async Task ObtainTokenViaOAuth_HttpFailure_ReturnsEmptyString()
{
var sutProvider = GetSutProvider();
_handler.When("https://login.example.com/common/oauth2/v2.0/token")
.RespondWith(HttpStatusCode.InternalServerError)
.WithContent(new StringContent(string.Empty));
var result = await sutProvider.Sut.ObtainTokenViaOAuth("test-code", "https://example.com/callback");
Assert.Equal(string.Empty, result);
}
[Fact]
public async Task ObtainTokenViaOAuth_UnknownResponse_ReturnsEmptyString()
{
var sutProvider = GetSutProvider();
_handler.When("https://login.example.com/common/oauth2/v2.0/token")
.RespondWith(HttpStatusCode.OK)
.WithContent(new StringContent("Not an expected response"));
var result = await sutProvider.Sut.ObtainTokenViaOAuth("test-code", "https://example.com/callback");
Assert.Equal(string.Empty, result);
}
[Fact]
public async Task GetJoinedTeamsAsync_Success_ReturnsTeams()
{
var sutProvider = GetSutProvider();
var jsonResponse = JsonSerializer.Serialize(new
{
value = new[]
{
new { id = "team1", displayName = "Team One" },
new { id = "team2", displayName = "Team Two" }
}
});
_handler.When("https://graph.example.com/me/joinedTeams")
.RespondWith(HttpStatusCode.OK)
.WithContent(new StringContent(jsonResponse));
var result = await sutProvider.Sut.GetJoinedTeamsAsync("fake-access-token");
Assert.Equal(2, result.Count);
Assert.Contains(result, t => t is { Id: "team1", DisplayName: "Team One" });
Assert.Contains(result, t => t is { Id: "team2", DisplayName: "Team Two" });
}
[Fact]
public async Task GetJoinedTeamsAsync_ServerReturnsEmpty_ReturnsEmptyList()
{
var sutProvider = GetSutProvider();
var jsonResponse = JsonSerializer.Serialize(new { value = (object?)null });
_handler.When("https://graph.example.com/me/joinedTeams")
.RespondWith(HttpStatusCode.OK)
.WithContent(new StringContent(jsonResponse));
var result = await sutProvider.Sut.GetJoinedTeamsAsync("fake-access-token");
Assert.NotNull(result);
Assert.Empty(result);
}
[Fact]
public async Task GetJoinedTeamsAsync_ServerErrorCode_ReturnsEmptyList()
{
var sutProvider = GetSutProvider();
_handler.When("https://graph.example.com/me/joinedTeams")
.RespondWith(HttpStatusCode.Unauthorized)
.WithContent(new StringContent("Unauthorized"));
var result = await sutProvider.Sut.GetJoinedTeamsAsync("fake-access-token");
Assert.NotNull(result);
Assert.Empty(result);
}
[Theory, BitAutoData]
public async Task HandleIncomingAppInstall_Success_UpdatesTeamsIntegration(
OrganizationIntegration integration)
{
var sutProvider = GetSutProvider();
var tenantId = Guid.NewGuid().ToString();
var teamId = Guid.NewGuid().ToString();
var conversationId = Guid.NewGuid().ToString();
var serviceUrl = new Uri("https://localhost");
var initiatedConfiguration = new TeamsIntegration(TenantId: tenantId, Teams:
[
new TeamInfo() { Id = teamId, DisplayName = "test team", TenantId = tenantId },
new TeamInfo() { Id = Guid.NewGuid().ToString(), DisplayName = "other team", TenantId = tenantId },
new TeamInfo() { Id = Guid.NewGuid().ToString(), DisplayName = "third team", TenantId = tenantId }
]);
integration.Configuration = JsonSerializer.Serialize(initiatedConfiguration);
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByTeamsConfigurationTenantIdTeamId(tenantId, teamId)
.Returns(integration);
OrganizationIntegration? capturedIntegration = null;
await sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.UpsertAsync(Arg.Do<OrganizationIntegration>(x => capturedIntegration = x));
await sutProvider.Sut.HandleIncomingAppInstallAsync(
conversationId: conversationId,
serviceUrl: serviceUrl,
teamId: teamId,
tenantId: tenantId
);
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1).GetByTeamsConfigurationTenantIdTeamId(tenantId, teamId);
Assert.NotNull(capturedIntegration);
var configuration = JsonSerializer.Deserialize<TeamsIntegration>(capturedIntegration.Configuration ?? string.Empty);
Assert.NotNull(configuration);
Assert.NotNull(configuration.ServiceUrl);
Assert.Equal(serviceUrl, configuration.ServiceUrl);
Assert.Equal(conversationId, configuration.ChannelId);
}
[Fact]
public async Task HandleIncomingAppInstall_NoIntegrationMatched_DoesNothing()
{
var sutProvider = GetSutProvider();
await sutProvider.Sut.HandleIncomingAppInstallAsync(
conversationId: "conversationId",
serviceUrl: new Uri("https://localhost"),
teamId: "teamId",
tenantId: "tenantId"
);
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1).GetByTeamsConfigurationTenantIdTeamId("tenantId", "teamId");
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().DidNotReceive().UpsertAsync(Arg.Any<OrganizationIntegration>());
}
[Theory, BitAutoData]
public async Task HandleIncomingAppInstall_MatchedIntegrationAlreadySetup_DoesNothing(
OrganizationIntegration integration)
{
var sutProvider = GetSutProvider();
var tenantId = Guid.NewGuid().ToString();
var teamId = Guid.NewGuid().ToString();
var initiatedConfiguration = new TeamsIntegration(
TenantId: tenantId,
Teams: [new TeamInfo() { Id = teamId, DisplayName = "test team", TenantId = tenantId }],
ChannelId: "ChannelId",
ServiceUrl: new Uri("https://localhost")
);
integration.Configuration = JsonSerializer.Serialize(initiatedConfiguration);
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByTeamsConfigurationTenantIdTeamId(tenantId, teamId)
.Returns(integration);
await sutProvider.Sut.HandleIncomingAppInstallAsync(
conversationId: "conversationId",
serviceUrl: new Uri("https://localhost"),
teamId: teamId,
tenantId: tenantId
);
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1).GetByTeamsConfigurationTenantIdTeamId(tenantId, teamId);
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().DidNotReceive().UpsertAsync(Arg.Any<OrganizationIntegration>());
}
[Theory, BitAutoData]
public async Task HandleIncomingAppInstall_MatchedIntegrationWithMissingConfiguration_DoesNothing(
OrganizationIntegration integration)
{
var sutProvider = GetSutProvider();
integration.Configuration = null;
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByTeamsConfigurationTenantIdTeamId("tenantId", "teamId")
.Returns(integration);
await sutProvider.Sut.HandleIncomingAppInstallAsync(
conversationId: "conversationId",
serviceUrl: new Uri("https://localhost"),
teamId: "teamId",
tenantId: "tenantId"
);
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1).GetByTeamsConfigurationTenantIdTeamId("tenantId", "teamId");
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().DidNotReceive().UpsertAsync(Arg.Any<OrganizationIntegration>());
}
}

View File

@@ -1,185 +0,0 @@
using System.Net;
using System.Net.Http.Headers;
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
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 Microsoft.Extensions.Time.Testing;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.Services;
[SutProviderCustomize]
public class WebhookIntegrationHandlerTests
{
private readonly MockedHttpMessageHandler _handler;
private readonly HttpClient _httpClient;
private const string _scheme = "Bearer";
private const string _token = "AUTH_TOKEN";
private static readonly Uri _webhookUri = new Uri("https://localhost");
public WebhookIntegrationHandlerTests()
{
_handler = new MockedHttpMessageHandler();
_handler.Fallback
.WithStatusCode(HttpStatusCode.OK)
.WithContent(new StringContent("<html><head><title>test</title></head><body>test</body></html>"));
_httpClient = _handler.ToHttpClient();
}
private SutProvider<WebhookIntegrationHandler> GetSutProvider()
{
var clientFactory = Substitute.For<IHttpClientFactory>();
clientFactory.CreateClient(WebhookIntegrationHandler.HttpClientName).Returns(_httpClient);
return new SutProvider<WebhookIntegrationHandler>()
.SetDependency(clientFactory)
.WithFakeTimeProvider()
.Create();
}
[Theory, BitAutoData]
public async Task HandleAsync_SuccessfulRequestWithoutAuth_ReturnsSuccess(IntegrationMessage<WebhookIntegrationConfigurationDetails> message)
{
var sutProvider = GetSutProvider();
message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUri);
var result = await sutProvider.Sut.HandleAsync(message);
Assert.True(result.Success);
Assert.Equal(result.Message, message);
Assert.Null(result.FailureReason);
sutProvider.GetDependency<IHttpClientFactory>().Received(1).CreateClient(
Arg.Is(AssertHelper.AssertPropertyEqual(WebhookIntegrationHandler.HttpClientName))
);
Assert.Single(_handler.CapturedRequests);
var request = _handler.CapturedRequests[0];
Assert.NotNull(request);
Assert.NotNull(request.Content);
var returned = await request.Content.ReadAsStringAsync();
Assert.Equal(HttpMethod.Post, request.Method);
Assert.Null(request.Headers.Authorization);
Assert.Equal(_webhookUri, request.RequestUri);
AssertHelper.AssertPropertyEqual(message.RenderedTemplate, returned);
}
[Theory, BitAutoData]
public async Task HandleAsync_SuccessfulRequestWithAuthorizationHeader_ReturnsSuccess(IntegrationMessage<WebhookIntegrationConfigurationDetails> message)
{
var sutProvider = GetSutProvider();
message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUri, _scheme, _token);
var result = await sutProvider.Sut.HandleAsync(message);
Assert.True(result.Success);
Assert.Equal(result.Message, message);
Assert.Null(result.FailureReason);
sutProvider.GetDependency<IHttpClientFactory>().Received(1).CreateClient(
Arg.Is(AssertHelper.AssertPropertyEqual(WebhookIntegrationHandler.HttpClientName))
);
Assert.Single(_handler.CapturedRequests);
var request = _handler.CapturedRequests[0];
Assert.NotNull(request);
Assert.NotNull(request.Content);
var returned = await request.Content.ReadAsStringAsync();
Assert.Equal(HttpMethod.Post, request.Method);
Assert.Equal(new AuthenticationHeaderValue(_scheme, _token), request.Headers.Authorization);
Assert.Equal(_webhookUri, request.RequestUri);
AssertHelper.AssertPropertyEqual(message.RenderedTemplate, returned);
}
[Theory, BitAutoData]
public async Task HandleAsync_TooManyRequests_ReturnsFailureSetsDelayUntilDate(IntegrationMessage<WebhookIntegrationConfigurationDetails> message)
{
var sutProvider = GetSutProvider();
var now = new DateTime(2014, 3, 2, 1, 0, 0, DateTimeKind.Utc);
var retryAfter = now.AddSeconds(60);
sutProvider.GetDependency<FakeTimeProvider>().SetUtcNow(now);
message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUri, _scheme, _token);
_handler.Fallback
.WithStatusCode(HttpStatusCode.TooManyRequests)
.WithHeader("Retry-After", "60")
.WithContent(new StringContent("<html><head><title>test</title></head><body>test</body></html>"));
var result = await sutProvider.Sut.HandleAsync(message);
Assert.False(result.Success);
Assert.True(result.Retryable);
Assert.Equal(result.Message, message);
Assert.True(result.DelayUntilDate.HasValue);
Assert.Equal(retryAfter, result.DelayUntilDate.Value);
Assert.Equal("Too Many Requests", result.FailureReason);
}
[Theory, BitAutoData]
public async Task HandleAsync_TooManyRequestsWithDate_ReturnsFailureSetsDelayUntilDate(IntegrationMessage<WebhookIntegrationConfigurationDetails> message)
{
var sutProvider = GetSutProvider();
var now = new DateTime(2014, 3, 2, 1, 0, 0, DateTimeKind.Utc);
var retryAfter = now.AddSeconds(60);
message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUri, _scheme, _token);
_handler.Fallback
.WithStatusCode(HttpStatusCode.TooManyRequests)
.WithHeader("Retry-After", retryAfter.ToString("r"))
.WithContent(new StringContent("<html><head><title>test</title></head><body>test</body></html>"));
var result = await sutProvider.Sut.HandleAsync(message);
Assert.False(result.Success);
Assert.True(result.Retryable);
Assert.Equal(result.Message, message);
Assert.True(result.DelayUntilDate.HasValue);
Assert.Equal(retryAfter, result.DelayUntilDate.Value);
Assert.Equal("Too Many Requests", result.FailureReason);
}
[Theory, BitAutoData]
public async Task HandleAsync_InternalServerError_ReturnsFailureSetsRetryable(IntegrationMessage<WebhookIntegrationConfigurationDetails> message)
{
var sutProvider = GetSutProvider();
message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUri, _scheme, _token);
_handler.Fallback
.WithStatusCode(HttpStatusCode.InternalServerError)
.WithContent(new StringContent("<html><head><title>test</title></head><body>test</body></html>"));
var result = await sutProvider.Sut.HandleAsync(message);
Assert.False(result.Success);
Assert.True(result.Retryable);
Assert.Equal(result.Message, message);
Assert.False(result.DelayUntilDate.HasValue);
Assert.Equal("Internal Server Error", result.FailureReason);
}
[Theory, BitAutoData]
public async Task HandleAsync_UnexpectedRedirect_ReturnsFailureNotRetryable(IntegrationMessage<WebhookIntegrationConfigurationDetails> message)
{
var sutProvider = GetSutProvider();
message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUri, _scheme, _token);
_handler.Fallback
.WithStatusCode(HttpStatusCode.TemporaryRedirect)
.WithContent(new StringContent("<html><head><title>test</title></head><body>test</body></html>"));
var result = await sutProvider.Sut.HandleAsync(message);
Assert.False(result.Success);
Assert.False(result.Retryable);
Assert.Equal(result.Message, message);
Assert.Null(result.DelayUntilDate);
Assert.Equal("Temporary Redirect", result.FailureReason);
}
}