diff --git a/JobTrackerApi.Tests/CvTemplateRendererTests.cs b/JobTrackerApi.Tests/CvTemplateRendererTests.cs new file mode 100644 index 0000000..f3554d7 --- /dev/null +++ b/JobTrackerApi.Tests/CvTemplateRendererTests.cs @@ -0,0 +1,86 @@ +using JobTrackerApi.Models; +using JobTrackerApi.Services; +using Xunit; + +namespace JobTrackerApi.Tests; + +// Regression safety net for the renderer ahead of the theme-extraction work in +// career-workspace-implementation-roadmap.md Phase F3. The renderer had zero test coverage; +// these tests lock in "renders without throwing, is deterministic, and contains the content it +// was given" for every shipped template so a future refactor (or accidental edit) that changes +// output is caught immediately. +public sealed class CvTemplateRendererTests +{ + private static readonly string[] AllTemplateIds = { "ats-minimal", "harvard", "auckland", "edinburgh", "monarch", "fjord" }; + + private readonly CvTemplateRenderer _renderer = new(); + + private static TailoredCvDocument SampleDocument(string templateId) => new() + { + TemplateId = templateId, + Headline = "Senior Backend Engineer", + Summary = { "Builds reliable distributed systems." }, + SelectedSkills = { "C#", ".NET", "SQL" }, + Experience = + { + new TailoredCvExperienceItem + { + Title = "Backend Engineer", + Company = "Acme Corp", + Start = "2020", + End = "Present", + IsCurrent = true, + Bullets = { "Shipped the payments service." }, + }, + }, + Education = + { + new TailoredCvEducationItem { Qualification = "BSc Computer Science", Institution = "Example University" }, + }, + }; + + [Theory] + [MemberData(nameof(TemplateIds))] + public void Render_produces_html_containing_candidate_and_content(string templateId) + { + var result = _renderer.Render(SampleDocument(templateId), templateId, "Jamie Rivera", "Backend Engineer", "Acme Corp"); + + Assert.Equal(templateId, result.TemplateId); + Assert.Contains("Jamie Rivera", result.Html); + Assert.Contains("Acme Corp", result.Html); + Assert.Contains("Shipped the payments service.", result.Html); + Assert.Contains("", result.Html); + } + + [Theory] + [MemberData(nameof(TemplateIds))] + public void Render_is_deterministic_for_identical_input(string templateId) + { + var a = _renderer.Render(SampleDocument(templateId), templateId, "Jamie Rivera", "Backend Engineer", "Acme Corp"); + var b = _renderer.Render(SampleDocument(templateId), templateId, "Jamie Rivera", "Backend Engineer", "Acme Corp"); + + Assert.Equal(a.Html, b.Html); + } + + [Fact] + public void Unknown_template_id_falls_back_to_ats_minimal() + { + var result = _renderer.Render(SampleDocument("unknown"), "not-a-real-template", "Jamie Rivera", "Backend Engineer", null); + + Assert.Equal("ats-minimal", result.TemplateId); + } + + [Fact] + public void Html_encodes_user_supplied_content_to_prevent_injection() + { + var document = SampleDocument("ats-minimal"); + document.Summary[0] = ""; + + var result = _renderer.Render(document, "ats-minimal", "Jamie Rivera", "Backend Engineer", null); + + Assert.DoesNotContain("", result.Html); + Assert.Contains("<script>", result.Html); + } + + public static IEnumerable TemplateIds() => AllTemplateIds.Select(id => new object[] { id }); +}