204 lines
8.0 KiB
C#
204 lines
8.0 KiB
C#
using System.Net;
|
|
using System.Text;
|
|
using FluentAssertions;
|
|
using Hl7.Fhir.Model;
|
|
using Hl7.Fhir.Serialization;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using StackExchange.Redis;
|
|
using Task = System.Threading.Tasks.Task;
|
|
|
|
[Collection("Integration")]
|
|
public class FhirIngestTests : IAsyncLifetime
|
|
{
|
|
private const string ApiKey = "dev-integration-key-change-in-production";
|
|
|
|
private readonly ApiFixture _fixture;
|
|
private readonly HttpClient _client;
|
|
private static readonly FhirJsonSerializer Serializer = new();
|
|
|
|
public FhirIngestTests(ApiFixture fixture)
|
|
{
|
|
_fixture = fixture;
|
|
_client = fixture.CreateClient();
|
|
_client.ClearAuth();
|
|
}
|
|
|
|
public async Task InitializeAsync()
|
|
{
|
|
using var scope = _fixture.Services.CreateScope();
|
|
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
|
await DbResetHelper.ResetAsync(db);
|
|
|
|
var redis = scope.ServiceProvider.GetRequiredService<IConnectionMultiplexer>();
|
|
await DataSeeder.SeedThresholdsOnlyAsync(db, redis);
|
|
}
|
|
|
|
public Task DisposeAsync() => Task.CompletedTask;
|
|
|
|
[Fact]
|
|
public async Task PatientUpsert_IsIdempotent()
|
|
{
|
|
var patient = BuildPatient("MRN-HOSP-001");
|
|
var json = Serializer.SerializeToString(patient);
|
|
|
|
var resp1 = await PostFhirAsync("/fhir/R4/Patient", json);
|
|
resp1.StatusCode.Should().Be(HttpStatusCode.Created);
|
|
|
|
var resp2 = await PostFhirAsync("/fhir/R4/Patient", json);
|
|
resp2.StatusCode.Should().Be(HttpStatusCode.Created);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Observation_HeartRate_Loinc8867_MapsAndIngests()
|
|
{
|
|
var encounterId = await SeedPatientAndEncounterAsync();
|
|
|
|
var obs = new Hl7.Fhir.Model.Observation
|
|
{
|
|
Status = ObservationStatus.Final,
|
|
Category = new List<CodeableConcept>
|
|
{
|
|
new("http://terminology.hl7.org/CodeSystem/observation-category", "vital-signs")
|
|
},
|
|
Code = new CodeableConcept("http://loinc.org", "8867-4", "Heart rate"),
|
|
Subject = new ResourceReference("Patient/test"),
|
|
Encounter = new ResourceReference($"Encounter/{encounterId}"),
|
|
Effective = new FhirDateTime(DateTimeOffset.UtcNow),
|
|
Value = new Quantity(110, "/min", "http://unitsofmeasure.org"),
|
|
Identifier = new List<Identifier>
|
|
{
|
|
new("http://hospital.example/obs", "hr-001")
|
|
}
|
|
};
|
|
|
|
var resp = await PostFhirAsync("/fhir/R4/Observation", Serializer.SerializeToString(obs));
|
|
resp.StatusCode.Should().Be(HttpStatusCode.Created);
|
|
|
|
using var scope = _fixture.Services.CreateScope();
|
|
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
|
var row = db.Observations.Single(o => o.IdempotencyKey == "hr-001");
|
|
row.ObservationCode.Should().Be("HEART_RATE");
|
|
row.Value.Should().Be(110m);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Observation_UnknownLoinc_Returns422()
|
|
{
|
|
var encounterId = await SeedPatientAndEncounterAsync();
|
|
var obs = new Hl7.Fhir.Model.Observation
|
|
{
|
|
Status = ObservationStatus.Final,
|
|
Code = new CodeableConcept("http://loinc.org", "99999-9"),
|
|
Encounter = new ResourceReference($"Encounter/{encounterId}"),
|
|
Value = new Quantity(1, "1")
|
|
};
|
|
|
|
var resp = await PostFhirAsync("/fhir/R4/Observation", Serializer.SerializeToString(obs));
|
|
resp.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity);
|
|
(await resp.Content.ReadAsStringAsync()).Should().Contain("OperationOutcome");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TransactionBundle_PatientEncounterObservation_Succeeds()
|
|
{
|
|
var bundle = BuildAdmitBundle();
|
|
var resp = await PostFhirAsync("/fhir/R4", Serializer.SerializeToString(bundle));
|
|
resp.StatusCode.Should().Be(HttpStatusCode.OK);
|
|
|
|
using var scope = _fixture.Services.CreateScope();
|
|
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
|
db.Patients.Should().Contain(p => p.Mrn == "MRN-ADT-001");
|
|
db.Encounters.Should().Contain(e => e.Status == EncounterStatus.Active);
|
|
db.Observations.Should().Contain(o => o.ObservationCode == "HEART_RATE");
|
|
}
|
|
|
|
private async Task<HttpResponseMessage> PostFhirAsync(string path, string json)
|
|
{
|
|
var req = new HttpRequestMessage(HttpMethod.Post, path)
|
|
{
|
|
Content = new StringContent(json, Encoding.UTF8, "application/fhir+json")
|
|
};
|
|
req.Headers.Add("X-Api-Key", ApiKey);
|
|
return await _client.SendAsync(req);
|
|
}
|
|
|
|
private static Hl7.Fhir.Model.Patient BuildPatient(string mrn) => new()
|
|
{
|
|
Identifier = new List<Identifier>
|
|
{
|
|
new("http://hospital.example/mrn", mrn)
|
|
{
|
|
Type = new CodeableConcept("http://terminology.hl7.org/CodeSystem/v2-0203", "MR")
|
|
}
|
|
},
|
|
Name = new List<HumanName> { new() { Given = new[] { "Jane" }, Family = "Doe" } },
|
|
BirthDate = "1980-01-15",
|
|
Gender = AdministrativeGender.Female
|
|
};
|
|
|
|
private async Task<Guid> SeedPatientAndEncounterAsync()
|
|
{
|
|
using var scope = _fixture.Services.CreateScope();
|
|
var patients = scope.ServiceProvider.GetRequiredService<IPatientService>();
|
|
var patient = await patients.RegisterOrUpdateByIdentifierAsync(new FhirPatientUpsertRequest(
|
|
"http://hospital.example/mrn", "MRN-TEST-001",
|
|
"Test", "Patient", new DateOnly(1990, 1, 1), "female"));
|
|
|
|
var encounters = scope.ServiceProvider.GetRequiredService<IEncounterService>();
|
|
var encounter = await encounters.OpenOrUpdateByIdentifierAsync(new FhirEncounterUpsertRequest(
|
|
"http://hospital.example/visit", "VISIT-001",
|
|
patient.Id, EncounterType.Inpatient, Department.Icu,
|
|
"Dr. Smith", EncounterStatus.Active));
|
|
|
|
return encounter.Id;
|
|
}
|
|
|
|
private static Bundle BuildAdmitBundle()
|
|
{
|
|
var patient = BuildPatient("MRN-ADT-001");
|
|
var encounter = new Hl7.Fhir.Model.Encounter
|
|
{
|
|
Status = Hl7.Fhir.Model.Encounter.EncounterStatus.InProgress,
|
|
Class = new Coding("http://terminology.hl7.org/CodeSystem/v3-ActCode", "IMP"),
|
|
Identifier = new List<Identifier> { new("http://hospital.example/visit", "VISIT-ADT-001") },
|
|
Subject = new ResourceReference
|
|
{
|
|
Identifier = new Identifier("http://hospital.example/mrn", "MRN-ADT-001")
|
|
},
|
|
Participant = new List<Hl7.Fhir.Model.Encounter.ParticipantComponent>
|
|
{
|
|
new()
|
|
{
|
|
Type = new List<CodeableConcept>
|
|
{
|
|
new("http://terminology.hl7.org/CodeSystem/v3-ParticipationType", "ATND")
|
|
},
|
|
Individual = new ResourceReference { Display = "Dr. Admit" }
|
|
}
|
|
}
|
|
};
|
|
var obs = new Hl7.Fhir.Model.Observation
|
|
{
|
|
Status = ObservationStatus.Final,
|
|
Code = new CodeableConcept("http://loinc.org", "8867-4"),
|
|
Encounter = new ResourceReference
|
|
{
|
|
Identifier = new Identifier("http://hospital.example/visit", "VISIT-ADT-001")
|
|
},
|
|
Value = new Quantity(88, "/min"),
|
|
Identifier = new List<Identifier> { new("http://hospital.example/obs", "adt-hr-1") }
|
|
};
|
|
|
|
return new Bundle
|
|
{
|
|
Type = Bundle.BundleType.Transaction,
|
|
Entry = new List<Bundle.EntryComponent>
|
|
{
|
|
new() { Resource = patient, Request = new Bundle.RequestComponent { Method = Bundle.HTTPVerb.POST, Url = "Patient" } },
|
|
new() { Resource = encounter, Request = new Bundle.RequestComponent { Method = Bundle.HTTPVerb.POST, Url = "Encounter" } },
|
|
new() { Resource = obs, Request = new Bundle.RequestComponent { Method = Bundle.HTTPVerb.POST, Url = "Observation" } }
|
|
}
|
|
};
|
|
}
|
|
}
|