Files
register-referentie/services/bff/Bff.Tests/BffFactory.cs
Niek Otten 751ca006a7 test(bff): endpoints, JWT auth and public-safe projection (refs #8)
Failing tests for the BFF walking-skeleton endpoints:
- POST /self-service/registrations rejects missing/malformed/wrong-key/expired
  tokens (401) and, with a valid digid token, forwards the bsn to the domain and
  returns 202 (WebApplicationFactory + a local test signing key, ADR-0010).
- GET /openbaar/register serves public-safe rows anonymously (never the bsn) and
  filters by q.
- OpenbaarProjection.PublicView (pure) filters by id and maps to id+status only.

Endpoints and PublicView are stubs so the tests compile and fail on their
assertions; the green commit implements them.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 10:59:55 +02:00

79 lines
3.1 KiB
C#

using System.Text;
using Bff.Api;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Microsoft.IdentityModel.Tokens;
namespace Bff.Tests;
/// <summary>
/// Test host for the BFF. It swaps the downstream clients for in-memory fakes and reconfigures the
/// JWT bearer to validate against a local test key (no live Keycloak) — so token validation is
/// exercised in-process with tokens the tests mint (ADR-0010).
/// </summary>
internal sealed class BffFactory : WebApplicationFactory<Program>
{
public static readonly SymmetricSecurityKey TestSigningKey =
new(Encoding.UTF8.GetBytes("bff-test-signing-key-that-is-at-least-256-bits-long!"));
public FakeDomainClient Domain { get; } = new();
public FakeProjectionClient Projection { get; } = new();
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.UseSetting("Keycloak:Authority", "https://keycloak.invalid/realms/digid");
builder.UseSetting("Downstream:Domain:BaseUrl", "http://domain.invalid/");
builder.UseSetting("Downstream:Projection:BaseUrl", "http://projection.invalid/");
builder.ConfigureTestServices(services =>
{
services.AddSingleton<IDomainClient>(Domain);
services.AddSingleton<IProjectionClient>(Projection);
services.Configure<JwtBearerOptions>(JwtBearerDefaults.AuthenticationScheme, options =>
{
// Validate locally against the test key; never reach out for OIDC metadata.
options.Authority = null;
options.MetadataAddress = null!;
options.RequireHttpsMetadata = false;
options.Configuration = new OpenIdConnectConfiguration();
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false,
ValidateAudience = false,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
IssuerSigningKey = TestSigningKey,
ClockSkew = TimeSpan.Zero,
};
});
});
}
}
/// <summary>Captures the bsn the BFF forwarded and returns a canned acceptance.</summary>
internal sealed class FakeDomainClient : IDomainClient
{
public string? SubmittedBsn { get; private set; }
public SubmitAccepted Result { get; set; } = new("reg-123", "Ingediend");
public Task<SubmitAccepted> SubmitRegistrationAsync(string bsn, CancellationToken ct = default)
{
SubmittedBsn = bsn;
return Task.FromResult(Result);
}
}
/// <summary>Serves a configurable set of projection rows.</summary>
internal sealed class FakeProjectionClient : IProjectionClient
{
public List<ProjectionEntry> Entries { get; } = [];
public Task<IReadOnlyList<ProjectionEntry>> GetRegisterAsync(CancellationToken ct = default)
=> Task.FromResult<IReadOnlyList<ProjectionEntry>>(Entries);
}