using System.Net; using System.Net.Http.Json; using FluentAssertions; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; /// /// Integration tests for correction supersession via the full HTTP pipeline. /// [Collection("Database")] public class CorrectionSupersessionTests : IAsyncLifetime { private readonly ApiFixture _fixture; public CorrectionSupersessionTests(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; [Fact] public async Task WrongPotassium_CorrectionBatch_SupersedesOriginal() { // Act 1: promote original batch with wrong potassium (3.5) var (originalBatchId, patientId) = await CorrectionPipelineHelper.PromoteLabBatchAsync(_fixture, potassiumValue: 3.5m); using var scope = _fixture.Services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); var liveObsAfterOriginal = await db.LiveObservations .Where(o => o.SourceBatchId == originalBatchId) .ToListAsync(); liveObsAfterOriginal.Should().HaveCount(2); liveObsAfterOriginal.First(o => o.ObservationCode == "K").Value.Should().Be(3.5m); // Act 2: promote correction batch with correct potassium (5.3) var correctionBatchId = await CorrectionPipelineHelper.PromoteCorrectionBatchAsync( _fixture, originalBatchId, patientId, potassiumValue: 5.3m); db.ChangeTracker.Clear(); // Assert: original observations superseded, correction observations live var originalObs = await db.LiveObservations .Where(o => o.SourceBatchId == originalBatchId) .ToListAsync(); originalObs.Should().AllSatisfy(o => { o.IsSuperseded.Should().BeTrue(); o.SupersededByBatchId.Should().Be(correctionBatchId); }); originalObs.First(o => o.ObservationCode == "K").Value.Should().Be(3.5m, "original erroneous value is preserved for audit"); var correctionObs = await db.LiveObservations .Where(o => o.SourceBatchId == correctionBatchId && !o.IsSuperseded) .ToListAsync(); correctionObs.Should().HaveCount(2); correctionObs.First(o => o.ObservationCode == "K").Value.Should().Be(5.3m); var allObs = await db.LiveObservations.Where(o => o.PatientId == patientId).ToListAsync(); allObs.Should().HaveCount(4, "2 original (superseded) + 2 correction (active)"); var originalEvents = await db.DigitizationEvents .Where(e => e.BatchId == originalBatchId) .ToListAsync(); originalEvents.Should().Contain(e => e.EventType == DigitizationEventType.Superseded); } [Fact] public async Task CannotSupersede_NonPromotedBatch() { // Arrange: batch still in PendingVerification using var scope = _fixture.Services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); var entry1Id = await BatchSeedHelper.UserIdAsync(db, "entry1"); var pendingBatch = await BatchSeedHelper.SeedBatchInPendingVerificationAsync(db, entry1Id); // Act: attempt correction upload against non-promoted batch var client = await AuthHelper.LoginAsync(_fixture, "intake1"); var fileContent = new ByteArrayContent([0x25, 0x50, 0x44, 0x46]); fileContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/pdf"); var formData = new MultipartFormDataContent { { fileContent, "file", "test.pdf" }, { new StringContent("LAB_RESULTS"), "batchType" }, { new StringContent("BACKFILL"), "track" }, { new StringContent(pendingBatch.Id.ToString()), "supersedesBatchId" } }; var response = await client.PostAsync("/api/v1/digitization-batches", formData); // Assert response.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); var body = await response.Content.ReadAsStringAsync(); body.Should().Contain("Only promoted batches can be superseded"); } [Fact] public async Task CannotSupersede_AlreadySupersededBatch() { // Arrange: promote original + one correction (supersedes original) var (originalBatchId, patientId) = await CorrectionPipelineHelper.PromoteLabBatchAsync(_fixture, potassiumValue: 100m); await CorrectionPipelineHelper.PromoteCorrectionBatchAsync( _fixture, originalBatchId, patientId, potassiumValue: 102m); // Act: attempt second correction against the already-superseded original var client = await AuthHelper.LoginAsync(_fixture, "intake1"); var fileContent = new ByteArrayContent([0x25, 0x50, 0x44, 0x46]); fileContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/pdf"); var formData = new MultipartFormDataContent { { fileContent, "file", "test2.pdf" }, { new StringContent("LAB_RESULTS"), "batchType" }, { new StringContent("BACKFILL"), "track" }, { new StringContent(patientId.ToString()), "patientId" }, { new StringContent(originalBatchId.ToString()), "supersedesBatchId" } }; var response = await client.PostAsync("/api/v1/digitization-batches", formData); // Assert response.StatusCode.Should().Be(HttpStatusCode.Conflict); var body = await response.Content.ReadAsStringAsync(); body.Should().Contain("already been superseded"); } [Fact] public async Task PatientDigitizationHistory_ShowsFullCorrectionChain() { // Arrange: promote original + correction var (originalBatchId, patientId) = await CorrectionPipelineHelper.PromoteLabBatchAsync(_fixture, potassiumValue: 3.5m); var correctionBatchId = await CorrectionPipelineHelper.PromoteCorrectionBatchAsync( _fixture, originalBatchId, patientId, potassiumValue: 5.3m); // Act var client = await AuthHelper.LoginAsync(_fixture, "admin1"); var response = await client.GetAsync( $"/api/v1/patients/{patientId}/digitization-history"); // Assert response.StatusCode.Should().Be(HttpStatusCode.OK); var result = await response.Content .ReadFromJsonAsync>(); result!.Data!.TotalBatches.Should().Be(2); result.Data.SupersededBatches.Should().Be(1); var originalEntry = result.Data.Entries.First(e => e.BatchId == originalBatchId); originalEntry.HasBeenSuperseded.Should().BeTrue(); originalEntry.SupersededByBatchId.Should().Be(correctionBatchId); var correctionEntry = result.Data.Entries.First(e => e.BatchId == correctionBatchId); correctionEntry.IsCorrection.Should().BeTrue(); correctionEntry.SupersedesBatchId.Should().Be(originalBatchId); } [Fact] public async Task PatientHistory_ReturnsNotFound_ForUnknownPatient() { var client = await AuthHelper.LoginAsync(_fixture, "admin1"); var response = await client.GetAsync( $"/api/v1/patients/{Guid.NewGuid()}/digitization-history"); response.StatusCode.Should().Be(HttpStatusCode.NotFound); } }