Files
jobtrackingapp/JobTrackerApi.MariaDb.Tests/CvImportReviewedPersistenceTests.cs
T

124 lines
6.4 KiB
C#

using System.Security.Claims;
using System.Text.Json;
using JobTrackerApi.Controllers;
using JobTrackerApi.Data;
using JobTrackerApi.Models;
using JobTrackerApi.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Moq;
using Xunit;
namespace JobTrackerApi.MariaDb.Tests;
public class CvImportReviewedPersistenceTests
{
private const string Owner = "rd21-owner";
private static ProfileCvController Controller(JobTrackerContext db, bool identitySucceeds = true)
{
var user = db.Users.Single(u => u.Id == Owner);
var users = TestHostFactory.CreateUserManager();
users.Setup(u => u.GetUserAsync(It.IsAny<ClaimsPrincipal>())).ReturnsAsync(user);
users.Setup(u => u.UpdateAsync(user)).Returns(async () =>
{
if (!identitySucceeds) return IdentityResult.Failed(new IdentityError { Description = "Synthetic rollback" });
await db.SaveChangesAsync();
return IdentityResult.Success;
});
return new(users.Object, Mock.Of<ISummarizerService>(), db, null!)
{ ControllerContext = new() { HttpContext = new DefaultHttpContext() } };
}
private static async Task<CvExtractionRun> Seed(JobTrackerContext db)
{
db.Users.Add(new() { Id = Owner, UserName = "import-synthetic" });
var run = new CvExtractionRun
{
OwnerUserId = Owner, Trigger = "document_import", ParserVersion = ProfileCvController.DocumentParserVersion,
Status = "pending_review", RawExtractedText = "Skills\nC#\nSQL", DocumentJson = "{\"synthetic\":true}",
StructuredProfileJson = StructuredCvProfileJson.Serialize(new() { Skills = { "C#", "SQL" } }),
};
db.CvExtractionRuns.Add(run);
await db.SaveChangesAsync();
return run;
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task Reviewed_selection_commits_once_or_rolls_back_entire_projection(bool succeeds)
{
await using var database = await MariaDbDatabase.CreateAsync();
await using var db = database.Context();
var run = await Seed(db);
var request = new ProfileCvController.ApplyReviewedRequest(0, new[] { new CvImportDecision("skills.0", "use"), new CvImportDecision("skills.1", "keep") });
var result = await Controller(db, succeeds).ApplyReviewed(run.Id, request);
if (succeeds)
{
Assert.IsType<OkObjectResult>(result);
Assert.IsType<OkObjectResult>(await Controller(db).ApplyReviewed(run.Id, request));
Assert.IsType<ConflictObjectResult>(await Controller(db).ApplyReviewed(run.Id, new(0, new[] { new CvImportDecision("skills.1", "use") })));
}
else Assert.IsType<BadRequestObjectResult>(result);
await db.SaveChangesAsync();
await using var verify = database.Context();
var saved = await verify.CvExtractionRuns.SingleAsync();
Assert.Equal(succeeds ? "applied" : "pending_review", saved.Status);
Assert.Equal(succeeds, saved.ReviewJson is not null);
Assert.Equal(succeeds ? 1 : 0, await verify.CareerProfileVersions.CountAsync());
Assert.Equal(succeeds ? new[] { "C#" } : Array.Empty<string>(), await verify.CareerSkills.Select(s => s.Name).ToArrayAsync());
Assert.Equal(succeeds, (await verify.Users.SingleAsync()).CurrentCvExtractionRunId.HasValue);
}
[Fact]
public async Task Stale_review_does_not_overwrite_acknowledged_edit_or_mark_run_applied()
{
await using var database = await MariaDbDatabase.CreateAsync();
await using var db = database.Context();
var run = await Seed(db);
await new CareerProfileService(db).SaveVersionAsync(Owner, new() { Skills = { "Saved elsewhere" } }, "manual", 0, default);
Assert.IsType<ConflictObjectResult>(await Controller(db).ApplyReviewed(run.Id, new(0, new[] { new CvImportDecision("skills.0", "use") })));
await db.SaveChangesAsync();
await using var verify = database.Context();
Assert.Equal("Saved elsewhere", (await new CareerProfileService(verify).LoadStructuredAsync(Owner, default)).Skills.Single());
Assert.Equal("pending_review", (await verify.CvExtractionRuns.SingleAsync()).Status);
Assert.Single(await verify.CareerProfileVersions.ToListAsync());
}
[Fact]
public async Task Review_owner_boundary_and_unknown_fields_fail_closed()
{
await using var database = await MariaDbDatabase.CreateAsync();
await using var db = database.Context();
var run = await Seed(db);
Assert.IsType<BadRequestObjectResult>(await Controller(db).ApplyReviewed(run.Id, new(0, new[] { new CvImportDecision("metadata.owner", "correct", "other") })));
(await db.CvExtractionRuns.SingleAsync()).OwnerUserId = "another-owner";
await db.SaveChangesAsync();
Assert.IsType<NotFoundResult>(await Controller(db).GetImportReview(run.Id));
Assert.IsType<NotFoundResult>(await Controller(db).ApplyReviewed(run.Id, new(0, Array.Empty<CvImportDecision>())));
Assert.Empty(await db.CareerProfiles.ToListAsync());
}
[Fact]
public async Task Additive_import_schema_down_up_preserves_canonical_and_historical_facts()
{
await using var database = await MariaDbDatabase.CreateAsync();
await using var db = database.Context();
var run = await Seed(db);
await new CareerProfileService(db).SaveVersionAsync(Owner, new() { Skills = { "Existing" }, Contact = new() { GitHub = "https://github.com/example" } }, "manual", 0, default);
var before = (await db.CareerProfiles.SingleAsync()).ProfileJson;
await db.GetService<IMigrator>().MigrateAsync("20261025090000_AddCoverLetterDocumentDesign");
db.ChangeTracker.Clear();
await db.GetService<IMigrator>().MigrateAsync();
Assert.Equal(before, (await db.CareerProfiles.AsNoTracking().SingleAsync()).ProfileJson);
Assert.Equal(before, (await db.CareerProfileVersions.AsNoTracking().SingleAsync()).ProfileJson);
Assert.Null((await db.CvExtractionRuns.AsNoTracking().SingleAsync()).ReviewJson);
Assert.Equal("pending_review", (await db.CvExtractionRuns.AsNoTracking().SingleAsync()).Status);
Assert.Empty(await db.Database.GetPendingMigrationsAsync());
}
}