using System.Net; using System.Net.Http.Json; using System.Text.Json; using FluentAssertions; using Microsoft.Extensions.DependencyInjection; /// /// Integration tests for concurrent operations: duplicate batch creation and /// parallel assignment attempts. /// [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(); await DbResetHelper.ResetAsync(db); await DataSeeder.SeedAsync(db); } public Task DisposeAsync() => Task.CompletedTask; /// /// Two parallel assignment requests for the same batch — exactly one should succeed, /// the other should get 409 BATCH_ALREADY_ASSIGNED. /// [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(); var batchId = uploadBody.GetProperty("data").GetProperty("id").GetGuid(); // Get user IDs using var scope = _fixture.Services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); 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); } /// /// Two uploads with the same file content for the same patient — the Redis dedup /// guard should ensure exactly one succeeds. /// [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(); 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 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); } }