mirror of
https://github.com/bitwarden/server
synced 2026-01-08 19:43:34 +00:00
Extend Unit Test Coverage of Event Integrations (#6517)
* Extend Unit Test Coverage of Event Integrations * Expanded SlackService error handling and tests * Cleaned up a few issues noted by Claude
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
namespace Bit.Core.Services;
|
||||
using Bit.Core.Models.Slack;
|
||||
|
||||
namespace Bit.Core.Services;
|
||||
|
||||
/// <summary>Defines operations for interacting with Slack, including OAuth authentication, channel discovery,
|
||||
/// and sending messages.</summary>
|
||||
@@ -54,6 +56,6 @@ public interface ISlackService
|
||||
/// <param name="token">A valid Slack OAuth access token.</param>
|
||||
/// <param name="message">The message text to send.</param>
|
||||
/// <param name="channelId">The channel ID to send the message to.</param>
|
||||
/// <returns>A task that completes when the message has been sent.</returns>
|
||||
Task SendSlackMessageByChannelIdAsync(string token, string message, string channelId);
|
||||
/// <returns>The response from Slack after sending the message.</returns>
|
||||
Task<SlackSendMessageResponse?> SendSlackMessageByChannelIdAsync(string token, string message, string channelId);
|
||||
}
|
||||
|
||||
@@ -6,14 +6,43 @@ public class SlackIntegrationHandler(
|
||||
ISlackService slackService)
|
||||
: IntegrationHandlerBase<SlackIntegrationConfigurationDetails>
|
||||
{
|
||||
private static readonly HashSet<string> _retryableErrors = new(StringComparer.Ordinal)
|
||||
{
|
||||
"internal_error",
|
||||
"message_limit_exceeded",
|
||||
"rate_limited",
|
||||
"ratelimited",
|
||||
"service_unavailable"
|
||||
};
|
||||
|
||||
public override async Task<IntegrationHandlerResult> HandleAsync(IntegrationMessage<SlackIntegrationConfigurationDetails> 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 class SlackService(
|
||||
public async Task<string> 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 class SlackService(
|
||||
}
|
||||
|
||||
var tokenResponse = await _httpClient.PostAsync($"{_slackApiBaseUrl}/oauth.v2.access",
|
||||
new FormUrlEncodedContent(new[]
|
||||
{
|
||||
new FormUrlEncodedContent([
|
||||
new KeyValuePair<string, string>("client_id", _clientId),
|
||||
new KeyValuePair<string, string>("client_secret", _clientSecret),
|
||||
new KeyValuePair<string, string>("code", code),
|
||||
new KeyValuePair<string, string>("redirect_uri", redirectUrl)
|
||||
}));
|
||||
]));
|
||||
|
||||
SlackOAuthResponse? result;
|
||||
try
|
||||
{
|
||||
result = await tokenResponse.Content.ReadFromJsonAsync<SlackOAuthResponse>();
|
||||
}
|
||||
catch
|
||||
catch (JsonException ex)
|
||||
{
|
||||
logger.LogError(ex, "Error parsing SlackOAuthResponse: invalid JSON");
|
||||
result = null;
|
||||
}
|
||||
|
||||
@@ -129,14 +130,25 @@ public class SlackService(
|
||||
return result.AccessToken;
|
||||
}
|
||||
|
||||
public async Task SendSlackMessageByChannelIdAsync(string token, string message, string channelId)
|
||||
public async Task<SlackSendMessageResponse?> 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<SlackSendMessageResponse>();
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
logger.LogError(ex, "Error parsing Slack message response: invalid JSON");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<string> GetUserIdByEmailAsync(string token, string email)
|
||||
@@ -144,7 +156,16 @@ public class SlackService(
|
||||
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>();
|
||||
SlackUserResponse? result;
|
||||
try
|
||||
{
|
||||
result = await response.Content.ReadFromJsonAsync<SlackUserResponse>();
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
logger.LogError(ex, "Error parsing SlackUserResponse: invalid JSON");
|
||||
result = null;
|
||||
}
|
||||
|
||||
if (result is null)
|
||||
{
|
||||
@@ -160,7 +181,7 @@ public class SlackService(
|
||||
return result.User.Id;
|
||||
}
|
||||
|
||||
private async Task<string> OpenDmChannel(string token, string userId)
|
||||
private async Task<string> OpenDmChannelAsync(string token, string userId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(userId))
|
||||
return string.Empty;
|
||||
@@ -170,7 +191,16 @@ public class SlackService(
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
||||
request.Content = payload;
|
||||
var response = await _httpClient.SendAsync(request);
|
||||
var result = await response.Content.ReadFromJsonAsync<SlackDmResponse>();
|
||||
SlackDmResponse? result;
|
||||
try
|
||||
{
|
||||
result = await response.Content.ReadFromJsonAsync<SlackDmResponse>();
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
logger.LogError(ex, "Error parsing SlackDmResponse: invalid JSON");
|
||||
result = null;
|
||||
}
|
||||
|
||||
if (result is null)
|
||||
{
|
||||
|
||||
@@ -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 class NoopSlackService : ISlackService
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
public Task SendSlackMessageByChannelIdAsync(string token, string message, string channelId)
|
||||
public Task<SlackSendMessageResponse?> SendSlackMessageByChannelIdAsync(string token, string message,
|
||||
string channelId)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
return Task.FromResult<SlackSendMessageResponse?>(null);
|
||||
}
|
||||
|
||||
public Task<string> ObtainTokenViaOAuth(string code, string redirectUrl)
|
||||
|
||||
Reference in New Issue
Block a user