using System.Net; using System.Net.Http.Headers; using System.Text.Json; using FluentAssertions; using Microsoft.Extensions.DependencyInjection; /// /// Integration tests for FHIR R4 read/search endpoints (Phase 11 Step 7). /// [Collection("Database")] public class FhirIntegrationTests : IAsyncLifetime { private readonly ApiFixture _fixture; private HttpClient _client = null!; private HttpClient _anonymousClient = null!; public FhirIntegrationTests(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); await FhirClinicalSeedHelper.SeedAsync(db); _client = await AuthHelper.LoginAsync(_fixture, "admin1"); _anonymousClient = _fixture.CreateClient(); } public Task DisposeAsync() => Task.CompletedTask; [Fact] public async Task Metadata_ReturnsCapabilityStatementWithSupportedResources() { var response = await GetFhirAsync("/fhir/metadata", authenticated: false); response.StatusCode.Should().Be(HttpStatusCode.OK); var body = await ParseJsonAsync(response); var resourceTypes = body.GetProperty("rest")[0] .GetProperty("resource") .EnumerateArray() .Select(r => r.GetProperty("type").GetString()) .ToList(); resourceTypes.Should().Contain(new[] { "Patient", "Encounter", "Observation" }); } [Fact] public async Task ReadPatient_ReturnsFhirPatientWithMrnNameAndGender() { var response = await GetFhirAsync($"/fhir/Patient/{FhirClinicalSeedHelper.Patient1Id}"); response.StatusCode.Should().Be(HttpStatusCode.OK); var body = await ParseJsonAsync(response); body.GetProperty("resourceType").GetString().Should().Be("Patient"); body.GetProperty("id").GetString() .Should().Be(FhirClinicalSeedHelper.Patient1Id.ToString()); var identifier = body.GetProperty("identifier")[0]; identifier.GetProperty("value").GetString().Should().Be("VCR-000001"); body.GetProperty("name")[0].GetProperty("text").GetString() .Should().Be("MARIA SANTOS"); body.GetProperty("gender").GetString().Should().Be("female"); body.GetProperty("birthDate").GetString().Should().Be("1978-03-15"); } [Fact] public async Task SearchPatients_ByName_ReturnsMatchingBundle() { var response = await GetFhirAsync("/fhir/Patient?name=Santos"); response.StatusCode.Should().Be(HttpStatusCode.OK); var body = await ParseJsonAsync(response); body.GetProperty("type").GetString().Should().Be("searchset"); body.GetProperty("total").GetInt32().Should().BeGreaterThan(0); body.GetProperty("entry").GetArrayLength().Should().BeGreaterThan(0); body.GetProperty("entry")[0].GetProperty("resource") .GetProperty("name")[0].GetProperty("text").GetString() .Should().Contain("SANTOS"); } [Fact] public async Task SearchPatients_ByIdentifier_ReturnsPatientByMrn() { var response = await GetFhirAsync("/fhir/Patient?identifier=VCR-000001"); response.StatusCode.Should().Be(HttpStatusCode.OK); var body = await ParseJsonAsync(response); body.GetProperty("entry").GetArrayLength().Should().Be(1); body.GetProperty("entry")[0].GetProperty("resource") .GetProperty("identifier")[0].GetProperty("value").GetString() .Should().Be("VCR-000001"); } [Fact] public async Task ReadEncounter_ReturnsFhirEncounterWithStatusAndPatientReference() { var response = await GetFhirAsync($"/fhir/Encounter/{FhirClinicalSeedHelper.Encounter1Id}"); response.StatusCode.Should().Be(HttpStatusCode.OK); var body = await ParseJsonAsync(response); body.GetProperty("resourceType").GetString().Should().Be("Encounter"); body.GetProperty("status").GetString().Should().Be("in-progress"); body.GetProperty("subject").GetProperty("reference").GetString() .Should().Be($"Patient/{FhirClinicalSeedHelper.Patient1Id}"); } [Fact] public async Task SearchEncounters_ByPatient_ReturnsMatchingEncounters() { var patientId = FhirClinicalSeedHelper.Patient1Id; var response = await GetFhirAsync($"/fhir/Encounter?patient={patientId}"); response.StatusCode.Should().Be(HttpStatusCode.OK); var body = await ParseJsonAsync(response); body.GetProperty("total").GetInt32().Should().BeGreaterThan(0); body.GetProperty("entry").EnumerateArray().Should().AllSatisfy(entry => { entry.GetProperty("resource").GetProperty("subject") .GetProperty("reference").GetString() .Should().Be($"Patient/{patientId}"); }); } [Fact] public async Task ReadObservation_ReturnsFhirObservationWithLoincCodeAndUcumUnit() { var response = await GetFhirAsync($"/fhir/Observation/{FhirClinicalSeedHelper.HeartRateObsId}"); response.StatusCode.Should().Be(HttpStatusCode.OK); var body = await ParseJsonAsync(response); body.GetProperty("resourceType").GetString().Should().Be("Observation"); var coding = body.GetProperty("code").GetProperty("coding")[0]; coding.GetProperty("system").GetString().Should().Be("http://loinc.org"); coding.GetProperty("code").GetString().Should().Be("8867-4"); if (coding.TryGetProperty("display", out var display)) display.GetString().Should().Be("Heart rate"); var value = body.GetProperty("valueQuantity"); value.GetProperty("value").GetDecimal().Should().Be(88m); value.GetProperty("unit").GetString().Should().Be("bpm"); value.GetProperty("code").GetString().Should().Be("/min"); } [Fact] public async Task SearchObservations_ByCategoryVitalSigns_ReturnsVitalSignObservationsOnly() { var patientId = FhirClinicalSeedHelper.Patient1Id; var response = await GetFhirAsync( $"/fhir/Observation?patient={patientId}&category=vital-signs"); response.StatusCode.Should().Be(HttpStatusCode.OK); var body = await ParseJsonAsync(response); body.GetProperty("entry").GetArrayLength().Should().BeGreaterThan(0); foreach (var entry in body.GetProperty("entry").EnumerateArray()) { var category = entry.GetProperty("resource").GetProperty("category")[0] .GetProperty("coding")[0].GetProperty("code").GetString(); category.Should().Be("vital-signs"); } } [Fact] public async Task SearchObservations_ByLoincCode_ResolvesToHeartRate() { var response = await GetFhirAsync("/fhir/Observation?code=8867-4"); response.StatusCode.Should().Be(HttpStatusCode.OK); var body = await ParseJsonAsync(response); body.GetProperty("entry").GetArrayLength().Should().BeGreaterThan(0); var coding = body.GetProperty("entry")[0].GetProperty("resource") .GetProperty("code").GetProperty("coding")[0]; coding.GetProperty("code").GetString().Should().Be("8867-4"); var value = body.GetProperty("entry")[0].GetProperty("resource") .GetProperty("valueQuantity").GetProperty("value").GetDecimal(); value.Should().Be(88m); } [Fact] public async Task SearchObservations_ByDateGreaterOrEqual_FiltersByRecordedAt() { var patientId = FhirClinicalSeedHelper.Patient1Id; var response = await GetFhirAsync( $"/fhir/Observation?patient={patientId}&date=ge2026-06-20"); response.StatusCode.Should().Be(HttpStatusCode.OK); var body = await ParseJsonAsync(response); body.GetProperty("total").GetInt32().Should().BeGreaterThan(0); body.GetProperty("entry").GetArrayLength().Should().BeGreaterThan(0); } [Fact] public async Task PatientEverything_ReturnsCompletePatientBundle() { var response = await GetFhirAsync( $"/fhir/Patient/{FhirClinicalSeedHelper.Patient1Id}/$everything"); response.StatusCode.Should().Be(HttpStatusCode.OK); var body = await ParseJsonAsync(response); body.GetProperty("type").GetString().Should().Be("searchset"); body.GetProperty("total").GetInt32().Should().BeGreaterThan(1); var resourceTypes = body.GetProperty("entry").EnumerateArray() .Select(e => e.GetProperty("resource").GetProperty("resourceType").GetString()) .ToList(); resourceTypes.Should().Contain("Patient"); resourceTypes.Should().Contain("Encounter"); resourceTypes.Should().Contain("Observation"); var includeModes = body.GetProperty("entry").EnumerateArray() .Where(e => e.GetProperty("resource").GetProperty("resourceType").GetString() != "Patient") .Select(e => e.GetProperty("search").GetProperty("mode").GetString()); includeModes.Should().AllBe("include"); } [Fact] public async Task ReadPatient_NotFound_Returns404OperationOutcome() { var missingId = Guid.Parse("00000000-0000-0000-0000-000000000001"); var response = await GetFhirAsync($"/fhir/Patient/{missingId}"); response.StatusCode.Should().Be(HttpStatusCode.NotFound); var body = await ParseJsonAsync(response); body.GetProperty("resourceType").GetString().Should().Be("OperationOutcome"); var issue = body.GetProperty("issue")[0]; issue.GetProperty("severity").GetString().Should().Be("error"); issue.GetProperty("code").GetString().Should().Be("not-found"); issue.GetProperty("diagnostics").GetString() .Should().Contain($"Patient/{missingId}"); } [Fact] public async Task ReadPatient_ReturnsApplicationFhirJsonContentType() { var response = await GetFhirAsync($"/fhir/Patient/{FhirClinicalSeedHelper.Patient1Id}"); response.StatusCode.Should().Be(HttpStatusCode.OK); response.Content.Headers.ContentType!.MediaType.Should().Be("application/fhir+json"); } [Fact] public async Task SearchPatients_PaginationLinks_AreCorrect() { var response = await GetFhirAsync("/fhir/Patient?_count=1&_offset=0"); response.StatusCode.Should().Be(HttpStatusCode.OK); var body = await ParseJsonAsync(response); body.GetProperty("total").GetInt32().Should().BeGreaterThanOrEqualTo(2); body.GetProperty("entry").GetArrayLength().Should().Be(1); var links = body.GetProperty("link").EnumerateArray() .ToDictionary(l => l.GetProperty("relation").GetString()!, l => l.GetProperty("url").GetString()); links.Should().ContainKey("self"); links["self"].Should().Contain("_count=1"); links["self"].Should().Contain("_offset=0"); links.Should().ContainKey("next"); links["next"].Should().Contain("_count=1"); links["next"].Should().Contain("_offset=1"); } private async Task GetFhirAsync(string path, bool authenticated = true) { var client = authenticated ? _client : _anonymousClient; var request = new HttpRequestMessage(HttpMethod.Get, path); request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/fhir+json")); return await client.SendAsync(request); } private static async Task ParseJsonAsync(HttpResponseMessage response) { var json = await response.Content.ReadAsStringAsync(); return JsonDocument.Parse(json).RootElement; } }