Skip to content

Commit

Permalink
Merge branch 'main' of github.com:ShipEngine/shipengine-dotnet into E…
Browse files Browse the repository at this point in the history
…NGINE-7190-funding-sources-wallet-errors
  • Loading branch information
christian.casado committed Sep 12, 2024
2 parents 2f61e08 + d47469e commit 24f6117
Show file tree
Hide file tree
Showing 10 changed files with 267 additions and 24 deletions.
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,3 +168,24 @@ Fixed handling of No Content responses
### Added

- Added basic ability to modify the HttpClient in a request

## 2.3.0

### Added

- Added ability to scope request modifiers by using the `.WithRequestModifier()` method instead of the `.ModifyRequest` property.
This will allow consumers to modify a single request without affecting any other consumers of the client. It also allows for
multiple modifiers to be added. For example, a modifier could be added at the global level that applies to all requests and then
another modifier can be added for a single request.

## 2.3.1

### Added

- Added license number, invoice number, and certificate number to advanced options

## 2.3.2

### Added

- Added funding_source and parcelguard to insurance provider enumeration
8 changes: 5 additions & 3 deletions ShipEngineSDK.Test/Helpers/MockShipEngineFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,13 @@ public void AssertRequest(HttpMethod method, string path, int numberOfCalls = 1)
/// <param name="path">The HTTP path.</param>
/// <param name="status">The status code to return.</param>
/// <param name="response">The response body to return.</param>
public string StubRequest(HttpMethod method, string path, HttpStatusCode status, string response)
public string StubRequest(HttpMethod method, string path, HttpStatusCode status = HttpStatusCode.OK, string response = null)
{
var requestId = Guid.NewGuid().ToString();
var responseMessage = new HttpResponseMessage(status);
responseMessage.Content = new StringContent(response ?? "");
var responseMessage = new HttpResponseMessage(status)
{
Content = new StringContent(response ?? "")
};
responseMessage.Headers.Add("x-shipengine-requestid", requestId);
responseMessage.Headers.Add("request-id", requestId);

Expand Down
150 changes: 143 additions & 7 deletions ShipEngineSDK.Test/ShipEngineClientTests.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
namespace ShipEngineTest
{
using Moq;
using Moq.Protected;
using ShipEngineSDK;
using ShipEngineSDK.VoidLabelWithLabelId;
using System;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Xunit;

Expand Down Expand Up @@ -173,7 +176,7 @@ public async Task SuccessResponseWithNullStringContentThrowsShipEngineExceptionW
var mockShipEngineFixture = new MockShipEngineFixture(config);
var shipengine = mockShipEngineFixture.ShipEngine;

// this scenario is similar to unparseable JSON - except that it is valid JSON
// this scenario is similar to unparsable JSON - except that it is valid JSON
var responseBody = @"null";
var requestId = mockShipEngineFixture.StubRequest(HttpMethod.Post, "/v1/something", System.Net.HttpStatusCode.OK,
responseBody);
Expand All @@ -197,7 +200,7 @@ public async Task SuccessResponseWhenStringRequestedReturnsUnparsedString()
var mockShipEngineFixture = new MockShipEngineFixture(config);
var shipengine = mockShipEngineFixture.ShipEngine;

// this scenario is similar to unparseable JSON - except that it is valid JSON
// this scenario is similar to unparsable JSON - except that it is valid JSON
var responseBody = @"The Response";
mockShipEngineFixture.StubRequest(HttpMethod.Delete, "/v1/something", System.Net.HttpStatusCode.OK,
responseBody);
Expand All @@ -214,14 +217,147 @@ public async Task SuccessResponseWithNoContentCanBeReturnedIfStringRequested()
var mockShipEngineFixture = new MockShipEngineFixture(config);
var shipengine = mockShipEngineFixture.ShipEngine;

// this scenario is similar to unparseable JSON - except that it is valid JSON
string responseBody = null;
mockShipEngineFixture.StubRequest(HttpMethod.Delete, "/v1/something", System.Net.HttpStatusCode.OK,
responseBody);
// this scenario is similar to unparsable JSON - except that it is valid JSON
mockShipEngineFixture.StubRequest(HttpMethod.Delete, "/v1/something", System.Net.HttpStatusCode.OK, null);
var result = await shipengine.SendHttpRequestAsync<string>(HttpMethod.Delete, "/v1/something", "",
mockShipEngineFixture.HttpClient, config);

Assert.Null(responseBody);
Assert.Empty(result);
}

[Fact]
public void WithRequestModifierDoesNotCreateNewHttpClient()
{
var config = new Config(apiKey: "test", timeout: TimeSpan.FromSeconds(0.5));
var mockShipEngineFixture = new MockShipEngineFixture(config);
var httpClient = mockShipEngineFixture.HttpClient;

var originalShipEngine = mockShipEngineFixture.ShipEngine;

var newShipEngine = originalShipEngine.WithRequestModifier(x => x.Headers.Add("X-Test-Header", "Test"));

Assert.Same(config, newShipEngine._config);
Assert.Same(httpClient, newShipEngine._client);
Assert.NotSame(originalShipEngine, newShipEngine);
}

[Fact]
public void ModifyRequestDoesNotCreateNewHttpClientNorShipEngineInstance()
{
var config = new Config(apiKey: "test", timeout: TimeSpan.FromSeconds(0.5));
var mockShipEngineFixture = new MockShipEngineFixture(config);
var httpClient = mockShipEngineFixture.HttpClient;

var originalShipEngine = mockShipEngineFixture.ShipEngine;

var newShipEngine = originalShipEngine.ModifyRequest(x => x.Headers.Add("X-Test-Header", "Test"));

Assert.Same(config, newShipEngine._config);
Assert.Same(httpClient, newShipEngine._client);
Assert.Same(originalShipEngine, newShipEngine);
}

[Fact]
public async Task WithSingleRequestModifierAppliesBeforeRequest()
{
var config = new Config(apiKey: "test", timeout: TimeSpan.FromSeconds(0.5));
var mockShipEngineFixture = new MockShipEngineFixture(config);
var shipengine = mockShipEngineFixture.ShipEngine.WithRequestModifier(x => x.Headers.Add("X-Test-Header", "Test"));
mockShipEngineFixture.StubRequest(HttpMethod.Get, "/foo");

await shipengine.SendHttpRequestAsync<string>(HttpMethod.Get, "/foo", "", mockShipEngineFixture.HttpClient, config);

mockShipEngineFixture.MockHandler.Protected()
.Verify(
"SendAsync",
Times.Once(),
ItExpr.Is<HttpRequestMessage>(m => m.Headers.Any(x => x.Key == "X-Test-Header")),
ItExpr.IsAny<CancellationToken>());
}

[Fact]
public async Task WithRequestModifierDoesNotAffectOriginalClient()
{
var config = new Config(apiKey: "test", timeout: TimeSpan.FromSeconds(0.5));
var mockShipEngineFixture = new MockShipEngineFixture(config);
var shipengine = mockShipEngineFixture.ShipEngine;
var modifiedShipEngine = shipengine.WithRequestModifier(x => x.Headers.Add("X-Test-Header", "Test"));
mockShipEngineFixture.StubRequest(HttpMethod.Get, "/foo");

await shipengine.SendHttpRequestAsync<string>(HttpMethod.Get, "/foo", "", mockShipEngineFixture.HttpClient, config);

mockShipEngineFixture.MockHandler.Protected()
.Verify(
"SendAsync",
Times.Once(),
ItExpr.Is<HttpRequestMessage>(m => !m.Headers.Any(x => x.Key == "X-Test-Header")),
ItExpr.IsAny<CancellationToken>());
}

[Fact]
public async Task WithTwoRequestModifierAppliesBeforeRequest()
{
var config = new Config(apiKey: "test", timeout: TimeSpan.FromSeconds(0.5));
var mockShipEngineFixture = new MockShipEngineFixture(config);
var shipengine = mockShipEngineFixture.ShipEngine
.WithRequestModifier(x =>
{
x.Headers.Add("X-Test-Header", "Test 1");
x.Headers.Add("X-Second-Header", "Test 2");
})
.WithRequestModifier(x => x.Headers.Remove("X-Test-Header"));
mockShipEngineFixture.StubRequest(HttpMethod.Get, "/foo");

await shipengine.SendHttpRequestAsync<string>(HttpMethod.Get, "/foo", "", mockShipEngineFixture.HttpClient, config);

mockShipEngineFixture.MockHandler.Protected()
.Verify(
"SendAsync",
Times.Once(),
ItExpr.Is<HttpRequestMessage>(m =>
!m.Headers.Any(x => x.Key == "X-Test-Header") &&
m.Headers.Any(x => x.Key == "X-Second-Header")),
ItExpr.IsAny<CancellationToken>());
}

[Fact]
public async Task ModifyRequestAppliesBeforeRequest()
{
var config = new Config(apiKey: "test", timeout: TimeSpan.FromSeconds(0.5));
var mockShipEngineFixture = new MockShipEngineFixture(config);
var shipengine = mockShipEngineFixture.ShipEngine.ModifyRequest(x => x.Headers.Add("X-Test-Header", "Test"));
mockShipEngineFixture.StubRequest(HttpMethod.Get, "/foo");

await shipengine.SendHttpRequestAsync<string>(HttpMethod.Get, "/foo", "", mockShipEngineFixture.HttpClient, config);

mockShipEngineFixture.MockHandler.Protected()
.Verify(
"SendAsync",
Times.Once(),
ItExpr.Is<HttpRequestMessage>(m => m.Headers.Any(x => x.Key == "X-Test-Header")),
ItExpr.IsAny<CancellationToken>());
}

[Fact]
public async Task ModifyRequestReplacesExistingModifiersAppliesBeforeRequest()
{
var config = new Config(apiKey: "test", timeout: TimeSpan.FromSeconds(0.5));
var mockShipEngineFixture = new MockShipEngineFixture(config);
var shipengine = mockShipEngineFixture.ShipEngine
.WithRequestModifier(x => x.Headers.Add("X-Test-Header", "Test 1"))
.ModifyRequest(x => x.Headers.Add("X-Second-Header", "Test 2"));
mockShipEngineFixture.StubRequest(HttpMethod.Get, "/foo");

await shipengine.SendHttpRequestAsync<string>(HttpMethod.Get, "/foo", "", mockShipEngineFixture.HttpClient, config);

mockShipEngineFixture.MockHandler.Protected()
.Verify(
"SendAsync",
Times.Once(),
ItExpr.Is<HttpRequestMessage>(m =>
m.Headers.Any(x => x.Key == "X-Second-Header") &&
!m.Headers.Any(x => x.Key == "X-Test-Header")),
ItExpr.IsAny<CancellationToken>());
}
}
}
17 changes: 16 additions & 1 deletion ShipEngineSDK/Models/Dto/Common/AdvancedShipmentOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -100,13 +100,28 @@ public class AdvancedShipmentOptions
public OriginType? OriginType { get; set; }

/// <summary>
/// Indicates if this is a shipper relesae
/// Indicates if this is a shipper release
/// </summary>
public bool? ShipperRelease { get; set; }

/// <summary>
/// Defer payment until package is delivered, instead of when it is ordered.
/// </summary>
public CollectOnDelivery? CollectOnDelivery { get; set; }

/// <summary>
/// License number for customs declaration
/// </summary>
public string? LicenseNumber { get; set; }

/// <summary>
/// Invoice number for customs declaration
/// </summary>
public string? InvoiceNumber { get; set; }

/// <summary>
/// Certificate number for customs declaration
/// </summary>
public string? CertificateNumber { get; set; }
}
}
14 changes: 13 additions & 1 deletion ShipEngineSDK/Models/Dto/Common/Enums/InsuranceProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,18 @@ public enum InsuranceProvider
/// The package is insured by a third-party insurance service, outside of ShipEngine.
/// </summary>
[EnumMember(Value = "third_party")]
ThirdParty
ThirdParty,

/// <summary>
/// This option will assign the insurance provider available with ShipEngine Carriers to the shipment (currently ParcelGuard). The response will auto-populate the insurance_provider field with parcelguard.
/// </summary>
[EnumMember(Value = "funding_source")]
FundingSource,

Check warning on line 38 in ShipEngineSDK/Models/Dto/Common/Enums/InsuranceProvider.cs

View workflow job for this annotation

GitHub Actions / .Net 8.0 on windows-latest


/// <summary>
/// This option will assign ParcelGuard as the insurance provider for the shipment. Using this value is functionally the same as using the 'funding_source' value.
/// </summary>
[EnumMember(Value = "parcelguard")]
ParcelGuard

Check warning on line 44 in ShipEngineSDK/Models/Dto/Common/Enums/InsuranceProvider.cs

View workflow job for this annotation

GitHub Actions / .Net 8.0 on windows-latest

Check warning on line 44 in ShipEngineSDK/Models/Dto/Common/Enums/InsuranceProvider.cs

View workflow job for this annotation

GitHub Actions / .Net 8.0 on windows-latest

}
}
35 changes: 30 additions & 5 deletions ShipEngineSDK/ShipEngine.cs
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using ShipEngineSDK.Common;
using ShipEngineSDK.Manifests;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Threading.Tasks;
using Result = ShipEngineSDK.ValidateAddresses.Result;

[assembly: InternalsVisibleTo("ShipEngineSDK.Test")]

Expand Down Expand Up @@ -225,14 +224,40 @@ public ShipEngine(HttpClient httpClient) : base()
_client = httpClient;
}

/// <summary>
/// Copy constructor that adds a request modifier to the existing collection
/// </summary>
/// <param name="client">Client to use for requests</param>
/// <param name="config">Config to use for the requests</param>
/// <param name="requestModifiers">List of request modifiers to use</param>
private ShipEngine(HttpClient client, Config config, IEnumerable<Action<HttpRequestMessage>> requestModifiers) :
base(requestModifiers)
{
_client = client;
_config = config;
}

/// <summary>
/// Gets a new instance of the ShipEngine client with the provided request modifier added to the collection
/// </summary>
/// <param name="modifier">Request modifier that will be added</param>
/// <returns>A new instance of the ShipEngine client</returns>
/// <remarks>The existing ShipEngine client is not modified</remarks>
public ShipEngine WithRequestModifier(Action<HttpRequestMessage> modifier) =>
new(_client, _config, requestModifiers.Append(modifier));

/// <summary>
/// Modifies the request before it is sent to the ShipEngine API
/// </summary>
/// <param name="modifyRequest"></param>
/// <returns></returns>
/// <param name="modifyRequest">Request modifier that will be used</param>
/// <returns>The current instance of the ShipEngine client</returns>
/// <remarks>
/// This method modifies the existing ShipEngine client and will replace any existing request modifiers with the one provided.
/// If you want to add a request modifier to the existing collection, use the WithRequestModifier method.
/// </remarks>
public ShipEngine ModifyRequest(Action<HttpRequestMessage> modifyRequest)
{
base.ModifyRequest = modifyRequest;
requestModifiers = [modifyRequest];
return this;
}

Expand Down
28 changes: 25 additions & 3 deletions ShipEngineSDK/ShipEngineClient.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using ShipEngineSDK.Common;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
Expand All @@ -19,6 +20,20 @@ namespace ShipEngineSDK
/// </summary>
public class ShipEngineClient
{
/// <summary>
/// Default constructor
/// </summary>
public ShipEngineClient() { }

/// <summary>
/// Constructor that takes a collection of request modifiers to apply to the request before it is sent
/// </summary>
/// <param name="requestModifiers">Collection of modifiers to be used for each request</param>
protected ShipEngineClient(IEnumerable<Action<HttpRequestMessage>> requestModifiers)
{
this.requestModifiers = requestModifiers;
}

/// <summary>
/// Options for serializing the method call params to JSON.
/// A separate inline setting is used for deserializing the response
Expand Down Expand Up @@ -46,9 +61,13 @@ public class ShipEngineClient
public CancellationToken CancellationToken { get; set; }

/// <summary>
/// Modifies the client request before it is sent
/// Collections of request modifiers to apply to the request before it is sent
/// </summary>
public Action<HttpRequestMessage>? ModifyRequest { get; set; }
/// <remarks>
/// This is a collection instead of a single action so that modifiers can be added at multiple levels.
/// For example, a consumer could add a modifier at the client level, and then add another at the method level.
/// </remarks>
protected IEnumerable<Action<HttpRequestMessage>> requestModifiers = [];

Check warning on line 70 in ShipEngineSDK/ShipEngineClient.cs

View workflow job for this annotation

GitHub Actions / .Net 8.0 on windows-latest


/// <summary>
/// Sets the HttpClient User agent, the json media type, and the API key to be used
Expand Down Expand Up @@ -209,7 +228,10 @@ public virtual async Task<T> SendHttpRequestAsync<T>(HttpMethod method, string p
try
{
var request = BuildRequest(method, path, jsonContent);
ModifyRequest?.Invoke(request);
foreach (var modifier in requestModifiers ?? [])
{
modifier?.Invoke(request);
}
response = await client.SendAsync(request, cancellationToken);

var deserializedResult = await DeserializedResultOrThrow<T>(response);
Expand Down
Loading

0 comments on commit 24f6117

Please sign in to comment.