LetterHtml.Render already receives the letter's instant and uses it for the letterhead date. The body's "datum" placeholder resolved through ResolveAuto, which ignored that instant and read the wall clock instead. This is not a shipped bug today, because every current caller passes Now() at render time. It becomes one the moment Render runs with a historical instant (an archive re-render, a back-dated letter): the letterhead and the body would then disagree within one document. Thread the existing "at" parameter down through RenderParagraphs and RenderNode into ResolveAuto's "datum" case. Render's own signature, and every call site, stays unchanged. Add two tests with a fixed historical "at": one pins the body's rendered date to the expected Dutch string, the other asserts the letterhead date and the body date agree. Both fail red against the old code, showing today's date instead of the pinned one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
164 lines
5.9 KiB
C#
164 lines
5.9 KiB
C#
using System.Text.RegularExpressions;
|
|
using BigRegister.Api.Contracts;
|
|
using BigRegister.Api.Data;
|
|
using BigRegister.Domain.Letters;
|
|
|
|
namespace BigRegister.Tests;
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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<LetterBlockDto>
|
|
{
|
|
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<LetterBlockDto>
|
|
{
|
|
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<LetterBlockDto>
|
|
{
|
|
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<LetterBlockDto>
|
|
{
|
|
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, "<dt>Datum</dt><dd>([^<]+)</dd>").Groups[1].Value;
|
|
|
|
private static string ExtractBodyDatumParagraph(string html)
|
|
{
|
|
var bodyStart = html.IndexOf("<div class=\"letter__body\">", StringComparison.Ordinal);
|
|
var bodyEnd = html.IndexOf("<div class=\"letter__signature\">", StringComparison.Ordinal);
|
|
var body = html[bodyStart..bodyEnd];
|
|
return Regex.Match(body, "<p>([^<]+)</p>").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));
|
|
}
|
|
}
|