Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dynamic API client for tests #921

Merged
merged 1 commit into from
Nov 15, 2023
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
Original file line number Diff line number Diff line change
Expand Up @@ -46,19 +46,19 @@ protected override Task<AuthenticateResult> HandleAuthenticateAsync()
return Task.FromResult(AuthenticateResult.Fail($"No client found with specified API key."));
}

var principal = CreatePrincipal(client);
var principal = CreatePrincipal(client.ClientId);
var ticket = new AuthenticationTicket(principal, Scheme.Name);

LogContext.PushProperty("ClientId", client.ClientId);

return Task.FromResult(AuthenticateResult.Success(ticket));
}

private static ClaimsPrincipal CreatePrincipal(ApiClient client)
public static ClaimsPrincipal CreatePrincipal(string clientId)
{
var identity = new ClaimsIdentity(new[]
{
new Claim(ClaimTypes.Name, client.ClientId)
new Claim(ClaimTypes.Name, clientId)
});

return new ClaimsPrincipal(identity);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
{
"Platform": "Local",
"ApiClients": {
"tests": {
"ApiKey": [ "tests" ]
}
},
"Serilog": {
"MinimumLevel": {
"Default": "Error",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
using System.Security.Cryptography;
using JustEat.HttpClientInterception;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using TeachingRecordSystem.Api.Infrastructure.Security;
using TeachingRecordSystem.Api.Tests.Infrastructure.Security;
using TeachingRecordSystem.Core.Dqt;
using TeachingRecordSystem.Core.Services.AccessYourQualifications;
using TeachingRecordSystem.Core.Services.Certificates;
Expand Down Expand Up @@ -41,9 +44,16 @@ protected override void ConfigureWebHost(IWebHostBuilder builder)
{
DbHelper.ConfigureDbServices(services, context.Configuration.GetRequiredConnectionString("DefaultConnection"));

// Replace ApiKeyAuthenticationHandler with a mechanism we can control from tests
services.Configure<AuthenticationOptions>(options =>
{
options.SchemeMap[ApiKeyAuthenticationHandler.AuthenticationScheme].HandlerType = typeof(TestAuthenticationHandler);
});

// Add controllers defined in this test assembly
services.AddMvc().AddApplicationPart(typeof(ApiFixture).Assembly);

services.AddSingleton<CurrentApiClientProvider>();
services.AddTestScoped<IClock>(tss => tss.Clock);
services.AddTestScoped<IDataverseAdapter>(tss => tss.DataverseAdapterMock.Object);
services.AddTestScoped<IGetAnIdentityApiClient>(tss => tss.GetAnIdentityApiClientMock.Object);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using TeachingRecordSystem.Api.Infrastructure.Json;
using TeachingRecordSystem.Api.Tests.Infrastructure.Security;
using TeachingRecordSystem.Core.DataStore.Postgres;
using TeachingRecordSystem.Core.Dqt;
using TeachingRecordSystem.Core.Services.Certificates;
Expand All @@ -20,11 +21,11 @@ protected ApiTestBase(ApiFixture apiFixture)
{
ApiFixture = apiFixture;
_testServices = TestScopedServices.Reset();
SetCurrentApiClient("tests");

{
var key = apiFixture.Services.GetRequiredService<IConfiguration>()["ApiClients:tests:ApiKey:0"];
HttpClientWithApiKey = apiFixture.CreateClient();
HttpClientWithApiKey.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", key);
HttpClientWithApiKey.DefaultRequestHeaders.Add("X-Use-CurrentClientIdProvider", "true"); // Signal for TestAuthenticationHandler to run
}
}

Expand Down Expand Up @@ -79,6 +80,12 @@ public HttpClient GetHttpClientWithIdentityAccessToken(string trn, string scope
return httpClient;
}

protected void SetCurrentApiClient(string clientId)
{
var currentUserProvider = ApiFixture.Services.GetRequiredService<CurrentApiClientProvider>();
currentUserProvider.CurrentApiClientId = clientId;
}

public virtual async Task<T> WithDbContext<T>(Func<TrsDbContext, Task<T>> action)
{
var dbContextFactory = ApiFixture.Services.GetRequiredService<IDbContextFactory<TrsDbContext>>();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
using System.Diagnostics.CodeAnalysis;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Options;
using TeachingRecordSystem.Api.Infrastructure.Security;

namespace TeachingRecordSystem.Api.Tests.Infrastructure.Security;

public class TestAuthenticationHandler : AuthenticationHandler<TestAuthenticationOptions>
{
private readonly CurrentApiClientProvider _currentApiClientProvider;

public TestAuthenticationHandler(
CurrentApiClientProvider currentApiClientProvider,
IOptionsMonitor<TestAuthenticationOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock) :
base(options, logger, encoder, clock)
{
_currentApiClientProvider = currentApiClientProvider;
}

protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
if (!Request.Headers.TryGetValue("X-Use-CurrentClientIdProvider", out var useCurrentClientIdProvider) ||
useCurrentClientIdProvider != "true")
{
return Task.FromResult(AuthenticateResult.NoResult());
}

var currentApiClientId = _currentApiClientProvider.CurrentApiClientId;

if (currentApiClientId is not null)
{
var principal = ApiKeyAuthenticationHandler.CreatePrincipal(currentApiClientId);

var ticket = new AuthenticationTicket(principal, Scheme.Name);

return Task.FromResult(AuthenticateResult.Success(ticket));
}
else
{
return Task.FromResult(AuthenticateResult.NoResult());
}
}
}

public class TestAuthenticationOptions : AuthenticationSchemeOptions { }

public class CurrentApiClientProvider
{
private readonly AsyncLocal<string> _currentApiClientId = new();

[DisallowNull]
public string? CurrentApiClientId
{
get => _currentApiClientId.Value;
set => _currentApiClientId.Value = value;
}
}
Loading