diff --git a/src/Api/AdminConsole/Models/Response/Organizations/OrganizationIntegrationConfigurationResponseModel.cs b/src/Api/AdminConsole/Models/Response/Organizations/OrganizationIntegrationConfigurationResponseModel.cs
index c7906318e8de..d070375d8867 100644
--- a/src/Api/AdminConsole/Models/Response/Organizations/OrganizationIntegrationConfigurationResponseModel.cs
+++ b/src/Api/AdminConsole/Models/Response/Organizations/OrganizationIntegrationConfigurationResponseModel.cs
@@ -2,8 +2,6 @@
using Bit.Core.Enums;
using Bit.Core.Models.Api;
-#nullable enable
-
namespace Bit.Api.AdminConsole.Models.Response.Organizations;
public class OrganizationIntegrationConfigurationResponseModel : ResponseModel
@@ -11,8 +9,6 @@ public class OrganizationIntegrationConfigurationResponseModel : ResponseModel
public OrganizationIntegrationConfigurationResponseModel(OrganizationIntegrationConfiguration organizationIntegrationConfiguration, string obj = "organizationIntegrationConfiguration")
: base(obj)
{
- ArgumentNullException.ThrowIfNull(organizationIntegrationConfiguration);
-
Id = organizationIntegrationConfiguration.Id;
Configuration = organizationIntegrationConfiguration.Configuration;
CreationDate = organizationIntegrationConfiguration.CreationDate;
diff --git a/src/Core/AdminConsole/Models/Slack/SlackApiResponse.cs b/src/Core/AdminConsole/Models/Slack/SlackApiResponse.cs
index 70d280c4280b..3c811e2b282e 100644
--- a/src/Core/AdminConsole/Models/Slack/SlackApiResponse.cs
+++ b/src/Core/AdminConsole/Models/Slack/SlackApiResponse.cs
@@ -33,6 +33,12 @@ public class SlackOAuthResponse : SlackApiResponse
public SlackTeam Team { get; set; } = new();
}
+public class SlackSendMessageResponse : SlackApiResponse
+{
+ [JsonPropertyName("channel")]
+ public string Channel { get; set; } = string.Empty;
+}
+
public class SlackTeam
{
public string Id { get; set; } = string.Empty;
diff --git a/src/Core/AdminConsole/Services/ISlackService.cs b/src/Core/AdminConsole/Services/ISlackService.cs
index 0577532ac2fe..60d3da8af4db 100644
--- a/src/Core/AdminConsole/Services/ISlackService.cs
+++ b/src/Core/AdminConsole/Services/ISlackService.cs
@@ -1,4 +1,6 @@
-namespace Bit.Core.Services;
+using Bit.Core.Models.Slack;
+
+namespace Bit.Core.Services;
/// Defines operations for interacting with Slack, including OAuth authentication, channel discovery,
/// and sending messages.
@@ -54,6 +56,6 @@ public interface ISlackService
/// A valid Slack OAuth access token.
/// The message text to send.
/// The channel ID to send the message to.
- /// A task that completes when the message has been sent.
- Task SendSlackMessageByChannelIdAsync(string token, string message, string channelId);
+ /// The response from Slack after sending the message.
+ Task SendSlackMessageByChannelIdAsync(string token, string message, string channelId);
}
diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/SlackIntegrationHandler.cs b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/SlackIntegrationHandler.cs
index 2d29494afc8c..16c756c8c480 100644
--- a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/SlackIntegrationHandler.cs
+++ b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/SlackIntegrationHandler.cs
@@ -6,14 +6,43 @@ public class SlackIntegrationHandler(
ISlackService slackService)
: IntegrationHandlerBase
{
+ private static readonly HashSet _retryableErrors = new(StringComparer.Ordinal)
+ {
+ "internal_error",
+ "message_limit_exceeded",
+ "rate_limited",
+ "ratelimited",
+ "service_unavailable"
+ };
+
public override async Task HandleAsync(IntegrationMessage message)
{
- await slackService.SendSlackMessageByChannelIdAsync(
+ var slackResponse = await slackService.SendSlackMessageByChannelIdAsync(
message.Configuration.Token,
message.RenderedTemplate,
message.Configuration.ChannelId
);
- return new IntegrationHandlerResult(success: true, message: message);
+ if (slackResponse is null)
+ {
+ return new IntegrationHandlerResult(success: false, message: message)
+ {
+ FailureReason = "Slack response was null"
+ };
+ }
+
+ if (slackResponse.Ok)
+ {
+ return new IntegrationHandlerResult(success: true, message: message);
+ }
+
+ var result = new IntegrationHandlerResult(success: false, message: message) { FailureReason = slackResponse.Error };
+
+ if (_retryableErrors.Contains(slackResponse.Error))
+ {
+ result.Retryable = true;
+ }
+
+ return result;
}
}
diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/SlackService.cs b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/SlackService.cs
index 8b691dd4bf61..7eec2ec37436 100644
--- a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/SlackService.cs
+++ b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/SlackService.cs
@@ -1,5 +1,6 @@
using System.Net.Http.Headers;
using System.Net.Http.Json;
+using System.Text.Json;
using System.Web;
using Bit.Core.Models.Slack;
using Bit.Core.Settings;
@@ -71,7 +72,7 @@ public async Task> GetChannelIdsAsync(string token, List ch
public async Task GetDmChannelByEmailAsync(string token, string email)
{
var userId = await GetUserIdByEmailAsync(token, email);
- return await OpenDmChannel(token, userId);
+ return await OpenDmChannelAsync(token, userId);
}
public string GetRedirectUrl(string callbackUrl, string state)
@@ -97,21 +98,21 @@ public async Task ObtainTokenViaOAuth(string code, string redirectUrl)
}
var tokenResponse = await _httpClient.PostAsync($"{_slackApiBaseUrl}/oauth.v2.access",
- new FormUrlEncodedContent(new[]
- {
+ new FormUrlEncodedContent([
new KeyValuePair("client_id", _clientId),
new KeyValuePair("client_secret", _clientSecret),
new KeyValuePair("code", code),
new KeyValuePair("redirect_uri", redirectUrl)
- }));
+ ]));
SlackOAuthResponse? result;
try
{
result = await tokenResponse.Content.ReadFromJsonAsync();
}
- catch
+ catch (JsonException ex)
{
+ logger.LogError(ex, "Error parsing SlackOAuthResponse: invalid JSON");
result = null;
}
@@ -129,14 +130,25 @@ public async Task ObtainTokenViaOAuth(string code, string redirectUrl)
return result.AccessToken;
}
- public async Task SendSlackMessageByChannelIdAsync(string token, string message, string channelId)
+ public async Task SendSlackMessageByChannelIdAsync(string token, string message,
+ string channelId)
{
var payload = JsonContent.Create(new { channel = channelId, text = message });
var request = new HttpRequestMessage(HttpMethod.Post, $"{_slackApiBaseUrl}/chat.postMessage");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
request.Content = payload;
- await _httpClient.SendAsync(request);
+ var response = await _httpClient.SendAsync(request);
+
+ try
+ {
+ return await response.Content.ReadFromJsonAsync();
+ }
+ catch (JsonException ex)
+ {
+ logger.LogError(ex, "Error parsing Slack message response: invalid JSON");
+ return null;
+ }
}
private async Task GetUserIdByEmailAsync(string token, string email)
@@ -144,7 +156,16 @@ private async Task GetUserIdByEmailAsync(string token, string email)
var request = new HttpRequestMessage(HttpMethod.Get, $"{_slackApiBaseUrl}/users.lookupByEmail?email={email}");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
var response = await _httpClient.SendAsync(request);
- var result = await response.Content.ReadFromJsonAsync();
+ SlackUserResponse? result;
+ try
+ {
+ result = await response.Content.ReadFromJsonAsync();
+ }
+ catch (JsonException ex)
+ {
+ logger.LogError(ex, "Error parsing SlackUserResponse: invalid JSON");
+ result = null;
+ }
if (result is null)
{
@@ -160,7 +181,7 @@ private async Task GetUserIdByEmailAsync(string token, string email)
return result.User.Id;
}
- private async Task OpenDmChannel(string token, string userId)
+ private async Task OpenDmChannelAsync(string token, string userId)
{
if (string.IsNullOrEmpty(userId))
return string.Empty;
@@ -170,7 +191,16 @@ private async Task OpenDmChannel(string token, string userId)
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
request.Content = payload;
var response = await _httpClient.SendAsync(request);
- var result = await response.Content.ReadFromJsonAsync();
+ SlackDmResponse? result;
+ try
+ {
+ result = await response.Content.ReadFromJsonAsync();
+ }
+ catch (JsonException ex)
+ {
+ logger.LogError(ex, "Error parsing SlackDmResponse: invalid JSON");
+ result = null;
+ }
if (result is null)
{
diff --git a/src/Core/AdminConsole/Services/NoopImplementations/NoopSlackService.cs b/src/Core/AdminConsole/Services/NoopImplementations/NoopSlackService.cs
index d6c8d08c4cea..a54df94814c4 100644
--- a/src/Core/AdminConsole/Services/NoopImplementations/NoopSlackService.cs
+++ b/src/Core/AdminConsole/Services/NoopImplementations/NoopSlackService.cs
@@ -1,4 +1,5 @@
-using Bit.Core.Services;
+using Bit.Core.Models.Slack;
+using Bit.Core.Services;
namespace Bit.Core.AdminConsole.Services.NoopImplementations;
@@ -24,9 +25,10 @@ public string GetRedirectUrl(string callbackUrl, string state)
return string.Empty;
}
- public Task SendSlackMessageByChannelIdAsync(string token, string message, string channelId)
+ public Task SendSlackMessageByChannelIdAsync(string token, string message,
+ string channelId)
{
- return Task.FromResult(0);
+ return Task.FromResult(null);
}
public Task ObtainTokenViaOAuth(string code, string redirectUrl)
diff --git a/test/Api.Test/AdminConsole/Controllers/OrganizationIntegrationControllerTests.cs b/test/Api.Test/AdminConsole/Controllers/OrganizationIntegrationControllerTests.cs
index 1dd0e86f392c..335859e0c402 100644
--- a/test/Api.Test/AdminConsole/Controllers/OrganizationIntegrationControllerTests.cs
+++ b/test/Api.Test/AdminConsole/Controllers/OrganizationIntegrationControllerTests.cs
@@ -133,6 +133,29 @@ await sutProvider.GetDependency().Received(1
.DeleteAsync(organizationIntegration);
}
+ [Theory, BitAutoData]
+ public async Task PostDeleteAsync_AllParamsProvided_Succeeds(
+ SutProvider sutProvider,
+ Guid organizationId,
+ OrganizationIntegration organizationIntegration)
+ {
+ organizationIntegration.OrganizationId = organizationId;
+ sutProvider.Sut.Url = Substitute.For();
+ sutProvider.GetDependency()
+ .OrganizationOwner(organizationId)
+ .Returns(true);
+ sutProvider.GetDependency()
+ .GetByIdAsync(Arg.Any())
+ .Returns(organizationIntegration);
+
+ await sutProvider.Sut.PostDeleteAsync(organizationId, organizationIntegration.Id);
+
+ await sutProvider.GetDependency().Received(1)
+ .GetByIdAsync(organizationIntegration.Id);
+ await sutProvider.GetDependency().Received(1)
+ .DeleteAsync(organizationIntegration);
+ }
+
[Theory, BitAutoData]
public async Task DeleteAsync_IntegrationDoesNotBelongToOrganization_ThrowsNotFound(
SutProvider sutProvider,
diff --git a/test/Api.Test/AdminConsole/Controllers/OrganizationIntegrationsConfigurationControllerTests.cs b/test/Api.Test/AdminConsole/Controllers/OrganizationIntegrationsConfigurationControllerTests.cs
index 4ccfa7030826..9ab626d3f0e0 100644
--- a/test/Api.Test/AdminConsole/Controllers/OrganizationIntegrationsConfigurationControllerTests.cs
+++ b/test/Api.Test/AdminConsole/Controllers/OrganizationIntegrationsConfigurationControllerTests.cs
@@ -51,6 +51,36 @@ await sutProvider.GetDependency
.DeleteAsync(organizationIntegrationConfiguration);
}
+ [Theory, BitAutoData]
+ public async Task PostDeleteAsync_AllParamsProvided_Succeeds(
+ SutProvider sutProvider,
+ Guid organizationId,
+ OrganizationIntegration organizationIntegration,
+ OrganizationIntegrationConfiguration organizationIntegrationConfiguration)
+ {
+ organizationIntegration.OrganizationId = organizationId;
+ organizationIntegrationConfiguration.OrganizationIntegrationId = organizationIntegration.Id;
+ sutProvider.Sut.Url = Substitute.For();
+ sutProvider.GetDependency()
+ .OrganizationOwner(organizationId)
+ .Returns(true);
+ sutProvider.GetDependency()
+ .GetByIdAsync(Arg.Any())
+ .Returns(organizationIntegration);
+ sutProvider.GetDependency()
+ .GetByIdAsync(Arg.Any())
+ .Returns(organizationIntegrationConfiguration);
+
+ await sutProvider.Sut.PostDeleteAsync(organizationId, organizationIntegration.Id, organizationIntegrationConfiguration.Id);
+
+ await sutProvider.GetDependency().Received(1)
+ .GetByIdAsync(organizationIntegration.Id);
+ await sutProvider.GetDependency().Received(1)
+ .GetByIdAsync(organizationIntegrationConfiguration.Id);
+ await sutProvider.GetDependency().Received(1)
+ .DeleteAsync(organizationIntegrationConfiguration);
+ }
+
[Theory, BitAutoData]
public async Task DeleteAsync_IntegrationConfigurationDoesNotExist_ThrowsNotFound(
SutProvider sutProvider,
@@ -199,27 +229,6 @@ await sutProvider.GetDependency
.GetManyByIntegrationAsync(organizationIntegration.Id);
}
- // [Theory, BitAutoData]
- // public async Task GetAsync_IntegrationConfigurationDoesNotExist_ThrowsNotFound(
- // SutProvider sutProvider,
- // Guid organizationId,
- // OrganizationIntegration organizationIntegration)
- // {
- // organizationIntegration.OrganizationId = organizationId;
- // sutProvider.Sut.Url = Substitute.For();
- // sutProvider.GetDependency()
- // .OrganizationOwner(organizationId)
- // .Returns(true);
- // sutProvider.GetDependency()
- // .GetByIdAsync(Arg.Any())
- // .Returns(organizationIntegration);
- // sutProvider.GetDependency()
- // .GetByIdAsync(Arg.Any())
- // .ReturnsNull();
- //
- // await Assert.ThrowsAsync(async () => await sutProvider.Sut.GetAsync(organizationId, Guid.Empty, Guid.Empty));
- // }
- //
[Theory, BitAutoData]
public async Task GetAsync_IntegrationDoesNotExist_ThrowsNotFound(
SutProvider sutProvider,
@@ -293,15 +302,16 @@ public async Task PostAsync_AllParamsProvided_Slack_Succeeds(
sutProvider.GetDependency()
.CreateAsync(Arg.Any())
.Returns(organizationIntegrationConfiguration);
- var requestAction = await sutProvider.Sut.CreateAsync(organizationId, organizationIntegration.Id, model);
+ var createResponse = await sutProvider.Sut.CreateAsync(organizationId, organizationIntegration.Id, model);
await sutProvider.GetDependency().Received(1)
.CreateAsync(Arg.Any());
- Assert.IsType(requestAction);
- Assert.Equal(expected.Id, requestAction.Id);
- Assert.Equal(expected.Configuration, requestAction.Configuration);
- Assert.Equal(expected.EventType, requestAction.EventType);
- Assert.Equal(expected.Template, requestAction.Template);
+ Assert.IsType(createResponse);
+ Assert.Equal(expected.Id, createResponse.Id);
+ Assert.Equal(expected.Configuration, createResponse.Configuration);
+ Assert.Equal(expected.EventType, createResponse.EventType);
+ Assert.Equal(expected.Filters, createResponse.Filters);
+ Assert.Equal(expected.Template, createResponse.Template);
}
[Theory, BitAutoData]
@@ -331,15 +341,16 @@ public async Task PostAsync_AllParamsProvided_Webhook_Succeeds(
sutProvider.GetDependency()
.CreateAsync(Arg.Any())
.Returns(organizationIntegrationConfiguration);
- var requestAction = await sutProvider.Sut.CreateAsync(organizationId, organizationIntegration.Id, model);
+ var createResponse = await sutProvider.Sut.CreateAsync(organizationId, organizationIntegration.Id, model);
await sutProvider.GetDependency().Received(1)
.CreateAsync(Arg.Any());
- Assert.IsType(requestAction);
- Assert.Equal(expected.Id, requestAction.Id);
- Assert.Equal(expected.Configuration, requestAction.Configuration);
- Assert.Equal(expected.EventType, requestAction.EventType);
- Assert.Equal(expected.Template, requestAction.Template);
+ Assert.IsType(createResponse);
+ Assert.Equal(expected.Id, createResponse.Id);
+ Assert.Equal(expected.Configuration, createResponse.Configuration);
+ Assert.Equal(expected.EventType, createResponse.EventType);
+ Assert.Equal(expected.Filters, createResponse.Filters);
+ Assert.Equal(expected.Template, createResponse.Template);
}
[Theory, BitAutoData]
@@ -369,15 +380,16 @@ public async Task PostAsync_OnlyUrlProvided_Webhook_Succeeds(
sutProvider.GetDependency()
.CreateAsync(Arg.Any())
.Returns(organizationIntegrationConfiguration);
- var requestAction = await sutProvider.Sut.CreateAsync(organizationId, organizationIntegration.Id, model);
+ var createResponse = await sutProvider.Sut.CreateAsync(organizationId, organizationIntegration.Id, model);
await sutProvider.GetDependency().Received(1)
.CreateAsync(Arg.Any());
- Assert.IsType(requestAction);
- Assert.Equal(expected.Id, requestAction.Id);
- Assert.Equal(expected.Configuration, requestAction.Configuration);
- Assert.Equal(expected.EventType, requestAction.EventType);
- Assert.Equal(expected.Template, requestAction.Template);
+ Assert.IsType(createResponse);
+ Assert.Equal(expected.Id, createResponse.Id);
+ Assert.Equal(expected.Configuration, createResponse.Configuration);
+ Assert.Equal(expected.EventType, createResponse.EventType);
+ Assert.Equal(expected.Filters, createResponse.Filters);
+ Assert.Equal(expected.Template, createResponse.Template);
}
[Theory, BitAutoData]
@@ -575,7 +587,7 @@ public async Task UpdateAsync_AllParamsProvided_Slack_Succeeds(
sutProvider.GetDependency()
.GetByIdAsync(Arg.Any())
.Returns(organizationIntegrationConfiguration);
- var requestAction = await sutProvider.Sut.UpdateAsync(
+ var updateResponse = await sutProvider.Sut.UpdateAsync(
organizationId,
organizationIntegration.Id,
organizationIntegrationConfiguration.Id,
@@ -583,11 +595,12 @@ public async Task UpdateAsync_AllParamsProvided_Slack_Succeeds(
await sutProvider.GetDependency().Received(1)
.ReplaceAsync(Arg.Any());
- Assert.IsType(requestAction);
- Assert.Equal(expected.Id, requestAction.Id);
- Assert.Equal(expected.Configuration, requestAction.Configuration);
- Assert.Equal(expected.EventType, requestAction.EventType);
- Assert.Equal(expected.Template, requestAction.Template);
+ Assert.IsType(updateResponse);
+ Assert.Equal(expected.Id, updateResponse.Id);
+ Assert.Equal(expected.Configuration, updateResponse.Configuration);
+ Assert.Equal(expected.EventType, updateResponse.EventType);
+ Assert.Equal(expected.Filters, updateResponse.Filters);
+ Assert.Equal(expected.Template, updateResponse.Template);
}
@@ -619,7 +632,7 @@ public async Task UpdateAsync_AllParamsProvided_Webhook_Succeeds(
sutProvider.GetDependency()
.GetByIdAsync(Arg.Any())
.Returns(organizationIntegrationConfiguration);
- var requestAction = await sutProvider.Sut.UpdateAsync(
+ var updateResponse = await sutProvider.Sut.UpdateAsync(
organizationId,
organizationIntegration.Id,
organizationIntegrationConfiguration.Id,
@@ -627,11 +640,12 @@ public async Task UpdateAsync_AllParamsProvided_Webhook_Succeeds(
await sutProvider.GetDependency().Received(1)
.ReplaceAsync(Arg.Any());
- Assert.IsType(requestAction);
- Assert.Equal(expected.Id, requestAction.Id);
- Assert.Equal(expected.Configuration, requestAction.Configuration);
- Assert.Equal(expected.EventType, requestAction.EventType);
- Assert.Equal(expected.Template, requestAction.Template);
+ Assert.IsType(updateResponse);
+ Assert.Equal(expected.Id, updateResponse.Id);
+ Assert.Equal(expected.Configuration, updateResponse.Configuration);
+ Assert.Equal(expected.EventType, updateResponse.EventType);
+ Assert.Equal(expected.Filters, updateResponse.Filters);
+ Assert.Equal(expected.Template, updateResponse.Template);
}
[Theory, BitAutoData]
@@ -662,7 +676,7 @@ public async Task UpdateAsync_OnlyUrlProvided_Webhook_Succeeds(
sutProvider.GetDependency()
.GetByIdAsync(Arg.Any())
.Returns(organizationIntegrationConfiguration);
- var requestAction = await sutProvider.Sut.UpdateAsync(
+ var updateResponse = await sutProvider.Sut.UpdateAsync(
organizationId,
organizationIntegration.Id,
organizationIntegrationConfiguration.Id,
@@ -670,11 +684,12 @@ public async Task UpdateAsync_OnlyUrlProvided_Webhook_Succeeds(
await sutProvider.GetDependency().Received(1)
.ReplaceAsync(Arg.Any());
- Assert.IsType(requestAction);
- Assert.Equal(expected.Id, requestAction.Id);
- Assert.Equal(expected.Configuration, requestAction.Configuration);
- Assert.Equal(expected.EventType, requestAction.EventType);
- Assert.Equal(expected.Template, requestAction.Template);
+ Assert.IsType(updateResponse);
+ Assert.Equal(expected.Id, updateResponse.Id);
+ Assert.Equal(expected.Configuration, updateResponse.Configuration);
+ Assert.Equal(expected.EventType, updateResponse.EventType);
+ Assert.Equal(expected.Filters, updateResponse.Filters);
+ Assert.Equal(expected.Template, updateResponse.Template);
}
[Theory, BitAutoData]
diff --git a/test/Api.Test/AdminConsole/Controllers/SlackIntegrationControllerTests.cs b/test/Api.Test/AdminConsole/Controllers/SlackIntegrationControllerTests.cs
index 61d3486c5160..c07944555973 100644
--- a/test/Api.Test/AdminConsole/Controllers/SlackIntegrationControllerTests.cs
+++ b/test/Api.Test/AdminConsole/Controllers/SlackIntegrationControllerTests.cs
@@ -71,6 +71,26 @@ await Assert.ThrowsAsync(async () =>
await sutProvider.Sut.CreateAsync(string.Empty, state.ToString()));
}
+ [Theory, BitAutoData]
+ public async Task CreateAsync_CallbackUrlIsEmpty_ThrowsBadRequest(
+ SutProvider sutProvider,
+ OrganizationIntegration integration)
+ {
+ integration.Type = IntegrationType.Slack;
+ integration.Configuration = null;
+ sutProvider.Sut.Url = Substitute.For();
+ sutProvider.Sut.Url
+ .RouteUrl(Arg.Is(c => c.RouteName == "SlackIntegration_Create"))
+ .Returns((string?)null);
+ sutProvider.GetDependency()
+ .GetByIdAsync(integration.Id)
+ .Returns(integration);
+ var state = IntegrationOAuthState.FromIntegration(integration, sutProvider.GetDependency());
+
+ await Assert.ThrowsAsync(async () =>
+ await sutProvider.Sut.CreateAsync(_validSlackCode, state.ToString()));
+ }
+
[Theory, BitAutoData]
public async Task CreateAsync_SlackServiceReturnsEmpty_ThrowsBadRequest(
SutProvider sutProvider,
@@ -153,6 +173,8 @@ public async Task CreateAsync_StateHasWrongOrganizationHash_ThrowsNotFound(
OrganizationIntegration wrongOrgIntegration)
{
wrongOrgIntegration.Id = integration.Id;
+ wrongOrgIntegration.Type = IntegrationType.Slack;
+ wrongOrgIntegration.Configuration = null;
sutProvider.Sut.Url = Substitute.For();
sutProvider.Sut.Url
@@ -304,6 +326,22 @@ public async Task RedirectAsync_IntegrationAlreadyExistsWithConfig_ThrowsBadRequ
await Assert.ThrowsAsync(async () => await sutProvider.Sut.RedirectAsync(organizationId));
}
+ [Theory, BitAutoData]
+ public async Task RedirectAsync_CallbackUrlReturnsEmpty_ThrowsBadRequest(
+ SutProvider sutProvider,
+ Guid organizationId)
+ {
+ sutProvider.Sut.Url = Substitute.For();
+ sutProvider.Sut.Url
+ .RouteUrl(Arg.Is(c => c.RouteName == "SlackIntegration_Create"))
+ .Returns((string?)null);
+ sutProvider.GetDependency()
+ .OrganizationOwner(organizationId)
+ .Returns(true);
+
+ await Assert.ThrowsAsync(async () => await sutProvider.Sut.RedirectAsync(organizationId));
+ }
+
[Theory, BitAutoData]
public async Task RedirectAsync_SlackServiceReturnsEmpty_ThrowsNotFound(
SutProvider sutProvider,
diff --git a/test/Api.Test/AdminConsole/Controllers/TeamsIntegrationControllerTests.cs b/test/Api.Test/AdminConsole/Controllers/TeamsIntegrationControllerTests.cs
index 3af2affdd83e..3302a8737202 100644
--- a/test/Api.Test/AdminConsole/Controllers/TeamsIntegrationControllerTests.cs
+++ b/test/Api.Test/AdminConsole/Controllers/TeamsIntegrationControllerTests.cs
@@ -60,6 +60,26 @@ await sutProvider.GetDependency().Received(1
Assert.IsType(requestAction);
}
+ [Theory, BitAutoData]
+ public async Task CreateAsync_CallbackUrlIsEmpty_ThrowsBadRequest(
+ SutProvider sutProvider,
+ OrganizationIntegration integration)
+ {
+ integration.Type = IntegrationType.Teams;
+ integration.Configuration = null;
+ sutProvider.Sut.Url = Substitute.For();
+ sutProvider.Sut.Url
+ .RouteUrl(Arg.Is(c => c.RouteName == "TeamsIntegration_Create"))
+ .Returns((string?)null);
+ sutProvider.GetDependency()
+ .GetByIdAsync(integration.Id)
+ .Returns(integration);
+ var state = IntegrationOAuthState.FromIntegration(integration, sutProvider.GetDependency());
+
+ await Assert.ThrowsAsync(async () =>
+ await sutProvider.Sut.CreateAsync(_validTeamsCode, state.ToString()));
+ }
+
[Theory, BitAutoData]
public async Task CreateAsync_CodeIsEmpty_ThrowsBadRequest(
SutProvider sutProvider,
@@ -315,6 +335,30 @@ public async Task RedirectAsync_IntegrationAlreadyExistsWithNullConfig_Success(
sutProvider.GetDependency().Received(1).GetRedirectUrl(Arg.Any(), expectedState.ToString());
}
+ [Theory, BitAutoData]
+ public async Task RedirectAsync_CallbackUrlIsEmpty_ThrowsBadRequest(
+ SutProvider sutProvider,
+ Guid organizationId,
+ OrganizationIntegration integration)
+ {
+ integration.OrganizationId = organizationId;
+ integration.Configuration = null;
+ integration.Type = IntegrationType.Teams;
+
+ sutProvider.Sut.Url = Substitute.For();
+ sutProvider.Sut.Url
+ .RouteUrl(Arg.Is(c => c.RouteName == "TeamsIntegration_Create"))
+ .Returns((string?)null);
+ sutProvider.GetDependency()
+ .OrganizationOwner(organizationId)
+ .Returns(true);
+ sutProvider.GetDependency()
+ .GetManyByOrganizationAsync(organizationId)
+ .Returns([integration]);
+
+ await Assert.ThrowsAsync(async () => await sutProvider.Sut.RedirectAsync(organizationId));
+ }
+
[Theory, BitAutoData]
public async Task RedirectAsync_IntegrationAlreadyExistsWithConfig_ThrowsBadRequest(
SutProvider sutProvider,
diff --git a/test/Api.Test/AdminConsole/Models/Request/Organizations/OrganizationIntegrationRequestModelTests.cs b/test/Api.Test/AdminConsole/Models/Request/Organizations/OrganizationIntegrationRequestModelTests.cs
index 1303e5fe8969..76e206abf471 100644
--- a/test/Api.Test/AdminConsole/Models/Request/Organizations/OrganizationIntegrationRequestModelTests.cs
+++ b/test/Api.Test/AdminConsole/Models/Request/Organizations/OrganizationIntegrationRequestModelTests.cs
@@ -1,14 +1,47 @@
using System.ComponentModel.DataAnnotations;
using System.Text.Json;
using Bit.Api.AdminConsole.Models.Request.Organizations;
+using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
using Bit.Core.Enums;
+using Bit.Test.Common.AutoFixture.Attributes;
using Xunit;
namespace Bit.Api.Test.AdminConsole.Models.Request.Organizations;
public class OrganizationIntegrationRequestModelTests
{
+ [Fact]
+ public void ToOrganizationIntegration_CreatesNewOrganizationIntegration()
+ {
+ var model = new OrganizationIntegrationRequestModel
+ {
+ Type = IntegrationType.Hec,
+ Configuration = JsonSerializer.Serialize(new HecIntegration(Uri: new Uri("http://localhost"), Scheme: "Bearer", Token: "Token"))
+ };
+
+ var organizationId = Guid.NewGuid();
+ var organizationIntegration = model.ToOrganizationIntegration(organizationId);
+
+ Assert.Equal(organizationIntegration.Type, model.Type);
+ Assert.Equal(organizationIntegration.Configuration, model.Configuration);
+ Assert.Equal(organizationIntegration.OrganizationId, organizationId);
+ }
+
+ [Theory, BitAutoData]
+ public void ToOrganizationIntegration_UpdatesExistingOrganizationIntegration(OrganizationIntegration integration)
+ {
+ var model = new OrganizationIntegrationRequestModel
+ {
+ Type = IntegrationType.Hec,
+ Configuration = JsonSerializer.Serialize(new HecIntegration(Uri: new Uri("http://localhost"), Scheme: "Bearer", Token: "Token"))
+ };
+
+ var organizationIntegration = model.ToOrganizationIntegration(integration);
+
+ Assert.Equal(organizationIntegration.Configuration, model.Configuration);
+ }
+
[Fact]
public void Validate_CloudBillingSync_ReturnsNotYetSupportedError()
{
diff --git a/test/Core.Test/AdminConsole/Models/Data/EventIntegrations/IntegrationTemplateContextTests.cs b/test/Core.Test/AdminConsole/Models/Data/EventIntegrations/IntegrationTemplateContextTests.cs
index 930b04121c34..cdb109e28597 100644
--- a/test/Core.Test/AdminConsole/Models/Data/EventIntegrations/IntegrationTemplateContextTests.cs
+++ b/test/Core.Test/AdminConsole/Models/Data/EventIntegrations/IntegrationTemplateContextTests.cs
@@ -20,6 +20,20 @@ public void EventMessage_ReturnsSerializedJsonOfEvent(EventMessage eventMessage)
Assert.Equal(expected, sut.EventMessage);
}
+ [Theory, BitAutoData]
+ public void DateIso8601_ReturnsIso8601FormattedDate(EventMessage eventMessage)
+ {
+ var testDate = new DateTime(2025, 10, 27, 13, 30, 0, DateTimeKind.Utc);
+ eventMessage.Date = testDate;
+ var sut = new IntegrationTemplateContext(eventMessage);
+
+ var result = sut.DateIso8601;
+
+ Assert.Equal("2025-10-27T13:30:00.0000000Z", result);
+ // Verify it's valid ISO 8601
+ Assert.True(DateTime.TryParse(result, out _));
+ }
+
[Theory, BitAutoData]
public void UserName_WhenUserIsSet_ReturnsName(EventMessage eventMessage, User user)
{
diff --git a/test/Core.Test/AdminConsole/Services/EventIntegrationEventWriteServiceTests.cs b/test/Core.Test/AdminConsole/Services/EventIntegrationEventWriteServiceTests.cs
index 03f9c7764d7a..16df23400421 100644
--- a/test/Core.Test/AdminConsole/Services/EventIntegrationEventWriteServiceTests.cs
+++ b/test/Core.Test/AdminConsole/Services/EventIntegrationEventWriteServiceTests.cs
@@ -38,6 +38,20 @@ await _eventIntegrationPublisher.Received(1).PublishEventAsync(
organizationId: Arg.Is(orgId => eventMessage.OrganizationId.ToString().Equals(orgId)));
}
+ [Fact]
+ public async Task CreateManyAsync_EmptyList_DoesNothing()
+ {
+ await Subject.CreateManyAsync([]);
+ await _eventIntegrationPublisher.DidNotReceiveWithAnyArgs().PublishEventAsync(Arg.Any(), Arg.Any());
+ }
+
+ [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(body);
diff --git a/test/Core.Test/AdminConsole/Services/EventIntegrationHandlerTests.cs b/test/Core.Test/AdminConsole/Services/EventIntegrationHandlerTests.cs
index 89207a9d3add..1d94d58aa5f9 100644
--- a/test/Core.Test/AdminConsole/Services/EventIntegrationHandlerTests.cs
+++ b/test/Core.Test/AdminConsole/Services/EventIntegrationHandlerTests.cs
@@ -120,6 +120,16 @@ public async Task HandleEventAsync_BaseTemplateNoConfigurations_DoesNothing(Even
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)
{
diff --git a/test/Core.Test/AdminConsole/Services/IntegrationFilterServiceTests.cs b/test/Core.Test/AdminConsole/Services/IntegrationFilterServiceTests.cs
index 4143469a4b08..fb33737c16a7 100644
--- a/test/Core.Test/AdminConsole/Services/IntegrationFilterServiceTests.cs
+++ b/test/Core.Test/AdminConsole/Services/IntegrationFilterServiceTests.cs
@@ -42,6 +42,35 @@ public void EvaluateFilterGroup_EqualsUserId_Matches(EventMessage eventMessage)
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(jsonGroup);
+ Assert.NotNull(roundtrippedGroup);
+ Assert.True(_service.EvaluateFilterGroup(roundtrippedGroup, eventMessage));
+ }
+
[Theory, BitAutoData]
public void EvaluateFilterGroup_EqualsUserId_DoesNotMatch(EventMessage eventMessage)
{
@@ -281,6 +310,45 @@ public void EvaluateFilterGroup_NestedGroups_AllMatch(EventMessage eventMessage)
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(jsonGroup);
+ Assert.NotNull(roundtrippedGroup);
+ Assert.True(_service.EvaluateFilterGroup(roundtrippedGroup, eventMessage));
+ }
+
[Theory, BitAutoData]
public void EvaluateFilterGroup_UnknownProperty_ReturnsFalse(EventMessage eventMessage)
{
diff --git a/test/Core.Test/AdminConsole/Services/SlackIntegrationHandlerTests.cs b/test/Core.Test/AdminConsole/Services/SlackIntegrationHandlerTests.cs
index dab6c41b616b..e2e459ceb390 100644
--- a/test/Core.Test/AdminConsole/Services/SlackIntegrationHandlerTests.cs
+++ b/test/Core.Test/AdminConsole/Services/SlackIntegrationHandlerTests.cs
@@ -1,4 +1,5 @@
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;
@@ -28,6 +29,9 @@ public async Task HandleAsync_SuccessfulRequest_ReturnsSuccess(IntegrationMessag
var sutProvider = GetSutProvider();
message.Configuration = new SlackIntegrationConfigurationDetails(_channelId, _token);
+ _slackService.SendSlackMessageByChannelIdAsync(Arg.Any(), Arg.Any(), Arg.Any())
+ .Returns(new SlackSendMessageResponse() { Ok = true, Channel = _channelId });
+
var result = await sutProvider.Sut.HandleAsync(message);
Assert.True(result.Success);
@@ -39,4 +43,97 @@ await sutProvider.GetDependency().Received(1).SendSlackMessageByC
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()
+ {
+ Configuration = new SlackIntegrationConfigurationDetails(_channelId, _token),
+ MessageId = "MessageId",
+ RenderedTemplate = "Test Message"
+ };
+
+ _slackService.SendSlackMessageByChannelIdAsync(Arg.Any(), Arg.Any(), Arg.Any())
+ .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().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()
+ {
+ Configuration = new SlackIntegrationConfigurationDetails(_channelId, _token),
+ MessageId = "MessageId",
+ RenderedTemplate = "Test Message"
+ };
+
+ _slackService.SendSlackMessageByChannelIdAsync(Arg.Any(), Arg.Any(), Arg.Any())
+ .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().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_ReturnsNonRetryableFailure()
+ {
+ var sutProvider = GetSutProvider();
+ var message = new IntegrationMessage()
+ {
+ Configuration = new SlackIntegrationConfigurationDetails(_channelId, _token),
+ MessageId = "MessageId",
+ RenderedTemplate = "Test Message"
+ };
+
+ _slackService.SendSlackMessageByChannelIdAsync(Arg.Any(), Arg.Any(), Arg.Any())
+ .Returns((SlackSendMessageResponse?)null);
+
+ var result = await sutProvider.Sut.HandleAsync(message);
+
+ Assert.False(result.Success);
+ Assert.False(result.Retryable);
+ Assert.Equal("Slack response was null", result.FailureReason);
+ Assert.Equal(result.Message, message);
+
+ await sutProvider.GetDependency().Received(1).SendSlackMessageByChannelIdAsync(
+ Arg.Is(AssertHelper.AssertPropertyEqual(_token)),
+ Arg.Is(AssertHelper.AssertPropertyEqual(message.RenderedTemplate)),
+ Arg.Is(AssertHelper.AssertPropertyEqual(_channelId))
+ );
+ }
}
diff --git a/test/Core.Test/AdminConsole/Services/SlackServiceTests.cs b/test/Core.Test/AdminConsole/Services/SlackServiceTests.cs
index 48dd9c490e24..068e5e8c8238 100644
--- a/test/Core.Test/AdminConsole/Services/SlackServiceTests.cs
+++ b/test/Core.Test/AdminConsole/Services/SlackServiceTests.cs
@@ -146,6 +146,27 @@ public async Task GetChannelIdsAsync_NoChannelsFound_ReturnsEmptyResult()
Assert.Empty(result);
}
+ [Fact]
+ public async Task GetChannelIdAsync_NoChannelFound_ReturnsEmptyResult()
+ {
+ var emptyResponse = JsonSerializer.Serialize(
+ new
+ {
+ ok = true,
+ channels = Array.Empty(),
+ 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()
{
@@ -235,6 +256,32 @@ public async Task GetDmChannelByEmailAsync_ApiErrorDmResponse_ReturnsEmptyString
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()
{
@@ -244,7 +291,7 @@ public async Task GetDmChannelByEmailAsync_ApiErrorUserResponse_ReturnsEmptyStri
var userResponse = new
{
ok = false,
- error = "An error occured"
+ error = "An error occurred"
};
_handler.When($"https://slack.com/api/users.lookupByEmail?email={email}")
@@ -256,6 +303,21 @@ public async Task GetDmChannelByEmailAsync_ApiErrorUserResponse_ReturnsEmptyStri
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()
{
@@ -341,18 +403,29 @@ public async Task ObtainTokenViaOAuth_ReturnsEmptyString_WhenHttpCallFails()
}
[Fact]
- public async Task SendSlackMessageByChannelId_Sends_Correct_Message()
+ 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(string.Empty));
+ .WithContent(new StringContent(jsonResponse));
- await sutProvider.Sut.SendSlackMessageByChannelIdAsync(_token, message, channelId);
+ 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);
@@ -365,4 +438,62 @@ public async Task SendSlackMessageByChannelId_Sends_Correct_Message()
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);
+ }
}