refactor: strip WP-/RB- ticket refs from backend (RD-19)

The backend half of the sweep RD-18 did for the front end. git blame
holds the provenance and stays correct when the code moves; the
comment names a closed ticket and tells the reader nothing the
sentence around it does not.

public/letter.css and LetterHtml.golden.html change together, because
the renderer inlines the CSS and the golden file snapshots the
result.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
eho
2026-09-04 21:48:08 +02:00
co-authored by Claude Sonnet 5
parent dd11eafe50
commit 8560746d15
89 changed files with 530 additions and 380 deletions
@@ -14,7 +14,7 @@ public class AanvraagTests(TestWebApplicationFactory factory) : IClassFixture<Te
private async Task<AanvraagDetailDto> Create(string type = "registratie")
{
// WP-35: one Concept per type is now server-enforced, and these tests share one DB
// One Concept per type is now server-enforced, and these tests share one DB
// (IClassFixture). Clear any leftover Concept so each test starts from a clean slate.
var existing = await List();
Assert.NotNull(existing);
@@ -103,7 +103,7 @@ public class AanvraagTests(TestWebApplicationFactory factory) : IClassFixture<Te
Assert.Equal(HttpStatusCode.Conflict, again.StatusCode);
}
// --- WP-35: one Concept per case type (server-enforced) ---
// --- One Concept per case type (server-enforced) ---
[Fact]
public async Task Creating_a_second_concept_of_the_same_type_conflicts()
@@ -146,7 +146,7 @@ public class AanvraagTests(TestWebApplicationFactory factory) : IClassFixture<Te
Assert.Equal(HttpStatusCode.Conflict, (await _client.DeleteAsync($"/api/v1/aanvragen/{a.Id}")).StatusCode);
}
// --- WP-53: citizen-scoping — GET /aanvragen must never leak across identities. ---
// --- Citizen-scoping — GET /aanvragen must never leak across identities. ---
[Fact]
public async Task Applications_are_scoped_to_the_caller_bsn()
@@ -181,7 +181,7 @@ public class AanvraagTests(TestWebApplicationFactory factory) : IClassFixture<Te
}
}
// --- WP-68 (F1): a citizen may only reference their own uploads — submit/draft-sync must
// --- A citizen may only reference their own uploads — submit/draft-sync must
// reject a foreign documentId rather than silently attaching it. ---
private static async Task<UploadResponse> UploadAs(HttpClient client, string owner, string localId)
@@ -9,8 +9,8 @@ using BigRegister.Tests.Builders;
namespace BigRegister.Tests.Acceptance;
/// <summary>
/// Behaviour-level tests for the besluit lifecycle (WP-65b/66/68), built through the
/// <see cref="Given"/> type-state builder (WP-70) rather than the full wizard/upload dance
/// Behaviour-level tests for the besluit lifecycle, built through the
/// <see cref="Given"/> type-state builder rather than the full wizard/upload dance
/// <see cref="BeoordelingTests"/> uses — a fixture that's already Submitted (or already
/// Decided) is a two-line Given, not fifteen. Each test persists its own Given-built
/// <see cref="Aanvraag"/> straight into the isolated per-class SQLite file (no HTTP round trip
@@ -8,8 +8,8 @@ using BigRegister.Tests.Builders;
namespace BigRegister.Tests.Acceptance;
/// <summary>
/// Behaviour-level tests for the scholing-threshold enforcement (WP-69) over
/// <c>POST /aanvragen/{id}/submit</c> (the wizard's real path — WP-72 deleted the legacy
/// Behaviour-level tests for the scholing-threshold enforcement over
/// <c>POST /aanvragen/{id}/submit</c> (the wizard's real path — a later change deleted the legacy
/// <c>POST /intakes</c> endpoint this once also covered). Built through the <see
/// cref="Given"/> type-state builder, mirroring <see cref="BesluitLifecycleTests"/> rather
/// than the full wizard/upload dance — the builder's default owner IS <see
@@ -5,7 +5,7 @@ using BigRegister.Api.Contracts;
namespace BigRegister.Tests.Acceptance;
/// <summary>
/// Contract test for the FE/BE seam on phone-number stripping (WP-75). Both sides share
/// Contract test for the FE/BE seam on phone-number stripping. Both sides share
/// the same format regex (<c>^0\d{9}$</c>) but, until this test, diverged on what they
/// strip before checking it: the FE's <c>parseTelefoonnummer</c>
/// (registratie/domain/value-objects/telefoonnummer.ts) also drops parentheses and maps a
@@ -6,7 +6,7 @@ using Microsoft.AspNetCore.Mvc.Testing;
namespace BigRegister.Tests;
/// WP-36: admin cross-owner case list + admin delete, gated by `cases:manage`.
/// Admin cross-owner case list + admin delete, gated by `cases:manage`.
public class AdminCasesTests(TestWebApplicationFactory factory) : IClassFixture<TestWebApplicationFactory>
{
private readonly HttpClient _client = factory.CreateClient();
@@ -35,7 +35,7 @@ public class AdminCasesTests(TestWebApplicationFactory factory) : IClassFixture<
list.EnsureSuccessStatusCode();
var cases = (await list.Content.ReadFromJsonAsync<List<AanvraagSummaryDto>>())!;
var mine = cases.Single(x => x.Id == a.Id);
// RB-03/BIO-003: the owner is carried, but masked — it is a BSN, and this list is
// BIO-003: the owner is carried, but masked — it is a BSN, and this list is
// read by someone who is not the subject.
Assert.Equal("******782", mine.Owner);
Assert.DoesNotContain(DocumentStore.DemoOwner, mine.Owner);
@@ -9,7 +9,7 @@ using Microsoft.AspNetCore.Mvc.Testing;
namespace BigRegister.Tests;
/// WP-41: the persisted authz/PII-reveal audit trail is queryable, data-minimised (no PII).
/// The persisted authz/PII-reveal audit trail is queryable, data-minimised (no PII).
public class AuthzAuditTests(TestWebApplicationFactory factory) : IClassFixture<TestWebApplicationFactory>
{
private readonly HttpClient _client = factory.CreateClient();
@@ -59,7 +59,7 @@ public class AuthzAuditTests(TestWebApplicationFactory factory) : IClassFixture<
Assert.Contains(await AuditLog(), e => e.Action == "brief:reveal-bignummer");
}
/// RB-07/BIO-007: the trail used to record only denials, so `/beheer/audit` could answer
/// BIO-007: the trail used to record only denials, so `/beheer/audit` could answer
/// "who was turned away" but not "who changed this" — for a register whose integrity is the
/// product, the wrong half. Every gate now audits the real decision.
[Fact]
@@ -69,10 +69,10 @@ public class AuthzAuditTests(TestWebApplicationFactory factory) : IClassFixture<
Assert.Contains(await AuditLog(), e => e.Action == "cases:manage" && e.Decision == "allow" && e.Role == "Admin");
}
/// RB-08/BIO-003: the admin upload delete used to be gated by a standalone X-Admin
/// BIO-003: the admin upload delete used to be gated by a standalone X-Admin
/// header, outside Authz and writing no AuthzAuditStore row at all. Routing it through
/// CasesAdmin (cases:manage) gives it the same allow-path row every other admin-cases
/// endpoint gets, for free, per RB-07. `CasesAdmin` audits under a fixed "cases"
/// endpoint gets, for free. `CasesAdmin` audits under a fixed "cases"
/// resource shared with the other admin-cases endpoints, so this asserts a **count**
/// increase — reading the store directly (not via `GET /admin/audit`, itself a
/// `CasesAdmin` endpoint that would write its own row and confound the count) —
@@ -120,7 +120,7 @@ public class AuthzAuditTests(TestWebApplicationFactory factory) : IClassFixture<
Assert.Contains(await AuditLog(), e => e.Action == "brief:submit" && e.Decision == "deny");
}
/// RB-02/BIO-008: the schema test below asserts on **column names**, so a BSN inside a
/// BIO-008: the schema test below asserts on **column names**, so a BSN inside a
/// column called `Resource` was invisible to it — and one was there, concatenated as
/// `"brief/" + Bsn`. This asserts on the stored **values** instead. Four documents
/// promise this trail holds no PII; this is the test that makes the promise checkable.
@@ -82,7 +82,7 @@ public class AuthzTests
Assert.False(Authz.Decisions(Approver, "draft", DrafterId).CanRevealBigNummer);
}
// --- CanBeoordelen (WP-62) --------------------------------------------------------------
// --- CanBeoordelen --------------------------------------------------------------
[Fact]
public void CanBeoordelen_true_for_a_medewerker_with_the_behandelaar_rol()
@@ -7,8 +7,8 @@ using Microsoft.AspNetCore.Mvc.Testing;
namespace BigRegister.Tests;
/// WP-65 (read side): one aanvraag's case-treatment detail, gated by the same medewerker
/// capability (`CanBeoordelen`, WP-62) as the werkvoorraad list (WP-64).
/// One aanvraag's case-treatment detail (read side), gated by the same medewerker
/// capability (`CanBeoordelen`) as the werkvoorraad list.
public class BeoordelingTests(TestWebApplicationFactory factory) : IClassFixture<TestWebApplicationFactory>
{
private readonly HttpClient _client = factory.CreateClient();
@@ -152,7 +152,7 @@ public class BeoordelingTests(TestWebApplicationFactory factory) : IClassFixture
Assert.Equal("Goedgekeurd", view.Aanvraag.Status.Tag);
Assert.False(view.Decisions.CanBesluiten); // terminal — no further decision allowed
// RB-07/BIO-007: the gate records that a behandelaar was allowed to act; this records
// BIO-007: the gate records that a behandelaar was allowed to act; this records
// what they decided, which is the question /beheer/audit exists to answer.
Assert.Contains(AuthzAuditStore.List(), e =>
e.Action == "aanvraag:besluit" && e.Decision == "allow" &&
@@ -222,7 +222,7 @@ public class BeoordelingTests(TestWebApplicationFactory factory) : IClassFixture
}
}
// WP-68 (F2): the transition-legality check now runs inside RecordBesluit's write lock, so
// The transition-legality check now runs inside RecordBesluit's write lock, so
// two besluiten racing on the same still-open aanvraag can't both pass the check before
// either writes — exactly one commits, the other sees the now-terminal status.
[Fact]
@@ -28,7 +28,7 @@ public class BriefEndpointTests(TestWebApplicationFactory factory) : IClassFixtu
return new SaveBriefRequest(sections);
}
/// RB-23: `GET /brief` no longer seeds a brief on first call, so every test that
/// `GET /brief` no longer seeds a brief on first call, so every test that
/// needs one present creates it explicitly through `POST /brief/reset`
/// (`BriefStore.ResetAndCreate`) — the same command the "start over" affordance uses.
private async Task<BriefDto> SeedBrief()
@@ -48,7 +48,7 @@ public class BriefEndpointTests(TestWebApplicationFactory factory) : IClassFixtu
return req;
}
// --- RB-23/CQ-007: GET /brief is a pure query — it must not create a row. ---
// --- CQ-007: GET /brief is a pure query — it must not create a row. ---
[Fact]
public async Task Get_returns_404_and_writes_no_row_when_no_brief_exists_for_the_owner()
@@ -181,8 +181,8 @@ public class BriefEndpointTests(TestWebApplicationFactory factory) : IClassFixtu
Assert.NotNull(submitted);
Assert.Equal("submitted", submitted.Brief.Status.Tag);
// RB-07/BIO-007: the allow side of the transition leaves a row, not just a log line.
// Resource is the bare "brief" — never the owner's BSN (RB-02).
// BIO-007: the allow side of the transition leaves a row, not just a log line.
// Resource is the bare "brief" — never the owner's BSN.
Assert.Contains(AuthzAuditStore.List(),
e => e.Action == "brief:submit" && e.Decision == "allow" && e.Resource == "brief");
}
@@ -14,9 +14,9 @@ public static class TestIdentities
}
/// <summary>
/// Type-state test-data builder for <see cref="Aanvraag"/> (WP-70; simplified at WP-73). "Build
/// Type-state test-data builder for <see cref="Aanvraag"/>. "Build
/// test data through the same door production code uses" — <see cref="Aanvraag"/> itself is now
/// the closed Concept/Submitted/Decided union WP-73 introduced, so this builder no longer needs
/// the closed Concept/Submitted/Decided union, so this builder no longer needs
/// to mirror production's guards (step-index bounds, "Afwijzen needs a toelichting") by hand —
/// it just calls the real nested constructors/required members, which enforce them. A call that
/// would build an illegal Aanvraag (e.g. deciding a still-Concept aanvraag, or an Afwijzen with
@@ -61,7 +61,7 @@ public sealed class ConceptAanvraag
/// Submits the draft — always assigns a Referentie AND SubmittedAt together (mirrors
/// <c>ApplicationStore.Submit</c>), so a fixture built this way can never hit the
/// null-forgiving derefs the pre-WP-73 flat Aanvraag needed (there's nothing to force any
/// null-forgiving derefs the earlier flat Aanvraag needed (there's nothing to force any
/// more: both are required, non-null members of <see cref="Aanvraag.Submitted"/>).
public SubmittedAanvraag Submitted(bool autoApprovable = false) => new(_type, _owner, autoApprovable);
@@ -110,7 +110,7 @@ public sealed class SubmittedAanvraag
return this;
}
/// <summary>Records a behandelaar's decision. Unlike the pre-WP-73 builder, there is no
/// <summary>Records a behandelaar's decision. Unlike the earlier builder, there is no
/// hand-written toelichting guard mirroring <c>BeoordelingRules.RequiresToelichting</c> any
/// more — <see cref="Aanvraag.Decided.Afgewezen"/>/<see cref="Aanvraag.Decided.MeerInfoGevraagd"/>
/// simply have a `required string Toelichting` member; the null-coalescing throw below is the
@@ -186,7 +186,7 @@ public sealed class SubmittedAanvraag
/// (<see cref="BigRegister.Domain.Beoordeling.BeoordelingRules.CanDecide"/>); a fixture that
/// needs a SECOND besluit (the MeerInfoGevraagd "still decidable" case) builds fresh from
/// <see cref="Given.Concept"/> again, exactly as a real second request would. Just a one-line
/// wrapper around the already-fully-built <see cref="Aanvraag.Decided"/> value — WP-73 moved
/// wrapper around the already-fully-built <see cref="Aanvraag.Decided"/> value — construction moved
/// all the actual construction (and its invariant enforcement) into
/// <see cref="SubmittedAanvraag.Decided"/> itself, so there's nothing left for this type to do
/// except keep <c>.Decided(...).Build()</c> a valid two-call chain for the existing test
@@ -4,9 +4,9 @@ namespace BigRegister.Tests.Domain;
public class ApplicationRuleTests
{
// WP-63: the published lifecycle (ADR-0002) must name exactly these five tags, in this
// The published lifecycle (ADR-0002) must name exactly these five tags, in this
// order — ToStatusDto's string literals must keep matching Enum.ToString(), and Ingediend/
// MeerInfoGevraagd (unreachable until WP-65 adds the behandelaar transition) stay defined.
// MeerInfoGevraagd (unreachable until a later change adds the behandelaar transition) stay defined.
[Fact]
public void AanvraagStatusTag_covers_the_published_lifecycle()
{
@@ -16,7 +16,7 @@ public class BeoordelingRuleTests
public void Only_open_statuses_are_decidable(AanvraagStatusTag tag, bool expected) =>
Assert.Equal(expected, BeoordelingRules.CanDecide(tag));
// WP-68 (F6): the toelichting rule, moved here from an inline endpoint check.
// The toelichting rule, moved here from an inline endpoint check.
[Theory]
[InlineData(Besluit.Goedkeuren, false)]
[InlineData(Besluit.Afwijzen, true)]
@@ -24,12 +24,12 @@ public class BeoordelingRuleTests
public void Only_a_non_approval_requires_a_toelichting(Besluit besluit, bool expected) =>
Assert.Equal(expected, BeoordelingRules.RequiresToelichting(besluit));
// WP-68 (T3): the transition table at the AGGREGATE level, not just against a bare tag —
// The transition table at the AGGREGATE level, not just against a bare tag —
// an Aanvraag whose BesluitStatus already records a terminal decision computes a terminal
// StatusAt, and CanDecide refuses a further besluit regardless of which one. Pins the
// domain statement "Afgewezen/Goedgekeurd → no further besluit" independent of the
// endpoint's own (integration-level) Already_decided_case_rejects_a_further_besluit.
// WP-70: built via Given, not a hand-rolled Aanvraag literal — Decided(Besluit.Afwijzen) with
// Built via Given, not a hand-rolled Aanvraag literal — Decided(Besluit.Afwijzen) with
// no toelichting simply couldn't compile as a fixture here.
[Theory]
[InlineData(Besluit.Goedkeuren)]
@@ -4,7 +4,7 @@ namespace BigRegister.Tests.Domain;
public class IntakeRuleTests
{
// The arguments ARE the Given (WP-69/bdd.mdx) — these degenerate to When/Then.
// The arguments ARE the Given (bdd.mdx) — these degenerate to When/Then.
[Fact]
public void Below_threshold_with_no_answer_is_incomplete() =>
@@ -87,7 +87,7 @@ public class EndpointTests(TestWebApplicationFactory factory) : IClassFixture<Te
new { telefoon = "nope" });
Assert.Equal(HttpStatusCode.UnprocessableEntity, res.StatusCode);
// The Submit helper's rejection shape — was asserted through POST /registrations until
// RB-06 deleted it; /change-requests is the other endpoint on the same helper.
// that endpoint was deleted; /change-requests is the other endpoint on the same helper.
var contentType = res.Content.Headers.ContentType;
Assert.NotNull(contentType);
Assert.Contains("application/problem+json", contentType.ToString());
@@ -200,8 +200,8 @@ public class EndpointTests(TestWebApplicationFactory factory) : IClassFixture<Te
public async Task User_delete_blocked_with_409_once_linked_to_submission()
{
var doc = await Upload(Guid.NewGuid().ToString());
// Through the real submit path (RB-06 deleted POST /registrations, which was the only
// other caller of DocumentStore.Link and had no ownership guard on it).
// Through the real submit path (POST /registrations was the only other caller of
// DocumentStore.Link and had no ownership guard on it; it has since been deleted).
var created = await _client.PostAsJsonAsync("/api/v1/aanvragen", new { type = "registratie" });
var aanvraag = (await created.Content.ReadFromJsonAsync<AanvraagDetailDto>())!;
var submit = await _client.PostAsJsonAsync($"/api/v1/aanvragen/{aanvraag.Id}/submit",
@@ -213,7 +213,7 @@ public class EndpointTests(TestWebApplicationFactory factory) : IClassFixture<Te
[Fact]
public async Task Admin_delete_requires_admin_role()
{
// RB-08: routed through CasesAdmin (cases:manage), like the other admin-cases
// Routed through CasesAdmin (cases:manage), like the other admin-cases
// endpoints, not the standalone X-Admin header this used to accept.
var doc = await Upload(Guid.NewGuid().ToString());
Assert.Equal(HttpStatusCode.Forbidden, (await _client.DeleteAsync($"/api/v1/admin/uploads/{doc.DocumentId}")).StatusCode);
@@ -6,7 +6,7 @@ using Microsoft.AspNetCore.Mvc.Testing;
namespace BigRegister.Tests;
/// WP-47: runtime feature flags — catalog in code, admin-toggled, server-enforced.
/// Runtime feature flags — catalog in code, admin-toggled, server-enforced.
public class FeatureFlagTests(TestWebApplicationFactory factory) : IClassFixture<TestWebApplicationFactory>
{
private readonly HttpClient _client = factory.CreateClient();
@@ -45,7 +45,7 @@ public class IdempotencyTests(TestWebApplicationFactory factory) : IClassFixture
Assert.NotEqual(firstBody!.Referentie, secondBody!.Referentie);
}
// RB-18/BIO-018: IdempotencyStore used to key on the raw client-supplied header alone, so
// BIO-018: IdempotencyStore used to key on the raw client-supplied header alone, so
// caller B replaying caller A's Idempotency-Key got caller A's cached reference back —
// a cross-caller leak of a value caller B never submitted. The store now keys on
// "{SubjectId}:{idemKey}", so the same header value from two different callers is two
@@ -1,7 +1,7 @@
<!doctype html><html lang="nl"><head><meta charset="utf-8"><title>golden-brief-1</title><style>/* letter.css — the FE⇄BE letter-rendering CONTRACT (WP-24/WP-25).
<!doctype html><html lang="nl"><head><meta charset="utf-8"><title>golden-brief-1</title><style>/* letter.css — the FE⇄BE letter-rendering CONTRACT.
*
* One stylesheet, two consumers: the FE letter canvas loads it via <link>
* (index.html + Storybook preview-head), the backend HTML renderer (WP-25)
* (index.html + Storybook preview-head), the backend HTML renderer
* inlines this same file. Its class-parity test is the fence against drift.
*
* Class vocabulary: .letter, .letter__letterhead, .letter__body,
@@ -6,7 +6,7 @@ using BigRegister.Domain.Letters;
namespace BigRegister.Tests;
/// <summary>
/// WP-25's fence against drift between the backend renderer and the FE letter
/// The fence against drift between the backend renderer and the FE letter
/// canvas: a golden-file snapshot of a fixed brief + template, and a class-parity
/// check that every `letter`-prefixed class the renderer emits exists in the
/// shared `public/letter.css` contract. Neither test launches a browser.
@@ -6,7 +6,7 @@ using Microsoft.AspNetCore.Mvc.Testing;
namespace BigRegister.Tests;
/// WP-52: the inbound Notificaties (NRC) webhook — auth accept/reject + the audit trail it
/// The inbound Notificaties (NRC) webhook — auth accept/reject + the audit trail it
/// writes via AuthzAuditStore (no Principal exists for an NRC caller, so this doesn't go
/// through the Principal-shaped AuditAuthz helper the user-facing endpoints use).
public class NotificatieTests(TestWebApplicationFactory factory) : IClassFixture<TestWebApplicationFactory>
@@ -6,7 +6,7 @@ using BigRegister.Domain.Authorization;
namespace BigRegister.Tests;
/// <summary>
/// Exercises the OpenZaak document source against a stub HttpMessageHandler (WP-51): an
/// Exercises the OpenZaak document source against a stub HttpMessageHandler: an
/// upload registers a DRC enkelvoudiginformatieobject, and linking to a zaak POSTs a
/// zaakinformatieobject per document once a zaak URL is known.
/// </summary>
@@ -48,7 +48,7 @@ public class OpenZaakDocumentSourceTests
Assert.Equal("local-1", response.LocalId);
Assert.NotEmpty(response.DocumentId);
// Registered locally too (dual-write, same reasoning as CreateZaak/WP-50) — content
// Registered locally too (dual-write, same reasoning as CreateZaak) — content
// preview/download keeps working regardless of Zgw:Enabled.
var stored = DocumentStore.Get(response.DocumentId);
Assert.NotNull(stored);
@@ -59,7 +59,7 @@ public class OpenZaakDocumentSourceTests
Assert.Contains("123443210", body); // bronorganisatie
Assert.Contains("paspoort.pdf", body);
Assert.Contains(Convert.ToBase64String("%PDF-1.4 fake"u8.ToArray()), body); // inhoud
// WP-59: "identiteit" is mapped to "vertrouwelijk" in the confidentialiteit stamdata.
// "identiteit" is mapped to "vertrouwelijk" in the confidentialiteit stamdata.
Assert.Contains("\"vertrouwelijkheidaanduiding\":\"vertrouwelijk\"", body);
}
@@ -78,7 +78,7 @@ public class OpenZaakDocumentSourceTests
Assert.Contains("\"vertrouwelijkheidaanduiding\":\"openbaar\"", body);
}
// WP-60: once DocumentStore.Add has committed, a ZGW-side failure (config gap or transport)
// Once DocumentStore.Add has committed, a ZGW-side failure (config gap or transport)
// no longer throws — the local document is authoritative and DrcUrl stays null (the same
// detector LinkToZaak already skips on for pre-Zgw documents).
@@ -8,7 +8,7 @@ using Microsoft.AspNetCore.Mvc.Testing;
namespace BigRegister.Tests;
/// <summary>
/// WP-54: the one test that proves the BFF actually talks to a REAL OpenZaak — auth accepted,
/// The one test that proves the BFF actually talks to a REAL OpenZaak — auth accepted,
/// real response shapes, real pagination/zaaktype→aanvraag-type mapping — rather than the stub
/// HttpMessageHandler every other Zgw test (<see cref="ZgwZaakMapperTests"/>,
/// <see cref="OpenZaakZaakSourceTests"/>) uses. Requires the harness in <c>backend/openzaak/</c>
@@ -36,7 +36,7 @@ public class OpenZaakIntegrationTests
.UseSetting("Zgw:ClientId", "bigregister-test")
.UseSetting("Zgw:Secret", "bigregister-test-secret")
.UseSetting("Zgw:UserId", "bigregister-test")
.UseSetting("Zgw:UserRepresentation", "WP-54 integration test")
.UseSetting("Zgw:UserRepresentation", "OpenZaak integration test")
.UseSetting("Zgw:ZaaktypeUrls:herregistratie", zaaktypeUrl));
}
@@ -182,7 +182,7 @@ public class OpenZaakZaakSourceTests
Assert.Throws<InvalidOperationException>(() => source.CreateZaak(aanvraag, DateTimeOffset.UtcNow, caller));
}
// --- WP-66: besluit write (a status transition on an existing zaak) --------------------
// --- Besluit write (a status transition on an existing zaak) --------------------
[Fact]
public void RecordBesluit_posts_the_last_statustype_with_besluit_and_toelichting()
@@ -308,7 +308,7 @@ public class OpenZaakZaakSourceTests
Assert.Throws<InvalidOperationException>(() => source.RecordBesluit(aanvraag, Besluit.Goedkeuren, null, DateTimeOffset.UtcNow, caller));
}
// --- WP-60: bounded retry in ZgwHttpClient, exercised through the create-zaak write path ---
// --- Bounded retry in ZgwHttpClient, exercised through the create-zaak write path ---
private static (ZgwOptions options, Aanvraag.Submitted aanvraag, CallerIdentity caller) CreateZaakFixture()
{
@@ -7,7 +7,7 @@ using Microsoft.AspNetCore.Mvc.Testing;
namespace BigRegister.Tests;
/// <summary>
/// Org templates (WP-23): admin-only endpoints, draft→publish versioning, and the
/// Org templates: admin-only endpoints, draft→publish versioning, and the
/// sent-brief immutability invariant (pin at send, republish touches unsent only).
/// Same reset discipline as BriefEndpointTests — the stores are process-global.
/// </summary>
@@ -58,7 +58,7 @@ public class OrgTemplateEndpointTests(TestWebApplicationFactory factory) : IClas
public async Task Publish_increments_the_version()
{
ResetStores();
// One unsent brief for this sub-org (RB-23: GET no longer seeds — create explicitly).
// One unsent brief for this sub-org (GET no longer seeds — create explicitly).
await _client.PostAsync("/api/v1/brief/reset", null);
var res = await _client.SendAsync(Req(HttpMethod.Post, $"/api/v1/admin/org-template/{Registers}/publish", role: "admin"));
@@ -74,7 +74,7 @@ public class OrgTemplateEndpointTests(TestWebApplicationFactory factory) : IClas
public async Task Publish_appends_to_the_version_history()
{
ResetStores();
await _client.PostAsync("/api/v1/brief/reset", null); // RB-23: GET no longer seeds — create explicitly
await _client.PostAsync("/api/v1/brief/reset", null); // GET no longer seeds — create explicitly
var res = await _client.SendAsync(Req(HttpMethod.Post, $"/api/v1/admin/org-template/{Registers}/publish", role: "admin"));
res.EnsureSuccessStatusCode();
@@ -87,7 +87,7 @@ public class OrgTemplateEndpointTests(TestWebApplicationFactory factory) : IClas
public async Task Publish_counts_the_unsent_briefs_it_affects()
{
ResetStores();
// One unsent brief for this sub-org (RB-23: GET no longer seeds — create explicitly).
// One unsent brief for this sub-org (GET no longer seeds — create explicitly).
await _client.PostAsync("/api/v1/brief/reset", null);
var res = await _client.SendAsync(Req(HttpMethod.Post, $"/api/v1/admin/org-template/{Registers}/publish", role: "admin"));
@@ -154,7 +154,7 @@ public class OrgTemplateEndpointTests(TestWebApplicationFactory factory) : IClas
private async Task WalkBriefToSentThenRepublish()
{
ResetStores();
var resetRes = await _client.PostAsync("/api/v1/brief/reset", null); // RB-23: create explicitly
var resetRes = await _client.PostAsync("/api/v1/brief/reset", null); // create explicitly
var brief = (await resetRes.Content.ReadFromJsonAsync<BriefViewDto>())!.Brief;
var filled = brief.Sections
.Select(s => new LetterSectionDto(s.SectionKey, s.Title, s.Required,
@@ -211,7 +211,7 @@ public class OrgTemplateEndpointTests(TestWebApplicationFactory factory) : IClas
public async Task Admin_cannot_slip_into_the_brief_review_flow()
{
ResetStores();
var resetRes = await _client.PostAsync("/api/v1/brief/reset", null); // RB-23: create explicitly
var resetRes = await _client.PostAsync("/api/v1/brief/reset", null); // create explicitly
var brief = (await resetRes.Content.ReadFromJsonAsync<BriefViewDto>())!.Brief;
var filled = brief.Sections
.Select(s => new LetterSectionDto(s.SectionKey, s.Title, s.Required,
@@ -7,7 +7,7 @@ using Microsoft.AspNetCore.Mvc.Testing;
namespace BigRegister.Tests;
/// <summary>
/// WP-25: the two HTML preview endpoints. Both are excluded from the OpenAPI doc
/// The two HTML preview endpoints. Both are excluded from the OpenAPI doc
/// (see the drift check in the API-client generation step) — these tests hit them
/// as plain HTTP, the same way the hand-written FE fetch does.
/// </summary>
@@ -41,7 +41,7 @@ public class PreviewEndpointTests(TestWebApplicationFactory factory) : IClassFix
public async Task Preview_of_an_unsent_brief_renders_live_with_a_watermark()
{
ResetStores();
await _client.PostAsync("/api/v1/brief/reset", null); // RB-23: GET no longer seeds — create explicitly
await _client.PostAsync("/api/v1/brief/reset", null); // GET no longer seeds — create explicitly
var res = await _client.GetAsync("/api/v1/brief/preview");
res.EnsureSuccessStatusCode();
@@ -54,7 +54,7 @@ public class PreviewEndpointTests(TestWebApplicationFactory factory) : IClassFix
public async Task Preview_of_a_sent_brief_serves_the_archive_unchanged_after_a_republish()
{
ResetStores();
var resetRes = await _client.PostAsync("/api/v1/brief/reset", null); // RB-23: create explicitly
var resetRes = await _client.PostAsync("/api/v1/brief/reset", null); // create explicitly
var brief = (await resetRes.Content.ReadFromJsonAsync<BriefViewDto>())!.Brief;
await _client.PutAsJsonAsync("/api/v1/brief", FilledFrom(brief));
await _client.SendAsync(Req(HttpMethod.Post, "/api/v1/brief/submit"));
@@ -4,11 +4,11 @@ using Microsoft.Extensions.DependencyInjection;
namespace BigRegister.Tests;
/// RB-12/BIO-016 (BL-006 — "the backend has zero automated architecture enforcement"): the
/// BIO-016 (BL-006 — "the backend has zero automated architecture enforcement"): the
/// only thing that used to keep an admin-shaped endpoint behind `Authz` was a human noticing
/// in review. BIO-003 (`X-Admin`, a second gate outside `Authz`) and BIO-004 (two endpoints
/// with no gate at all) are exactly the failure mode this test is a safety net for — and it is
/// the safety net RB-19 (a 900-line `Program.cs` reorder) leans on, so its value is entirely in
/// the safety net a 900-line `Program.cs` reorder leans on, so its value is entirely in
/// being hard to fool.
///
/// Every mapped route must be accounted for exactly one of two ways:
@@ -48,14 +48,14 @@ public class RouteInventoryTests(TestWebApplicationFactory factory) : IClassFixt
new("GET", "/api/v1/duo/diplomas", "Static DUO reference fixture + manual-diploma policy — same for every caller."),
new("GET", "/api/v1/intake/policy", "Config VALUE shipped for instant FE feedback (ADR-0001); the server re-validates as authority."),
new("GET", "/api/v1/uploads/categories", "Static per-wizard category config, no PII, no per-caller distinction."),
new("GET", "/api/v1/flags", "Feature-flag catalog + state, readable by any principal by design (WP-47) — only the PUT toggle is admin-gated."),
new("GET", "/api/v1/flags", "Feature-flag catalog + state, readable by any principal by design — only the PUT toggle is admin-gated."),
new("GET", "/api/v1/me", "Reflects only the ACTING caller's own role-derived capabilities — no other caller's data to leak."),
// --- Citizen-submitted writes / ownership-scoped inline (ctx.Zorgverlener()/ctx.Caller()),
// not a role-only admin wrapper because the boundary is resource ownership, not a role. ---
new("POST", "/api/v1/change-requests", "Citizen submission; Submit() records outcome + idempotency, attributed to the acting caller."),
new("POST", "/api/v1/uploads", "Upload is attributed to ctx.Zorgverlener() as owner — there is no pre-existing resource to own yet."),
new("GET", "/api/v1/uploads/{documentId}/content", "Ownership-scoped inline (RB-01/BIO-004): owning citizen, or a behandelaar via Authz.CanBeoordelen."),
new("GET", "/api/v1/uploads/{documentId}/content", "Ownership-scoped inline (BIO-004): owning citizen, or a behandelaar via Authz.CanBeoordelen."),
new("GET", "/api/v1/uploads/status", "Ownership-scoped inline: DocumentStore.ByLocalIds filtered to ctx.Zorgverlener().Bsn."),
new("DELETE", "/api/v1/uploads/{documentId}", "Ownership-scoped inline: DocumentStore.DeleteOwned keyed by ctx.Zorgverlener().Bsn."),
new("GET", "/api/v1/aanvragen", "Ownership-scoped inline: IZaakSource.ListMyCases(ctx.Zorgverlener(), ...)."),
@@ -72,14 +72,14 @@ public class RouteInventoryTests(TestWebApplicationFactory factory) : IClassFixt
// enforce/emit twin for this whole surface (Authz.CanActOn via BriefStore, ToView's
// Decisions dto) — a different single-source-of-truth than the five Program.cs wrappers,
// not a missing one. ---
new("GET", "/api/v1/brief", "Ownership-scoped inline: BriefStore.Get(ctx.Zorgverlener().Bsn), 404 when absent (RB-23)."),
new("GET", "/api/v1/brief", "Ownership-scoped inline: BriefStore.Get(ctx.Zorgverlener().Bsn), 404 when absent."),
new("PUT", "/api/v1/brief", "Brief status-machine enforcement: BriefStore.Save + Authz.CanActOn (drafter-only)."),
new("POST", "/api/v1/brief/submit", "Brief status-machine enforcement: BriefStore.Submit + Authz.CanActOn."),
new("POST", "/api/v1/brief/approve", "Brief status-machine enforcement: BriefStore.Approve + Authz.CanActOn (approver != drafter)."),
new("POST", "/api/v1/brief/reject", "Brief status-machine enforcement: BriefStore.Reject + Authz.CanActOn."),
new("POST", "/api/v1/brief/send", "Brief status-machine enforcement: BriefStore.Send; not role-gated today, per the endpoint's own comment."),
new("POST", "/api/v1/brief/reveal-bignummer", "Own inline capability + step-up check (Authz.CanRevealBigNummer + X-Step-Up), audited directly."),
new("GET", "/api/v1/brief/preview", "Ownership-scoped inline: BriefStore.Get(ctx.Zorgverlener().Bsn), 404 when absent (RB-23); hand-written FE fetch."),
new("GET", "/api/v1/brief/preview", "Ownership-scoped inline: BriefStore.Get(ctx.Zorgverlener().Bsn), 404 when absent; hand-written FE fetch."),
new("POST", "/api/v1/brief/reset", "Deliberately unguarded demo affordance — the endpoint's own comment says so: 'showcase affordance only'."),
];
@@ -59,7 +59,7 @@ public class StamdataEndpointTests(TestWebApplicationFactory factory) : IClassFi
Assert.Empty(table.Rows);
}
/// RB-16/BIO-019: DateOnly.Parse used to throw FormatException on unparseable input,
/// BIO-019: DateOnly.Parse used to throw FormatException on unparseable input,
/// surfacing as an unhandled 500 instead of the 400-with-problem-details every other
/// bad-input check in this endpoint file returns.
[Fact]
@@ -12,8 +12,8 @@ namespace BigRegister.Tests;
/// </summary>
public class StamdataValidationTests
{
/// Declared references INTO stamdata keys — the FK-like invariants the build gate enforces
/// (WP-48). Add an entry when a consumer starts depending on a stamdata key; the gate then
/// Declared references INTO stamdata keys — the FK-like invariants the build gate enforces.
/// Add an entry when a consumer starts depending on a stamdata key; the gate then
/// fails a delete/rename/expire that orphans it. Resolvers use the "valid today" views, so
/// expiring a row (geldigTot in the past) that current data still references also fails —
/// which steers the editor toward closing validity only once nothing current relies on it.
@@ -23,7 +23,7 @@ public class StamdataValidationTests
private static readonly IReadOnlySet<string> BeroepCodes =
StamdataFile.Load<Beroep>("beroepen").Select(b => b.Code).ToHashSet(StringComparer.Ordinal);
// Every document category id that exists across any wizard (WP-59's confidentialiteit
// Every document category id that exists across any wizard (the confidentialiteit
// table points at these) — "org-logo" resolves too, even though it's deliberately absent
// from the confidentialiteit table itself (falls back to "openbaar").
private static readonly IReadOnlySet<string> DocumentCategoryIds = new[] { "registratie", "herregistratie", "org-template" }
@@ -38,7 +38,7 @@ public class StamdataValidationTests
SeedData.Diplomas.Select(d => d.Opleiding),
key => Professions.ByProgram.ContainsKey(key)),
// Stamdata → stamdata references: two tables point at beroepen.code, so deleting or
// renaming a beroep that either still uses fails the build (WP-48 gate, generalized).
// renaming a beroep that either still uses fails the build (a stamdata gate, generalized).
new StamdataRef(
"Opleiding.beroep → beroepen.code",
StamdataFile.Load<Opleiding>("opleidingen").Select(o => o.Beroep),
@@ -47,7 +47,7 @@ public class StamdataValidationTests
"Specialisme.beroep → beroepen.code",
StamdataFile.Load<Specialisme>("specialismen").Select(s => s.Beroep),
key => BeroepCodes.Contains(key)),
// WP-59: a confidentialiteit row for a category that no wizard ever asks for is dead
// A confidentialiteit row for a category that no wizard ever asks for is dead
// config — fail the build rather than let it silently rot.
new StamdataRef(
"DocumentConfidentialiteit.CategoryId → a real document category",
@@ -6,10 +6,10 @@ using Microsoft.AspNetCore.Mvc.Testing;
namespace BigRegister.Tests;
/// WP-53 (extended WP-62): the dev stub identity provider — role from X-Role (unchanged
/// The dev stub identity provider — role from X-Role (unchanged
/// behaviour, applies to either actor kind), subject BSN from X-Subject defaulting to the
/// single seeded citizen so every existing request (none of which send X-Subject) resolves
/// exactly as before this WP. X-Medewerker (+ X-Rollen) selects the medewerker actor kind.
/// exactly as before. X-Medewerker (+ X-Rollen) selects the medewerker actor kind.
public class StubIdentityProviderTests
{
private static CallerIdentity Resolve(
@@ -96,7 +96,7 @@ public class StubIdentityProviderTests
Assert.Equal(PrincipalRole.Admin, caller.Role);
}
/// RB-09/BIO-002: IIdentityProvider.Resolve can now return null ("no identity"), but this
/// BIO-002: IIdentityProvider.Resolve can now return null ("no identity"), but this
/// stub's own contract stays non-nullable — it is a developer convenience that always invents
/// a caller, never a source of "no identity" itself. A request with genuinely no headers at
/// all still resolves to the seeded citizen, unchanged.
@@ -107,7 +107,7 @@ public class StubIdentityProviderTests
}
}
/// RB-09/BIO-002: in Production, StubIdentityProvider is not registered at all (it is
/// BIO-002: in Production, StubIdentityProvider is not registered at all (it is
/// Development-only) and there is no real DigiD/employee-SSO IIdentityProvider in this POC yet —
/// so a Production build must fail at startup rather than silently resolving every request to
/// the seeded citizen (the failure mode BIO-002 documents).
@@ -6,7 +6,7 @@ using Microsoft.Extensions.DependencyInjection;
namespace BigRegister.Tests;
/// RB-15/BIO-015: `app.UseSwagger()`/`app.UseSwaggerUI()` used to run unconditionally — the
/// BIO-015: `app.UseSwagger()`/`app.UseSwaggerUI()` used to run unconditionally — the
/// OpenAPI document (every route + request/response shape) and SwaggerUI's "Try it out" were
/// reachable in every environment, including a real deployment. Both are now gated behind
/// `app.Environment.IsDevelopment()`.
@@ -16,26 +16,26 @@ public class SwaggerGateTests(TestWebApplicationFactory factory) : IClassFixture
public async Task Swagger_document_is_served_in_development()
{
// The default test environment (WebApplicationFactory<T> defaults to "Development" when
// nothing overrides it — same fact RB-09's implementation note relies on) — this is the
// nothing overrides it — same fact a related implementation note relies on) — this is the
// regression guard that the gate didn't also break the documented `npm run gen:api` /
// local-dev-Swagger-UI experience.
var res = await factory.CreateClient().GetAsync("/swagger/v1/swagger.json");
Assert.Equal(HttpStatusCode.OK, res.StatusCode);
}
/// Production cannot boot at all today (RB-09: no real IIdentityProvider exists yet), which
/// Production cannot boot at all today (no real IIdentityProvider exists yet), which
/// is a *stronger* guarantee than "no Swagger in Production" — but it also means a plain
/// `UseEnvironment("Production")` host never reaches this middleware to prove the gate
/// itself works, only that the whole app refuses to start. This uses a third environment
/// name (neither "Development" nor "Production") with a test-supplied `IIdentityProvider` —
/// the one thing Program.cs doesn't register outside those two branches — so the host
/// actually boots and this test exercises the real gate, not RB-09's unrelated startup throw.
/// actually boots and this test exercises the real gate, not that unrelated startup throw.
[Fact]
public async Task Swagger_document_is_not_served_outside_development()
{
// Built on top of the shared `factory` fixture (via WithWebHostBuilder), not a bare `new
// WebApplicationFactory<Program>()` — that keeps this host on the fixture's own per-class
// isolated AppDb temp path (see TestWebApplicationFactory's doc comment; RB-12's
// isolated AppDb temp path (see TestWebApplicationFactory's doc comment; an earlier
// implementation note records the "table already exists" collision a bare factory hits
// by sharing the mutable static Db.ConnectionString instead).
using var staging = factory.WithWebHostBuilder(builder => builder
@@ -1,8 +1,8 @@
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
// WP-22's stores read a single static Db.ConnectionString (there's no DI, matching
// their pre-WP-22 static-Dictionary shape — see Data/Db.cs). That's correct for a
// These stores read a single static Db.ConnectionString (there's no DI, matching
// their earlier static-Dictionary shape — see Data/Db.cs). That's correct for a
// real single-instance process, but xUnit's default parallel-across-classes
// execution would run multiple WebApplicationFactory hosts concurrently in this
// ONE test process, each overwriting that same static field with its own temp-file
@@ -14,7 +14,7 @@ using Microsoft.AspNetCore.Mvc.Testing;
namespace BigRegister.Tests;
/// <summary>
/// WP-22 moved Applications/Documents/Briefs off in-memory dictionaries onto a real
/// A migration moved Applications/Documents/Briefs off in-memory dictionaries onto a real
/// SQLite file (see Data/Db.cs). Unlike static dictionaries, a shared file path
/// would let concurrent test classes' WebApplicationFactory instances hit the same
/// file at once — xUnit runs different test classes in parallel by default, and
@@ -29,7 +29,7 @@ public sealed class TestWebApplicationFactory : WebApplicationFactory<Program>
protected override void ConfigureWebHost(IWebHostBuilder builder) => builder
.UseSetting("ConnectionStrings:AppDb", $"Data Source={_dbPath}")
// WP-52: a fixed shared secret so NotificatieTests can exercise the accept path —
// A fixed shared secret so NotificatieTests can exercise the accept path —
// the appsettings.json default is "" (reject everything), which no test should rely on.
.UseSetting("Zgw:NotificatieAuthorization", "test-nrc-secret");
@@ -7,10 +7,10 @@ using Microsoft.AspNetCore.Mvc.Testing;
namespace BigRegister.Tests;
/// Who may see what about an upload. RB-01/BIO-004: GET /uploads/{id}/content and
/// Who may see what about an upload. BIO-004: GET /uploads/{id}/content and
/// /uploads/status used to take no HttpContext at all — a diploma or identity scan was
/// protected by GUID unguessability alone, while DELETE on the same resource was
/// owner-scoped. RB-04/BIO-005: the document audit trail recorded the raw owner BSN as
/// owner-scoped. BIO-005: the document audit trail recorded the raw owner BSN as
/// its Actor, on a store whose own doc comment says it holds no PII.
public class UploadAccessTests(TestWebApplicationFactory factory) : IClassFixture<TestWebApplicationFactory>
{
@@ -5,8 +5,8 @@ using Microsoft.AspNetCore.Mvc.Testing;
namespace BigRegister.Tests;
/// WP-64: the behandelportal's queue of aanvragen needing treatment, gated by the
/// medewerker capability `CanBeoordelen` (WP-62) — not the admin role.
/// The behandelportal's queue of aanvragen needing treatment, gated by the
/// medewerker capability `CanBeoordelen` — not the admin role.
public class WerkvoorraadTests(TestWebApplicationFactory factory) : IClassFixture<TestWebApplicationFactory>
{
private readonly HttpClient _client = factory.CreateClient();
@@ -38,7 +38,7 @@ public class WerkvoorraadTests(TestWebApplicationFactory factory) : IClassFixtur
var queue = (await res.Content.ReadFromJsonAsync<List<AanvraagSummaryDto>>())!;
var mine = queue.Single(x => x.Id == a.Id);
Assert.Equal("InBehandeling", mine.Status.Tag);
// RB-03/BIO-003: masked, like /admin/cases — both inherit ToAdminSummaryDto.
// BIO-003: masked, like /admin/cases — both inherit ToAdminSummaryDto.
Assert.Equal("******782", mine.Owner);
}
finally
@@ -9,7 +9,7 @@ using Microsoft.Extensions.DependencyInjection;
namespace BigRegister.Tests;
/// <summary>
/// WP-60's required verification: a ZGW failure mid-submit must not leave the two write sides
/// The required verification: a ZGW failure mid-submit must not leave the two write sides
/// silently diverged — it's flagged (<see cref="Aanvraag.ZgwError"/>, an audit row) instead.
/// Not an <see cref="IClassFixture{TFixture}"/> off <see cref="TestWebApplicationFactory"/>: that
/// fixture hardcodes <c>Zgw:Enabled=false</c>, so this builds its own factory the same way
@@ -111,7 +111,7 @@ public class ZgwDivergenceTests
Assert.Null(stored.ZgwError);
}
/// RB-05/BIO-009: `ZgwError` is persisted to SQLite and written to the application log, so
/// BIO-009: `ZgwError` is persisted to SQLite and written to the application log, so
/// the message it carries may not include the response body (OpenZaak echoes the request in
/// its errors) or the request's query string (ZGW filters travel there, and one of them is
/// `rol__betrokkeneIdentificatie__natuurlijkPersoon__inpBsn`).
@@ -10,7 +10,7 @@ namespace BigRegister.Tests;
/// Factored out of OpenZaakZaakSourceTests once OpenZaakDocumentSourceTests needed the
/// identical stub.
///
/// WP-60: an optional <paramref name="status"/> callback lets a test inject a failing status
/// An optional <paramref name="status"/> callback lets a test inject a failing status
/// for a given url on a given (0-based) attempt — e.g. "503 on the first call to /zaken, then
/// let it through" — to exercise ZgwHttpClient's retry without a live server. When it returns
/// a non-2xx code, <paramref name="respond"/> is not called for that attempt (so a test that
@@ -59,7 +59,7 @@ public class ZgwTokenProviderTests
[Fact]
public void Mint_with_a_medewerker_caller_uses_the_medewerkerId_as_user_id()
{
// WP-62: SubjectId is what ZgwTokenProvider.Mint reads — a medewerker's is its
// SubjectId is what ZgwTokenProvider.Mint reads — a medewerker's is its
// medewerkerId, not a BSN, and this is the only place that's directly observable.
var caller = new MedewerkerCaller("m.jansen", [MedewerkerRol.Behandelaar], "M. Jansen", PrincipalRole.Drafter);
var token = new ZgwTokenProvider(Options).Mint(caller);