fix(documents): bind submitted CV captures to their saved revision

This commit is contained in:
cesnimda
2026-11-11 18:17:08 +01:00
parent 9f341750ec
commit e161ea3346
4 changed files with 111 additions and 7 deletions
@@ -90,16 +90,60 @@ public sealed class SubmittedApplicationPackageTests
Assert.Empty(db.SubmittedApplicationPackages);
}
private static (JobTrackerContext Db, SubmittedApplicationPackageService Service, Mock<IAttachmentStorage> Storage) New(string userId)
[Theory]
[InlineData(false, 4)]
[InlineData(true, 4)]
[InlineData(false, 0)]
[InlineData(true, 0)]
public async Task Cv_changed_between_metadata_and_render_aborts_without_a_partial_package(bool deleted, int version)
{
var renderer = new Mock<ICvVariantService>();
var (db, service, storage) = New("owner-1", renderer);
await using var dispose = db;
var job = await SeedAsync(db, "owner-1", null);
(await db.CvVariants.SingleAsync()).Version = version;
await db.SaveChangesAsync();
var real = new CvVariantService(db, new CareerProfileService(db), new ThemedCvRenderer());
async Task<ThemedCvRenderResult?> RenderAfterChange(string owner, int id, CvRenderPerson person, CancellationToken ct, int? expected)
{
var current = await db.CvVariants.SingleAsync(x => x.Id == id, ct);
if (deleted) db.CvVariants.Remove(current);
else
{
current.Version += 1;
current.SettingsJson = CvVariantSettingsJson.Serialize(new CvVariantSettings { ThemeId = "nordic", Headline = "Newer CV content" });
}
await db.SaveChangesAsync(ct);
return await real.RenderAsync(owner, id, person, ct, expected);
}
renderer.Setup(x => x.RenderAsync("owner-1", It.IsAny<int>(), It.IsAny<CvRenderPerson>(), It.IsAny<CancellationToken>()))
.Returns((string owner, int id, CvRenderPerson person, CancellationToken ct) => RenderAfterChange(owner, id, person, ct, null));
renderer.Setup(x => x.RenderAsync("owner-1", It.IsAny<int>(), It.IsAny<CvRenderPerson>(), It.IsAny<CancellationToken>(), It.IsAny<int?>()))
.Returns((string owner, int id, CvRenderPerson person, CancellationToken ct, int? expected) => RenderAfterChange(owner, id, person, ct, expected));
await Assert.ThrowsAsync<DbUpdateConcurrencyException>(() => service.CaptureAsync("owner-1", job.Id, new CvRenderPerson("Ada", null), default));
Assert.Empty(db.SubmittedApplicationPackages);
Assert.Empty(db.SubmittedPackageAttachments);
renderer.Verify(x => x.RenderAsync("owner-1", It.IsAny<int>(), It.IsAny<CvRenderPerson>(), It.IsAny<CancellationToken>(), version > 0 ? version : null), Times.Once);
storage.Verify(x => x.SnapshotAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Never);
}
private static (JobTrackerContext Db, SubmittedApplicationPackageService Service, Mock<IAttachmentStorage> Storage) New(string userId, Mock<ICvVariantService>? renderOverride = null)
{
var currentUser = new Mock<ICurrentUserService>();
currentUser.SetupGet(x => x.UserId).Returns(userId);
var options = new DbContextOptionsBuilder<JobTrackerContext>().UseInMemoryDatabase(Guid.NewGuid().ToString()).Options;
var db = new JobTrackerContext(options, currentUser.Object);
var variants = new Mock<ICvVariantService>();
variants.Setup(x => x.RenderAsync(userId, It.IsAny<int>(), It.IsAny<CvRenderPerson>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new ThemedCvRenderResult("nordic", "cv.pdf", "<html>Frozen CV</html>"));
var variants = renderOverride ?? new Mock<ICvVariantService>();
if (renderOverride is null)
{
variants.Setup(x => x.RenderAsync(userId, It.IsAny<int>(), It.IsAny<CvRenderPerson>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new ThemedCvRenderResult("nordic", "cv.pdf", "<html>Frozen CV</html>"));
variants.Setup(x => x.RenderAsync(userId, It.IsAny<int>(), It.IsAny<CvRenderPerson>(), It.IsAny<CancellationToken>(), It.IsAny<int?>()))
.ReturnsAsync(new ThemedCvRenderResult("nordic", "cv.pdf", "<html>Frozen CV</html>"));
}
var storage = new Mock<IAttachmentStorage>();
storage.Setup(x => x.IsManagedPath(It.IsAny<string>())).Returns(true);
@@ -0,0 +1,44 @@
using System.Security.Claims;
using System.Text.Json;
using JobTrackerApi.Controllers;
using JobTrackerApi.Models;
using JobTrackerApi.Services;
using JobTrackerApi.Tests.TestSupport;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Moq;
using Xunit;
namespace JobTrackerApi.Tests;
public sealed class SubmittedPackageConflictTests
{
[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task Capture_reports_revision_conflict_only_for_an_authenticated_caller(bool authenticated)
{
var user = new ApplicationUser { Id = "owner" };
var users = TestHostFactory.CreateUserManager(user);
users.Setup(x => x.GetUserAsync(It.IsAny<ClaimsPrincipal>())).ReturnsAsync(authenticated ? user : null);
var packages = new Mock<ISubmittedApplicationPackageService>();
packages.Setup(x => x.CaptureAsync("owner", 7, It.IsAny<CvRenderPerson>(), It.IsAny<CancellationToken>()))
.ThrowsAsync(new DbUpdateConcurrencyException("CV changed"));
var controller = new ApplicationAssetsController(users.Object, Mock.Of<IApplicationAssetsService>(), packages.Object)
{ ControllerContext = new() { HttpContext = new DefaultHttpContext() } };
var result = await controller.CaptureSubmittedPackage(7, default);
if (authenticated)
{
var conflict = Assert.IsType<ConflictObjectResult>(result.Result);
Assert.Equal("revision_conflict", JsonSerializer.SerializeToElement(conflict.Value).GetProperty("code").GetString());
}
else
{
Assert.IsType<UnauthorizedResult>(result.Result);
packages.Verify(x => x.CaptureAsync(It.IsAny<string>(), It.IsAny<int>(), It.IsAny<CvRenderPerson>(), It.IsAny<CancellationToken>()), Times.Never);
}
}
}