Skip to content

Commit 9754040

Browse files
authored
feat(integration-tests): implement fedex tests (#241)
Signed-off-by: Alan Brault <alan.brault@visus.io>
1 parent b9d322d commit 9754040

26 files changed

Lines changed: 990 additions & 76 deletions

Directory.Packages.props

Lines changed: 39 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -4,33 +4,34 @@
44
<CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
55
</PropertyGroup>
66
<ItemGroup>
7-
<PackageVersion Include="AutoFixture" Version="4.18.1"/>
8-
<PackageVersion Include="AutoFixture.AutoNSubstitute" Version="4.18.1"/>
9-
<PackageVersion Include="AwesomeAssertions" Version="9.4.0"/>
10-
<PackageVersion Include="AwesomeAssertions.Analyzers" Version="9.0.8"/>
11-
<PackageVersion Include="JetBrains.Annotations" Version="2025.2.4"/>
12-
<PackageVersion Include="Meziantou.Analyzer" Version="3.0.85"/>
13-
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="[4.14.0]"/>
14-
<PackageVersion Include="Microsoft.Extensions.Caching.Hybrid" Version="10.6.0"/>
15-
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.8"/>
16-
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.8"/>
17-
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.8"/>
18-
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.8"/>
19-
<PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="10.6.0"/>
20-
<PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.8"/>
21-
<PackageVersion Include="Microsoft.IdentityModel.Tokens" Version="8.18.0"/>
22-
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.5.1"/>
23-
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="10.0.300"/>
24-
<PackageVersion Include="NSubstitute" Version="5.3.0"/>
25-
<PackageVersion Include="PolySharp" Version="1.15.0"/>
26-
<PackageVersion Include="PublicApiGenerator" Version="11.5.4"/>
27-
<PackageVersion Include="RichardSzalay.MockHttp" Version="7.0.0"/>
28-
<PackageVersion Include="Roslynator.Analyzers" Version="4.15.0"/>
29-
<PackageVersion Include="System.IdentityModel.Tokens.Jwt" Version="8.18.0"/>
30-
<PackageVersion Include="TUnit" Version="1.44.39"/>
31-
<PackageVersion Include="Verify" Version="31.16.3"/>
32-
<PackageVersion Include="Verify.DiffPlex" Version="3.1.2"/>
33-
<PackageVersion Include="Verify.TUnit" Version="31.16.3"/>
7+
<PackageVersion Include="AutoFixture" Version="4.18.1" />
8+
<PackageVersion Include="AutoFixture.AutoNSubstitute" Version="4.18.1" />
9+
<PackageVersion Include="AwesomeAssertions" Version="9.4.0" />
10+
<PackageVersion Include="AwesomeAssertions.Analyzers" Version="9.0.8" />
11+
<PackageVersion Include="JetBrains.Annotations" Version="2025.2.4" />
12+
<PackageVersion Include="Meziantou.Analyzer" Version="3.0.92" />
13+
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="[4.14.0]" />
14+
<PackageVersion Include="Microsoft.Extensions.Caching.Hybrid" Version="10.6.0" />
15+
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.8" />
16+
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.8" />
17+
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.8" />
18+
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.8" />
19+
<PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="10.6.0" />
20+
<PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.8" />
21+
<PackageVersion Include="Microsoft.IdentityModel.Tokens" Version="8.18.0" />
22+
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.4.0" />
23+
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="10.0.300" />
24+
<PackageVersion Include="NSubstitute" Version="5.3.0" />
25+
<PackageVersion Include="PolySharp" Version="1.15.0" />
26+
<PackageVersion Include="PublicApiGenerator" Version="11.5.4" />
27+
<PackageVersion Include="RichardSzalay.MockHttp" Version="7.0.0" />
28+
<PackageVersion Include="Roslynator.Analyzers" Version="4.15.0" />
29+
<PackageVersion Include="System.IdentityModel.Tokens.Jwt" Version="8.18.0" />
30+
<PackageVersion Include="TUnit" Version="1.45.29" />
31+
<PackageVersion Include="Verify" Version="31.16.1" />
32+
<PackageVersion Include="Verify.DiffPlex" Version="3.1.2" />
33+
<PackageVersion Include="Verify.TUnit" Version="31.16.1" />
34+
<PackageVersion Include="WireMock.Net" Version="2.6.0" />
3435
</ItemGroup>
3536
<ItemGroup>
3637
<PackageReference Include="Meziantou.Analyzer">
@@ -39,19 +40,19 @@
3940
</PackageReference>
4041
</ItemGroup>
4142
<ItemGroup Condition="'$(IsTestProject)' == 'true'">
42-
<PackageReference Include="AutoFixture"/>
43-
<PackageReference Include="AutoFixture.AutoNSubstitute"/>
44-
<PackageReference Include="AwesomeAssertions"/>
43+
<PackageReference Include="AutoFixture" />
44+
<PackageReference Include="AutoFixture.AutoNSubstitute" />
45+
<PackageReference Include="AwesomeAssertions" />
4546
<PackageReference Include="AwesomeAssertions.Analyzers">
4647
<PrivateAssets>all</PrivateAssets>
4748
<IncludeAssets>analyzers</IncludeAssets>
4849
</PackageReference>
49-
<PackageReference Include="Microsoft.Extensions.Caching.Hybrid"/>
50-
<PackageReference Include="Microsoft.Extensions.DependencyInjection"/>
51-
<PackageReference Include="NSubstitute"/>
52-
<PackageReference Include="PublicApiGenerator"/>
53-
<PackageReference Include="Verify"/>
54-
<PackageReference Include="Verify.DiffPlex"/>
55-
<PackageReference Include="Verify.TUnit"/>
50+
<PackageReference Include="Microsoft.Extensions.Caching.Hybrid" />
51+
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
52+
<PackageReference Include="NSubstitute" />
53+
<PackageReference Include="PublicApiGenerator" />
54+
<PackageReference Include="Verify" />
55+
<PackageReference Include="Verify.DiffPlex" />
56+
<PackageReference Include="Verify.TUnit" />
5657
</ItemGroup>
57-
</Project>
58+
</Project>
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
namespace Visus.AddressValidation.Integration.FedEx.Abstractions;
2+
3+
using System.Text.Json.Serialization;
4+
5+
/// <summary>
6+
/// Represents the type of alert returned in a FedEx API response.
7+
/// </summary>
8+
[JsonConverter(typeof(JsonStringEnumConverter<AlertType>))]
9+
internal enum AlertType
10+
{
11+
/// <summary>
12+
/// An informational note that does not indicate a problem.
13+
/// </summary>
14+
NOTE,
15+
16+
/// <summary>
17+
/// A warning that may require attention but does not prevent processing.
18+
/// </summary>
19+
WARNING,
20+
}

src/Visus.AddressValidation.Integration.FedEx/Clients/FedExAddressValidationClient.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,13 @@ public FedExAddressValidationClient(HttpClient httpClient, IOptions<FedExService
3131

3232
using HttpRequestMessage httpRequest = new(HttpMethod.Post, requestUri);
3333

34+
if ( !string.IsNullOrWhiteSpace(request.CustomerTransactionId) )
35+
{
36+
httpRequest.Headers.Add("x-customer-transaction-id", request.CustomerTransactionId);
37+
}
38+
39+
httpRequest.Headers.Add("x-locale", _options.Value.Locale);
40+
3441
httpRequest.Content = JsonContent.Create(request, ApiRequestJsonSerializerContext.Default.ApiRequest);
3542

3643
using HttpResponseMessage response = await _httpClient.SendAsync(httpRequest, cancellationToken)

src/Visus.AddressValidation.Integration.FedEx/Configuration/FedExServiceOptions.cs

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
namespace Visus.AddressValidation.Integration.FedEx.Configuration;
22

33
using System.ComponentModel.DataAnnotations;
4+
using System.Globalization;
45
using AddressValidation.Abstractions;
56

67
/// <summary>
78
/// Configuration options for the FedEx address validation service.
89
/// </summary>
9-
public sealed class FedExServiceOptions
10+
public sealed class FedExServiceOptions : IValidatableObject
1011
{
1112
/// <summary>
1213
/// The configuration section path used to bind these options from
@@ -23,6 +24,7 @@ public sealed class FedExServiceOptions
2324
{
2425
ClientEnvironment.DEVELOPMENT => Constants.DevelopmentEndpointBaseUri,
2526
ClientEnvironment.PRODUCTION => Constants.ProductionEndpointBaseUri,
27+
ClientEnvironment.SANDBOX => EndpointOverrideUri!,
2628
_ => Constants.DevelopmentEndpointBaseUri,
2729
};
2830

@@ -54,4 +56,70 @@ public sealed class FedExServiceOptions
5456
/// </summary>
5557
[Required(AllowEmptyStrings = false)]
5658
public required string ClientSecret { get; set; }
59+
60+
/// <summary>
61+
/// Gets or sets a URI that overrides the default endpoint derived from
62+
/// <see cref="ClientEnvironment" />.
63+
/// </summary>
64+
/// <remarks>
65+
/// <para>
66+
/// This property is <b>required</b> when
67+
/// <see cref="ClientEnvironment" /> is
68+
/// <see cref="ClientEnvironment.SANDBOX" />; validation will fail
69+
/// if it is <see langword="null" /> in that case.
70+
/// </para>
71+
/// <para>
72+
/// For all other environments this property is optional and, when
73+
/// set, has no effect — the endpoint is always resolved from
74+
/// <see cref="ClientEnvironment" />.
75+
/// </para>
76+
/// </remarks>
77+
public Uri? EndpointOverrideUri { get; set; }
78+
79+
/// <summary>
80+
/// Gets or sets the IETF BCP 47 language tag that identifies the locale
81+
/// used for address validation responses returned by the FedEx API.
82+
/// </summary>
83+
/// <remarks>
84+
/// Defaults to <see cref="CultureInfo.CurrentCulture" />.<see cref="CultureInfo.Name" />
85+
/// of the executing thread at the time the options object is
86+
/// constructed. Use a value such as <c>en-US</c> or <c>fr-FR</c> to
87+
/// request responses in a specific language and region.
88+
/// </remarks>
89+
public string Locale { get; set; } = CultureInfo.CurrentCulture.Name;
90+
91+
/// <summary>
92+
/// Performs cross-property validation on the options object.
93+
/// </summary>
94+
/// <param name="validationContext">
95+
/// The context in which validation is performed.
96+
/// </param>
97+
/// <returns>
98+
/// A collection of <see cref="ValidationResult" /> instances describing
99+
/// any validation failures, or an empty collection if the options are
100+
/// valid.
101+
/// </returns>
102+
/// <remarks>
103+
/// Validates that <see cref="EndpointOverrideUri" /> is not
104+
/// <see langword="null" /> when <see cref="ClientEnvironment" /> is
105+
/// <see cref="ClientEnvironment.SANDBOX" />, since the
106+
/// sandbox environment requires an explicit endpoint to target a local
107+
/// mock server.
108+
/// </remarks>
109+
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
110+
{
111+
if ( ClientEnvironment == ClientEnvironment.SANDBOX && EndpointOverrideUri is null )
112+
{
113+
yield return new ValidationResult(
114+
$"{nameof(EndpointOverrideUri)} must be set when {nameof(ClientEnvironment)} is {nameof(ClientEnvironment.SANDBOX)}.",
115+
[nameof(EndpointOverrideUri),]);
116+
}
117+
118+
if ( !Constants.SupportedLocales.Contains(Locale) )
119+
{
120+
yield return new ValidationResult(
121+
$"'{Locale}' is not a supported IETF BCP 47 language tag.",
122+
[nameof(Locale),]);
123+
}
124+
}
57125
}

src/Visus.AddressValidation.Integration.FedEx/Constants.cs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,4 +69,50 @@ public static class Constants
6969
CountryCode.VI,
7070
CountryCode.ZA,
7171
];
72+
73+
/// <summary>
74+
/// IETF BCP 47 locale tags that are supported by the FedEx Address Validation API.
75+
/// </summary>
76+
public static readonly FrozenSet<string> SupportedLocales =
77+
[
78+
"ar-SA",
79+
"bg-BG",
80+
"zh-CN",
81+
"zh-TW",
82+
"hr-HR",
83+
"cs-CZ",
84+
"da-DK",
85+
"nl-NL",
86+
"en-US",
87+
"et-EE",
88+
"fi-FI",
89+
"fr-FR",
90+
"de-DE",
91+
"el-GR",
92+
"he-IL",
93+
"hu-HU",
94+
"is-IS",
95+
"it-IT",
96+
"ja-JP",
97+
"ko-KR",
98+
"lv-LV",
99+
"lt-LT",
100+
"ms-MY",
101+
"nb-NO",
102+
"pl-PL",
103+
"pt-BR",
104+
"pt-PT",
105+
"ro-RO",
106+
"ru-RU",
107+
"sr-RS",
108+
"sk-SK",
109+
"sl-SI",
110+
"es-419",
111+
"es-ES",
112+
"sv-SE",
113+
"th-TH",
114+
"tr-TR",
115+
"uk-UA",
116+
"vi-VN",
117+
];
72118
}

src/Visus.AddressValidation.Integration.FedEx/Contracts/ApiRequest.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
namespace Visus.AddressValidation.Integration.FedEx.Contracts;
22

3+
using System.Text.Json.Serialization;
34
using AddressValidation.Abstractions;
45

56
internal sealed class ApiRequest
67
{
78
public required IReadOnlyList<FedExAddressToValidate> AddressesToValidate { get; init; }
89

10+
[JsonIgnore]
11+
public string? CustomerTransactionId { get; init; }
12+
913
internal sealed class FedExAddress
1014
{
1115
public string? City { get; init; }

src/Visus.AddressValidation.Integration.FedEx/Contracts/ApiResponse.cs

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,16 @@ internal sealed partial class ApiResponse
1717
public Response? Result { get; set; }
1818

1919
[CustomResponseDataProperty]
20-
public Guid TransactionId { get; set; }
20+
public string? TransactionId { get; set; }
21+
22+
internal sealed class Alert
23+
{
24+
public AlertType AlertType { get; set; }
25+
26+
public string Code { get; set; } = null!;
27+
28+
public string Message { get; set; } = null!;
29+
}
2130

2231
internal sealed partial class Attribute
2332
{
@@ -55,7 +64,7 @@ internal sealed partial class Attribute
5564

5665
[CustomResponseDataProperty]
5766
[JsonConverter(typeof(JsonStringBooleanConverter))]
58-
[JsonPropertyName(nameof(IsBaseAddressForMultiUnit))]
67+
[JsonPropertyName("MultiUnitBase")]
5968
public bool IsBaseAddressForMultiUnit { get; set; }
6069

6170
[CustomResponseDataProperty]
@@ -99,10 +108,19 @@ internal sealed partial class Attribute
99108
[JsonPropertyName(nameof(MatchSource))]
100109
public string MatchSource { get; set; } = null!;
101110

111+
[CustomResponseDataProperty]
112+
[JsonConverter(typeof(JsonStringBooleanConverter))]
113+
[JsonPropertyName(nameof(Matched))]
114+
public bool Matched { get; set; }
115+
102116
[JsonConverter(typeof(JsonStringBooleanConverter))]
103117
[JsonPropertyName(nameof(MultipleMatches))]
104118
public bool MultipleMatches { get; set; }
105119

120+
[CustomResponseDataProperty]
121+
[JsonPropertyName(nameof(ResolutionInput))]
122+
public string? ResolutionInput { get; set; }
123+
106124
[CustomResponseDataProperty]
107125
[JsonPropertyName(nameof(ResolutionMethod))]
108126
public ResolutionMethod ResolutionMethod { get; set; }
@@ -155,6 +173,10 @@ internal sealed partial class ResolvedAddress
155173

156174
public string PostalCode { get; set; } = null!;
157175

176+
[CustomResponseDataProperty]
177+
[JsonPropertyName("postOfficeBox")]
178+
public bool PostOfficeBox { get; set; }
179+
158180
[JsonPropertyName("resolutionMethodName")]
159181
public ResolutionMethod ResolutionMethod { get; set; }
160182

@@ -172,6 +194,8 @@ internal sealed partial class ResolvedAddress
172194

173195
internal sealed class Response
174196
{
197+
public Alert[] Alerts { get; set; } = [];
198+
175199
public ResolvedAddress[] ResolvedAddresses { get; set; } = [];
176200
}
177201
}

src/Visus.AddressValidation.Integration.FedEx/Mappers/AddressValidationRequestMapper.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ public ApiRequest Map(FedExAddressValidationRequest request)
2727
ClientReferenceId = request.ClientReferenceId,
2828
},
2929
],
30+
CustomerTransactionId = request.CustomerTransactionId,
3031
};
3132
}
3233
}

src/Visus.AddressValidation.Integration.FedEx/Mappers/AddressValidationResponseMapper.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,11 @@ public IAddressValidationResponse Map(ApiResponse response, IValidationResult? v
4040
ApiResponse response, ApiResponse.ResolvedAddress address)
4141
{
4242
Dictionary<string, object?> data = new(StringComparer.OrdinalIgnoreCase);
43+
4344
data.Merge(response.GetCustomResponseData());
4445
data.Merge(address.GetCustomResponseData());
4546
data.Merge(address.Attributes.GetCustomResponseData());
47+
4648
return data.AsReadOnly();
4749
}
4850
}

src/Visus.AddressValidation.Integration.FedEx/Models/FedExAddressValidationRequest.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,9 @@ public sealed class FedExAddressValidationRequest : AbstractAddressValidationReq
1616
/// Gets or sets the Client Reference ID
1717
/// </summary>
1818
public string? ClientReferenceId { get; set; }
19+
20+
/// <summary>
21+
/// Gets or sets the Customer Transaction ID used to identify the transaction.
22+
/// </summary>
23+
public string? CustomerTransactionId { get; set; }
1924
}

0 commit comments

Comments
 (0)