Files
atomic-design-poc/backend/tests/BigRegister.Tests/StamdataValidationTests.cs
T
ehoandClaude Sonnet 5 8560746d15 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>
2026-09-04 21:48:08 +02:00

117 lines
5.2 KiB
C#

using BigRegister.Api.Data;
using BigRegister.Domain.Diplomas;
using BigRegister.Domain.Documents;
using BigRegister.Stamdata;
namespace BigRegister.Tests;
/// <summary>
/// The compile-time gate for business-editable stamdata (ADR-0004). The C# compiler
/// already catches shape/type mistakes; these tests catch the referential integrity it
/// can't, so a bad config edit fails the build instead of reaching production.
/// </summary>
public class StamdataValidationTests
{
/// 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.
private sealed record StamdataRef(string Description, IEnumerable<string> Keys, Func<string, bool> Resolves);
// The beroepen master-list keys every other profession table points at.
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 (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" }
.SelectMany(DocumentRules.AllCategoriesFor)
.Select(c => c.CategoryId)
.ToHashSet(StringComparer.Ordinal);
private static readonly IReadOnlyList<StamdataRef> References = new[]
{
new StamdataRef(
"Diploma.Opleiding → professions.program (valid today)",
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 (a stamdata gate, generalized).
new StamdataRef(
"Opleiding.beroep → beroepen.code",
StamdataFile.Load<Opleiding>("opleidingen").Select(o => o.Beroep),
key => BeroepCodes.Contains(key)),
new StamdataRef(
"Specialisme.beroep → beroepen.code",
StamdataFile.Load<Specialisme>("specialismen").Select(s => s.Beroep),
key => BeroepCodes.Contains(key)),
// 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",
StamdataFile.Load<DocumentConfidentialiteit>("documentconfidentialiteit").Select(d => d.CategoryId),
key => DocumentCategoryIds.Contains(key)),
};
[Fact]
public void Every_declared_reference_into_stamdata_resolves()
{
// The dangling-reference guard (generalized): a referenced key with no (currently valid)
// stamdata row would silently break its consumer. Fail the build instead of prod.
foreach (var r in References)
foreach (var key in r.Keys)
Assert.True(r.Resolves(key),
$"Dangling stamdata reference [{r.Description}]: '{key}' no longer resolves — " +
"deleting or expiring the referenced row would break it.");
}
[Fact]
public void Profession_map_has_no_blank_programs_or_professions()
{
Assert.All(Professions.ByProgram, kv =>
{
Assert.False(string.IsNullOrWhiteSpace(kv.Key), "A profession-map program key is blank.");
Assert.False(string.IsNullOrWhiteSpace(kv.Value), $"Program '{kv.Key}' maps to a blank profession.");
});
}
[Fact]
public void Manual_professions_are_non_empty_and_distinct()
{
var professions = DiplomaRules.ManualProfessions();
Assert.NotEmpty(professions);
Assert.Equal(professions.Count, professions.Distinct().Count());
}
[Fact]
public void Every_policy_question_has_an_id_and_wording()
{
Assert.All(PolicyQuestions.All, q =>
{
Assert.False(string.IsNullOrWhiteSpace(q.Id), "A policy question has a blank id.");
Assert.False(string.IsNullOrWhiteSpace(q.Vraag), $"Policy question '{q.Id}' has blank wording.");
});
}
[Fact]
public void Manual_question_set_has_distinct_ids() // no field is asked twice
{
var ids = PolicyQuestions.ManualSet.Select(q => q.Id).ToList();
Assert.Equal(ids.Count, ids.Distinct().Count());
}
// The GENERIC gate (ADR-0004): every table registered in the catalog is validated the
// same way — its JSON deserializes into its typed record, keys are non-blank and don't
// overlap in time, and valid-time windows are well-formed. A new stamdata type is covered
// the moment it's added to StamdataCatalog; no new test needed. A bad edit fails the build.
[Fact]
public void Every_catalog_table_is_valid()
{
foreach (var table in StamdataCatalog.All)
Assert.True(table.Validate().Count == 0,
$"Stamdata table '{table.Id}' has problems: {string.Join("; ", table.Validate())}");
}
}