test(cv-qa): prove connection-loss rollback and bounded parser safety
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
using System.Security.Claims;
|
||||
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 Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace JobTrackerApi.MariaDb.Tests;
|
||||
|
||||
public class CvImportConnectionLossTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task Connection_loss_after_all_writes_before_commit_rolls_back_and_exact_retry_is_durable()
|
||||
{
|
||||
await using var database = await MariaDbDatabase.CreateAsync();
|
||||
const string owner = "rd21-owner";
|
||||
int runId;
|
||||
await using (var seed = database.Context())
|
||||
{
|
||||
seed.Users.Add(new() { Id = owner, UserName = "connection-loss-synthetic" });
|
||||
var run = new CvExtractionRun { OwnerUserId = owner, Trigger = "document_import", ParserVersion = ProfileCvController.DocumentParserVersion,
|
||||
Status = "pending_review", StartedAtUtc = DateTimeOffset.UtcNow, RawExtractedText = "C#", DocumentJson = "{}",
|
||||
StructuredProfileJson = StructuredCvProfileJson.SerializePersisted(new() { Skills = { "C#" } }) };
|
||||
seed.CvExtractionRuns.Add(run);
|
||||
await seed.SaveChangesAsync();
|
||||
runId = run.Id;
|
||||
}
|
||||
bool killed = false;
|
||||
ProfileCvController Controller(JobTrackerContext db, bool interrupt)
|
||||
{
|
||||
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 () =>
|
||||
{
|
||||
await db.SaveChangesAsync();
|
||||
if (interrupt)
|
||||
{
|
||||
var connection = Assert.IsType<MySqlConnector.MySqlConnection>(db.Database.GetDbConnection());
|
||||
var thread = connection.ServerThread;
|
||||
Assert.True(thread > 0);
|
||||
await using var killer = database.Context();
|
||||
// Only the connection opened by this guarded disposable database fixture is terminated.
|
||||
await killer.Database.ExecuteSqlRawAsync("KILL CONNECTION " + thread.ToString(System.Globalization.CultureInfo.InvariantCulture));
|
||||
killed = true;
|
||||
}
|
||||
return IdentityResult.Success;
|
||||
});
|
||||
return new(users.Object, Mock.Of<ISummarizerService>(), db, null!)
|
||||
{ ControllerContext = new() { HttpContext = new DefaultHttpContext() } };
|
||||
}
|
||||
var request = new ProfileCvController.ApplyReviewedRequest(0, new[] { new CvImportDecision("skills.0", "use") });
|
||||
await using (var broken = database.Context())
|
||||
{
|
||||
using var lifetime = new CancellationTokenSource();
|
||||
var controller = Controller(broken, true);
|
||||
controller.HttpContext.RequestAborted = lifetime.Token;
|
||||
var applying = controller.ApplyReviewed(runId, request);
|
||||
var failure = await Record.ExceptionAsync(() => applying.WaitAsync(TimeSpan.FromSeconds(15)));
|
||||
if (failure is TimeoutException)
|
||||
{
|
||||
lifetime.Cancel();
|
||||
await Record.ExceptionAsync(() => applying.WaitAsync(TimeSpan.FromSeconds(5)));
|
||||
}
|
||||
Assert.True(killed);
|
||||
Assert.NotNull(failure);
|
||||
Assert.IsNotType<TimeoutException>(failure);
|
||||
}
|
||||
await using (var verify = database.Context())
|
||||
{
|
||||
var run = await verify.CvExtractionRuns.SingleAsync();
|
||||
Assert.Equal("pending_review", run.Status);
|
||||
Assert.Null(run.ReviewJson);
|
||||
Assert.Empty(await verify.CareerProfileVersions.ToListAsync());
|
||||
Assert.Empty(await verify.CareerSkills.ToListAsync());
|
||||
Assert.Null((await verify.Users.SingleAsync()).CurrentCvExtractionRunId);
|
||||
var snapshot = await new CareerProfileService(verify).LoadSnapshotAsync(owner, default);
|
||||
Assert.Equal(0, snapshot.Revision);
|
||||
Assert.Empty(snapshot.Profile.Skills);
|
||||
Assert.IsType<OkObjectResult>(await Controller(verify, false).ApplyReviewed(runId, request));
|
||||
}
|
||||
await using var replay = database.Context();
|
||||
Assert.IsType<OkObjectResult>(await Controller(replay, false).ApplyReviewed(runId, request));
|
||||
Assert.Single(await replay.CareerProfileVersions.ToListAsync());
|
||||
Assert.Equal("C#", (await replay.CareerSkills.SingleAsync()).Name);
|
||||
Assert.Equal("applied", (await replay.CvExtractionRuns.SingleAsync()).Status);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user