mirror of
https://github.com/bitwarden/server.git
synced 2025-02-01 23:31:41 +01:00
[EC-307] Fresh desk custom fields integration (#2114)
* Using correct ILogger on FreshdeskController * Submitting custom fields to Freshdesk * Set up FreshdeskController to use IHttpClientFactory * Added unit test for FreshdeskController * Moved ControllerCustomizeAttribute and ControllerCustomization to Common * Modified FreshdeskController to use FreshdeskWebhookModel; Edited unit tests to use AutoFixture
This commit is contained in:
parent
448e255fb6
commit
6e19bfeb22
@ -1,7 +1,7 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Bit.Billing.Models;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
@ -17,18 +17,18 @@ namespace Bit.Billing.Controllers
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
private readonly ILogger<AppleController> _logger;
|
||||
private readonly ILogger<FreshdeskController> _logger;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private readonly HttpClient _httpClient = new HttpClient();
|
||||
private readonly string _freshdeskAuthkey;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
|
||||
public FreshdeskController(
|
||||
IUserRepository userRepository,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IOptions<BillingSettings> billingSettings,
|
||||
ILogger<AppleController> logger,
|
||||
GlobalSettings globalSettings)
|
||||
ILogger<FreshdeskController> logger,
|
||||
GlobalSettings globalSettings,
|
||||
IHttpClientFactory httpClientFactory)
|
||||
{
|
||||
_billingSettings = billingSettings?.Value;
|
||||
_userRepository = userRepository;
|
||||
@ -36,37 +36,23 @@ namespace Bit.Billing.Controllers
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
_logger = logger;
|
||||
_globalSettings = globalSettings;
|
||||
_freshdeskAuthkey = Convert.ToBase64String(
|
||||
Encoding.UTF8.GetBytes($"{_billingSettings.FreshdeskApiKey}:X"));
|
||||
_httpClientFactory = httpClientFactory;
|
||||
}
|
||||
|
||||
[HttpPost("webhook")]
|
||||
public async Task<IActionResult> PostWebhook()
|
||||
public async Task<IActionResult> PostWebhook([FromQuery, Required] string key,
|
||||
[FromBody, Required] FreshdeskWebhookModel model)
|
||||
{
|
||||
if (HttpContext?.Request?.Query == null)
|
||||
{
|
||||
return new BadRequestResult();
|
||||
}
|
||||
|
||||
var key = HttpContext.Request.Query.ContainsKey("key") ?
|
||||
HttpContext.Request.Query["key"].ToString() : null;
|
||||
if (!CoreHelpers.FixedTimeEquals(key, _billingSettings.FreshdeskWebhookKey))
|
||||
{
|
||||
return new BadRequestResult();
|
||||
}
|
||||
|
||||
using var body = await JsonSerializer.DeserializeAsync<JsonDocument>(HttpContext.Request.Body);
|
||||
var root = body.RootElement;
|
||||
if (root.ValueKind != JsonValueKind.Object)
|
||||
if (string.IsNullOrWhiteSpace(key) || !CoreHelpers.FixedTimeEquals(key, _billingSettings.FreshdeskWebhookKey))
|
||||
{
|
||||
return new BadRequestResult();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var ticketId = root.GetProperty("ticket_id").GetString();
|
||||
var ticketContactEmail = root.GetProperty("ticket_contact_email").GetString();
|
||||
var ticketTags = root.GetProperty("ticket_tags").GetString();
|
||||
var ticketId = model.TicketId;
|
||||
var ticketContactEmail = model.TicketContactEmail;
|
||||
var ticketTags = model.TicketTags;
|
||||
if (string.IsNullOrWhiteSpace(ticketId) || string.IsNullOrWhiteSpace(ticketContactEmail))
|
||||
{
|
||||
return new BadRequestResult();
|
||||
@ -74,20 +60,34 @@ namespace Bit.Billing.Controllers
|
||||
|
||||
var updateBody = new Dictionary<string, object>();
|
||||
var note = string.Empty;
|
||||
var customFields = new Dictionary<string, object>();
|
||||
var user = await _userRepository.GetByEmailAsync(ticketContactEmail);
|
||||
if (user != null)
|
||||
{
|
||||
note += $"<li>User, {user.Email}: {_globalSettings.BaseServiceUri.Admin}/users/edit/{user.Id}</li>";
|
||||
var userLink = $"{_globalSettings.BaseServiceUri.Admin}/users/edit/{user.Id}";
|
||||
note += $"<li>User, {user.Email}: {userLink}</li>";
|
||||
customFields.Add("cf_user", userLink);
|
||||
var tags = new HashSet<string>();
|
||||
if (user.Premium)
|
||||
{
|
||||
tags.Add("Premium");
|
||||
}
|
||||
var orgs = await _organizationRepository.GetManyByUserIdAsync(user.Id);
|
||||
|
||||
foreach (var org in orgs)
|
||||
{
|
||||
note += $"<li>Org, {org.Name} ({org.Seats.GetValueOrDefault()}): " +
|
||||
$"{_globalSettings.BaseServiceUri.Admin}/organizations/edit/{org.Id}</li>";
|
||||
var orgNote = $"{org.Name} ({org.Seats.GetValueOrDefault()}): " +
|
||||
$"{_globalSettings.BaseServiceUri.Admin}/organizations/edit/{org.Id}";
|
||||
note += $"<li>Org, {orgNote}</li>";
|
||||
if (!customFields.Any(kvp => kvp.Key == "cf_org"))
|
||||
{
|
||||
customFields.Add("cf_org", orgNote);
|
||||
}
|
||||
else
|
||||
{
|
||||
customFields["cf_org"] += $"\n{orgNote}";
|
||||
}
|
||||
|
||||
var planName = GetAttribute<DisplayAttribute>(org.PlanType).Name.Split(" ").FirstOrDefault();
|
||||
if (!string.IsNullOrWhiteSpace(planName))
|
||||
{
|
||||
@ -107,15 +107,18 @@ namespace Bit.Billing.Controllers
|
||||
}
|
||||
updateBody.Add("tags", tagsToUpdate);
|
||||
}
|
||||
|
||||
if (customFields.Any())
|
||||
{
|
||||
updateBody.Add("custom_fields", customFields);
|
||||
}
|
||||
var updateRequest = new HttpRequestMessage(HttpMethod.Put,
|
||||
string.Format("https://bitwarden.freshdesk.com/api/v2/tickets/{0}", ticketId))
|
||||
{
|
||||
Content = JsonContent.Create(updateBody),
|
||||
};
|
||||
|
||||
await CallFreshdeskApiAsync(updateRequest);
|
||||
|
||||
|
||||
var noteBody = new Dictionary<string, object>
|
||||
{
|
||||
{ "body", $"<ul>{note}</ul>" },
|
||||
@ -142,8 +145,10 @@ namespace Bit.Billing.Controllers
|
||||
{
|
||||
try
|
||||
{
|
||||
request.Headers.Add("Authorization", _freshdeskAuthkey);
|
||||
var response = await _httpClient.SendAsync(request);
|
||||
var freshdeskAuthkey = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{_billingSettings.FreshdeskApiKey}:X"));
|
||||
var httpClient = _httpClientFactory.CreateClient("FreshdeskApi");
|
||||
request.Headers.Add("Authorization", freshdeskAuthkey);
|
||||
var response = await httpClient.SendAsync(request);
|
||||
if (response.StatusCode != System.Net.HttpStatusCode.TooManyRequests || retriedCount > 3)
|
||||
{
|
||||
return response;
|
||||
|
16
src/Billing/Models/FreshdeskWebhookModel.cs
Normal file
16
src/Billing/Models/FreshdeskWebhookModel.cs
Normal file
@ -0,0 +1,16 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Bit.Billing.Models
|
||||
{
|
||||
public class FreshdeskWebhookModel
|
||||
{
|
||||
[JsonPropertyName("ticket_id")]
|
||||
public string TicketId { get; set; }
|
||||
|
||||
[JsonPropertyName("ticket_contact_email")]
|
||||
public string TicketContactEmail { get; set; }
|
||||
|
||||
[JsonPropertyName("ticket_tags")]
|
||||
public string TicketTags { get; set; }
|
||||
}
|
||||
}
|
@ -68,6 +68,9 @@ namespace Bit.Billing
|
||||
// Jobs service, uncomment when we have some jobs to run
|
||||
// Jobs.JobsHostedService.AddJobsServices(services);
|
||||
// services.AddHostedService<Jobs.JobsHostedService>();
|
||||
|
||||
// Set up HttpClients
|
||||
services.AddHttpClient("FreshdeskApi");
|
||||
}
|
||||
|
||||
public void Configure(
|
||||
|
@ -1,6 +1,5 @@
|
||||
using Bit.Api.Controllers;
|
||||
using Bit.Api.Models.Request;
|
||||
using Bit.Api.Test.AutoFixture.Attributes;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
|
@ -2,7 +2,6 @@
|
||||
using Bit.Api.Controllers;
|
||||
using Bit.Api.Models.Request.Organizations;
|
||||
using Bit.Api.Models.Response.Organizations;
|
||||
using Bit.Api.Test.AutoFixture.Attributes;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
|
@ -1,6 +1,5 @@
|
||||
using Bit.Api.Controllers;
|
||||
using Bit.Api.Models.Request.Organizations;
|
||||
using Bit.Api.Test.AutoFixture.Attributes;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
|
@ -1,6 +1,5 @@
|
||||
using Bit.Api.Controllers;
|
||||
using Bit.Api.Models.Request.Organizations;
|
||||
using Bit.Api.Test.AutoFixture.Attributes;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.Repositories;
|
||||
|
80
test/Billing.Test/Controllers/FreshdeskControllerTests.cs
Normal file
80
test/Billing.Test/Controllers/FreshdeskControllerTests.cs
Normal file
@ -0,0 +1,80 @@
|
||||
using Bit.Billing.Controllers;
|
||||
using Bit.Billing.Models;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Billing.Test.Controllers
|
||||
{
|
||||
[ControllerCustomize(typeof(FreshdeskController))]
|
||||
[SutProviderCustomize]
|
||||
public class FreshdeskControllerTests
|
||||
{
|
||||
private const string ApiKey = "TESTFRESHDESKAPIKEY";
|
||||
private const string WebhookKey = "TESTKEY";
|
||||
|
||||
[Theory]
|
||||
[BitAutoData((string)null, null)]
|
||||
[BitAutoData((string)null)]
|
||||
[BitAutoData(WebhookKey, null)]
|
||||
public async Task PostWebhook_NullRequiredParameters_BadRequest(string freshdeskWebhookKey, FreshdeskWebhookModel model,
|
||||
BillingSettings billingSettings, SutProvider<FreshdeskController> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<IOptions<BillingSettings>>().Value.FreshdeskWebhookKey.Returns(billingSettings.FreshdeskWebhookKey);
|
||||
|
||||
var response = await sutProvider.Sut.PostWebhook(freshdeskWebhookKey, model);
|
||||
|
||||
var statusCodeResult = Assert.IsAssignableFrom<StatusCodeResult>(response);
|
||||
Assert.Equal(StatusCodes.Status400BadRequest, statusCodeResult.StatusCode);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PostWebhook_Success(User user, FreshdeskWebhookModel model,
|
||||
List<Organization> organizations, SutProvider<FreshdeskController> sutProvider)
|
||||
{
|
||||
model.TicketContactEmail = user.Email;
|
||||
|
||||
sutProvider.GetDependency<IUserRepository>().GetByEmailAsync(user.Email).Returns(user);
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetManyByUserIdAsync(user.Id).Returns(organizations);
|
||||
|
||||
var mockHttpMessageHandler = Substitute.ForPartsOf<MockHttpMessageHandler>();
|
||||
var mockResponse = new HttpResponseMessage(System.Net.HttpStatusCode.OK);
|
||||
mockHttpMessageHandler.Send(Arg.Any<HttpRequestMessage>(), Arg.Any<CancellationToken>())
|
||||
.Returns(mockResponse);
|
||||
var httpClient = new HttpClient(mockHttpMessageHandler);
|
||||
|
||||
sutProvider.GetDependency<IHttpClientFactory>().CreateClient("FreshdeskApi").Returns(httpClient);
|
||||
|
||||
sutProvider.GetDependency<IOptions<BillingSettings>>().Value.FreshdeskWebhookKey.Returns(WebhookKey);
|
||||
sutProvider.GetDependency<IOptions<BillingSettings>>().Value.FreshdeskApiKey.Returns(ApiKey);
|
||||
|
||||
var response = await sutProvider.Sut.PostWebhook(WebhookKey, model);
|
||||
|
||||
var statusCodeResult = Assert.IsAssignableFrom<StatusCodeResult>(response);
|
||||
Assert.Equal(StatusCodes.Status200OK, statusCodeResult.StatusCode);
|
||||
|
||||
_ = mockHttpMessageHandler.Received(1).Send(Arg.Is<HttpRequestMessage>(m => m.Method == HttpMethod.Put && m.RequestUri.ToString().EndsWith(model.TicketId)), Arg.Any<CancellationToken>());
|
||||
_ = mockHttpMessageHandler.Received(1).Send(Arg.Is<HttpRequestMessage>(m => m.Method == HttpMethod.Post && m.RequestUri.ToString().EndsWith($"{model.TicketId}/notes")), Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
public class MockHttpMessageHandler : HttpMessageHandler
|
||||
{
|
||||
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
{
|
||||
return Send(request, cancellationToken);
|
||||
}
|
||||
|
||||
public virtual Task<HttpResponseMessage> Send(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,7 +1,6 @@
|
||||
using AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
|
||||
namespace Bit.Api.Test.AutoFixture.Attributes
|
||||
namespace Bit.Test.Common.AutoFixture.Attributes
|
||||
{
|
||||
/// <summary>
|
||||
/// Disables setting of Auto Properties on the Controller to avoid ASP.net initialization errors from a mock environment. Still sets constructor dependencies.
|
@ -1,9 +1,8 @@
|
||||
using AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Org.BouncyCastle.Security;
|
||||
|
||||
namespace Bit.Api.Test.AutoFixture
|
||||
namespace Bit.Test.Common.AutoFixture
|
||||
{
|
||||
/// <summary>
|
||||
/// Disables setting of Auto Properties on the Controller to avoid ASP.net initialization errors. Still sets constructor dependencies.
|
Loading…
Reference in New Issue
Block a user