mirror of
https://github.com/bitwarden/server.git
synced 2025-05-25 13:24:50 -05: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.ComponentModel.DataAnnotations;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using Bit.Billing.Models;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
@ -17,18 +17,18 @@ namespace Bit.Billing.Controllers
|
|||||||
private readonly IUserRepository _userRepository;
|
private readonly IUserRepository _userRepository;
|
||||||
private readonly IOrganizationRepository _organizationRepository;
|
private readonly IOrganizationRepository _organizationRepository;
|
||||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||||
private readonly ILogger<AppleController> _logger;
|
private readonly ILogger<FreshdeskController> _logger;
|
||||||
private readonly GlobalSettings _globalSettings;
|
private readonly GlobalSettings _globalSettings;
|
||||||
private readonly HttpClient _httpClient = new HttpClient();
|
private readonly IHttpClientFactory _httpClientFactory;
|
||||||
private readonly string _freshdeskAuthkey;
|
|
||||||
|
|
||||||
public FreshdeskController(
|
public FreshdeskController(
|
||||||
IUserRepository userRepository,
|
IUserRepository userRepository,
|
||||||
IOrganizationRepository organizationRepository,
|
IOrganizationRepository organizationRepository,
|
||||||
IOrganizationUserRepository organizationUserRepository,
|
IOrganizationUserRepository organizationUserRepository,
|
||||||
IOptions<BillingSettings> billingSettings,
|
IOptions<BillingSettings> billingSettings,
|
||||||
ILogger<AppleController> logger,
|
ILogger<FreshdeskController> logger,
|
||||||
GlobalSettings globalSettings)
|
GlobalSettings globalSettings,
|
||||||
|
IHttpClientFactory httpClientFactory)
|
||||||
{
|
{
|
||||||
_billingSettings = billingSettings?.Value;
|
_billingSettings = billingSettings?.Value;
|
||||||
_userRepository = userRepository;
|
_userRepository = userRepository;
|
||||||
@ -36,37 +36,23 @@ namespace Bit.Billing.Controllers
|
|||||||
_organizationUserRepository = organizationUserRepository;
|
_organizationUserRepository = organizationUserRepository;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_globalSettings = globalSettings;
|
_globalSettings = globalSettings;
|
||||||
_freshdeskAuthkey = Convert.ToBase64String(
|
_httpClientFactory = httpClientFactory;
|
||||||
Encoding.UTF8.GetBytes($"{_billingSettings.FreshdeskApiKey}:X"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("webhook")]
|
[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)
|
if (string.IsNullOrWhiteSpace(key) || !CoreHelpers.FixedTimeEquals(key, _billingSettings.FreshdeskWebhookKey))
|
||||||
{
|
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
return new BadRequestResult();
|
return new BadRequestResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var ticketId = root.GetProperty("ticket_id").GetString();
|
var ticketId = model.TicketId;
|
||||||
var ticketContactEmail = root.GetProperty("ticket_contact_email").GetString();
|
var ticketContactEmail = model.TicketContactEmail;
|
||||||
var ticketTags = root.GetProperty("ticket_tags").GetString();
|
var ticketTags = model.TicketTags;
|
||||||
if (string.IsNullOrWhiteSpace(ticketId) || string.IsNullOrWhiteSpace(ticketContactEmail))
|
if (string.IsNullOrWhiteSpace(ticketId) || string.IsNullOrWhiteSpace(ticketContactEmail))
|
||||||
{
|
{
|
||||||
return new BadRequestResult();
|
return new BadRequestResult();
|
||||||
@ -74,20 +60,34 @@ namespace Bit.Billing.Controllers
|
|||||||
|
|
||||||
var updateBody = new Dictionary<string, object>();
|
var updateBody = new Dictionary<string, object>();
|
||||||
var note = string.Empty;
|
var note = string.Empty;
|
||||||
|
var customFields = new Dictionary<string, object>();
|
||||||
var user = await _userRepository.GetByEmailAsync(ticketContactEmail);
|
var user = await _userRepository.GetByEmailAsync(ticketContactEmail);
|
||||||
if (user != null)
|
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>();
|
var tags = new HashSet<string>();
|
||||||
if (user.Premium)
|
if (user.Premium)
|
||||||
{
|
{
|
||||||
tags.Add("Premium");
|
tags.Add("Premium");
|
||||||
}
|
}
|
||||||
var orgs = await _organizationRepository.GetManyByUserIdAsync(user.Id);
|
var orgs = await _organizationRepository.GetManyByUserIdAsync(user.Id);
|
||||||
|
|
||||||
foreach (var org in orgs)
|
foreach (var org in orgs)
|
||||||
{
|
{
|
||||||
note += $"<li>Org, {org.Name} ({org.Seats.GetValueOrDefault()}): " +
|
var orgNote = $"{org.Name} ({org.Seats.GetValueOrDefault()}): " +
|
||||||
$"{_globalSettings.BaseServiceUri.Admin}/organizations/edit/{org.Id}</li>";
|
$"{_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();
|
var planName = GetAttribute<DisplayAttribute>(org.PlanType).Name.Split(" ").FirstOrDefault();
|
||||||
if (!string.IsNullOrWhiteSpace(planName))
|
if (!string.IsNullOrWhiteSpace(planName))
|
||||||
{
|
{
|
||||||
@ -107,15 +107,18 @@ namespace Bit.Billing.Controllers
|
|||||||
}
|
}
|
||||||
updateBody.Add("tags", tagsToUpdate);
|
updateBody.Add("tags", tagsToUpdate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (customFields.Any())
|
||||||
|
{
|
||||||
|
updateBody.Add("custom_fields", customFields);
|
||||||
|
}
|
||||||
var updateRequest = new HttpRequestMessage(HttpMethod.Put,
|
var updateRequest = new HttpRequestMessage(HttpMethod.Put,
|
||||||
string.Format("https://bitwarden.freshdesk.com/api/v2/tickets/{0}", ticketId))
|
string.Format("https://bitwarden.freshdesk.com/api/v2/tickets/{0}", ticketId))
|
||||||
{
|
{
|
||||||
Content = JsonContent.Create(updateBody),
|
Content = JsonContent.Create(updateBody),
|
||||||
};
|
};
|
||||||
|
|
||||||
await CallFreshdeskApiAsync(updateRequest);
|
await CallFreshdeskApiAsync(updateRequest);
|
||||||
|
|
||||||
|
|
||||||
var noteBody = new Dictionary<string, object>
|
var noteBody = new Dictionary<string, object>
|
||||||
{
|
{
|
||||||
{ "body", $"<ul>{note}</ul>" },
|
{ "body", $"<ul>{note}</ul>" },
|
||||||
@ -142,8 +145,10 @@ namespace Bit.Billing.Controllers
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
request.Headers.Add("Authorization", _freshdeskAuthkey);
|
var freshdeskAuthkey = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{_billingSettings.FreshdeskApiKey}:X"));
|
||||||
var response = await _httpClient.SendAsync(request);
|
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)
|
if (response.StatusCode != System.Net.HttpStatusCode.TooManyRequests || retriedCount > 3)
|
||||||
{
|
{
|
||||||
return response;
|
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 service, uncomment when we have some jobs to run
|
||||||
// Jobs.JobsHostedService.AddJobsServices(services);
|
// Jobs.JobsHostedService.AddJobsServices(services);
|
||||||
// services.AddHostedService<Jobs.JobsHostedService>();
|
// services.AddHostedService<Jobs.JobsHostedService>();
|
||||||
|
|
||||||
|
// Set up HttpClients
|
||||||
|
services.AddHttpClient("FreshdeskApi");
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Configure(
|
public void Configure(
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
using Bit.Api.Controllers;
|
using Bit.Api.Controllers;
|
||||||
using Bit.Api.Models.Request;
|
using Bit.Api.Models.Request;
|
||||||
using Bit.Api.Test.AutoFixture.Attributes;
|
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
using Bit.Api.Controllers;
|
using Bit.Api.Controllers;
|
||||||
using Bit.Api.Models.Request.Organizations;
|
using Bit.Api.Models.Request.Organizations;
|
||||||
using Bit.Api.Models.Response.Organizations;
|
using Bit.Api.Models.Response.Organizations;
|
||||||
using Bit.Api.Test.AutoFixture.Attributes;
|
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
using Bit.Api.Controllers;
|
using Bit.Api.Controllers;
|
||||||
using Bit.Api.Models.Request.Organizations;
|
using Bit.Api.Models.Request.Organizations;
|
||||||
using Bit.Api.Test.AutoFixture.Attributes;
|
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
using Bit.Api.Controllers;
|
using Bit.Api.Controllers;
|
||||||
using Bit.Api.Models.Request.Organizations;
|
using Bit.Api.Models.Request.Organizations;
|
||||||
using Bit.Api.Test.AutoFixture.Attributes;
|
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Models.Data.Organizations.Policies;
|
using Bit.Core.Models.Data.Organizations.Policies;
|
||||||
using Bit.Core.Repositories;
|
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 AutoFixture;
|
||||||
using Bit.Test.Common.AutoFixture.Attributes;
|
|
||||||
|
|
||||||
namespace Bit.Api.Test.AutoFixture.Attributes
|
namespace Bit.Test.Common.AutoFixture.Attributes
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Disables setting of Auto Properties on the Controller to avoid ASP.net initialization errors from a mock environment. Still sets constructor dependencies.
|
/// 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 AutoFixture;
|
||||||
using Bit.Test.Common.AutoFixture;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Org.BouncyCastle.Security;
|
using Org.BouncyCastle.Security;
|
||||||
|
|
||||||
namespace Bit.Api.Test.AutoFixture
|
namespace Bit.Test.Common.AutoFixture
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Disables setting of Auto Properties on the Controller to avoid ASP.net initialization errors. Still sets constructor dependencies.
|
/// Disables setting of Auto Properties on the Controller to avoid ASP.net initialization errors. Still sets constructor dependencies.
|
Loading…
x
Reference in New Issue
Block a user