1
0
mirror of https://github.com/bitwarden/server.git synced 2025-07-19 16:37:08 -05:00

[PM-23575] Use the input text as question and avoid additional call to freshdesk (#6073)

This commit is contained in:
Vijay Oommen
2025-07-10 08:34:45 -05:00
committed by GitHub
parent 7f65a655d4
commit 9a97384670
4 changed files with 13 additions and 162 deletions

View File

@ -144,7 +144,7 @@ public class FreshdeskController : Controller
[HttpPost("webhook-onyx-ai")] [HttpPost("webhook-onyx-ai")]
public async Task<IActionResult> PostWebhookOnyxAi([FromQuery, Required] string key, public async Task<IActionResult> PostWebhookOnyxAi([FromQuery, Required] string key,
[FromBody, Required] FreshdeskWebhookModel model) [FromBody, Required] FreshdeskOnyxAiWebhookModel model)
{ {
// ensure that the key is from Freshdesk // ensure that the key is from Freshdesk
if (!IsValidRequestFromFreshdesk(key)) if (!IsValidRequestFromFreshdesk(key))
@ -152,28 +152,8 @@ public class FreshdeskController : Controller
return new BadRequestResult(); return new BadRequestResult();
} }
// get ticket info from Freshdesk
var getTicketRequest = new HttpRequestMessage(HttpMethod.Get,
string.Format("https://bitwarden.freshdesk.com/api/v2/tickets/{0}", model.TicketId));
var getTicketResponse = await CallFreshdeskApiAsync(getTicketRequest);
// check if we have a valid response from freshdesk
if (getTicketResponse.StatusCode != System.Net.HttpStatusCode.OK)
{
_logger.LogError("Error getting ticket info from Freshdesk. Ticket Id: {0}. Status code: {1}",
model.TicketId, getTicketResponse.StatusCode);
return BadRequest("Failed to retrieve ticket info from Freshdesk");
}
// extract info from the response
var ticketInfo = await ExtractTicketInfoFromResponse(getTicketResponse);
if (ticketInfo == null)
{
return BadRequest("Failed to extract ticket info from Freshdesk response");
}
// create the onyx `answer-with-citation` request // create the onyx `answer-with-citation` request
var onyxRequestModel = new OnyxAnswerWithCitationRequestModel(ticketInfo.DescriptionText); var onyxRequestModel = new OnyxAnswerWithCitationRequestModel(model.TicketDescriptionText);
var onyxRequest = new HttpRequestMessage(HttpMethod.Post, var onyxRequest = new HttpRequestMessage(HttpMethod.Post,
string.Format("{0}/query/answer-with-citation", _billingSettings.Onyx.BaseUrl)) string.Format("{0}/query/answer-with-citation", _billingSettings.Onyx.BaseUrl))
{ {
@ -249,29 +229,6 @@ public class FreshdeskController : Controller
} }
} }
private async Task<FreshdeskViewTicketModel> ExtractTicketInfoFromResponse(HttpResponseMessage getTicketResponse)
{
var responseString = string.Empty;
try
{
responseString = await getTicketResponse.Content.ReadAsStringAsync();
var ticketInfo = JsonSerializer.Deserialize<FreshdeskViewTicketModel>(responseString,
options: new System.Text.Json.JsonSerializerOptions
{
PropertyNameCaseInsensitive = true,
});
return ticketInfo;
}
catch (System.Exception ex)
{
_logger.LogError("Error deserializing ticket info from Freshdesk response. Response: {0}. Exception {1}",
responseString, ex.ToString());
}
return null;
}
private async Task<HttpResponseMessage> CallFreshdeskApiAsync(HttpRequestMessage request, int retriedCount = 0) private async Task<HttpResponseMessage> CallFreshdeskApiAsync(HttpRequestMessage request, int retriedCount = 0)
{ {
try try

View File

@ -1,47 +0,0 @@
// FIXME: Update this file to be null safe and then delete the line below
#nullable disable
namespace Bit.Billing.Models;
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
public class FreshdeskViewTicketModel
{
[JsonPropertyName("spam")]
public bool? Spam { get; set; }
[JsonPropertyName("priority")]
public int? Priority { get; set; }
[JsonPropertyName("source")]
public int? Source { get; set; }
[JsonPropertyName("status")]
public int? Status { get; set; }
[JsonPropertyName("subject")]
public string Subject { get; set; }
[JsonPropertyName("support_email")]
public string SupportEmail { get; set; }
[JsonPropertyName("id")]
public int Id { get; set; }
[JsonPropertyName("description")]
public string Description { get; set; }
[JsonPropertyName("description_text")]
public string DescriptionText { get; set; }
[JsonPropertyName("created_at")]
public DateTime CreatedAt { get; set; }
[JsonPropertyName("updated_at")]
public DateTime UpdatedAt { get; set; }
[JsonPropertyName("tags")]
public List<string> Tags { get; set; }
}

View File

@ -16,3 +16,9 @@ public class FreshdeskWebhookModel
[JsonPropertyName("ticket_tags")] [JsonPropertyName("ticket_tags")]
public string TicketTags { get; set; } public string TicketTags { get; set; }
} }
public class FreshdeskOnyxAiWebhookModel : FreshdeskWebhookModel
{
[JsonPropertyName("ticket_description_text")]
public string TicketDescriptionText { get; set; }
}

View File

@ -112,7 +112,7 @@ public class FreshdeskControllerTests
[BitAutoData((string)null)] [BitAutoData((string)null)]
[BitAutoData(WebhookKey, null)] [BitAutoData(WebhookKey, null)]
public async Task PostWebhookOnyxAi_InvalidWebhookKey_results_in_BadRequest( public async Task PostWebhookOnyxAi_InvalidWebhookKey_results_in_BadRequest(
string freshdeskWebhookKey, FreshdeskWebhookModel model, string freshdeskWebhookKey, FreshdeskOnyxAiWebhookModel model,
BillingSettings billingSettings, SutProvider<FreshdeskController> sutProvider) BillingSettings billingSettings, SutProvider<FreshdeskController> sutProvider)
{ {
sutProvider.GetDependency<IOptions<BillingSettings>>() sutProvider.GetDependency<IOptions<BillingSettings>>()
@ -124,59 +124,11 @@ public class FreshdeskControllerTests
Assert.Equal(StatusCodes.Status400BadRequest, statusCodeResult.StatusCode); Assert.Equal(StatusCodes.Status400BadRequest, statusCodeResult.StatusCode);
} }
[Theory]
[BitAutoData(WebhookKey)]
public async Task PostWebhookOnyxAi_invalid_ticketid_results_in_BadRequest(
string freshdeskWebhookKey, FreshdeskWebhookModel model, SutProvider<FreshdeskController> sutProvider)
{
sutProvider.GetDependency<IOptions<BillingSettings>>()
.Value.FreshDesk.WebhookKey.Returns(freshdeskWebhookKey);
var mockHttpMessageHandler = Substitute.ForPartsOf<MockHttpMessageHandler>();
var mockResponse = new HttpResponseMessage(System.Net.HttpStatusCode.BadRequest);
mockHttpMessageHandler.Send(Arg.Any<HttpRequestMessage>(), Arg.Any<CancellationToken>())
.Returns(mockResponse);
var httpClient = new HttpClient(mockHttpMessageHandler);
sutProvider.GetDependency<IHttpClientFactory>().CreateClient("FreshdeskApi").Returns(httpClient);
var response = await sutProvider.Sut.PostWebhookOnyxAi(freshdeskWebhookKey, model);
var result = Assert.IsAssignableFrom<BadRequestObjectResult>(response);
Assert.Equal(StatusCodes.Status400BadRequest, result.StatusCode);
}
[Theory]
[BitAutoData(WebhookKey)]
public async Task PostWebhookOnyxAi_invalid_freshdesk_response_results_in_BadRequest(
string freshdeskWebhookKey, FreshdeskWebhookModel model,
SutProvider<FreshdeskController> sutProvider)
{
sutProvider.GetDependency<IOptions<BillingSettings>>()
.Value.FreshDesk.WebhookKey.Returns(freshdeskWebhookKey);
var mockHttpMessageHandler = Substitute.ForPartsOf<MockHttpMessageHandler>();
var mockResponse = new HttpResponseMessage(System.Net.HttpStatusCode.OK)
{
Content = new StringContent("non json content. expect json deserializer to throw error")
};
mockHttpMessageHandler.Send(Arg.Any<HttpRequestMessage>(), Arg.Any<CancellationToken>())
.Returns(mockResponse);
var httpClient = new HttpClient(mockHttpMessageHandler);
sutProvider.GetDependency<IHttpClientFactory>().CreateClient("FreshdeskApi").Returns(httpClient);
var response = await sutProvider.Sut.PostWebhookOnyxAi(freshdeskWebhookKey, model);
var result = Assert.IsAssignableFrom<BadRequestObjectResult>(response);
Assert.Equal(StatusCodes.Status400BadRequest, result.StatusCode);
}
[Theory] [Theory]
[BitAutoData(WebhookKey)] [BitAutoData(WebhookKey)]
public async Task PostWebhookOnyxAi_invalid_onyx_response_results_in_BadRequest( public async Task PostWebhookOnyxAi_invalid_onyx_response_results_in_BadRequest(
string freshdeskWebhookKey, FreshdeskWebhookModel model, string freshdeskWebhookKey, FreshdeskOnyxAiWebhookModel model,
FreshdeskViewTicketModel freshdeskTicketInfo, SutProvider<FreshdeskController> sutProvider) SutProvider<FreshdeskController> sutProvider)
{ {
var billingSettings = sutProvider.GetDependency<IOptions<BillingSettings>>().Value; var billingSettings = sutProvider.GetDependency<IOptions<BillingSettings>>().Value;
billingSettings.FreshDesk.WebhookKey.Returns(freshdeskWebhookKey); billingSettings.FreshDesk.WebhookKey.Returns(freshdeskWebhookKey);
@ -184,12 +136,6 @@ public class FreshdeskControllerTests
// mocking freshdesk Api request for ticket info // mocking freshdesk Api request for ticket info
var mockFreshdeskHttpMessageHandler = Substitute.ForPartsOf<MockHttpMessageHandler>(); var mockFreshdeskHttpMessageHandler = Substitute.ForPartsOf<MockHttpMessageHandler>();
var mockFreshdeskResponse = new HttpResponseMessage(System.Net.HttpStatusCode.OK)
{
Content = new StringContent(JsonSerializer.Serialize(freshdeskTicketInfo))
};
mockFreshdeskHttpMessageHandler.Send(Arg.Any<HttpRequestMessage>(), Arg.Any<CancellationToken>())
.Returns(mockFreshdeskResponse);
var freshdeskHttpClient = new HttpClient(mockFreshdeskHttpMessageHandler); var freshdeskHttpClient = new HttpClient(mockFreshdeskHttpMessageHandler);
// mocking Onyx api response given a ticket description // mocking Onyx api response given a ticket description
@ -211,8 +157,7 @@ public class FreshdeskControllerTests
[Theory] [Theory]
[BitAutoData(WebhookKey)] [BitAutoData(WebhookKey)]
public async Task PostWebhookOnyxAi_success( public async Task PostWebhookOnyxAi_success(
string freshdeskWebhookKey, FreshdeskWebhookModel model, string freshdeskWebhookKey, FreshdeskOnyxAiWebhookModel model,
FreshdeskViewTicketModel freshdeskTicketInfo,
OnyxAnswerWithCitationResponseModel onyxResponse, OnyxAnswerWithCitationResponseModel onyxResponse,
SutProvider<FreshdeskController> sutProvider) SutProvider<FreshdeskController> sutProvider)
{ {
@ -220,18 +165,8 @@ public class FreshdeskControllerTests
billingSettings.FreshDesk.WebhookKey.Returns(freshdeskWebhookKey); billingSettings.FreshDesk.WebhookKey.Returns(freshdeskWebhookKey);
billingSettings.Onyx.BaseUrl.Returns("http://simulate-onyx-api.com/api"); billingSettings.Onyx.BaseUrl.Returns("http://simulate-onyx-api.com/api");
// mocking freshdesk Api request for ticket info (GET)
var mockFreshdeskHttpMessageHandler = Substitute.ForPartsOf<MockHttpMessageHandler>();
var mockFreshdeskResponse = new HttpResponseMessage(System.Net.HttpStatusCode.OK)
{
Content = new StringContent(JsonSerializer.Serialize(freshdeskTicketInfo))
};
mockFreshdeskHttpMessageHandler.Send(
Arg.Is<HttpRequestMessage>(_ => _.Method == HttpMethod.Get),
Arg.Any<CancellationToken>())
.Returns(mockFreshdeskResponse);
// mocking freshdesk api add note request (POST) // mocking freshdesk api add note request (POST)
var mockFreshdeskHttpMessageHandler = Substitute.ForPartsOf<MockHttpMessageHandler>();
var mockFreshdeskAddNoteResponse = new HttpResponseMessage(System.Net.HttpStatusCode.BadRequest); var mockFreshdeskAddNoteResponse = new HttpResponseMessage(System.Net.HttpStatusCode.BadRequest);
mockFreshdeskHttpMessageHandler.Send( mockFreshdeskHttpMessageHandler.Send(
Arg.Is<HttpRequestMessage>(_ => _.Method == HttpMethod.Post), Arg.Is<HttpRequestMessage>(_ => _.Method == HttpMethod.Post),