add: Integration test gaps for deferred promotion and retry

This commit is contained in:
voltsrage
2026-06-27 20:56:02 +08:00
parent b50a0ec305
commit 903cd54a51
5 changed files with 521 additions and 2 deletions
@@ -0,0 +1,131 @@
using System.Net;
using System.Net.Http.Json;
using System.Text.Json;
using FluentAssertions;
using Microsoft.Extensions.DependencyInjection;
/// <summary>
/// Integration tests for concurrent operations: duplicate batch creation and
/// parallel assignment attempts.
/// </summary>
[Collection("Database")]
public class ConcurrencyTests : IAsyncLifetime
{
private readonly ApiFixture _fixture;
public ConcurrencyTests(ApiFixture fixture) => _fixture = fixture;
public async Task InitializeAsync()
{
using var scope = _fixture.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await DbResetHelper.ResetAsync(db);
await DataSeeder.SeedAsync(db);
}
public Task DisposeAsync() => Task.CompletedTask;
/// <summary>
/// Two parallel assignment requests for the same batch — exactly one should succeed,
/// the other should get 409 BATCH_ALREADY_ASSIGNED.
/// </summary>
[Fact]
public async Task ParallelAssign_SameBatch_ExactlyOneSucceeds()
{
// Arrange: upload a batch
var intakeClient = await AuthHelper.LoginAsync(_fixture, "intake1");
var fileContent = new ByteArrayContent(
System.Text.Encoding.ASCII.GetBytes($"%PDF-1.4\n%%EOF\n%test-{Guid.NewGuid()}"));
fileContent.Headers.ContentType =
new System.Net.Http.Headers.MediaTypeHeaderValue("application/pdf");
var formData = new MultipartFormDataContent
{
{ fileContent, "file", "test.pdf" },
{ new StringContent("VITALS_SHEET"), "batchType" }
};
var uploadResponse = await intakeClient.PostAsync("/api/v1/digitization-batches", formData);
uploadResponse.EnsureSuccessStatusCode();
var uploadBody = await uploadResponse.Content.ReadFromJsonAsync<JsonElement>();
var batchId = uploadBody.GetProperty("data").GetProperty("id").GetGuid();
// Get user IDs
using var scope = _fixture.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
var entry1Id = await BatchSeedHelper.UserIdAsync(db, "entry1");
var entry2Id = await BatchSeedHelper.UserIdAsync(db, "entry2");
// Act: parallel assignment
var task1 = intakeClient.PatchAsJsonAsync(
$"/api/v1/digitization-batches/{batchId}/assign",
new { entryClerkUserId = entry1Id });
var task2 = intakeClient.PatchAsJsonAsync(
$"/api/v1/digitization-batches/{batchId}/assign",
new { entryClerkUserId = entry2Id });
var results = await Task.WhenAll(task1, task2);
// Assert: exactly one 200 and one 409
var statuses = results.Select(r => r.StatusCode).OrderBy(s => s).ToList();
statuses.Should().Contain(HttpStatusCode.OK);
statuses.Should().Contain(HttpStatusCode.Conflict);
}
/// <summary>
/// Two uploads with the same file content for the same patient — the Redis dedup
/// guard should ensure exactly one succeeds.
/// </summary>
[Fact]
public async Task ParallelUpload_SameSha256_ExactlyOneSucceeds()
{
// Arrange: create a patient first via a promoted batch
var batchId = await BatchPipelineHelper.CreateAndVerifyBatchAsync(_fixture);
var approverClient = await AuthHelper.LoginAsync(_fixture, "approver1");
approverClient.DefaultRequestHeaders.Add("Idempotency-Key", Guid.NewGuid().ToString());
var approveResp = await approverClient.PostAsJsonAsync(
$"/api/v1/digitization-batches/{batchId}/approve",
new ApproveRequest());
approveResp.EnsureSuccessStatusCode();
approverClient.DefaultRequestHeaders.Remove("Idempotency-Key");
var approveBody = await approveResp.Content.ReadFromJsonAsync<JsonElement>();
var patientId = approveBody.GetProperty("data").GetProperty("patientId").GetGuid();
// Same file content for both uploads
var fileBytes = System.Text.Encoding.ASCII.GetBytes(
$"%PDF-1.4\n%%EOF\n%duplicate-test-{Guid.NewGuid()}");
var intakeClient = await AuthHelper.LoginAsync(_fixture, "intake1");
// Act: two parallel uploads with same content + same patient
var task1 = UploadWithBytes(intakeClient, fileBytes, patientId);
var task2 = UploadWithBytes(intakeClient, fileBytes, patientId);
var results = await Task.WhenAll(task1, task2);
// Assert: exactly one success and one conflict
var statuses = results.Select(r => r.StatusCode).OrderBy(s => s).ToList();
statuses.Should().Contain(HttpStatusCode.Created);
statuses.Should().Contain(HttpStatusCode.Conflict);
}
private static async Task<HttpResponseMessage> UploadWithBytes(
HttpClient client, byte[] fileBytes, Guid patientId)
{
var fileContent = new ByteArrayContent(fileBytes);
fileContent.Headers.ContentType =
new System.Net.Http.Headers.MediaTypeHeaderValue("application/pdf");
var formData = new MultipartFormDataContent
{
{ fileContent, "file", "duplicate.pdf" },
{ new StringContent("VITALS_SHEET"), "batchType" },
{ new StringContent(patientId.ToString()), "patientId" }
};
return await client.PostAsync("/api/v1/digitization-batches", formData);
}
}