Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions docs/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@ IGeoLocationApiClient
├── .GeoLookup (IVersionedGeoLookupApi)
│ ├── .V1 (IGeoLookupApi) → GetGeoLocation, GetGeoLocations, DeleteMetadata
│ └── .V1_1 (IGeoLookupApi) → GetCityGeoLocation, GetInsightsGeoLocation
├── .ApiInfo (IApiInfoApi) → GetApiInfo
└── .ApiHealth (IApiHealthApi) → CheckHealth
├── .ApiInfo (IVersionedApiInfoApi)
│ └── .V1 (IApiInfoApi) → GetApiInfo
└── .ApiHealth (IVersionedApiHealthApi)
└── .V1 (IApiHealthApi) → CheckHealth
```

Without the testing package, each test needs 3+ levels of nested mocks just to call a single method. Additionally, all DTO properties use `internal set`, so external consumers cannot construct DTOs with custom values.
Expand Down

This file was deleted.

10 changes: 1 addition & 9 deletions src/MX.GeoLocation.Abstractions.V1/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,7 @@ public interface IGeoLookupApi

### IVersionedGeoLookupApi

Wraps both API versions for version-aware consumers:

```csharp
public interface IVersionedGeoLookupApi
{
V1.IGeoLookupApi V1 { get; }
V1_1.IGeoLookupApi V1_1 { get; }
}
```
> **Note:** Version selector interfaces (`IVersionedGeoLookupApi`, `IVersionedApiHealthApi`, `IVersionedApiInfoApi`) are defined in the `MX.GeoLocation.Api.Client.V1` package, not in this abstractions package.

## Data Models

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public async Task ApiInfo_DelegatesToInfoFake()
var client = new FakeGeoLocationApiClient();
client.InfoApi.WithInfo(GeoLocationDtoFactory.CreateApiInfo(buildVersion: "2.0.0.1"));

var result = await client.ApiInfo.GetApiInfo();
var result = await client.ApiInfo.V1.GetApiInfo();

Assert.Equal("2.0.0.1", result.Result!.Data!.BuildVersion);
}
Expand All @@ -49,7 +49,7 @@ public async Task ApiHealth_DelegatesToHealthFake()
var client = new FakeGeoLocationApiClient();
client.HealthApi.WithStatusCode(System.Net.HttpStatusCode.ServiceUnavailable);

var result = await client.ApiHealth.CheckHealth();
var result = await client.ApiHealth.V1.CheckHealth();

Assert.Equal(System.Net.HttpStatusCode.ServiceUnavailable, result.StatusCode);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ public void AddFakeGeoLocationApiClient_RegistersAllServices()

Assert.NotNull(provider.GetRequiredService<IGeoLocationApiClient>());
Assert.NotNull(provider.GetRequiredService<IVersionedGeoLookupApi>());
Assert.NotNull(provider.GetRequiredService<IVersionedApiHealthApi>());
Assert.NotNull(provider.GetRequiredService<IVersionedApiInfoApi>());
Assert.NotNull(provider.GetRequiredService<IGeoLookupApi>());
Assert.NotNull(provider.GetRequiredService<V1_1.IGeoLookupApi>());
Assert.NotNull(provider.GetRequiredService<IApiInfoApi>());
Expand Down
34 changes: 32 additions & 2 deletions src/MX.GeoLocation.Api.Client.Testing/FakeGeoLocationApiClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,32 @@

namespace MX.GeoLocation.Api.Client.Testing;

/// <summary>
/// In-memory fake of <see cref="IVersionedApiHealthApi"/> for tests.
/// </summary>
public class FakeVersionedApiHealthApi : IVersionedApiHealthApi
{
public FakeVersionedApiHealthApi(FakeApiHealthApi v1)
{
V1 = v1;
}

public IApiHealthApi V1 { get; }
}

/// <summary>
/// In-memory fake of <see cref="IVersionedApiInfoApi"/> for tests.
/// </summary>
public class FakeVersionedApiInfoApi : IVersionedApiInfoApi
{
public FakeVersionedApiInfoApi(FakeApiInfoApi v1)
{
V1 = v1;
}

public IApiInfoApi V1 { get; }
}

/// <summary>
/// In-memory fake of <see cref="IVersionedGeoLookupApi"/> composing the V1 and V1.1 fakes.
/// </summary>
Expand Down Expand Up @@ -61,15 +87,19 @@ public class FakeGeoLocationApiClient : IGeoLocationApiClient
public FakeApiHealthApi HealthApi { get; } = new();

private readonly Lazy<FakeVersionedGeoLookupApi> _geoLookup;
private readonly Lazy<FakeVersionedApiHealthApi> _apiHealth;
private readonly Lazy<FakeVersionedApiInfoApi> _apiInfo;

public FakeGeoLocationApiClient()
{
_geoLookup = new Lazy<FakeVersionedGeoLookupApi>(() => new FakeVersionedGeoLookupApi(V1Lookup, V1_1Lookup));
_apiHealth = new Lazy<FakeVersionedApiHealthApi>(() => new FakeVersionedApiHealthApi(HealthApi));
_apiInfo = new Lazy<FakeVersionedApiInfoApi>(() => new FakeVersionedApiInfoApi(InfoApi));
}

public IVersionedGeoLookupApi GeoLookup => _geoLookup.Value;
public IApiInfoApi ApiInfo => InfoApi;
public IApiHealthApi ApiHealth => HealthApi;
public IVersionedApiInfoApi ApiInfo => _apiInfo.Value;
public IVersionedApiHealthApi ApiHealth => _apiHealth.Value;

/// <summary>
/// Resets all fakes to their initial state, clearing configured responses,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,17 @@ public static IServiceCollection AddFakeGeoLocationApiClient(

services.RemoveAll<IGeoLocationApiClient>();
services.RemoveAll<IVersionedGeoLookupApi>();
services.RemoveAll<IVersionedApiHealthApi>();
services.RemoveAll<IVersionedApiInfoApi>();
services.RemoveAll<IGeoLookupApi>();
services.RemoveAll<V1_1.IGeoLookupApi>();
services.RemoveAll<IApiInfoApi>();
services.RemoveAll<IApiHealthApi>();

services.AddSingleton<IGeoLocationApiClient>(fakeClient);
services.AddSingleton<IVersionedGeoLookupApi>(fakeClient.GeoLookup);
services.AddSingleton<IVersionedApiHealthApi>(fakeClient.ApiHealth);
services.AddSingleton<IVersionedApiInfoApi>(fakeClient.ApiInfo);
services.AddSingleton<IGeoLookupApi>(fakeClient.V1Lookup);
services.AddSingleton<V1_1.IGeoLookupApi>(fakeClient.V1_1Lookup);
services.AddSingleton<IApiInfoApi>(fakeClient.InfoApi);
Expand Down
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,36 @@

namespace MX.GeoLocation.Api.Client.V1
{
/// <summary>
/// Provides version-specific access to the GeoLookup API endpoints
/// </summary>
// Implementation classes for version selectors
public class VersionedGeoLookupApi : IVersionedGeoLookupApi
{
/// <summary>
/// Initializes a new instance of the <see cref="VersionedGeoLookupApi"/> class
/// </summary>
public VersionedGeoLookupApi(IGeoLookupApi v1, Abstractions.Interfaces.V1_1.IGeoLookupApi v1_1)
{
V1 = v1;
V1_1 = v1_1;
}

/// <summary>
/// Gets the V1 GeoLookup API implementation
/// </summary>
public IGeoLookupApi V1 { get; }

/// <summary>
/// Gets the V1.1 GeoLookup API implementation
/// </summary>
public Abstractions.Interfaces.V1_1.IGeoLookupApi V1_1 { get; }
}

public class VersionedApiHealthApi : IVersionedApiHealthApi
{
public VersionedApiHealthApi(IApiHealthApi v1)
{
V1 = v1;
}

public IApiHealthApi V1 { get; }
}

public class VersionedApiInfoApi : IVersionedApiInfoApi
{
public VersionedApiInfoApi(IApiInfoApi v1)
{
V1 = v1;
}

public IApiInfoApi V1 { get; }
}
}
22 changes: 22 additions & 0 deletions src/MX.GeoLocation.Api.Client.V1/ApiVersionSelectors.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using MX.GeoLocation.Abstractions.Interfaces;
using MX.GeoLocation.Abstractions.Interfaces.V1;

namespace MX.GeoLocation.Api.Client.V1
{
// Version selectors for GeoLookup API (V1 and V1.1)
public interface IVersionedGeoLookupApi
{
IGeoLookupApi V1 { get; }
Abstractions.Interfaces.V1_1.IGeoLookupApi V1_1 { get; }
}

public interface IVersionedApiHealthApi
{
IApiHealthApi V1 { get; }
}

public interface IVersionedApiInfoApi
{
IApiInfoApi V1 { get; }
}
}
18 changes: 8 additions & 10 deletions src/MX.GeoLocation.Api.Client.V1/GeoLocationApiClient.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
using MX.GeoLocation.Abstractions.Interfaces;

namespace MX.GeoLocation.Api.Client.V1
namespace MX.GeoLocation.Api.Client.V1
{
/// <summary>
/// Implementation of the GeoLocation API client that provides access to versioned API endpoints
Expand All @@ -11,9 +9,9 @@ public class GeoLocationApiClient : IGeoLocationApiClient
/// Initializes a new instance of the <see cref="GeoLocationApiClient"/> class
/// </summary>
/// <param name="versionedGeoLookupApi">The versioned GeoLookup API</param>
/// <param name="apiInfoApi">The API info endpoint</param>
/// <param name="apiHealthApi">The API health endpoint</param>
public GeoLocationApiClient(IVersionedGeoLookupApi versionedGeoLookupApi, IApiInfoApi apiInfoApi, IApiHealthApi apiHealthApi)
/// <param name="apiInfoApi">The versioned API info endpoint</param>
/// <param name="apiHealthApi">The versioned API health endpoint</param>
public GeoLocationApiClient(IVersionedGeoLookupApi versionedGeoLookupApi, IVersionedApiInfoApi apiInfoApi, IVersionedApiHealthApi apiHealthApi)
{
GeoLookup = versionedGeoLookupApi;
ApiInfo = apiInfoApi;
Expand All @@ -26,13 +24,13 @@ public GeoLocationApiClient(IVersionedGeoLookupApi versionedGeoLookupApi, IApiIn
public IVersionedGeoLookupApi GeoLookup { get; }

/// <summary>
/// Gets the API info endpoint
/// Gets the versioned API info endpoint
/// </summary>
public IApiInfoApi ApiInfo { get; }
public IVersionedApiInfoApi ApiInfo { get; }

/// <summary>
/// Gets the API health endpoint
/// Gets the versioned API health endpoint
/// </summary>
public IApiHealthApi ApiHealth { get; }
public IVersionedApiHealthApi ApiHealth { get; }
}
}
12 changes: 5 additions & 7 deletions src/MX.GeoLocation.Api.Client.V1/IGeoLocationApiClient.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
using MX.GeoLocation.Abstractions.Interfaces;

namespace MX.GeoLocation.Api.Client.V1
namespace MX.GeoLocation.Api.Client.V1
{
/// <summary>
/// Interface for the GeoLocation API client
Expand All @@ -13,13 +11,13 @@ public interface IGeoLocationApiClient
IVersionedGeoLookupApi GeoLookup { get; }

/// <summary>
/// Gets the API info endpoint
/// Gets the versioned API info endpoint
/// </summary>
IApiInfoApi ApiInfo { get; }
IVersionedApiInfoApi ApiInfo { get; }

/// <summary>
/// Gets the API health endpoint
/// Gets the versioned API health endpoint
/// </summary>
IApiHealthApi ApiHealth { get; }
IVersionedApiHealthApi ApiHealth { get; }
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using Microsoft.Extensions.DependencyInjection;

using MX.GeoLocation.Api.Client.V1;
using MX.GeoLocation.Abstractions.Interfaces;
using MX.GeoLocation.Abstractions.Interfaces.V1;

Expand Down Expand Up @@ -35,10 +34,12 @@ public static IServiceCollection AddGeoLocationApiClient(
// Register API health endpoint
serviceCollection.AddTypedApiClient<IApiHealthApi, ApiHealthApi, GeoLocationApiClientOptions, GeoLocationApiOptionsBuilder>(configureOptions);

// Register versioned API wrapper
// Register version selectors as scoped
serviceCollection.AddScoped<IVersionedGeoLookupApi, VersionedGeoLookupApi>();
serviceCollection.AddScoped<IVersionedApiHealthApi, VersionedApiHealthApi>();
serviceCollection.AddScoped<IVersionedApiInfoApi, VersionedApiInfoApi>();

// Register main client
// Register the unified client as scoped
serviceCollection.AddScoped<IGeoLocationApiClient, GeoLocationApiClient>();

return serviceCollection;
Expand Down
2 changes: 2 additions & 0 deletions src/MX.GeoLocation.Api.V1/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,9 @@

app.MapControllers();

app.MapGet("/", () => Results.Ok()).ExcludeFromDescription();

app.Run();

Check warning on line 139 in src/MX.GeoLocation.Api.V1/Program.cs

View workflow job for this annotation

GitHub Actions / quality / Code Quality

Await RunAsync instead. (https://rules.sonarsource.com/csharp/RSPEC-6966)

// Make Program accessible for WebApplicationFactory in integration tests
public partial class Program { }

Check warning on line 142 in src/MX.GeoLocation.Api.V1/Program.cs

View workflow job for this annotation

GitHub Actions / quality / Code Quality

Add a 'protected' constructor or the 'static' keyword to the class declaration. (https://rules.sonarsource.com/csharp/RSPEC-1118)
35 changes: 21 additions & 14 deletions terraform/azurerm_api_management_product_policy.tf
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,27 @@ resource "azurerm_api_management_product_policy" "api_product_policy" {
<inbound>
<base/>
<cache-lookup vary-by-developer="false" vary-by-developer-groups="false" downstream-caching-type="none" />
<validate-jwt header-name="Authorization" failed-validation-httpcode="401" failed-validation-error-message="JWT validation was unsuccessful" require-expiration-time="true" require-scheme="Bearer" require-signed-tokens="true">
<openid-config url="https://login.microsoftonline.com/${data.azuread_client_config.current.tenant_id}/v2.0/.well-known/openid-configuration" />
<audiences>
<audience>${local.entra_api_identifier_uri}</audience>
</audiences>
<issuers>
<issuer>https://sts.windows.net/${data.azuread_client_config.current.tenant_id}/</issuer>
</issuers>
<required-claims>
<claim name="roles" match="any">
<value>LookupApiUser</value>
</claim>
</required-claims>
</validate-jwt>
<choose>
<when condition="@(System.Text.RegularExpressions.Regex.IsMatch(context.Request.Url.Path, @"/v\d+(\.\d+)?/(health|info)$"))">
<!-- Allow anonymous access to health and info endpoints -->
</when>
<otherwise>
<validate-jwt header-name="Authorization" failed-validation-httpcode="401" failed-validation-error-message="JWT validation was unsuccessful" require-expiration-time="true" require-scheme="Bearer" require-signed-tokens="true">
<openid-config url="https://login.microsoftonline.com/${data.azuread_client_config.current.tenant_id}/v2.0/.well-known/openid-configuration" />
<audiences>
<audience>${local.entra_api_identifier_uri}</audience>
</audiences>
<issuers>
<issuer>https://sts.windows.net/${data.azuread_client_config.current.tenant_id}/</issuer>
</issuers>
<required-claims>
<claim name="roles" match="any">
<value>LookupApiUser</value>
</claim>
</required-claims>
</validate-jwt>
</otherwise>
</choose>
</inbound>
<backend>
<forward-request />
Expand Down
Loading