using JobTrackerApi.Data;
using JobTrackerApi.Models;
using JobTrackerApi.Services;
using Microsoft.Data.Sqlite;
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) ----
[Theory]
[InlineData("modern")]
[InlineData("ats-classic")]
[InlineData("code")]
public void Revised_design_has_space_below_the_main_title_and_header(string themeId)
{
var settings = new CvVariantSettings { ThemeId = themeId, DesignVersion = 2 };
var html = new ThemedCvRenderer().Render(CvVariantResolver.Build(Rich(), settings, "", null), CvThemeCatalog.Resolve(themeId), settings).Html;
Assert.Contains(".header .name{margin-bottom:3mm;}", html);
Assert.Contains(".header + .main{padding-top:5mm;}", html);
}
[Theory]
[InlineData("technical", "left")]
[InlineData("creative", "left")]
[InlineData("nordic", "right")]
public void Revised_sidebar_background_fills_each_physical_page_including_its_margins(string themeId, string edge)
{
var settings = new CvVariantSettings { ThemeId = themeId, DesignVersion = 2 };
var html = new ThemedCvRenderer().Render(CvVariantResolver.Build(Rich(), settings, "", null), CvThemeCatalog.Resolve(themeId), settings).Html;
Assert.Contains("@top-" + edge + "{content:", html);
Assert.Contains("@bottom-" + edge + "{content:", html);
Assert.Contains("body::before{content:", html);
}
[Fact]
public void Design_revision_missing_and_explicit_one_preserve_identical_legacy_html_for_every_theme()
{
foreach (var theme in CvThemeCatalog.Themes)
{
var legacy = new CvVariantSettings { ThemeId = theme.Id };
var explicitOne = CvVariantSettingsJson.Deserialize("{\"designVersion\":1,\"themeId\":\"" + theme.Id + "\"}");
var renderer = new ThemedCvRenderer();
var model = CvVariantResolver.Build(Rich(), legacy, "Fallback", null);
Assert.Equal(renderer.Render(model, theme, legacy).Html, renderer.Render(model, theme, explicitOne).Html);
Assert.DoesNotContain("designVersion", CvVariantSettingsJson.Serialize(legacy));
}
}
[Fact]
public void Design_revision_two_is_persisted_and_changes_presentation_without_changing_content()
{
var settings = CvVariantSettingsJson.Deserialize("{\"designVersion\":2,\"themeId\":\"modern\"}");
Assert.Contains("\"designVersion\":2", CvVariantSettingsJson.Serialize(settings));
var model = CvVariantResolver.Build(Rich(), settings, "Fallback", null);
var theme = CvThemeCatalog.Resolve("modern");
var renderer = new ThemedCvRenderer();
var revised = renderer.Render(model, theme, settings).Html;
var legacy = renderer.Render(model, theme, new() { ThemeId = "modern" }).Html;
Assert.NotEqual(legacy, revised);
Assert.Contains("Ada Lovelace", revised);
Assert.Contains("Builder of engines.", revised);
Assert.Contains("Built X", revised);
Assert.Contains("AZ-204", revised);
}
[Theory]
[InlineData("technical", "#0f4c5c", "#ffffff")]
[InlineData("nordic", "#eef3f8", "#1f2937")]
[InlineData("creative", "#be185d", "#ffffff")]
public void Design_revision_two_sidebar_uses_its_own_palette_and_room_for_identity(string themeId, string background, string ink)
{
var settings = CvVariantSettingsJson.Deserialize("{\"designVersion\":2,\"themeId\":\"" + themeId + "\"}");
var model = CvVariantResolver.Build(Rich(), settings, "Fallback", null);
var html = new ThemedCvRenderer().Render(model, CvThemeCatalog.Resolve(themeId), settings).Html;
Assert.Contains("--cv-sidebar-bg:" + background, html);
Assert.Contains("--cv-sidebar-ink:" + ink, html);
Assert.Contains("background:var(--cv-sidebar-bg)", html);
Assert.Contains("color:var(--cv-sidebar-ink)", html);
Assert.Contains("padding:8mm", html);
Assert.Contains("68mm", html);
}
[Fact]
public async Task Design_revision_duplicate_and_restore_keep_their_stored_opt_in()
{
var (db, service) = NewService("user-1");
await using var dispose = db;
var created = await service.CreateAsync("user-1", "Legacy design", null, new(), default);
var revisionTwo = CvVariantSettingsJson.Deserialize("{\"designVersion\":2,\"themeId\":\"technical\"}");
await service.SaveAsync("user-1", created.Id, null, revisionTwo, "manual", 1, default);
var copy = (await service.DuplicateAsync("user-1", created.Id, null, default))!;
Assert.Contains("\"designVersion\":2", copy.SettingsJson);
var restored = (await service.RestoreVersionAsync("user-1", created.Id, 1, 2, default))!;
Assert.DoesNotContain("designVersion", restored.SettingsJson);
Assert.Contains("\"designVersion\":2", (await db.CvVariantVersions.SingleAsync(row => row.CvVariantId == created.Id && row.Version == 2)).SettingsJson);
}
[Fact]
public void Explicit_blank_roundtrips_without_inheriting_profile_content_or_header()
{
var settings = CvVariantSettingsJson.Deserialize("{\"sectionsExplicit\":true,\"includeProfileContact\":false,\"sections\":[],\"showPhoto\":true}");
var json = CvVariantSettingsJson.Serialize(settings);
Assert.Contains("\"sectionsExplicit\":true", json);
Assert.Contains("\"includeProfileContact\":false", json);
var model = CvVariantResolver.Build(Rich(), CvVariantSettingsJson.Deserialize(json), "Fallback", "data:image/png;base64,AA==");
Assert.Empty(model.Sections);
Assert.Empty(model.FullName);
Assert.Null(model.Headline);
Assert.Empty(model.Contact);
Assert.Null(model.PhotoDataUrl);
foreach (var theme in CvThemeCatalog.Themes)
{
var html = new ThemedCvRenderer().Render(model, theme, settings).Html;
Assert.DoesNotContain(" 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("",
HeadingFont = "Arial;}",
BodyFont = "'Segoe UI', Roboto, Arial, sans-serif",
});
Assert.Null(settings.AccentColor);
Assert.Null(settings.HeadingFont);
Assert.Equal("'Segoe UI', Roboto, Arial, sans-serif", settings.BodyFont);
}
[Fact]
public void Accent_override_reaches_the_css()
{
var renderer = new ThemedCvRenderer();
var model = CvVariantResolver.Build(Rich(), new CvVariantSettings(), "F", null);
var result = renderer.Render(model, CvThemeCatalog.Resolve("modern"), new CvVariantSettings { ThemeId = "modern", AccentColor = "#123456" });
Assert.Contains("#123456", result.Html);
}
[Fact]
public void Design_and_layout_overrides_are_normalized_and_reach_the_shared_renderer()
{
var settings = CvVariantSettingsJson.Normalize(new CvVariantSettings
{
ThemeId = "modern",
Layout = "sidebar-right",
TextColor = "#123456",
MutedColor = "#456789",
BackgroundColor = "#fafafa",
BaseFontSizePt = 99,
HeadingSizePt = 15,
LineHeight = 1.55,
PageMarginMm = 20,
SidebarWidthMm = 70,
SidebarSections = new() { "skills", "languages" },
SkillsStyle = "text",
});
var model = CvVariantResolver.Build(Rich(), settings, "F", null);
var html = new ThemedCvRenderer().Render(model, CvThemeCatalog.Resolve("modern"), settings).Html;
Assert.Equal(13, settings.BaseFontSizePt);
Assert.Contains("grid-template-columns:minmax(0,1fr) 70mm", html);
Assert.Contains("--cv-ink:#123456", html);
Assert.Contains("--cv-paper:#fafafa", html);
Assert.Contains("line-height:1.55", html);
Assert.Contains("font-size:13pt", html);
Assert.Contains("class=\"skills-text\">C# · SQL
", html);
Assert.Contains("--cv-page-margin:20mm", html);
Assert.Contains("@page{size:210mm 297mm;margin:20mm 0;}", html);
Assert.Contains(".page{width:auto;min-height:0;margin:0;}", html);
}
[Fact]
public void Section_presentation_is_normalized_and_shared_by_preview_and_pdf_renderer()
{
var settings = CvVariantSettingsJson.Normalize(new CvVariantSettings
{
ThemeId = "modern",
Sections =
{
new CvSectionSetting { Key = "experience", Presentation = "grid", Columns = 2 },
new CvSectionSetting { Key = "skills", Presentation = "bubble", Columns = 99 },
},
});
var model = CvVariantResolver.Build(Rich(), settings, "F", null);
var html = new ThemedCvRenderer().Render(model, CvThemeCatalog.Resolve("modern"), settings).Html;
Assert.Equal(2, settings.Sections[0].Columns);
Assert.Null(settings.Sections[1].Columns);
Assert.Contains("section-experience section-presentation-grid", html);
Assert.Contains("--cv-section-columns:2", html);
Assert.Contains("section-skills section-presentation-bubble", html);
}
[Fact]
public void Norwegian_variant_localizes_default_labels_dates_and_keeps_custom_links_clickable()
{
var profile = Rich();
profile.Jobs[0].StartDate = "2020-01";
profile.Contact.GitHub = "github.com/ada";
profile.Contact.Links.Add(new StructuredCvLink { Label = "Portfolio", Url = "https://ada.example/work" });
var settings = new CvVariantSettings { Language = "no", DateFormat = "year" };
var model = CvVariantResolver.Build(profile, settings, "F", null);
var html = new ThemedCvRenderer().Render(model, CvThemeCatalog.Resolve("modern"), settings).Html;
Assert.Equal("Arbeidserfaring", model.Sections.First(section => section.Key == "experience").Title);
Assert.Equal("2020 – nå", model.Sections.First(section => section.Key == "experience").Entries[0].Meta);
Assert.Contains("href=\"https://github.com/ada\"", html);
Assert.Contains("href=\"https://ada.example/work\">Portfolio", html);
}
// ---- Variant service (CRUD, autosave history, public, render) ----
private static (JobTrackerContext db, CvVariantService svc) NewService(string userId, string? dbName = null)
{
var options = new DbContextOptionsBuilder()
.UseInMemoryDatabase(dbName ?? Guid.NewGuid().ToString())
.Options;
var currentUser = new Mock();
currentUser.SetupGet(s => s.UserId).Returns(userId);
var db = new JobTrackerContext(options, currentUser.Object);
var svc = new CvVariantService(db, new CareerProfileService(db), new ThemedCvRenderer());
return (db, svc);
}
[Fact]
public async Task Create_then_save_increments_version_and_keeps_history()
{
var (db, svc) = NewService("user-1");
await using var _ = db;
var created = await svc.CreateAsync("user-1", "My CV", null, new CvVariantSettings { ThemeId = "modern" }, default);
Assert.Equal(1, created.Version);
var saved = await svc.SaveAsync("user-1", created.Id, "My CV v2", new CvVariantSettings { ThemeId = "nordic" }, "autosave", 1, default);
Assert.Equal(2, saved!.Version);
var versions = await svc.ListVersionsAsync("user-1", created.Id, default);
Assert.Equal(new[] { 2, 1 }, versions.Select(v => v.Version));
Assert.True(versions[0].IsCurrent);
}
[Fact]
public async Task Create_persists_variant_and_initial_history_on_a_relational_store()
{
await using var connection = new SqliteConnection("Data Source=:memory:");
await connection.OpenAsync();
var options = new DbContextOptionsBuilder().UseSqlite(connection).Options;
var currentUser = new Mock();
currentUser.SetupGet(service => service.UserId).Returns("user-1");
await using var db = new JobTrackerContext(options, currentUser.Object);
await db.Database.EnsureCreatedAsync();
var service = new CvVariantService(db, new CareerProfileService(db), new ThemedCvRenderer());
var created = await service.CreateAsync("user-1", "Atomic CV", null, new CvVariantSettings(), default);
Assert.Single(await db.CvVariants.IgnoreQueryFilters().Where(item => item.Id == created.Id).ToListAsync());
var history = await db.CvVariantVersions.IgnoreQueryFilters().Where(item => item.CvVariantId == created.Id).ToListAsync();
Assert.Single(history);
Assert.Equal(1, history[0].Version);
}
[Fact]
public async Task Restore_reapplies_old_settings_as_a_new_version()
{
var (db, svc) = NewService("user-1");
await using var _ = db;
var v = await svc.CreateAsync("user-1", "CV", null, new CvVariantSettings { ThemeId = "modern" }, default);
await svc.SaveAsync("user-1", v.Id, null, new CvVariantSettings { ThemeId = "creative" }, "autosave", 1, default);
var restored = await svc.RestoreVersionAsync("user-1", v.Id, 1, 2, default);
Assert.Equal(3, restored!.Version);
Assert.Equal("modern", CvVariantSettingsJson.Deserialize(restored.SettingsJson).ThemeId);
}
[Fact]
public async Task Duplicate_copies_settings_with_a_fresh_slug()
{
var (db, svc) = NewService("user-1");
await using var _ = db;
var v = await svc.CreateAsync("user-1", "CV", null, new CvVariantSettings { ThemeId = "elegant" }, default);
var copy = await svc.DuplicateAsync("user-1", v.Id, null, default);
Assert.NotEqual(v.PublicSlug, copy!.PublicSlug);
Assert.Equal("elegant", CvVariantSettingsJson.Deserialize(copy.SettingsJson).ThemeId);
}
[Fact]
public async Task Create_rejects_a_job_application_owned_by_another_user()
{
var (db, svc) = NewService("user-1");
await using var _ = db;
var company = new Company { OwnerUserId = "user-2", Name = "Other tenant" };
var foreignJob = new JobApplication { OwnerUserId = "user-2", JobTitle = "Private role", Company = company };
db.Add(foreignJob);
await db.SaveChangesAsync();
await Assert.ThrowsAsync(() =>
svc.CreateAsync("user-1", "CV", foreignJob.Id, new CvVariantSettings(), default));
Assert.Empty(await db.CvVariants.IgnoreQueryFilters().ToListAsync());
}
[Fact]
public async Task Delete_removes_the_variant_and_its_versions()
{
var (db, svc) = NewService("user-1");
await using var _ = db;
var v = await svc.CreateAsync("user-1", "CV", null, null, default);
Assert.True(await svc.DeleteAsync("user-1", v.Id, default));
Assert.Null(await svc.GetAsync("user-1", v.Id, default));
Assert.Empty(await db.CvVariantVersions.IgnoreQueryFilters().Where(x => x.CvVariantId == v.Id).ToListAsync());
}
[Fact]
public async Task Render_pulls_content_from_the_master_profile()
{
var (db, svc) = NewService("user-1");
await using var _ = db;
await new CareerProfileService(db).SaveVersionAsync("user-1", Rich(), "manual", 0, default);
var v = await svc.CreateAsync("user-1", "CV", null, new CvVariantSettings { ThemeId = "modern" }, default);
var render = await svc.RenderAsync("user-1", v.Id, new CvRenderPerson("Fallback", null), default);
Assert.NotNull(render);
Assert.Contains("Ada Lovelace", render!.Html);
Assert.Contains("Senior Eng", render.Html);
}
[Fact]
public async Task Public_render_bypasses_the_tenant_filter()
{
var dbName = Guid.NewGuid().ToString();
var (ownerDb, ownerSvc) = NewService("user-1", dbName);
await new CareerProfileService(ownerDb).SaveVersionAsync("user-1", Rich(), "manual", 0, default);
var v = await ownerSvc.CreateAsync("user-1", "CV", null, new CvVariantSettings { ThemeId = "minimal" }, default);
await ownerSvc.SetPublicAsync("user-1", v.Id, true, default);
await ownerDb.DisposeAsync();
// Anonymous context: no current user, so the tenant filter denies everything by default.
var (anonDb, anonSvc) = NewService(null!, dbName);
await using var _ = anonDb;
var owner = await anonSvc.GetPublicOwnerAsync(v.PublicSlug, default);
Assert.Equal("user-1", owner);
var result = await anonSvc.RenderPublicAsync(v.PublicSlug, new CvRenderPerson("Ada Lovelace", null), default);
Assert.NotNull(result);
Assert.Contains("Ada Lovelace", result!.Value.render.Html);
}
[Fact]
public async Task Private_variant_is_not_served_publicly()
{
var (db, svc) = NewService("user-1");
await using var _ = db;
var v = await svc.CreateAsync("user-1", "CV", null, null, default);
Assert.Null(await svc.GetPublicOwnerAsync(v.PublicSlug, default));
}
[Fact]
public async Task Re_enabling_public_access_rotates_the_revoked_link()
{
var (db, svc) = NewService("user-1");
await using var _ = db;
var v = await svc.CreateAsync("user-1", "CV", null, null, default);
await svc.SetPublicAsync("user-1", v.Id, true, default);
var revokedSlug = v.PublicSlug;
await svc.SetPublicAsync("user-1", v.Id, false, default);
await svc.SetPublicAsync("user-1", v.Id, true, default);
Assert.NotEqual(revokedSlug, v.PublicSlug);
Assert.Null(await svc.GetPublicOwnerAsync(revokedSlug, default));
Assert.Equal("user-1", await svc.GetPublicOwnerAsync(v.PublicSlug, default));
}
[Fact]
public void Premium_theme_policy_keeps_free_themes_available()
{
Assert.Equal(4, CvThemeCatalog.Themes.Count(t => CvThemeCatalog.CanUse(t.Id, false)));
Assert.Equal(5, CvThemeCatalog.Themes.Count(t => t.Premium));
Assert.All(CvThemeCatalog.Themes, t => Assert.True(CvThemeCatalog.CanUse(t.Id, true)));
Assert.False(CvThemeCatalog.CanUse("unknown", true));
}
[Fact]
public void Code_theme_uses_registry_driven_numbered_sections_and_shared_accent_tokens()
{
var settings = new CvVariantSettings { ThemeId = "code", AccentColor = "#126b55" };
var model = CvVariantResolver.Build(Rich(), settings, "F", null);
var html = new ThemedCvRenderer().Render(model, CvThemeCatalog.Resolve("code"), settings).Html;
Assert.Contains("counter-reset:cv-section", html);
Assert.Contains("--cv-accent-color:#126b55", html);
Assert.Contains("Consolas", html);
// A lone quote here caused Chromium to consume all following layout and @page rules.
Assert.Contains("content:\"\";", html);
Assert.True(CvThemeCatalog.Resolve("code").AtsFriendly);
}
[Fact]
public void Section_overrides_skill_groups_and_custom_paragraphs_share_the_render_model()
{
var settings = new CvVariantSettings
{
Sections = { new CvSectionSetting { Key = "summary", Items = new() { "Tailored summary" } } },
SkillsStyle = "grouped",
SkillGroups = new() { new CvSkillGroupSetting { Name = "Backend", Items = { "C#", ".NET" } } },
CustomSections = { new CvCustomSectionSetting { Key = "decl", Title = "Declaration", ContentType = "paragraphs", Items = { "I confirm these details." } } },
};
var model = CvVariantResolver.Build(Rich(), settings, "F", null);
var html = new ThemedCvRenderer().Render(model, CvThemeCatalog.Resolve("modern"), settings).Html;
Assert.Equal(new[] { "Tailored summary" }, model.Sections.Single(section => section.Key == "summary").Bullets);
Assert.Contains("class=\"skill-group-name\">Backend", html);
Assert.Contains("class=\"paragraphs\">I confirm these details.
", html);
}
[Fact]
public void Settings_normalization_removes_duplicate_entry_order_keys()
{
var settings = CvVariantSettingsJson.Normalize(new CvVariantSettings
{
Sections = { new CvSectionSetting { Key = "experience", ItemOrder = new() { "first", "first", "second", " " } } },
});
Assert.Equal(new[] { "first", "second" }, settings.Sections[0].ItemOrder);
}
}