413 lines
23 KiB
C#
413 lines
23 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.Diagnostics;
|
|
using Moq;
|
|
using Xunit;
|
|
|
|
namespace JobTrackerApi.MariaDb.Tests;
|
|
|
|
public sealed class Rd21CareerPersistenceTests
|
|
{
|
|
private static JobTrackerContext Context(MariaDbDatabase connection, IInterceptor? interceptor = null) => connection.Context(interceptor: interceptor);
|
|
|
|
private static CareerProfileController Controller(JobTrackerContext db, bool identitySucceeds = true)
|
|
=> new(Users(db, identitySucceeds).Object, new CareerProfileService(db), db)
|
|
{
|
|
ControllerContext = new() { HttpContext = new DefaultHttpContext() },
|
|
};
|
|
|
|
private static Mock<UserManager<ApplicationUser>> Users(JobTrackerContext db, bool identitySucceeds)
|
|
{
|
|
var user = db.Users.Single();
|
|
var users = TestHostFactory.CreateUserManager();
|
|
users.Setup(x => x.GetUserAsync(It.IsAny<ClaimsPrincipal>())).ReturnsAsync(user);
|
|
users.Setup(x => x.UpdateAsync(user)).Returns(async () =>
|
|
{
|
|
if (!identitySucceeds) return IdentityResult.Failed(new IdentityError { Description = "Synthetic Identity failure" });
|
|
await db.SaveChangesAsync();
|
|
return IdentityResult.Success;
|
|
});
|
|
return users;
|
|
}
|
|
|
|
private static ProfileCvController ImportController(JobTrackerContext db, bool identitySucceeds = true) =>
|
|
new(Users(db, identitySucceeds).Object, new Mock<ISummarizerService>().Object, db, null!)
|
|
{ ControllerContext = new() { HttpContext = new DefaultHttpContext() } };
|
|
|
|
private static CareerProfileSaveRequest Request(string title, int? expectedVersion) =>
|
|
JsonSerializer.Deserialize<CareerProfileSaveRequest>(JsonSerializer.Serialize(new
|
|
{
|
|
profile = new StructuredCvProfile { Jobs = { new() { Id = "stable-role", Title = title } } },
|
|
expectedVersion,
|
|
}), new JsonSerializerOptions(JsonSerializerDefaults.Web))!;
|
|
|
|
[Fact]
|
|
public async Task Independent_stale_snapshot_cannot_replace_acknowledged_career_edit()
|
|
{
|
|
await using var connection = await MariaDbDatabase.CreateAsync();
|
|
await using (var seed = Context(connection))
|
|
{
|
|
seed.Users.Add(new() { Id = "rd21-owner", UserName = "synthetic" });
|
|
await seed.SaveChangesAsync();
|
|
Assert.IsType<OkObjectResult>((await Controller(seed).Put(Request("Original", 0), default)).Result);
|
|
}
|
|
await using var first = Context(connection);
|
|
await using var stale = Context(connection);
|
|
var staleController = Controller(stale);
|
|
var source = Assert.IsType<CareerProfileDto>(Assert.IsType<OkObjectResult>((await staleController.Get(default)).Result).Value);
|
|
Assert.Equal(1, source.Revision);
|
|
Assert.Equal("Original", source.Profile.Jobs.Single().Title);
|
|
Assert.IsType<OkObjectResult>((await Controller(first).Put(Request("Acknowledged winner", 1), default)).Result);
|
|
var rejected = await staleController.Put(Request("Stale draft", 1), default);
|
|
Assert.IsType<ConflictObjectResult>(rejected.Result);
|
|
await stale.SaveChangesAsync();
|
|
await using var verify = Context(connection);
|
|
Assert.Equal("Acknowledged winner", (await verify.CareerExperiences.SingleAsync()).Title);
|
|
Assert.Equal(2, (await verify.CareerProfiles.SingleAsync()).Version);
|
|
Assert.Equal(2, await verify.CareerProfileVersions.CountAsync());
|
|
Assert.Contains("Acknowledged winner", (await verify.Users.SingleAsync()).ProfileCvStructureJson);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Identity_failure_rolls_back_canonical_children_history_and_projection()
|
|
{
|
|
await using var connection = await MariaDbDatabase.CreateAsync();
|
|
await using var db = Context(connection);
|
|
db.Users.Add(new() { Id = "rd21-owner", UserName = "synthetic" });
|
|
await db.SaveChangesAsync();
|
|
var result = await Controller(db, false).Put(Request("Must not persist", 0), default);
|
|
Assert.IsType<BadRequestObjectResult>(result.Result);
|
|
await db.SaveChangesAsync();
|
|
await using var verify = Context(connection);
|
|
Assert.Empty(await verify.CareerProfiles.ToListAsync());
|
|
Assert.Empty(await verify.CareerExperiences.ToListAsync());
|
|
Assert.Empty(await verify.CareerProfileVersions.ToListAsync());
|
|
Assert.Null((await verify.Users.SingleAsync()).ProfileCvStructureJson);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Stale_tracked_entity_is_guarded_at_database_update_and_stale_restore_is_rejected()
|
|
{
|
|
await using var connection = await MariaDbDatabase.CreateAsync();
|
|
await using var first = Context(connection);
|
|
var a = new CareerProfileService(first);
|
|
await a.SaveVersionAsync("rd21-owner", new() { Jobs = { new() { Id = "role", Title = "Original" } } }, "manual", 0, default);
|
|
await using var stale = Context(connection);
|
|
await stale.CareerProfiles.SingleAsync();
|
|
await a.SaveVersionAsync("rd21-owner", new() { Jobs = { new() { Id = "role", Title = "Winner" } } }, "manual", 1, default);
|
|
var b = new CareerProfileService(stale);
|
|
await Assert.ThrowsAsync<DbUpdateConcurrencyException>(() => b.SaveVersionAsync("rd21-owner", new() { Jobs = { new() { Title = "Stale" } } }, "manual", 1, default));
|
|
await stale.SaveChangesAsync();
|
|
await Assert.ThrowsAsync<DbUpdateConcurrencyException>(() => b.RestoreVersionAsync("rd21-owner", 1, 1, default));
|
|
await using var verify = Context(connection);
|
|
Assert.Equal("Winner", (await verify.CareerExperiences.SingleAsync()).Title);
|
|
Assert.Equal(2, await verify.CareerProfileVersions.CountAsync());
|
|
Assert.Equal(0, (await b.LoadSnapshotAsync("another-owner", default)).Revision);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Competing_first_save_sources_have_one_acknowledged_winner()
|
|
{
|
|
await using var database = await MariaDbDatabase.CreateAsync();
|
|
await using (var setup = Context(database))
|
|
{
|
|
setup.Users.Add(new() { Id = "rd21-owner", UserName = "synthetic" });
|
|
await setup.SaveChangesAsync();
|
|
}
|
|
var ready = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
var release = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
var readers = 0;
|
|
async Task<IActionResult?> Save(string title)
|
|
{
|
|
await using var db = Context(database);
|
|
var controller = Controller(db);
|
|
Assert.Equal(0, Assert.IsType<CareerProfileDto>(Assert.IsType<OkObjectResult>((await controller.Get(default)).Result).Value).Revision);
|
|
if (Interlocked.Increment(ref readers) == 2) ready.TrySetResult();
|
|
await release.Task.WaitAsync(TimeSpan.FromSeconds(15));
|
|
return (await controller.Put(Request(title, 0), default)).Result;
|
|
}
|
|
var a = Save("Candidate A");
|
|
var b = Save("Candidate B");
|
|
try { await ready.Task.WaitAsync(TimeSpan.FromSeconds(10)); }
|
|
finally { release.TrySetResult(); }
|
|
var outcomes = await Task.WhenAll(a, b);
|
|
Assert.Single(outcomes.OfType<OkObjectResult>());
|
|
Assert.Single(outcomes.OfType<ConflictObjectResult>());
|
|
await using var verify = Context(database);
|
|
var profile = await verify.CareerProfiles.SingleAsync();
|
|
Assert.Equal(1, profile.Version);
|
|
Assert.Single(await verify.CareerProfileVersions.ToListAsync());
|
|
Assert.Equal(profile.ProfileJson, (await verify.Users.SingleAsync()).ProfileCvStructureJson);
|
|
Assert.Equal(StructuredCvProfileJson.DeserializePersisted(profile.ProfileJson).Jobs.Single().Title, (await verify.CareerExperiences.SingleAsync()).Title);
|
|
}
|
|
|
|
private sealed class FailAfterChildDeletion : SaveChangesInterceptor
|
|
{
|
|
private int _saves;
|
|
public override ValueTask<InterceptionResult<int>> SavingChangesAsync(DbContextEventData eventData, InterceptionResult<int> result, CancellationToken cancellationToken = default)
|
|
{
|
|
if (++_saves == 3) throw new InvalidOperationException("Synthetic child insert failure");
|
|
return base.SavingChangesAsync(eventData, result, cancellationToken);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Database_failure_after_deletion_restores_children_history_and_revision_and_clears_tracker()
|
|
{
|
|
await using var connection = await MariaDbDatabase.CreateAsync();
|
|
await using (var seed = Context(connection))
|
|
{
|
|
await new CareerProfileService(seed).SaveVersionAsync("rd21-owner", new() { Jobs = { new() { Id = "role", Title = "Original" } } }, "manual", 0, default);
|
|
}
|
|
await using var failed = Context(connection, new FailAfterChildDeletion());
|
|
await Assert.ThrowsAsync<InvalidOperationException>(() => new CareerProfileService(failed).SaveVersionAsync("rd21-owner", new() { Jobs = { new() { Title = "Rejected" } } }, "manual", 1, default));
|
|
Assert.Empty(failed.ChangeTracker.Entries());
|
|
await failed.SaveChangesAsync();
|
|
await using var verify = Context(connection);
|
|
Assert.Equal("Original", (await verify.CareerExperiences.SingleAsync()).Title);
|
|
Assert.Equal(1, (await verify.CareerProfiles.SingleAsync()).Version);
|
|
Assert.Single(await verify.CareerProfileVersions.ToListAsync());
|
|
await new CareerProfileService(failed).SaveVersionAsync("rd21-owner", new() { Jobs = { new() { Id = "role", Title = "Recovered" } } }, "manual", 1, default);
|
|
await using var recovered = Context(connection);
|
|
Assert.Equal("Recovered", (await recovered.CareerExperiences.SingleAsync()).Title);
|
|
Assert.Equal(2, (await recovered.CareerProfiles.SingleAsync()).Version);
|
|
Assert.Equal(2, await recovered.CareerProfileVersions.CountAsync());
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(null)]
|
|
[InlineData(-1)]
|
|
public async Task Replacement_restore_parse_and_import_fail_closed_without_valid_revision(int? expectedVersion)
|
|
{
|
|
await using var connection = await MariaDbDatabase.CreateAsync();
|
|
await using var db = Context(connection);
|
|
db.Users.Add(new() { Id = "rd21-owner", UserName = "synthetic" });
|
|
await db.SaveChangesAsync();
|
|
var controller = Controller(db);
|
|
Assert.IsType<BadRequestObjectResult>((await controller.Put(Request("Rejected", expectedVersion), default)).Result);
|
|
Assert.IsType<BadRequestObjectResult>((await controller.Restore(1, new(expectedVersion), default)).Result);
|
|
var imports = ImportController(db);
|
|
Assert.IsType<BadRequestObjectResult>((await imports.Parse(new("# Synthetic", expectedVersion))).Result);
|
|
Assert.IsType<BadRequestObjectResult>(await imports.AcceptRun(1, new(null, expectedVersion)));
|
|
Assert.Empty(await db.CareerProfiles.ToListAsync());
|
|
Assert.Empty(await db.CvExtractionRuns.ToListAsync());
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(true)]
|
|
[InlineData(false)]
|
|
public async Task Reviewed_import_commits_or_rolls_back_canonical_projection_and_application_metadata(bool identitySucceeds)
|
|
{
|
|
await using var connection = await MariaDbDatabase.CreateAsync();
|
|
await using var db = Context(connection);
|
|
db.Users.Add(new() { Id = "rd21-owner", UserName = "synthetic" });
|
|
var run = new CvExtractionRun
|
|
{
|
|
OwnerUserId = "rd21-owner", Trigger = "upload", Status = "pending_review",
|
|
StructuredProfileJson = StructuredCvProfileJson.Serialize(new() { Jobs = { new() { Id = "imported-role", Title = "Reviewed role" } } }),
|
|
};
|
|
db.CvExtractionRuns.Add(run);
|
|
await db.SaveChangesAsync();
|
|
var result = await ImportController(db, identitySucceeds).AcceptRun(run.Id, new(null, 0));
|
|
if (identitySucceeds) Assert.IsType<OkObjectResult>(result);
|
|
else Assert.IsType<BadRequestObjectResult>(result);
|
|
await db.SaveChangesAsync();
|
|
await using var verify = Context(connection);
|
|
var savedRun = await verify.CvExtractionRuns.SingleAsync();
|
|
var user = await verify.Users.SingleAsync();
|
|
Assert.Equal(identitySucceeds ? "applied" : "pending_review", savedRun.Status);
|
|
Assert.Equal(identitySucceeds, savedRun.AppliedAtUtc.HasValue);
|
|
Assert.Equal(identitySucceeds, user.CurrentCvExtractionRunId.HasValue);
|
|
Assert.Equal(identitySucceeds ? 1 : 0, await verify.CareerProfileVersions.CountAsync());
|
|
Assert.Equal(identitySucceeds ? 1 : 0, await verify.CareerExperiences.CountAsync());
|
|
Assert.Equal(identitySucceeds, user.ProfileCvStructureJson is not null);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Import_acceptance_requires_the_revision_shown_in_its_diff()
|
|
{
|
|
await using var connection = await MariaDbDatabase.CreateAsync();
|
|
await using var db = Context(connection);
|
|
db.Users.Add(new() { Id = "rd21-owner", UserName = "synthetic" });
|
|
var run = new CvExtractionRun { OwnerUserId = "rd21-owner", Trigger = "upload", Status = "pending_review", StructuredProfileJson = "{}" };
|
|
db.CvExtractionRuns.Add(run);
|
|
await db.SaveChangesAsync();
|
|
var imports = ImportController(db);
|
|
var diff = Assert.IsType<OkObjectResult>(await imports.GetRunDiff(run.Id));
|
|
Assert.Equal(0, JsonSerializer.SerializeToElement(diff.Value).GetProperty("revision").GetInt32());
|
|
Assert.IsType<OkObjectResult>((await Controller(db).Put(Request("Winner", 0), default)).Result);
|
|
Assert.IsType<ConflictObjectResult>(await imports.AcceptRun(run.Id, new(null, 0)));
|
|
await using var verify = Context(connection);
|
|
Assert.Equal("pending_review", (await verify.CvExtractionRuns.SingleAsync()).Status);
|
|
Assert.Equal("Winner", (await verify.CareerExperiences.SingleAsync()).Title);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(true)]
|
|
[InlineData(false)]
|
|
public async Task Parsing_commits_or_rolls_back_its_entire_import_application(bool identitySucceeds)
|
|
{
|
|
await using var connection = await MariaDbDatabase.CreateAsync();
|
|
await using var db = Context(connection);
|
|
db.Users.Add(new() { Id = "rd21-owner", UserName = "synthetic" });
|
|
await db.SaveChangesAsync();
|
|
const string text = "# Contact\nSynthetic Person\n\n# Professional Summary\nBuilt APIs.\n\n# Skills\nC#\nSQL";
|
|
var result = await ImportController(db, identitySucceeds).Parse(new(text, 0));
|
|
if (identitySucceeds) Assert.IsType<OkObjectResult>(result.Result);
|
|
else Assert.IsType<BadRequestObjectResult>(result.Result);
|
|
await db.SaveChangesAsync();
|
|
await using var verify = Context(connection);
|
|
Assert.Equal(identitySucceeds ? 1 : 0, await verify.CvExtractionRuns.CountAsync());
|
|
Assert.Equal(identitySucceeds ? 1 : 0, await verify.CareerProfileVersions.CountAsync());
|
|
var user = await verify.Users.SingleAsync();
|
|
Assert.Equal(identitySucceeds, user.CurrentCvExtractionRunId.HasValue);
|
|
Assert.Equal(identitySucceeds, user.ProfileCvStructureJson is not null);
|
|
if (identitySucceeds)
|
|
{
|
|
var profile = await verify.CareerProfiles.SingleAsync();
|
|
Assert.Equal(profile.ProfileJson, user.ProfileCvStructureJson);
|
|
Assert.Equal(profile.ProfileJson, (await verify.CvExtractionRuns.SingleAsync()).StructuredProfileJson);
|
|
Assert.Equal(user.ProfileCvText, (await new CareerProfileService(verify).LoadSnapshotAsync("rd21-owner", default)).CvText);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Editor_snapshot_uses_relational_canonical_source_and_never_writes_during_read()
|
|
{
|
|
await using var connection = await MariaDbDatabase.CreateAsync();
|
|
await using var db = Context(connection);
|
|
db.Users.Add(new() { Id = "rd21-owner", UserName = "synthetic", ProfileCvText = "Stored source text" });
|
|
var profile = new CareerProfile
|
|
{
|
|
OwnerUserId = "rd21-owner", Version = 7,
|
|
ProfileJson = StructuredCvProfileJson.SerializePersisted(new() { Jobs = { new() { Title = "Outdated projection" } } }),
|
|
LongTailJson = CareerProfileMapper.SerializeLongTail(new() { Summary = { "Canonical summary" } }),
|
|
Experiences = { new() { OwnerUserId = "rd21-owner", ItemKey = "canonical-role", Title = "Canonical role" } },
|
|
};
|
|
db.CareerProfiles.Add(profile);
|
|
await db.SaveChangesAsync();
|
|
db.ChangeTracker.Clear();
|
|
var snapshot = await new CareerProfileService(db).LoadSnapshotAsync("rd21-owner", default);
|
|
Assert.Equal(7, snapshot.Revision);
|
|
Assert.Equal("Canonical role", snapshot.Profile.Jobs.Single().Title);
|
|
Assert.Equal("canonical-role", snapshot.Profile.Jobs.Single().Id);
|
|
Assert.Equal("Canonical summary", snapshot.Profile.Summary.Single());
|
|
Assert.Equal("Stored source text", snapshot.CvText);
|
|
Assert.Empty(db.ChangeTracker.Entries());
|
|
Assert.Equal(7, (await db.CareerProfiles.SingleAsync()).Version);
|
|
Assert.Single(await db.CareerExperiences.ToListAsync());
|
|
Assert.Empty(await db.CareerProfileVersions.ToListAsync());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Empty_canonical_profile_preserves_legacy_raw_text_in_its_revision_zero_snapshot()
|
|
{
|
|
await using var connection = await MariaDbDatabase.CreateAsync();
|
|
await using var db = Context(connection);
|
|
db.Users.Add(new() { Id = "rd21-owner", UserName = "synthetic", ProfileCvText = "Legacy raw CV text" });
|
|
await db.SaveChangesAsync();
|
|
var response = await Controller(db).Get(default);
|
|
var dto = Assert.IsType<CareerProfileDto>(Assert.IsType<OkObjectResult>(response.Result).Value);
|
|
Assert.Equal(0, dto.Revision);
|
|
Assert.Empty(dto.Profile.Jobs);
|
|
Assert.Equal("Legacy raw CV text", dto.CvText);
|
|
Assert.Empty(await db.CareerProfiles.ToListAsync());
|
|
Assert.Empty(await db.CareerProfileVersions.ToListAsync());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Extraction_history_returns_only_the_ten_newest_owned_runs_with_exact_metadata()
|
|
{
|
|
await using var database = await MariaDbDatabase.CreateAsync();
|
|
var started = new DateTimeOffset(2026, 1, 1, 12, 0, 0, TimeSpan.Zero);
|
|
var owner = new ApplicationUser { Id = "rd21-owner", UserName = "history-owner@example.test" };
|
|
CvExtractionRun[] expected;
|
|
Guid latestOperationId;
|
|
await using (var seed = Context(database))
|
|
{
|
|
seed.Users.AddRange(owner, new ApplicationUser { Id = "other-owner", UserName = "history-other@example.test" });
|
|
var owned = Enumerable.Range(0, 13).Select(index => new CvExtractionRun
|
|
{
|
|
OwnerUserId = owner.Id, Trigger = "upload", Status = index == 12 ? "failed" : "applied",
|
|
Artifact = new CvUploadArtifact
|
|
{
|
|
OwnerUserId = owner.Id, OriginalFileName = $"owned-{index}.pdf", StoredFileName = $"stored-{index}.pdf",
|
|
MimeType = "application/pdf", Sha256 = new string('a', 64), StoragePath = $"fixture-{index}", ByteSize = 32,
|
|
},
|
|
StartedAtUtc = started.AddMinutes(index), CompletedAtUtc = started.AddMinutes(index).AddSeconds(10),
|
|
AppliedAtUtc = index == 12 ? null : started.AddMinutes(index).AddSeconds(20),
|
|
ParserVersion = $"parser-{index}", NormalizerVersion = $"normalizer-{index}", LlmPromptVersion = $"prompt-{index}",
|
|
ErrorMessage = index == 12 ? "Synthetic parser failure" : null,
|
|
}).ToArray();
|
|
seed.CvExtractionRuns.AddRange(owned);
|
|
seed.CvExtractionRuns.AddRange(Enumerable.Range(0, 3).Select(index => new CvExtractionRun
|
|
{
|
|
OwnerUserId = "other-owner", Trigger = "private-upload", Status = "pending_review", StartedAtUtc = started.AddDays(1).AddMinutes(index),
|
|
}));
|
|
await seed.SaveChangesAsync();
|
|
expected = owned.OrderByDescending(run => run.StartedAtUtc).Take(10).ToArray();
|
|
var subject = expected[0].Id.ToString(System.Globalization.CultureInfo.InvariantCulture);
|
|
var latest = new UserOperation
|
|
{
|
|
OwnerUserId = owner.Id, TaskType = CvProcessingQueue.TaskType, SubjectType = CvProcessingQueue.SubjectType,
|
|
SubjectId = subject, IdempotencyKey = "history-latest", Status = OperationStatuses.Succeeded,
|
|
CreatedAtUtc = started.AddHours(2).UtcDateTime, AvailableAtUtc = started.UtcDateTime,
|
|
};
|
|
seed.UserOperations.AddRange(latest,
|
|
new UserOperation
|
|
{
|
|
OwnerUserId = owner.Id, TaskType = CvProcessingQueue.TaskType, SubjectId = subject,
|
|
IdempotencyKey = "history-older", Status = OperationStatuses.Failed,
|
|
CreatedAtUtc = started.AddHours(1).UtcDateTime, AvailableAtUtc = started.UtcDateTime,
|
|
},
|
|
new UserOperation
|
|
{
|
|
OwnerUserId = "other-owner", TaskType = CvProcessingQueue.TaskType, SubjectId = subject,
|
|
IdempotencyKey = "history-private", Status = OperationStatuses.Running,
|
|
CreatedAtUtc = started.AddDays(2).UtcDateTime, AvailableAtUtc = started.UtcDateTime,
|
|
});
|
|
await seed.SaveChangesAsync();
|
|
latestOperationId = latest.Id;
|
|
}
|
|
await using var read = Context(database);
|
|
var users = TestHostFactory.CreateUserManager(owner);
|
|
users.Setup(manager => manager.GetUserAsync(It.IsAny<ClaimsPrincipal>())).ReturnsAsync(owner);
|
|
var controller = new ProfileCvController(users.Object, Mock.Of<ISummarizerService>(), read, null!)
|
|
{
|
|
ControllerContext = new() { HttpContext = new DefaultHttpContext() },
|
|
};
|
|
var response = Assert.IsType<OkObjectResult>((await controller.GetRuns()).Result);
|
|
Assert.Equal(200, response.StatusCode);
|
|
var actual = Assert.IsAssignableFrom<IEnumerable<CvExtractionRunListItem>>(response.Value).ToArray();
|
|
Assert.Equal(10, actual.Length);
|
|
Assert.Equal(expected.Select(run => run.Id), actual.Select(run => run.Id));
|
|
Assert.Equal(expected.Select(run => run.StartedAtUtc), actual.Select(run => run.StartedAtUtc));
|
|
for (var index = 0; index < actual.Length; index++)
|
|
{
|
|
Assert.Equal(expected[index].Trigger, actual[index].Trigger);
|
|
Assert.Equal(expected[index].Status, actual[index].Status);
|
|
Assert.Equal(expected[index].Artifact!.OriginalFileName, actual[index].ArtifactFileName);
|
|
Assert.Equal(expected[index].CompletedAtUtc, actual[index].CompletedAtUtc);
|
|
Assert.Equal(expected[index].AppliedAtUtc, actual[index].AppliedAtUtc);
|
|
Assert.Equal(expected[index].ParserVersion, actual[index].ParserVersion);
|
|
Assert.Equal(expected[index].NormalizerVersion, actual[index].NormalizerVersion);
|
|
Assert.Equal(expected[index].LlmPromptVersion, actual[index].LlmPromptVersion);
|
|
Assert.Equal(expected[index].ErrorMessage, actual[index].ErrorMessage);
|
|
}
|
|
Assert.Equal(latestOperationId, actual[0].Operation!.Id);
|
|
Assert.Equal(OperationStatuses.Succeeded, actual[0].Operation!.Status);
|
|
Assert.All(actual.Skip(1), run => Assert.Null(run.Operation));
|
|
Assert.Empty(read.ChangeTracker.Entries());
|
|
}
|
|
}
|