add: Integration test gaps for deferred promotion and retry
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user