test: close illegal-state escape hatches in spec type-safety (WP-71)

ESLint blanket-exempted every *.spec.ts from the any ban, and no gate
type-checked spec files at all (ng test is transpile-only), so a wrong
cast in a test could never fail the build. 76 `as any` + 12 `as
Extract<>` state-narrowing casts in the three biggest wizard specs read
one variant's fields off a whole-union value: if the reducer returned
the wrong variant, the assertion silently read undefined instead of
failing.

expectTag(state, tag) (libs/shared/src/testing/expect-tag.ts) asserts
and narrows in one call, replacing every one of those casts. Removes
the spec-file any exemption, adds `npm run typecheck` (tsc --noEmit
over each project's tsconfig.spec.json) to CI, and forbids production
code from importing libs/shared/src/testing via dependency-cruiser.
Backend: AanvraagBuilder now models ZaakUrl (closing the last
post-Build() mutation) and guards AtStep; null-forgiving `!` on
endpoint assertions replaced with Assert.NotNull so a null DTO fails by
name, not NullReferenceException.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
eho
2026-08-18 20:24:53 +02:00
co-authored by Claude Sonnet 5
parent 66224b1644
commit b937e55ad3
21 changed files with 536 additions and 286 deletions
@@ -32,7 +32,8 @@ public class BriefEndpointTests(TestWebApplicationFactory factory) : IClassFixtu
{
BriefStore.Reset();
var view = await _client.GetFromJsonAsync<BriefViewDto>("/api/v1/brief");
return view!.Brief;
Assert.NotNull(view);
return view.Brief;
}
private HttpRequestMessage Post(string path, string? role = null, object? body = null)
@@ -64,8 +65,9 @@ public class BriefEndpointTests(TestWebApplicationFactory factory) : IClassFixtu
{
await Get();
var view = await _client.GetFromJsonAsync<BriefViewDto>("/api/v1/brief");
Assert.NotNull(view);
// global passages + the arts-scoped one; no other-beroep passages leak in.
Assert.Contains(view!.AvailablePassages, p => p.PassageId == "p-kern-arts");
Assert.Contains(view.AvailablePassages, p => p.PassageId == "p-kern-arts");
Assert.All(view.AvailablePassages, p => Assert.True(p.Scope == "global" || p.Beroep == "arts"));
// Guided-drafting tags (WP-brief-v3): positief + negatief + reason-specific negatief.
@@ -78,9 +80,10 @@ public class BriefEndpointTests(TestWebApplicationFactory factory) : IClassFixtu
{
await Get();
var view = await _client.GetFromJsonAsync<BriefViewDto>("/api/v1/brief");
Assert.NotNull(view);
// Case context is joined onto the screen DTO for the behandel scherm header.
// The BIG-nummer ships MASKED by default (PRD-0002 §5c) — reveal is a separate call.
Assert.Equal("********601", view!.CaseContext.BigNummer);
Assert.Equal("********601", view.CaseContext.BigNummer);
Assert.Equal("arts", view.CaseContext.Beroep);
Assert.False(string.IsNullOrWhiteSpace(view.CaseContext.ZorgverlenerNaam));
Assert.False(string.IsNullOrWhiteSpace(view.CaseContext.AanvraagReferentie));
@@ -98,7 +101,8 @@ public class BriefEndpointTests(TestWebApplicationFactory factory) : IClassFixtu
Assert.Equal(HttpStatusCode.OK, res.StatusCode);
var body = await res.Content.ReadFromJsonAsync<RevealBigNummerResponse>();
Assert.Equal("19012345601", body!.BigNummer);
Assert.NotNull(body);
Assert.Equal("19012345601", body.BigNummer);
}
[Fact]
@@ -147,13 +151,15 @@ public class BriefEndpointTests(TestWebApplicationFactory factory) : IClassFixtu
public async Task Submit_succeeds_when_required_sections_filled()
{
await Get();
var brief = (await _client.GetFromJsonAsync<BriefViewDto>("/api/v1/brief"))!.Brief;
await _client.PutAsJsonAsync("/api/v1/brief", FilledFrom(brief));
var view = await _client.GetFromJsonAsync<BriefViewDto>("/api/v1/brief");
Assert.NotNull(view);
await _client.PutAsJsonAsync("/api/v1/brief", FilledFrom(view.Brief));
var res = await _client.SendAsync(Post("/api/v1/brief/submit"));
res.EnsureSuccessStatusCode();
var submitted = await res.Content.ReadFromJsonAsync<BriefViewDto>();
Assert.Equal("submitted", submitted!.Brief.Status.Tag);
Assert.NotNull(submitted);
Assert.Equal("submitted", submitted.Brief.Status.Tag);
}
[Fact]
@@ -168,7 +174,9 @@ public class BriefEndpointTests(TestWebApplicationFactory factory) : IClassFixtu
var res = await _client.SendAsync(Post("/api/v1/brief/approve", role: "approver"));
res.EnsureSuccessStatusCode();
Assert.Equal("approved", (await res.Content.ReadFromJsonAsync<BriefViewDto>())!.Brief.Status.Tag);
var approved = await res.Content.ReadFromJsonAsync<BriefViewDto>();
Assert.NotNull(approved);
Assert.Equal("approved", approved.Brief.Status.Tag);
}
[Fact]
@@ -178,9 +186,11 @@ public class BriefEndpointTests(TestWebApplicationFactory factory) : IClassFixtu
await _client.PutAsJsonAsync("/api/v1/brief", FilledFrom(brief));
await _client.SendAsync(Post("/api/v1/brief/submit"));
var rejected = await (await _client.SendAsync(
Post("/api/v1/brief/reject", role: "approver", body: new RejectBriefRequest("Graag aanvullen.")))).Content.ReadFromJsonAsync<BriefViewDto>();
Assert.Equal("rejected", rejected!.Brief.Status.Tag);
var rejectRes = await _client.SendAsync(
Post("/api/v1/brief/reject", role: "approver", body: new RejectBriefRequest("Graag aanvullen.")));
var rejected = await rejectRes.Content.ReadFromJsonAsync<BriefViewDto>();
Assert.NotNull(rejected);
Assert.Equal("rejected", rejected.Brief.Status.Tag);
Assert.Equal("Graag aanvullen.", rejected.Brief.Status.Comments);
}
@@ -194,8 +204,10 @@ public class BriefEndpointTests(TestWebApplicationFactory factory) : IClassFixtu
Post("/api/v1/brief/reject", role: "approver", body: new RejectBriefRequest("Graag aanvullen.")));
// A drafter save on a rejected letter reopens it to draft.
var reopened = await (await _client.PutAsJsonAsync("/api/v1/brief", FilledFrom(brief))).Content.ReadFromJsonAsync<BriefViewDto>();
Assert.Equal("draft", reopened!.Brief.Status.Tag);
var putRes = await _client.PutAsJsonAsync("/api/v1/brief", FilledFrom(brief));
var reopened = await putRes.Content.ReadFromJsonAsync<BriefViewDto>();
Assert.NotNull(reopened);
Assert.Equal("draft", reopened.Brief.Status.Tag);
}
[Fact]
@@ -211,7 +223,9 @@ public class BriefEndpointTests(TestWebApplicationFactory factory) : IClassFixtu
await _client.SendAsync(Post("/api/v1/brief/approve", role: "approver"));
var res = await _client.SendAsync(Post("/api/v1/brief/send"));
res.EnsureSuccessStatusCode();
Assert.Equal("sent", (await res.Content.ReadFromJsonAsync<BriefViewDto>())!.Brief.Status.Tag);
var sent = await res.Content.ReadFromJsonAsync<BriefViewDto>();
Assert.NotNull(sent);
Assert.Equal("sent", sent.Brief.Status.Tag);
}
[Fact]
@@ -219,7 +233,8 @@ public class BriefEndpointTests(TestWebApplicationFactory factory) : IClassFixtu
{
var brief = await Get();
var view = await _client.GetFromJsonAsync<BriefViewDto>("/api/v1/brief");
Assert.True(view!.Decisions.CanEdit); // default (no X-Role) = drafter, draft status
Assert.NotNull(view);
Assert.True(view.Decisions.CanEdit); // default (no X-Role) = drafter, draft status
Assert.False(view.Decisions.CanApprove);
await _client.PutAsJsonAsync("/api/v1/brief", FilledFrom(brief));
@@ -228,7 +243,8 @@ public class BriefEndpointTests(TestWebApplicationFactory factory) : IClassFixtu
var asApprover = await _client.SendAsync(
new HttpRequestMessage(HttpMethod.Get, "/api/v1/brief") { Headers = { { "X-Role", "approver" } } });
var approverView = await asApprover.Content.ReadFromJsonAsync<BriefViewDto>();
Assert.True(approverView!.Decisions.CanApprove);
Assert.NotNull(approverView);
Assert.True(approverView.Decisions.CanApprove);
Assert.False(approverView.Decisions.CanEdit); // approver never edits
}
@@ -236,11 +252,13 @@ public class BriefEndpointTests(TestWebApplicationFactory factory) : IClassFixtu
public async Task Me_returns_no_capabilities_for_drafter_and_the_brief_set_for_approver()
{
var asDrafter = await _client.GetFromJsonAsync<MeDto>("/api/v1/me");
Assert.Empty(asDrafter!.Capabilities);
Assert.NotNull(asDrafter);
Assert.Empty(asDrafter.Capabilities);
var res = await _client.SendAsync(new HttpRequestMessage(HttpMethod.Get, "/api/v1/me") { Headers = { { "X-Role", "approver" } } });
var asApprover = await res.Content.ReadFromJsonAsync<MeDto>();
Assert.Equal(new[] { "brief:approve", "brief:reject", "brief:send" }, asApprover!.Capabilities);
Assert.NotNull(asApprover);
Assert.Equal(new[] { "brief:approve", "brief:reject", "brief:send" }, asApprover.Capabilities);
}
[Fact]
@@ -254,7 +272,8 @@ public class BriefEndpointTests(TestWebApplicationFactory factory) : IClassFixtu
var res = await _client.SendAsync(Post("/api/v1/brief/reset"));
res.EnsureSuccessStatusCode();
var view = await res.Content.ReadFromJsonAsync<BriefViewDto>();
Assert.Equal("draft", view!.Brief.Status.Tag);
Assert.NotNull(view);
Assert.Equal("draft", view.Brief.Status.Tag);
var aanhef = view.Brief.Sections.Single(s => s.SectionKey == "aanhef");
Assert.True(aanhef.Locked);
Assert.NotEmpty(aanhef.Blocks);