using System.Text.RegularExpressions; using BigRegister.Api.Contracts; using BigRegister.Api.Data; using BigRegister.Domain.Letters; namespace BigRegister.Tests; /// /// WP-25's 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. /// public class LetterHtmlTests { private static BriefEntity FixtureBrief() => new() { BriefId = "golden-brief-1", Owner = "golden", Beroep = "arts", TemplateId = "besluit-arts", DrafterId = BriefStore.DrafterId, Placeholders = new[] { new PlaceholderDefDto("naam_zorgverlener", "Naam zorgverlener", true), new PlaceholderDefDto("datum", "Datum", true), new PlaceholderDefDto("reden_besluit", "Reden besluit", false), }, Sections = new() { new("aanhef", "Aanhef", true, new List { new("freeText", "aanhef-1", new RichTextBlockDto(new[] { new ParagraphDto(new[] { new RichTextNodeDto("text", Text: "Geachte heer/mevrouw "), new RichTextNodeDto("placeholder", Key: "naam_zorgverlener"), new RichTextNodeDto("text", Text: ","), }), })), }, Locked: true), new("kern", "Kern van het besluit", true, new List { new("freeText", "kern-1", new RichTextBlockDto(new[] { new ParagraphDto(new[] { new RichTextNodeDto("text", Text: "Op ") }), new ParagraphDto(new RichTextNodeDto[] { new("text", Text: "Eerste punt: "), new("placeholder", Key: "reden_besluit"), }, List: "bullet"), new ParagraphDto(new RichTextNodeDto[] { new("text", Text: "Tweede punt"), }, List: "bullet"), })), }), new("slot", "Slot", false, new List { new("freeText", "slot-1", new RichTextBlockDto(new[] { new ParagraphDto(new[] { new RichTextNodeDto("text", Text: "Met vriendelijke groet,") }), })), }, Locked: true), }, Status = new BriefStatusDto("draft"), }; private static readonly OrgTemplateDto Template = new( "cibg-registers", "BIG-register", "Retouradres: Postbus 00000, 2500 AA Den Haag", LogoDocumentId: null, "BIG-register · Postbus 00000, 2500 AA Den Haag · 070 000 00 00 · info@voorbeeld.example", "Dit is een gegenereerd voorbeelddocument uit de register-reference PoC. Alle gegevens zijn fictief.", "A. de Vries", "Hoofd Registratie, BIG-register", "Met vriendelijke groet,", new MarginsDto(25, 20, 20, 25), Version: 1); private const string At = "2026-07-05T12:00:00.0000000+00:00"; private static readonly string GoldenPath = Path.Combine(AppContext.BaseDirectory, "LetterHtml.golden.html"); // A minimal brief whose body renders the "datum" placeholder — the golden-file // fixture above never uses it in the body, only in the letterhead, so it cannot // exercise ResolveAuto's "datum" case (TE-007). private static BriefEntity FixtureBriefWithDatumInBody() => new() { BriefId = "datum-brief-1", Owner = "golden", Beroep = "arts", TemplateId = "besluit-arts", DrafterId = BriefStore.DrafterId, Placeholders = new[] { new PlaceholderDefDto("datum", "Datum", true), }, Sections = new() { new("kern", "Kern van het besluit", true, new List { new("freeText", "kern-1", new RichTextBlockDto(new[] { new ParagraphDto(new[] { new RichTextNodeDto("placeholder", Key: "datum") }), })), }), }, Status = new BriefStatusDto("draft"), }; private static string ExtractLetterheadDate(string html) => Regex.Match(html, "
Datum
([^<]+)
").Groups[1].Value; private static string ExtractBodyDatumParagraph(string html) { var bodyStart = html.IndexOf("
", StringComparison.Ordinal); var bodyEnd = html.IndexOf("
", StringComparison.Ordinal); var body = html[bodyStart..bodyEnd]; return Regex.Match(body, "

([^<]+)

").Groups[1].Value; } [Fact] public void Render_matches_the_golden_file() { var html = LetterHtml.Render(FixtureBrief(), Template, At, watermark: true); var golden = File.ReadAllText(GoldenPath); Assert.Equal(golden, html); } [Fact] public void Render_resolves_the_body_datum_placeholder_from_the_given_at_not_the_wall_clock() { const string historicalAt = "2019-03-14T08:00:00.0000000+00:00"; var html = LetterHtml.Render(FixtureBriefWithDatumInBody(), Template, historicalAt, watermark: false); Assert.Equal("14 maart 2019", ExtractBodyDatumParagraph(html)); } [Fact] public void Render_keeps_the_letterhead_date_and_the_body_datum_in_agreement_for_a_historical_at() { // A historical `at` (an archive re-render, a back-dated letter) is the case // where the letterhead and the body datum placeholder could disagree within // one document, if the body still read the wall clock (TE-007). const string historicalAt = "2019-03-14T08:00:00.0000000+00:00"; var html = LetterHtml.Render(FixtureBriefWithDatumInBody(), Template, historicalAt, watermark: false); Assert.Equal(ExtractLetterheadDate(html), ExtractBodyDatumParagraph(html)); } [Fact] public void Every_letter_prefixed_class_exists_in_letter_css() { var html = LetterHtml.Render(FixtureBrief(), Template, At, watermark: true); var classes = Regex.Matches(html, "class=\"([^\"]+)\"") .SelectMany(m => m.Groups[1].Value.Split(' ')) .Where(c => c.StartsWith("letter")) .Distinct(); Assert.NotEmpty(classes); Assert.All(classes, c => Assert.Contains($".{c}", LetterHtml.StyleSheet)); } }