fix(documents): bind submitted CV captures to their saved revision
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -148,8 +148,15 @@ public sealed class ApplicationAssetsController : ControllerBase
|
||||
{
|
||||
var user = await _users.GetUserAsync(User);
|
||||
if (user is null) return Unauthorized();
|
||||
var result = await _packages.CaptureAsync(user.Id, jobId, Person(user), ct);
|
||||
return result is null ? NotFound() : Ok(result);
|
||||
try
|
||||
{
|
||||
var result = await _packages.CaptureAsync(user.Id, jobId, Person(user), ct);
|
||||
return result is null ? NotFound() : Ok(result);
|
||||
}
|
||||
catch (DbUpdateConcurrencyException)
|
||||
{
|
||||
return Conflict(new { code = "revision_conflict", message = "The application documents changed. Reload and review them before capturing a submitted package." });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("submitted-packages/{packageId:int}")]
|
||||
|
||||
@@ -72,7 +72,16 @@ public sealed class SubmittedApplicationPackageService : ISubmittedApplicationPa
|
||||
? (await variantQuery.ToListAsync(ct)).MaxBy(v => v.UpdatedAtUtc)
|
||||
: await variantQuery.OrderByDescending(v => v.UpdatedAtUtc).FirstOrDefaultAsync(ct);
|
||||
var profile = await _db.CareerProfiles.AsNoTracking().FirstOrDefaultAsync(p => p.OwnerUserId == ownerUserId, ct);
|
||||
var render = variant is null ? null : await _variants.RenderAsync(ownerUserId, variant.Id, person, ct);
|
||||
var render = variant is null ? null : await _variants.RenderAsync(ownerUserId, variant.Id, person, ct, variant.Version > 0 ? variant.Version : null);
|
||||
if (variant is not null)
|
||||
{
|
||||
// Legacy version-zero variants remain capturable; recheck the stored snapshot after rendering.
|
||||
var current = await _db.CvVariants.AsNoTracking()
|
||||
.Where(v => v.Id == variant.Id && v.OwnerUserId == ownerUserId)
|
||||
.Select(v => new { v.Version, v.SettingsJson }).SingleOrDefaultAsync(ct);
|
||||
if (render is null || current is null || current.Version != variant.Version || current.SettingsJson != variant.SettingsJson)
|
||||
throw new DbUpdateConcurrencyException("The selected CV changed before the submitted package could be captured.");
|
||||
}
|
||||
var version = (await _db.SubmittedApplicationPackages.AsNoTracking()
|
||||
.Where(x => x.OwnerUserId == ownerUserId && x.JobApplicationId == jobApplicationId)
|
||||
.Select(x => (int?)x.Version)
|
||||
|
||||
Reference in New Issue
Block a user