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.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Moq; using Xunit; namespace JobTrackerApi.Tests; public class CvImportMappingControllerTests { [Fact] public async Task Mapping_order_is_part_of_replay_identity_while_exact_legacy_receipts_still_work() { await using var db = TestHostFactory.CreateInMemoryDb("user-1"); var user = new ApplicationUser { Id = "user-1", UserName = "mapping-order" }; db.Users.Add(user); var blocks = new[] { new CvImportBlock("b1", "Engineer First", "paragraph", 1), new CvImportBlock("b2", "Lead Second", "paragraph", 1) }; var document = new CvImportDocument("Engineer First Lead Second", false, "application/pdf", 1, 26, "synthetic.pdf", blocks, Array.Empty(), "synthetic"); var run = new CvExtractionRun { OwnerUserId = user.Id, ParserVersion = ProfileCvController.DocumentParserVersion, Status = "pending_review", DocumentJson = JsonSerializer.Serialize(document), StructuredProfileJson = StructuredCvProfileJson.SerializePersisted(new()) }; db.CvExtractionRuns.Add(run); await db.SaveChangesAsync(); var users = TestHostFactory.CreateUserManager(); users.Setup(u => u.GetUserAsync(It.IsAny())).ReturnsAsync(user); users.Setup(u => u.UpdateAsync(user)).ReturnsAsync(IdentityResult.Success); var controller = new ProfileCvController(users.Object, Mock.Of(), db, null!) { ControllerContext = new() { HttpContext = new DefaultHttpContext() } }; var mappings = new[] { new CvImportRecordMapping("z-first", "jobs", null, new[] { new CvImportMappedField("title", "Engineer", "b1"), new CvImportMappedField("company", "First", "b1") }), new CvImportRecordMapping("a-second", "jobs", null, new[] { new CvImportMappedField("title", "Lead", "b2"), new CvImportMappedField("company", "Second", "b2") }), }; var request = new ProfileCvController.ApplyReviewedRequest(0, Array.Empty(), mappings); Assert.IsType(await controller.ApplyReviewed(run.Id, request)); var receipt = System.Text.Json.Nodes.JsonNode.Parse(run.ReviewJson!)!; receipt["Hash"] = Convert.ToHexString(System.Security.Cryptography.SHA256.HashData(JsonSerializer.SerializeToUtf8Bytes(new { request.ExpectedVersion, Decisions = request.Decisions!.OrderBy(d => d.Id, StringComparer.Ordinal), Mappings = mappings.OrderBy(m => m.ClientId, StringComparer.Ordinal) }))); run.ReviewJson = receipt.ToJsonString(); await db.SaveChangesAsync(); Assert.IsType(await controller.ApplyReviewed(run.Id, request)); Assert.IsType(await controller.ApplyReviewed(run.Id, request with { Mappings = mappings.Reverse().ToArray() })); Assert.Single(await db.CareerProfileVersions.ToListAsync()); } [Fact] public async Task Mapping_only_apply_is_revision_bound_audited_and_idempotent() { await using var db = TestHostFactory.CreateInMemoryDb("user-1"); var user = new ApplicationUser { Id = "user-1", UserName = "mapping-user" }; db.Users.Add(user); var document = new CvImportDocument("Engineer Example 2020", false, "application/pdf", 1, 21, "synthetic.pdf", new[] { new CvImportBlock("b1", "Engineer Example 2020", "paragraph", 1) }, Array.Empty(), "docling"); var run = new CvExtractionRun { OwnerUserId = user.Id, ParserVersion = ProfileCvController.DocumentParserVersion, Status = "pending_review", DocumentJson = JsonSerializer.Serialize(document), StructuredProfileJson = StructuredCvProfileJson.SerializePersisted(new()) }; db.CvExtractionRuns.Add(run); await db.SaveChangesAsync(); var users = TestHostFactory.CreateUserManager(); users.Setup(u => u.GetUserAsync(It.IsAny())).ReturnsAsync(user); users.Setup(u => u.UpdateAsync(user)).ReturnsAsync(IdentityResult.Success); var controller = new ProfileCvController(users.Object, Mock.Of(), db, null!) { ControllerContext = new() { HttpContext = new DefaultHttpContext() } }; var review = Assert.IsType(Assert.IsType(await controller.GetImportReview(run.Id)).Value); Assert.Single(review.SourceBlocks); Assert.Contains(review.MappingCategories, c => c.Category == "jobs" && c.Fields.Any(f => f.Name == "title")); var mappings = new[] { new CvImportRecordMapping("job-1", "jobs", null, new[] { new CvImportMappedField("title", "Engineer", "b1"), new CvImportMappedField("company", "Example", "b1"), new CvImportMappedField("start", "2020", "b1") }) }; var request = new ProfileCvController.ApplyReviewedRequest(0, Array.Empty(), mappings); Assert.IsType(await controller.ApplyReviewed(run.Id, request)); Assert.IsType(await controller.ApplyReviewed(run.Id, request)); Assert.Single((await new CareerProfileService(db).LoadStructuredAsync(user.Id, default)).Jobs); Assert.Single(await db.CareerProfileVersions.ToListAsync()); Assert.Contains("job-1", run.ReviewJson); Assert.IsType(await controller.ApplyReviewed(run.Id, request with { Mappings = new[] { mappings[0] with { ClientId = "changed" } } })); var updatedReview = Assert.IsType(Assert.IsType(await controller.GetImportReview(run.Id)).Value); Assert.Contains(updatedReview.Targets, t => t.Category == "jobs" && t.Index == 0); Assert.Equal("Engineer", updatedReview.Targets.Single(t => t.Category == "jobs" && t.Index == 0).Values!["title"]); } }