135 lines
7.8 KiB
C#
135 lines
7.8 KiB
C#
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<ICurrentUserService>();
|
|
principal.SetupGet(x => x.UserId).Returns(owner);
|
|
return new JobTrackerContext(new DbContextOptionsBuilder<JobTrackerContext>().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<DbUpdateConcurrencyException>(() => 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<DbUpdateConcurrencyException>(() => 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());
|
|
}
|
|
}
|