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.JsonWebTokens; using Microsoft.IdentityModel.Protocols.OpenIdConnect; using Microsoft.IdentityModel.Tokens; namespace Acceptance.Support; /// Hosts the real BFF in-process for the acceptance scenario, with fake downstreams and a /// local test signing key so tokens can be minted without a live Keycloak (ADR-0010). public sealed class BffAcceptanceHost : WebApplicationFactory { private static readonly SymmetricSecurityKey TestKey = new(Encoding.UTF8.GetBytes("acceptance-bff-signing-key-at-least-256-bits-long!!")); public CapturingDomainClient Domain { get; } = new(); public StubProjectionClient Projection { get; } = new(); public static string MintToken(string bsn) => new JsonWebTokenHandler().CreateToken(new SecurityTokenDescriptor { Claims = new Dictionary { ["bsn"] = bsn }, Expires = DateTime.UtcNow.AddMinutes(30), SigningCredentials = new SigningCredentials(TestKey, SecurityAlgorithms.HmacSha256), }); 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(Domain); services.AddSingleton(Projection); services.Configure(JwtBearerDefaults.AuthenticationScheme, options => { 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 = TestKey, ClockSkew = TimeSpan.Zero, }; }); }); } } /// Records the bsn the BFF forwarded. public sealed class CapturingDomainClient : IDomainClient { public string? SubmittedBsn { get; private set; } public Task SubmitRegistrationAsync(string bsn, CancellationToken ct = default) { SubmittedBsn = bsn; return Task.FromResult(new SubmitAccepted("reg-acc-1", "Ingediend")); } } /// Serves configurable projection rows. public sealed class StubProjectionClient : IProjectionClient { public List Entries { get; } = []; public Task> GetRegisterAsync(CancellationToken ct = default) => Task.FromResult>(Entries); }