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 CvRevisionReadinessTests { private static JobTrackerContext Context(SqliteConnection connection, string owner = "revision-owner") { var principal = new Mock(); principal.SetupGet(x => x.UserId).Returns(owner); return new JobTrackerContext(new DbContextOptionsBuilder().UseSqlite(connection).Options, principal.Object); } private static CvVariantService Service(JobTrackerContext db) => new(db, new CareerProfileService(db), new ThemedCvRenderer()); [Theory] [InlineData(false)] [InlineData(true)] public async Task Independent_stale_save_or_restore_cannot_replace_the_newer_revision(bool restore) { await using var connection = new SqliteConnection("Data Source=:memory:"); await connection.OpenAsync(); await using var first = Context(connection); await first.Database.EnsureCreatedAsync(); var a = Service(first); var cv = await a.CreateAsync("revision-owner", "Revision fixture", null, new CvVariantSettings { Headline = "Original" }, default); await using var second = Context(connection); var b = Service(second); Assert.Equal(1, (await b.GetAsync("revision-owner", cv.Id, default))!.Version); await a.SaveAsync("revision-owner", cv.Id, null, new CvVariantSettings { Headline = "Newer reviewed content" }, "manual", 1, default); await Assert.ThrowsAsync(() => restore ? b.RestoreVersionAsync("revision-owner", cv.Id, 1, 1, default) : b.SaveAsync("revision-owner", cv.Id, null, new CvVariantSettings { Headline = "Stale draft" }, "autosave", 1, default)); await using var verify = Context(connection); var saved = await verify.CvVariants.SingleAsync(); Assert.Equal(2, saved.Version); Assert.Equal("Newer reviewed content", CvVariantSettingsJson.Deserialize(saved.SettingsJson).Headline); var history = await verify.CvVariantVersions.OrderBy(x => x.Version).ToListAsync(); Assert.Equal(new[] { 1, 2 }, history.Select(x => x.Version)); Assert.Equal("Original", CvVariantSettingsJson.Deserialize(history[0].SettingsJson).Headline); } [Fact] public async Task Fresh_request_with_a_stale_expected_version_is_rejected_before_appending_history() { await using var connection = new SqliteConnection("Data Source=:memory:"); await connection.OpenAsync(); await using var first = Context(connection); await first.Database.EnsureCreatedAsync(); var service = Service(first); var cv = await service.CreateAsync("revision-owner", "CV", null, null, default); await service.SaveAsync("revision-owner", cv.Id, null, new CvVariantSettings { Headline = "Winner" }, "manual", 1, default); await using var fresh = Context(connection); await Assert.ThrowsAsync(() => Service(fresh).SaveAsync("revision-owner", cv.Id, "Stale rename", new CvVariantSettings(), "autosave", 1, default)); Assert.Equal(2, await fresh.CvVariantVersions.CountAsync()); Assert.Equal("CV", (await fresh.CvVariants.SingleAsync()).Name); } [Fact] public async Task Settings_history_restore_and_override_reset_preserve_the_canonical_source_contract() { await using var connection = new SqliteConnection("Data Source=:memory:"); await connection.OpenAsync(); await using var db = Context(connection); await db.Database.EnsureCreatedAsync(); var career = new CareerProfileService(db); var profile = new StructuredCvProfile { Contact = new() { FullName = "Åse Fixture", Headline = "Canonical source" }, Jobs = { new() { Id = "source-entry", Title = "Canonical role", Company = "Nordlys", Bullets = { "Canonical evidence" } } }, Skills = { "C#", "Norsk" }, }; await career.SaveVersionAsync("revision-owner", profile, "manual", default); var service = Service(db); var lens = new CvVariantSettings { Headline = "CV-only headline", ThemeId = "code", PageMarginMm = 28, Density = "compact", Sections = { new() { Key = "skills", Hidden = true }, new() { Key = "experience" }, new() { Key = "summary" } }, Overrides = { ["source-entry"] = new() { Title = "CV-only role", Bullets = new() { "Reviewed override" } } }, }; var cv = await service.CreateAsync("revision-owner", "Variant", null, lens, default); var originalHistory = (await db.CvVariantVersions.SingleAsync()).SettingsJson; Assert.Equal("Canonical role", (await career.LoadStructuredAsync("revision-owner", default)).Jobs[0].Title); var rendered = await service.RenderAsync("revision-owner", cv.Id, new("Fallback", null), default); Assert.Contains("CV-only role", rendered!.Html); Assert.DoesNotContain("Canonical evidence", rendered.Html); Assert.DoesNotContain("section-skills", rendered.Html); // History is the immutable lens; inherited source values intentionally remain live. profile.Contact.Headline = "Updated canonical source"; profile.Jobs[0].Title = "Updated canonical role"; await career.SaveVersionAsync("revision-owner", profile, "manual", default); Assert.Equal(originalHistory, (await db.CvVariantVersions.SingleAsync()).SettingsJson); var reset = new CvVariantSettings { ThemeId = "minimal" }; await service.SaveAsync("revision-owner", cv.Id, null, reset, "manual", 1, default); rendered = await service.RenderAsync("revision-owner", cv.Id, new("Fallback", null), default); Assert.Contains("Updated canonical role", rendered!.Html); Assert.Contains("Updated canonical source", rendered.Html); var restored = await service.RestoreVersionAsync("revision-owner", cv.Id, 1, 2, default); Assert.Equal(3, restored!.Version); Assert.Equal(originalHistory, restored.SettingsJson); await service.SaveAsync("revision-owner", cv.Id, null, new CvVariantSettings { Headline = "After restore" }, "autosave", 3, default); Assert.Equal(originalHistory, (await db.CvVariantVersions.SingleAsync(x => x.Version == 1)).SettingsJson); Assert.Equal(originalHistory, (await db.CvVariantVersions.SingleAsync(x => x.Version == 3)).SettingsJson); Assert.Equal(new[] { 4, 3, 2, 1 }, (await service.ListVersionsAsync("revision-owner", cv.Id, default)).Select(x => x.Version)); Assert.Equal("Updated canonical role", (await career.LoadStructuredAsync("revision-owner", default)).Jobs[0].Title); } [Fact] public async Task Foreign_account_cannot_read_save_or_restore_revision_history() { await using var connection = new SqliteConnection("Data Source=:memory:"); await connection.OpenAsync(); await using var owner = Context(connection); await owner.Database.EnsureCreatedAsync(); var cv = await Service(owner).CreateAsync("revision-owner", "Private", null, null, default); await using var other = Context(connection, "other-account"); var service = Service(other); Assert.Null(await service.GetAsync("other-account", cv.Id, default)); Assert.Empty(await service.ListVersionsAsync("other-account", cv.Id, default)); Assert.Null(await service.SaveAsync("other-account", cv.Id, "Overwrite", new(), "manual", 1, default)); Assert.Null(await service.RestoreVersionAsync("other-account", cv.Id, 1, 1, default)); Assert.Single(await owner.CvVariantVersions.ToListAsync()); } }