fix(documents): reject profile changes during submitted CV rendering

This commit is contained in:
cesnimda
2026-11-11 18:31:58 +01:00
parent dbae196291
commit 2cd561d71d
2 changed files with 64 additions and 0 deletions
@@ -4,6 +4,7 @@ using JobTrackerApi.Services;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using System.Text.Json;
using Moq;
using Xunit;
@@ -129,6 +130,65 @@ public sealed class SubmittedApplicationPackageTests
storage.Verify(x => x.SnapshotAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Never);
}
[Fact]
public async Task Profile_changed_during_render_cannot_publish_old_profile_metadata_with_new_html()
{
var renderer = new Mock<ICvVariantService>();
var (db, service, _) = New("owner-1", renderer);
await using var dispose = db;
var job = await SeedAsync(db, "owner-1", null);
var career = new CareerProfileService(db);
var real = new CvVariantService(db, career, new ThemedCvRenderer());
renderer.Setup(x => x.RenderAsync("owner-1", It.IsAny<int>(), It.IsAny<CvRenderPerson>(), It.IsAny<CancellationToken>(), It.IsAny<int?>()))
.Returns(async (string owner, int id, CvRenderPerson person, CancellationToken ct, int? expected) =>
{
var changed = JsonSerializer.Deserialize<StructuredCvProfile>("{\"version\":\"1\",\"contact\":{\"fullName\":\"New during capture\",\"headline\":\"Updated canonical profile\"}}", new JsonSerializerOptions { PropertyNameCaseInsensitive = true })!;
await career.SaveVersionAsync(owner, changed, "manual", 0, ct);
var rendered = await real.RenderAsync(owner, id, person, ct, expected);
Assert.Contains("New during capture", rendered!.Html);
return rendered;
});
await Assert.ThrowsAsync<DbUpdateConcurrencyException>(() => service.CaptureAsync("owner-1", job.Id, new CvRenderPerson("Ada", null), default));
Assert.Empty(db.SubmittedApplicationPackages);
Assert.Equal(1, (await db.CareerProfiles.SingleAsync()).Version);
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task Profile_created_or_removed_during_render_aborts_capture(bool created)
{
var renderer = new Mock<ICvVariantService>();
var (db, service, _) = New("owner-1", renderer);
await using var dispose = db;
var job = await SeedAsync(db, "owner-1", null);
if (created) { db.CareerProfiles.Remove(await db.CareerProfiles.SingleAsync()); await db.SaveChangesAsync(); }
var real = new CvVariantService(db, new CareerProfileService(db), new ThemedCvRenderer());
renderer.Setup(x => x.RenderAsync("owner-1", It.IsAny<int>(), It.IsAny<CvRenderPerson>(), It.IsAny<CancellationToken>(), It.IsAny<int?>()))
.Returns(async (string owner, int id, CvRenderPerson person, CancellationToken ct, int? expected) =>
{
if (created) db.CareerProfiles.Add(new CareerProfile { OwnerUserId = owner, Version = 1, ProfileJson = "{}" });
else db.CareerProfiles.Remove(await db.CareerProfiles.SingleAsync(ct));
await db.SaveChangesAsync(ct);
return await real.RenderAsync(owner, id, person, ct, expected);
});
await Assert.ThrowsAsync<DbUpdateConcurrencyException>(() => service.CaptureAsync("owner-1", job.Id, new CvRenderPerson("Ada", null), default));
Assert.Empty(db.SubmittedApplicationPackages);
}
[Fact]
public async Task Unchanged_absent_legacy_profile_remains_capturable()
{
var (db, service, _) = New("owner-1");
await using var dispose = db;
var job = await SeedAsync(db, "owner-1", null);
db.CareerProfiles.Remove(await db.CareerProfiles.SingleAsync()); await db.SaveChangesAsync();
Assert.NotNull(await service.CaptureAsync("owner-1", job.Id, new CvRenderPerson("Ada", null), default));
Assert.Null((await db.SubmittedApplicationPackages.SingleAsync()).CareerProfileJson);
}
private static (JobTrackerContext Db, SubmittedApplicationPackageService Service, Mock<IAttachmentStorage> Storage) New(string userId, Mock<ICvVariantService>? renderOverride = null)
{
var currentUser = new Mock<ICurrentUserService>();