fix(documents): reject profile changes during submitted CV rendering
This commit is contained in:
@@ -4,6 +4,7 @@ using JobTrackerApi.Services;
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using System.Text.Json;
|
||||||
using Moq;
|
using Moq;
|
||||||
using Xunit;
|
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);
|
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)
|
private static (JobTrackerContext Db, SubmittedApplicationPackageService Service, Mock<IAttachmentStorage> Storage) New(string userId, Mock<ICvVariantService>? renderOverride = null)
|
||||||
{
|
{
|
||||||
var currentUser = new Mock<ICurrentUserService>();
|
var currentUser = new Mock<ICurrentUserService>();
|
||||||
|
|||||||
@@ -81,6 +81,10 @@ public sealed class SubmittedApplicationPackageService : ISubmittedApplicationPa
|
|||||||
.Select(v => new { v.Version, v.SettingsJson }).SingleOrDefaultAsync(ct);
|
.Select(v => new { v.Version, v.SettingsJson }).SingleOrDefaultAsync(ct);
|
||||||
if (render is null || current is null || current.Version != variant.Version || current.SettingsJson != variant.SettingsJson)
|
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.");
|
throw new DbUpdateConcurrencyException("The selected CV changed before the submitted package could be captured.");
|
||||||
|
var currentProfile = await _db.CareerProfiles.AsNoTracking().Where(p => p.OwnerUserId == ownerUserId)
|
||||||
|
.Select(p => new { p.Version, p.ProfileJson }).FirstOrDefaultAsync(ct);
|
||||||
|
if (currentProfile?.Version != profile?.Version || currentProfile?.ProfileJson != profile?.ProfileJson)
|
||||||
|
throw new DbUpdateConcurrencyException("The Career Profile changed before the submitted package could be captured.");
|
||||||
}
|
}
|
||||||
var version = (await _db.SubmittedApplicationPackages.AsNoTracking()
|
var version = (await _db.SubmittedApplicationPackages.AsNoTracking()
|
||||||
.Where(x => x.OwnerUserId == ownerUserId && x.JobApplicationId == jobApplicationId)
|
.Where(x => x.OwnerUserId == ownerUserId && x.JobApplicationId == jobApplicationId)
|
||||||
|
|||||||
Reference in New Issue
Block a user