using JobTrackerApi.Data;
using JobTrackerApi.Models;
using JobTrackerApi.Services;
using Microsoft.EntityFrameworkCore;
using Moq;
using Xunit;
namespace JobTrackerApi.Tests;
public sealed class CvBuilderTests
{
private static StructuredCvProfile Rich() => new()
{
Contact = { FullName = "Ada Lovelace", Headline = "Engineer", Email = "ada@example.com", Location = "Oslo" },
Summary = { "Builder of engines." },
Jobs =
{
new StructuredCvJob { Id = "job1", Title = "Senior Eng", Company = "Acme", Location = "Oslo", Start = "Jan 2020", End = "Present", IsCurrent = true, Bullets = { "Built X" }, Skills = { "C#" } },
new StructuredCvJob { Id = "job2", Title = "Eng", Company = "Beta", Start = "2018", End = "2019", Bullets = { "Built Y" } },
},
Education = { new StructuredCvEducation { Id = "ed1", Qualification = "BSc", Institution = "Uni" } },
Skills = { "C#", "SQL" },
Projects = { new StructuredCvProject { Id = "pr1", Name = "Proj", Role = "Lead", Skills = { "React" } } },
Certifications = { new StructuredCvCertification { Id = "c1", Name = "AZ-204", Issuer = "MS" } },
Languages = { new StructuredCvLanguage { Name = "English", Level = "Native" } },
};
// ---- Resolver (the lens over the master profile) ----
[Fact]
public void Resolver_defaults_include_every_populated_section()
{
var model = CvVariantResolver.Build(Rich(), new CvVariantSettings(), "Fallback", null);
Assert.Equal("Ada Lovelace", model.FullName);
var keys = model.Sections.Select(s => s.Key).ToList();
Assert.Contains("summary", keys);
Assert.Contains("experience", keys);
Assert.Contains("skills", keys);
Assert.Contains("languages", keys);
}
[Fact]
public void Resolver_hides_a_section_when_configured()
{
var settings = new CvVariantSettings { Sections = { new CvSectionSetting { Key = "skills", Hidden = true } } };
var model = CvVariantResolver.Build(Rich(), settings, "F", null);
Assert.DoesNotContain("skills", model.Sections.Select(s => s.Key));
// Other sections still render (appended after the explicit order).
Assert.Contains("experience", model.Sections.Select(s => s.Key));
}
[Fact]
public void Resolver_respects_explicit_section_order()
{
var settings = new CvVariantSettings
{
Sections = { new CvSectionSetting { Key = "skills" }, new CvSectionSetting { Key = "summary" } },
};
var model = CvVariantResolver.Build(Rich(), settings, "F", null);
var keys = model.Sections.Select(s => s.Key).ToList();
Assert.True(keys.IndexOf("skills") < keys.IndexOf("summary"));
}
[Fact]
public void Resolver_applies_item_override_bullets_without_touching_the_master()
{
var profile = Rich();
var settings = new CvVariantSettings();
settings.Overrides["job1"] = new CvItemOverride { Bullets = new() { "Tailored bullet" } };
var model = CvVariantResolver.Build(profile, settings, "F", null);
var exp = model.Sections.First(s => s.Key == "experience");
Assert.Equal(new[] { "Tailored bullet" }, exp.Entries[0].Bullets);
// Master profile is untouched — the override is a lens, not a write.
Assert.Equal(new[] { "Built X" }, profile.Jobs[0].Bullets);
}
[Fact]
public void Resolver_hides_a_single_item_by_key()
{
var settings = new CvVariantSettings();
settings.Overrides["job2"] = new CvItemOverride { Hidden = true };
var model = CvVariantResolver.Build(Rich(), settings, "F", null);
var exp = model.Sections.First(s => s.Key == "experience");
Assert.Single(exp.Entries);
Assert.Equal("Senior Eng", exp.Entries[0].Title);
}
[Fact]
public void Resolver_adds_variant_only_custom_sections()
{
var settings = new CvVariantSettings
{
CustomSections = { new CvCustomSectionSetting { Key = "vol", Title = "Volunteering", Items = { "Coached juniors" } } },
};
var model = CvVariantResolver.Build(Rich(), settings, "F", null);
Assert.Contains(model.Sections, s => s.Title == "Volunteering" && s.Bullets.Contains("Coached juniors"));
}
[Fact]
public void Resolver_places_custom_sections_in_the_shared_section_order()
{
var settings = new CvVariantSettings
{
Sections =
{
new CvSectionSetting { Key = "custom:vol" },
new CvSectionSetting { Key = "summary" },
},
CustomSections =
{
new CvCustomSectionSetting { Key = "vol", Title = "Volunteering", Items = { "Coached juniors" } },
},
};
var model = CvVariantResolver.Build(Rich(), settings, "F", null);
Assert.True(model.Sections.FindIndex(section => section.Key == "custom:vol")
< model.Sections.FindIndex(section => section.Key == "summary"));
}
// ---- Renderer (one path, every theme is data) ----
[Fact]
public void Every_catalog_theme_renders_valid_html()
{
var renderer = new ThemedCvRenderer();
var model = CvVariantResolver.Build(Rich(), new CvVariantSettings { ShowPhoto = true }, "F", "data:image/png;base64,AAAA");
foreach (var theme in CvThemeCatalog.Themes)
{
var result = renderer.Render(model, theme, new CvVariantSettings { ThemeId = theme.Id });
Assert.Contains("alert(1)" } };
var model = CvVariantResolver.Build(profile, new CvVariantSettings(), "F", null);
var html = renderer.Render(model, CvThemeCatalog.Resolve("modern"), new CvVariantSettings { ThemeId = "modern" }).Html;
Assert.Contains("bold", html);
Assert.Contains("italic", html);
Assert.Contains("under", html);
Assert.Contains("site", html);
Assert.Contains("<script>", html); // the real tag is escaped, inert
Assert.DoesNotContain("