Skip to content

Commit

Permalink
feat: Backend slack integration (#14168)
Browse files Browse the repository at this point in the history
  • Loading branch information
nkylstad authored Nov 26, 2024
1 parent ab1a970 commit a629a0e
Show file tree
Hide file tree
Showing 11 changed files with 318 additions and 0 deletions.
23 changes: 23 additions & 0 deletions backend/src/Designer/Configuration/FeedbackFormSettings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using Altinn.Studio.Designer.Configuration.Marker;

namespace Altinn.Studio.Designer.Configuration
{
/// <summary>
/// Class representation for basic FeedbackForm configuration
/// </summary>
public class FeedbackFormSettings : ISettingsMarker
{
/// <summary>
/// Gets or sets the Slack settings
/// </summary>
public SlackSettings SlackSettings { get; set; }
}

public class SlackSettings
{
/// <summary>
/// Gets or sets the WebhookUrl
/// </summary>
public string WebhookUrl { get; set; }
}
}
75 changes: 75 additions & 0 deletions backend/src/Designer/Controllers/FeedbackFormController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Altinn.Studio.Designer.Configuration;
using Altinn.Studio.Designer.Models.Dto;
using Altinn.Studio.Designer.TypedHttpClients.Slack;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;

namespace Altinn.Studio.Designer.Controllers;

/// <summary>
/// Controller containing actions related to feedback form
/// </summary>
[Authorize]
[ApiController]
[ValidateAntiForgeryToken]
[Route("designer/api/{org}/{app:regex(^(?!datamodels$)[[a-z]][[a-z0-9-]]{{1,28}}[[a-z0-9]]$)}/feedbackform")]
public class FeedbackFormController : ControllerBase
{
private readonly ISlackClient _slackClient;
private readonly GeneralSettings _generalSettings;

/// <summary>
/// Initializes a new instance of the <see cref="FeedbackFormController"/> class.
/// </summary>
/// <param name="slackClient">A http client to send messages to slack</param>
/// <param name="generalSettings">the general settings</param>
public FeedbackFormController(ISlackClient slackClient, GeneralSettings generalSettings)
{
_slackClient = slackClient;
_generalSettings = generalSettings;
}

/// <summary>
/// Endpoint for submitting feedback
/// </summary>
[HttpPost]
[Route("submit")]
public async Task<IActionResult> SubmitFeedback([FromRoute] string org, [FromRoute] string app, [FromBody] FeedbackForm feedback, CancellationToken cancellationToken)
{
if (feedback == null)
{
return BadRequest("Feedback object is null");
}

if (feedback.Answers == null || feedback.Answers.Count == 0)
{
return BadRequest("Feedback answers are null or empty");
}

if (!feedback.Answers.ContainsKey("org"))
{
feedback.Answers.Add("org", org);
}

if (!feedback.Answers.ContainsKey("app"))
{
feedback.Answers.Add("app", app);
}

if (!feedback.Answers.ContainsKey("env"))
{
feedback.Answers.Add("env", _generalSettings.HostName);
}

await _slackClient.SendMessage(new SlackRequest
{
Text = JsonSerializer.Serialize(feedback.Answers, new JsonSerializerOptions { WriteIndented = true })
}, cancellationToken);

return Ok();
}
}
14 changes: 14 additions & 0 deletions backend/src/Designer/Models/Dto/FeedbackForm.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;

namespace Altinn.Studio.Designer.Models.Dto
{
/// <summary>
/// Represents a feedback form
/// </summary>
public class FeedbackForm
{
[JsonPropertyName("answers")]
public Dictionary<string, string> Answers { get; set; }
}
}
10 changes: 10 additions & 0 deletions backend/src/Designer/TypedHttpClients/Slack/ISlackClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using System.Threading;
using System.Threading.Tasks;

namespace Altinn.Studio.Designer.TypedHttpClients.Slack
{
public interface ISlackClient
{
public Task SendMessage(SlackRequest request, CancellationToken cancellationToken = default);
}
}
32 changes: 32 additions & 0 deletions backend/src/Designer/TypedHttpClients/Slack/SlackClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using System.Net.Http;
using System.Net.Mime;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

namespace Altinn.Studio.Designer.TypedHttpClients.Slack;

public class SlackClient : ISlackClient
{
private readonly HttpClient _httpClient;
private readonly JsonSerializerOptions _jsonSerializerOptions = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};

public SlackClient(HttpClient httpClient)
{
_httpClient = httpClient;
}

public async Task SendMessage(SlackRequest request, CancellationToken cancellationToken = default)
{
using var payloadContent = new StringContent(JsonSerializer.Serialize(request, _jsonSerializerOptions),
Encoding.UTF8,
MediaTypeNames.Application.Json);

using var response = await _httpClient.PostAsync(string.Empty, payloadContent, cancellationToken);
response.EnsureSuccessStatusCode();
}
}
7 changes: 7 additions & 0 deletions backend/src/Designer/TypedHttpClients/Slack/SlackRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Altinn.Studio.Designer.TypedHttpClients.Slack
{
public class SlackRequest
{
public string Text { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
using Altinn.Studio.Designer.TypedHttpClients.KubernetesWrapper;
using Altinn.Studio.Designer.TypedHttpClients.MaskinPorten;
using Altinn.Studio.Designer.TypedHttpClients.ResourceRegistryOptions;
using Altinn.Studio.Designer.TypedHttpClients.Slack;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
Expand Down Expand Up @@ -55,6 +56,7 @@ public static IServiceCollection RegisterTypedHttpClients(this IServiceCollectio
services.AddEidLoggerTypedHttpClient(config);
services.AddTransient<GiteaTokenDelegatingHandler>();
services.AddMaskinportenHttpClient();
services.AddSlackClient(config);

return services;
}
Expand Down Expand Up @@ -135,5 +137,17 @@ private static IHttpClientBuilder AddMaskinportenHttpClient(this IServiceCollect
.AddHttpMessageHandler<AnsattPortenTokenDelegatingHandler>();

}

private static IHttpClientBuilder AddSlackClient(this IServiceCollection services, IConfiguration config)
{
FeedbackFormSettings feedbackFormSettings = config.GetSection("FeedbackFormSettings").Get<FeedbackFormSettings>();
string token = config["FeedbackFormSlackToken"];
return services.AddHttpClient<ISlackClient, SlackClient>(client =>
{
client.BaseAddress = new Uri(feedbackFormSettings.SlackSettings.WebhookUrl + config["FeedbackFormSlackWebhookSecret"]);
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", token);
}).AddHttpMessageHandler<EnsureSuccessHandler>();
}
}
}
5 changes: 5 additions & 0 deletions backend/src/Designer/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -130,5 +130,10 @@
},
"MaskinPortenHttpClientSettings" : {
"BaseUrl": "https://api.test.samarbeid.digdir.no"
},
"FeedbackFormSettings": {
"SlackSettings": {
"WebhookUrl": "https://hooks.slack.com/services/"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@

using Designer.Tests.Controllers.ApiTests;
using Designer.Tests.Fixtures;
using Microsoft.AspNetCore.Mvc.Testing;

namespace Designer.Tests.Controllers.FeedbackFormController.Base;

public class FeedbackFormControllerTestBase<TControllerTest> : DesignerEndpointsTestsBase<TControllerTest>
where TControllerTest : class
{
public FeedbackFormControllerTestBase(WebApplicationFactory<Program> factory) : base(factory)
{
JsonConfigOverrides.Add(
$$"""
{
"GeneralSettings": {
"HostName": "TestHostName"
}
}
""");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@

using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Net.Http.Json;
using System.Threading.Tasks;
using Altinn.Studio.Designer.Models.Dto;
using Designer.Tests.Controllers.FeedbackFormController.Base;
using Designer.Tests.Controllers.FeedbackFormController.Utils;
using Designer.Tests.Fixtures;
using FluentAssertions;
using Microsoft.AspNetCore.Mvc.Testing;
using Xunit;

namespace Designer.Tests.Controllers.FeedbackFormController;

public class SendMessageToSlackTests : FeedbackFormControllerTestBase<SendMessageToSlackTests>, IClassFixture<WebApplicationFactory<Program>>, IClassFixture<MockServerFixture>
{
private static string VersionPrefix(string org, string repository) =>
$"/designer/api/{org}/{repository}/feedbackform/submit";

private readonly MockServerFixture _mockServerFixture;

public SendMessageToSlackTests(WebApplicationFactory<Program> factory, MockServerFixture mockServerFixture) : base(factory)
{
_mockServerFixture = mockServerFixture;
JsonConfigOverrides.Add(
$$"""
{
"FeedbackFormSettings" : {
"SlackSettings": {
"WebhookUrl": "{{mockServerFixture.MockApi.Url}}"
}
}
}
"""
);
}

[Fact]
public async Task SendMessageToSlack_Should_ReturnOk()
{
_mockServerFixture.PrepareSlackResponse(_mockServerFixture.MockApi.Url);
var mockAnswers = new FeedbackForm
{
Answers = new Dictionary<string, string>
{
{ "message", "test" }
}
};
using var httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, VersionPrefix("ttd", "non-existing-app"))
{
Content = JsonContent.Create(mockAnswers)
};


using var response = await HttpClient.SendAsync(httpRequestMessage);
response.StatusCode.Should().Be(HttpStatusCode.OK);
}

[Fact]
public async Task SendMessageToSlack_NullAnswers_Should_ReturnBadRequest()
{
object mockAnswers = null;
using var httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, VersionPrefix("ttd", "non-existing-app"))
{
Content = JsonContent.Create(mockAnswers)
};

using var response = await HttpClient.SendAsync(httpRequestMessage);
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
}

[Fact]
public async Task SendMessageToSlack_WithMissingAnswers_Should_ReturnBadRequest()
{
var mockAnswers = new FeedbackForm
{
Answers = null
};
using var httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, VersionPrefix("ttd", "non-existing-app"))
{
Content = JsonContent.Create(mockAnswers)
};

using var response = await HttpClient.SendAsync(httpRequestMessage);
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using System.Net.Mime;
using Designer.Tests.Fixtures;
using WireMock.RequestBuilders;
using WireMock.ResponseBuilders;

namespace Designer.Tests.Controllers.FeedbackFormController.Utils;

public static class SlackMockServerExtensions
{
public static void PrepareSlackResponse(this MockServerFixture mockServerFixture, string path)
{
var request = Request.Create()
.UsingPost()
.WithPath("/");

var response = Response.Create()
.WithStatusCode(200)
.WithHeader("content-type", MediaTypeNames.Application.Json);

mockServerFixture.MockApi.Given(request)
.RespondWith(
response
);

}

}

0 comments on commit a629a0e

Please sign in to comment.