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
@@ -27,15 +27,18 @@ public class EndpointTests(TestWebApplicationFactory factory) : IClassFixture<Te
public async Task Notes_returns_seeded_aantekeningen()
{
var notes = await _client.GetFromJsonAsync<List<AantekeningDto>>("/api/v1/notes");
Assert.Equal(3, notes!.Count);
Assert.NotNull(notes);
Assert.Equal(3, notes.Count);
}
[Fact]
public async Task Brp_returns_address()
{
var dto = await _client.GetFromJsonAsync<BrpAddressDto>("/api/v1/brp/address");
Assert.True(dto!.Gevonden);
Assert.Equal("2514 EA", dto.Adres!.Postcode);
Assert.NotNull(dto);
Assert.True(dto.Gevonden);
Assert.NotNull(dto.Adres);
Assert.Equal("2514 EA", dto.Adres.Postcode);
}
[Fact]
@@ -62,7 +65,8 @@ public class EndpointTests(TestWebApplicationFactory factory) : IClassFixture<Te
public async Task IntakePolicy_returns_scholing_threshold()
{
var dto = await _client.GetFromJsonAsync<IntakePolicyDto>("/api/v1/intake/policy");
Assert.Equal(1000, dto!.ScholingThreshold);
Assert.NotNull(dto);
Assert.Equal(1000, dto.ScholingThreshold);
}
[Fact]
@@ -71,7 +75,8 @@ public class EndpointTests(TestWebApplicationFactory factory) : IClassFixture<Te
var res = await _client.PostAsJsonAsync("/api/v1/registrations", new RegistratieRequest("duo"));
res.EnsureSuccessStatusCode();
var body = await res.Content.ReadFromJsonAsync<ReferentieResponse>();
Assert.StartsWith("BIG-2026-", body!.Referentie);
Assert.NotNull(body);
Assert.StartsWith("BIG-2026-", body.Referentie);
}
[Fact]
@@ -79,7 +84,9 @@ public class EndpointTests(TestWebApplicationFactory factory) : IClassFixture<Te
{
var res = await _client.PostAsJsonAsync("/api/v1/registrations", new RegistratieRequest("handmatig"));
Assert.Equal(HttpStatusCode.UnprocessableEntity, res.StatusCode);
Assert.Contains("application/problem+json", res.Content.Headers.ContentType!.ToString());
var contentType = res.Content.Headers.ContentType;
Assert.NotNull(contentType);
Assert.Contains("application/problem+json", contentType.ToString());
}
[Theory]
@@ -107,7 +114,8 @@ public class EndpointTests(TestWebApplicationFactory factory) : IClassFixture<Te
new { telefoon = "0612345678" });
res.EnsureSuccessStatusCode();
var body = await res.Content.ReadFromJsonAsync<ReferentieResponse>();
Assert.StartsWith("BIG-2026-", body!.Referentie);
Assert.NotNull(body);
Assert.StartsWith("BIG-2026-", body.Referentie);
}
[Fact]
@@ -159,7 +167,9 @@ public class EndpointTests(TestWebApplicationFactory factory) : IClassFixture<Te
{
var res = await _client.PostAsync("/api/v1/uploads", UploadForm(localId, categoryId, "registratie", file, type));
Assert.Equal(HttpStatusCode.Created, res.StatusCode);
return (await res.Content.ReadFromJsonAsync<UploadResponse>())!;
var uploaded = await res.Content.ReadFromJsonAsync<UploadResponse>();
Assert.NotNull(uploaded);
return uploaded;
}
[Fact]
@@ -167,7 +177,8 @@ public class EndpointTests(TestWebApplicationFactory factory) : IClassFixture<Te
{
// A manual diploma requires a diploma upload; identiteit is always required.
var dto = await _client.GetFromJsonAsync<UploadCategoriesDto>("/api/v1/uploads/categories?wizardId=registratie&diplomaHerkomst=handmatig");
Assert.Contains(dto!.Categories, c => c.CategoryId == "diploma" && c.Required && !c.AllowPostDelivery);
Assert.NotNull(dto);
Assert.Contains(dto.Categories, c => c.CategoryId == "diploma" && c.Required && !c.AllowPostDelivery);
Assert.Contains(dto.Categories, c => c.CategoryId == "identiteit" && c.AllowPostDelivery);
}
@@ -177,7 +188,8 @@ public class EndpointTests(TestWebApplicationFactory factory) : IClassFixture<Te
var localId = Guid.NewGuid().ToString();
var doc = await Upload(localId);
var status = await _client.GetFromJsonAsync<UploadStatusDto>($"/api/v1/uploads/status?localIds={localId},onbekend");
Assert.Contains(status!.Results, r => r.LocalId == localId && r.Status == "complete" && r.DocumentId == doc.DocumentId);
Assert.NotNull(status);
Assert.Contains(status.Results, r => r.LocalId == localId && r.Status == "complete" && r.DocumentId == doc.DocumentId);
Assert.Contains(status.Results, r => r.LocalId == "onbekend" && r.Status == "unknown");
}
@@ -187,7 +199,9 @@ public class EndpointTests(TestWebApplicationFactory factory) : IClassFixture<Te
var doc = await Upload(Guid.NewGuid().ToString());
var res = await _client.GetAsync($"/api/v1/uploads/{doc.DocumentId}/content");
res.EnsureSuccessStatusCode();
Assert.Equal("application/pdf", res.Content.Headers.ContentType!.MediaType);
var contentType = res.Content.Headers.ContentType;
Assert.NotNull(contentType);
Assert.Equal("application/pdf", contentType.MediaType);
Assert.Equal(new byte[] { 1, 2, 3 }, await res.Content.ReadAsByteArrayAsync());
// pdf/image → inline (no attachment disposition) so the browser previews it
Assert.NotEqual("attachment", res.Content.Headers.ContentDisposition?.DispositionType);