All checks were successful
## What & why
Second half of **S-12c** (behandel-portal backend), completing the decision path per **ADR-0013**:
- **Domain:** `BeoordeelRegistratie` now, after applying the decision (aggregate + ACL for approval), **completes the open Flowable `Beoordelen` task** for that registration (found by registrationId) with the besluit, so the workflow advances. No open task → the decision still stands (completes nothing); idempotent.
- **BFF:** `POST /behandel/registrations/{id}/decide` behind the medewerker/`behandelaar` policy, forwarding `goedkeuren`/`afwijzen` to the domain. Validates the besluit vocabulary (400 on unknown) without troubling the domain.
Behavior: decide is **401** without a token, **403** without the role, **400** for an unknown besluit, **204** (forwarded) for a behandelaar.
This completes the behandel backend. **S-12d** (the Angular behandel-portal + Playwright e2e) closes umbrella #13 and retires the temporary `/approve`.
## Definition of Done
- [x] Linked issue: #13 (umbrella, `refs`)
- [x] Tests first; red → green per layer
- [x] Unit + acceptance green (`make unit`): domain 79, bff 27, acceptance 9 (acl/event-subscriber unaffected)
- [x] Beoordeling acceptance scenario asserts task completion (goedkeuren + afwijzen)
- [x] openapi.json + api-client regenerated (drift guard passes)
- [x] Mutation ≥ break(90): **domain 100%, bff 100%**
- [ ] CI green (pending)
Part of #13.
Reviewed-on: #86
87 lines
3.5 KiB
C#
87 lines
3.5 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.JsonWebTokens;
|
|
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
|
|
using Microsoft.IdentityModel.Tokens;
|
|
|
|
namespace Acceptance.Support;
|
|
|
|
/// <summary>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).</summary>
|
|
public sealed class BffAcceptanceHost : WebApplicationFactory<Program>
|
|
{
|
|
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<string, object> { ["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<IDomainClient>(Domain);
|
|
services.AddSingleton<IProjectionClient>(Projection);
|
|
services.Configure<JwtBearerOptions>(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,
|
|
};
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
/// <summary>Records the bsn the BFF forwarded.</summary>
|
|
public sealed class CapturingDomainClient : IDomainClient
|
|
{
|
|
public string? SubmittedBsn { get; private set; }
|
|
|
|
public Task<SubmitAccepted> SubmitRegistrationAsync(string bsn, CancellationToken ct = default)
|
|
{
|
|
SubmittedBsn = bsn;
|
|
return Task.FromResult(new SubmitAccepted("reg-acc-1", "Ingediend"));
|
|
}
|
|
|
|
public Task<IReadOnlyList<WerkbakItem>> GetWerkbakAsync(CancellationToken ct = default)
|
|
=> Task.FromResult<IReadOnlyList<WerkbakItem>>([]);
|
|
|
|
public Task DecideAsync(string registrationId, string besluit, CancellationToken ct = default)
|
|
=> Task.CompletedTask;
|
|
}
|
|
|
|
/// <summary>Serves configurable projection rows.</summary>
|
|
public sealed class StubProjectionClient : IProjectionClient
|
|
{
|
|
public List<ProjectionEntry> Entries { get; } = [];
|
|
|
|
public Task<IReadOnlyList<ProjectionEntry>> GetRegisterAsync(CancellationToken ct = default)
|
|
=> Task.FromResult<IReadOnlyList<ProjectionEntry>>(Entries);
|
|
}
|