Skip to content

Commit

Permalink
#372: Throw a MollieApiException for all types of unsuccesfull API re…
Browse files Browse the repository at this point in the history
…sponse codes (#374)

* #372: Throw a MollieApiException for all type of unsuccesfull API response codes

* #372: Move error parsing from MollieApiException to BaseMollieClient
  • Loading branch information
Viincenttt committed Jun 30, 2024
1 parent 6e37edc commit 52f2f65
Show file tree
Hide file tree
Showing 4 changed files with 109 additions and 25 deletions.
31 changes: 17 additions & 14 deletions src/Mollie.Api/Client/BaseMollieClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
using Mollie.Api.Extensions;
using Mollie.Api.Framework;
using Mollie.Api.Framework.Idempotency;
using Mollie.Api.Models.Error;
using Mollie.Api.Models.Url;
using Newtonsoft.Json;

namespace Mollie.Api.Client {
public abstract class BaseMollieClient : IDisposable {
Expand Down Expand Up @@ -92,20 +94,8 @@ public abstract class BaseMollieClient : IDisposable {
return _jsonConverterService.Deserialize<T>(resultContent)!;
}

switch (response.StatusCode) {
case HttpStatusCode.BadRequest:
case HttpStatusCode.Unauthorized:
case HttpStatusCode.Forbidden:
case HttpStatusCode.NotFound:
case HttpStatusCode.MethodNotAllowed:
case HttpStatusCode.UnsupportedMediaType:
case HttpStatusCode.Gone:
case (HttpStatusCode) 422: // Unprocessable entity
throw new MollieApiException(resultContent);
default:
throw new HttpRequestException(
$"Unknown http exception occured with status code: {(int) response.StatusCode}.");
}
MollieErrorMessage errorDetails = ParseMollieErrorMessage(response.StatusCode, resultContent);
throw new MollieApiException(errorDetails);
}

protected void ValidateApiKeyIsOauthAccesstoken(bool isConstructor = false) {
Expand Down Expand Up @@ -163,6 +153,19 @@ public abstract class BaseMollieClient : IDisposable {
return $"{packageName}/{versionNumber}";
}

private MollieErrorMessage ParseMollieErrorMessage(HttpStatusCode responseStatusCode, string responseBody) {
try {
return _jsonConverterService.Deserialize<MollieErrorMessage>(responseBody)!;
}
catch (JsonReaderException) {
return new MollieErrorMessage {
Title = "Unknown error",
Status = (int)responseStatusCode,
Detail = responseBody
};
}
}

protected void ValidateRequiredUrlParameter(string parameterName, string parameterValue) {
if (string.IsNullOrWhiteSpace(parameterValue)) {
throw new ArgumentException($"Required URL argument '{parameterName}' is null or empty");
Expand Down
9 changes: 2 additions & 7 deletions src/Mollie.Api/Client/MollieApiException.cs
Original file line number Diff line number Diff line change
@@ -1,17 +1,12 @@
using System;
using Mollie.Api.Models.Error;
using Newtonsoft.Json;

namespace Mollie.Api.Client {
public class MollieApiException : Exception {
public MollieErrorMessage Details { get; set; }

public MollieApiException(string json) : base(ParseErrorMessage(json).ToString()){
Details = ParseErrorMessage(json);
}

private static MollieErrorMessage ParseErrorMessage(string json) {
return JsonConvert.DeserializeObject<MollieErrorMessage>(json)!;
public MollieApiException(MollieErrorMessage details) : base(details.ToString()) {
Details = details;
}
}
}
17 changes: 13 additions & 4 deletions tests/Mollie.Tests.Unit/Client/BaseClientTests.cs
Original file line number Diff line number Diff line change
@@ -1,14 +1,23 @@
using RichardSzalay.MockHttp;
using System.Net;
using RichardSzalay.MockHttp;
using System.Net.Http;
using System.Net.Mime;

namespace Mollie.Tests.Unit.Client {
public abstract class BaseClientTests {
protected const string DefaultRedirectUrl = "https://www.mollie.com";

protected MockHttpMessageHandler CreateMockHttpMessageHandler(HttpMethod httpMethod, string url, string response, string expectedPartialContent = null) {
MockHttpMessageHandler mockHttp = new MockHttpMessageHandler();
protected MockHttpMessageHandler CreateMockHttpMessageHandler(
HttpMethod httpMethod,
string url,
string response,
string expectedPartialContent = null,
string responseContentType = MediaTypeNames.Application.Json,
HttpStatusCode responseStatusCode = HttpStatusCode.OK) {

MockHttpMessageHandler mockHttp = new();
MockedRequest mockedRequest = mockHttp.Expect(httpMethod, url)
.Respond("application/json", response);
.Respond(responseStatusCode, responseContentType, response);

if (!string.IsNullOrEmpty(expectedPartialContent))
{
Expand Down
77 changes: 77 additions & 0 deletions tests/Mollie.Tests.Unit/Client/BaseMollieClientTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
using System.Net;
using System.Net.Http;
using System.Net.Mime;
using System.Threading.Tasks;
using FluentAssertions;
using Mollie.Api.Client;
using Mollie.Api.Models;
using Mollie.Api.Models.Payment.Request;
using Xunit;

namespace Mollie.Tests.Unit.Client;

public class BaseMollieClientTests : BaseClientTests {
[Fact]
public async Task HttpResponseStatusCodeIsNotSuccesfull_ResponseBodyContainsMollieErrorDetails_MollieApiExceptionIsThrown() {

// Arrange
const string errorMessage = "A validation error occured";
const int errorStatus = (int)HttpStatusCode.UnprocessableEntity;
string responseBody = @$"{{
""_links"": {{
""documentation"": {{
""href"": ""https://docs.mollie.com/overview/handling-errors"",
""type"": ""text/html""
}}
}},
""detail"": ""{errorMessage}"",
""status"": {errorStatus},
""title"": ""Error""
}}";
const string expectedUrl = $"{BaseMollieClient.ApiEndPoint}payments";
var mockHttp = CreateMockHttpMessageHandler(
HttpMethod.Post,
expectedUrl,
responseBody,
responseStatusCode: HttpStatusCode.UnprocessableEntity);
HttpClient httpClient = mockHttp.ToHttpClient();
PaymentClient paymentClient = new("api-key", httpClient);
PaymentRequest paymentRequest = new() {
Amount = new Amount(Currency.EUR, 50m),
Description = "description"
};

// Act
var exception = await Assert.ThrowsAsync<MollieApiException>(() => paymentClient.CreatePaymentAsync(paymentRequest));

// Assert
exception.Details.Detail.Should().Be(errorMessage);
exception.Details.Status.Should().Be(errorStatus);
}

[Fact]
public async Task HttpResponseStatusCodeIsNotSuccesfull_ResponseBodyContainsHtml_MollieApiExceptionIsThrown() {
// Arrange
string responseBody = "<html><body>Whoops!</body></html>";
const string expectedUrl = $"{BaseMollieClient.ApiEndPoint}payments";
var mockHttp = CreateMockHttpMessageHandler(
HttpMethod.Post,
expectedUrl,
responseBody,
responseContentType: MediaTypeNames.Text.Html,
responseStatusCode: HttpStatusCode.UnprocessableEntity);
HttpClient httpClient = mockHttp.ToHttpClient();
PaymentClient paymentClient = new("api-key", httpClient);
PaymentRequest paymentRequest = new() {
Amount = new Amount(Currency.EUR, 50m),
Description = "description"
};

// Act
var exception = await Assert.ThrowsAsync<MollieApiException>(() => paymentClient.CreatePaymentAsync(paymentRequest));

// Assert
exception.Details.Detail.Should().Be(responseBody);
exception.Details.Status.Should().Be((int)HttpStatusCode.UnprocessableEntity);
}
}

0 comments on commit 52f2f65

Please sign in to comment.