feature: HL7 FHIR R4 Integration
This commit is contained in:
+585
@@ -0,0 +1,585 @@
|
||||
#!/usr/bin/env bash
|
||||
# Runs Phase 11 verification checks from docs/plans/phase-11-plan.md.
|
||||
#
|
||||
# Covers FHIR metadata, Patient/Encounter/Observation read & search,
|
||||
# LOINC mapping, $everything, content-type negotiation, 404 OperationOutcome,
|
||||
# and FhirIntegrationTests.
|
||||
#
|
||||
# Prerequisites:
|
||||
# docker compose up -d (PostgreSQL, Redis, MinIO)
|
||||
# dotnet ef database update --project VigilCareRecordsAPI
|
||||
# dotnet run --project VigilCareRecordsAPI
|
||||
# Phase 1–10 seed data (admin1)
|
||||
#
|
||||
# Environment overrides:
|
||||
# VIGILCARE_API_URL default: http://localhost:5217
|
||||
# VIGILCARE_COMPOSE_FILE default: <repo>/docker-compose.yml
|
||||
# VIGILCARE_PG_HOST default: localhost
|
||||
# VIGILCARE_PG_PORT default: 5437
|
||||
# VIGILCARE_PG_DB default: vigilcare_records
|
||||
# VIGILCARE_PG_USER default: postgres
|
||||
# VIGILCARE_PG_PASSWORD default: password
|
||||
# VIGILCARE_SKIP_DB_CHECKS set to 1 to skip PostgreSQL seed/assertions
|
||||
# VIGILCARE_SKIP_TEST_CHECKS set to 1 to skip dotnet integration tests
|
||||
#
|
||||
# Usage:
|
||||
# chmod +x scripts/run-vigilcare-records-phase-11-verification.sh
|
||||
# ./scripts/run-vigilcare-records-phase-11-verification.sh
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
|
||||
API_URL="${VIGILCARE_API_URL:-http://localhost:5217}"
|
||||
FHIR_URL="${API_URL}/fhir"
|
||||
COMPOSE_FILE="${VIGILCARE_COMPOSE_FILE:-$REPO_ROOT/docker-compose.yml}"
|
||||
COMPOSE=(docker compose -f "$COMPOSE_FILE")
|
||||
PG_HOST="${VIGILCARE_PG_HOST:-localhost}"
|
||||
PG_PORT="${VIGILCARE_PG_PORT:-5437}"
|
||||
PG_DB="${VIGILCARE_PG_DB:-vigilcare_records}"
|
||||
PG_USER="${VIGILCARE_PG_USER:-postgres}"
|
||||
PG_PASSWORD="${VIGILCARE_PG_PASSWORD:-password}"
|
||||
SKIP_DB_CHECKS="${VIGILCARE_SKIP_DB_CHECKS:-0}"
|
||||
SKIP_TEST_CHECKS="${VIGILCARE_SKIP_TEST_CHECKS:-0}"
|
||||
|
||||
# Deterministic clinical IDs (match FhirClinicalSeedHelper)
|
||||
PATIENT1_ID="b1000000-0000-0000-0000-000000000001"
|
||||
PATIENT2_ID="b1000000-0000-0000-0000-000000000002"
|
||||
ENCOUNTER1_ID="d1000000-0000-0000-0000-000000000001"
|
||||
HEART_RATE_OBS_ID="e1000000-0000-0000-0000-000000000001"
|
||||
WBC_OBS_ID="e1000000-0000-0000-0000-000000000002"
|
||||
BATCH1_ID="c1000000-0000-0000-0000-000000000001"
|
||||
|
||||
ADMIN_TOKEN=""
|
||||
RESOLVED_PATIENT_ID=""
|
||||
|
||||
PASS_COUNT=0
|
||||
FAIL_COUNT=0
|
||||
FAILED_TESTS=()
|
||||
|
||||
log() {
|
||||
printf '%s\n' "$*"
|
||||
}
|
||||
|
||||
section() {
|
||||
log ""
|
||||
log "== $1 =="
|
||||
}
|
||||
|
||||
pass() {
|
||||
PASS_COUNT=$((PASS_COUNT + 1))
|
||||
log " PASS: $1"
|
||||
}
|
||||
|
||||
fail() {
|
||||
FAIL_COUNT=$((FAIL_COUNT + 1))
|
||||
FAILED_TESTS+=("$1")
|
||||
log " FAIL: $1"
|
||||
}
|
||||
|
||||
require_cmd() {
|
||||
local cmd="$1"
|
||||
if ! command -v "$cmd" >/dev/null 2>&1; then
|
||||
log "ERROR: required command not found: $cmd"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
http_code() {
|
||||
curl -sS -o /dev/null -w '%{http_code}' "$@"
|
||||
}
|
||||
|
||||
json_post() {
|
||||
local url="$1"
|
||||
local body="$2"
|
||||
curl -sS -X POST "$url" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d "$body"
|
||||
}
|
||||
|
||||
fhir_get() {
|
||||
local path_query="$1"
|
||||
local token="${2:-}"
|
||||
if [[ -n "$token" ]]; then
|
||||
curl -sS "${FHIR_URL}${path_query}" \
|
||||
-H "Authorization: Bearer $token" \
|
||||
-H 'Accept: application/fhir+json'
|
||||
else
|
||||
curl -sS "${FHIR_URL}${path_query}" \
|
||||
-H 'Accept: application/fhir+json'
|
||||
fi
|
||||
}
|
||||
|
||||
fhir_get_status() {
|
||||
local path_query="$1"
|
||||
local token="$2"
|
||||
local body_file http_status
|
||||
body_file="$(mktemp)"
|
||||
http_status="$(curl -sS -o "$body_file" -w '%{http_code}' \
|
||||
"${FHIR_URL}${path_query}" \
|
||||
-H "Authorization: Bearer $token" \
|
||||
-H 'Accept: application/fhir+json')"
|
||||
cat "$body_file"
|
||||
rm -f "$body_file"
|
||||
printf '\n__HTTP_STATUS__:%s' "$http_status"
|
||||
}
|
||||
|
||||
assert_api_reachable() {
|
||||
local code
|
||||
code="$(http_code "$API_URL/swagger/index.html" || true)"
|
||||
if [[ "$code" != "200" ]]; then
|
||||
log "ERROR: API not reachable at $API_URL (HTTP $code)."
|
||||
log "Start infrastructure with: docker compose up -d"
|
||||
log "Apply migrations with: dotnet ef database update --project VigilCareRecordsAPI"
|
||||
log "Start API with: dotnet run --project VigilCareRecordsAPI"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
compose_service_running() {
|
||||
local service="$1"
|
||||
"${COMPOSE[@]}" ps --status running --services 2>/dev/null | grep -qx "$service"
|
||||
}
|
||||
|
||||
psql_available() {
|
||||
[[ "$SKIP_DB_CHECKS" == "1" ]] && return 1
|
||||
compose_service_running postgres && return 0
|
||||
command -v psql >/dev/null 2>&1 && return 0
|
||||
return 1
|
||||
}
|
||||
|
||||
psql_query() {
|
||||
if [[ "$SKIP_DB_CHECKS" == "1" ]]; then
|
||||
return 1
|
||||
fi
|
||||
if compose_service_running postgres; then
|
||||
"${COMPOSE[@]}" exec -T postgres \
|
||||
psql -U "$PG_USER" -d "$PG_DB" -Atqc "$1"
|
||||
elif command -v psql >/dev/null 2>&1; then
|
||||
PGPASSWORD="$PG_PASSWORD" psql -h "$PG_HOST" -p "$PG_PORT" -U "$PG_USER" -d "$PG_DB" -Atqc "$1"
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
psql_exec() {
|
||||
if [[ "$SKIP_DB_CHECKS" == "1" ]]; then
|
||||
return 1
|
||||
fi
|
||||
if compose_service_running postgres; then
|
||||
"${COMPOSE[@]}" exec -T postgres \
|
||||
psql -U "$PG_USER" -d "$PG_DB" -v ON_ERROR_STOP=1 -q -c "$1"
|
||||
elif command -v psql >/dev/null 2>&1; then
|
||||
PGPASSWORD="$PG_PASSWORD" psql -h "$PG_HOST" -p "$PG_PORT" -U "$PG_USER" -d "$PG_DB" \
|
||||
-v ON_ERROR_STOP=1 -q -c "$1"
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
test_authentication() {
|
||||
section "0. Authentication"
|
||||
|
||||
local admin_json
|
||||
admin_json="$(json_post "$API_URL/api/v1/auth/login" \
|
||||
'{"username":"admin1","password":"password"}')"
|
||||
|
||||
ADMIN_TOKEN="$(jq -er '.data.token // empty' <<<"$admin_json" 2>/dev/null || true)"
|
||||
|
||||
if [[ -n "$ADMIN_TOKEN" ]]; then
|
||||
pass "admin1 login returns JWT"
|
||||
else
|
||||
log "ERROR: admin1 login failed."
|
||||
log "Response: ${admin_json:-<empty>}"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
ensure_fhir_clinical_seed() {
|
||||
section "1. Clinical seed data for FHIR endpoints"
|
||||
|
||||
if ! psql_available; then
|
||||
log " SKIP: PostgreSQL checks (set VIGILCARE_SKIP_DB_CHECKS=0 and start postgres)"
|
||||
log " NOTE: FHIR curl tests require clinical.patients with MRN VCR-000001"
|
||||
log " Promote a batch or run with postgres available to auto-seed."
|
||||
return
|
||||
fi
|
||||
|
||||
local patient_count
|
||||
patient_count="$(psql_query "SELECT count(*) FROM clinical.patients WHERE mrn = 'VCR-000001';" || echo "0")"
|
||||
|
||||
if [[ "${patient_count:-0}" -ge 1 ]]; then
|
||||
pass "clinical patient VCR-000001 already present"
|
||||
return
|
||||
fi
|
||||
|
||||
log " Seeding demo clinical records for FHIR verification..."
|
||||
|
||||
psql_exec "
|
||||
INSERT INTO clinical.patients (
|
||||
id, mrn, full_name, date_of_birth, sex, blood_type,
|
||||
emergency_contact, allergies_json, no_known_allergies, created_at, updated_at
|
||||
) VALUES
|
||||
('${PATIENT1_ID}', 'VCR-000001', 'MARIA SANTOS', '1978-03-15', 'female', 'A+',
|
||||
'Juan Santos - 555-0101', '[\"Penicillin\", \"Sulfa drugs\"]', false,
|
||||
NOW() - interval '3 days', NOW() - interval '12 hours'),
|
||||
('${PATIENT2_ID}', 'VCR-000002', 'KENJI NAKAMURA', '1952-11-08', 'male', 'O-',
|
||||
'Yuki Nakamura - 555-0202', NULL, true,
|
||||
NOW() - interval '1 day', NOW() - interval '1 day')
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
" || {
|
||||
fail "seed clinical.patients for FHIR verification"
|
||||
return
|
||||
}
|
||||
|
||||
psql_exec "
|
||||
INSERT INTO clinical.encounters (
|
||||
id, patient_id, admission_date, department, room_bed, admission_reason,
|
||||
status, source_batch_id, created_at, updated_at
|
||||
) VALUES (
|
||||
'${ENCOUNTER1_ID}', '${PATIENT1_ID}', NOW() - interval '5 days',
|
||||
'Internal Medicine', '2A-04', 'Pneumonia with elevated WBC',
|
||||
'active', '${BATCH1_ID}', NOW() - interval '3 days', NOW() - interval '12 hours'
|
||||
)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
" || {
|
||||
fail "seed clinical.encounters for FHIR verification"
|
||||
return
|
||||
}
|
||||
|
||||
psql_exec "
|
||||
INSERT INTO clinical.observations (
|
||||
id, encounter_id, patient_id, observation_code, value, unit,
|
||||
recorded_at, source, source_batch_id, created_at
|
||||
) VALUES
|
||||
('${HEART_RATE_OBS_ID}', '${ENCOUNTER1_ID}', '${PATIENT1_ID}',
|
||||
'HEART_RATE', 88.000, 'bpm', NOW() - interval '5 days',
|
||||
'digitization_backfill', '${BATCH1_ID}', NOW() - interval '12 hours'),
|
||||
('${WBC_OBS_ID}', '${ENCOUNTER1_ID}', '${PATIENT1_ID}',
|
||||
'WBC_K_UL', 14.200, 'K/uL', NOW() - interval '5 days',
|
||||
'digitization_backfill', '${BATCH1_ID}', NOW() - interval '12 hours')
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
" || {
|
||||
fail "seed clinical.observations for FHIR verification"
|
||||
return
|
||||
}
|
||||
|
||||
patient_count="$(psql_query "SELECT count(*) FROM clinical.patients WHERE mrn = 'VCR-000001';" || echo "0")"
|
||||
if [[ "${patient_count:-0}" -ge 1 ]]; then
|
||||
pass "seeded clinical patient VCR-000001 for FHIR verification"
|
||||
else
|
||||
fail "seeded clinical patient VCR-000001 for FHIR verification"
|
||||
fi
|
||||
}
|
||||
|
||||
test_fhir_metadata() {
|
||||
section "2. FHIR metadata — GET /fhir/metadata"
|
||||
|
||||
local metadata types
|
||||
metadata="$(fhir_get '/metadata')"
|
||||
types="$(jq -r '.rest[0].resource[].type' <<<"$metadata" 2>/dev/null | sort | tr '\n' ' ')"
|
||||
|
||||
if jq -e '.resourceType == "CapabilityStatement"' <<<"$metadata" >/dev/null 2>&1; then
|
||||
pass "metadata returns CapabilityStatement"
|
||||
else
|
||||
fail "metadata returns CapabilityStatement"
|
||||
fi
|
||||
|
||||
if grep -q 'Patient' <<<"$types" && grep -q 'Encounter' <<<"$types" && grep -q 'Observation' <<<"$types"; then
|
||||
pass "metadata lists Patient, Encounter, Observation resources"
|
||||
else
|
||||
fail "metadata lists Patient, Encounter, Observation resources (got: $types)"
|
||||
fi
|
||||
}
|
||||
|
||||
resolve_patient_id() {
|
||||
section "3. Resolve FHIR Patient ID"
|
||||
|
||||
local search_json
|
||||
search_json="$(fhir_get "/Patient?name=Santos" "$ADMIN_TOKEN")"
|
||||
RESOLVED_PATIENT_ID="$(jq -er '.entry[0].resource.id // empty' <<<"$search_json" 2>/dev/null || true)"
|
||||
|
||||
if [[ -z "$RESOLVED_PATIENT_ID" ]]; then
|
||||
RESOLVED_PATIENT_ID="$PATIENT1_ID"
|
||||
log " NOTE: Patient search returned no entries; using seeded ID $PATIENT1_ID"
|
||||
fi
|
||||
|
||||
if [[ -n "$RESOLVED_PATIENT_ID" ]]; then
|
||||
pass "resolved FHIR patient ID ($RESOLVED_PATIENT_ID)"
|
||||
else
|
||||
fail "resolved FHIR patient ID"
|
||||
fi
|
||||
}
|
||||
|
||||
test_fhir_patient_read() {
|
||||
section "4. FHIR Patient read & search"
|
||||
|
||||
local patient_json mrn gender birth_date identifier_json
|
||||
patient_json="$(fhir_get "/Patient/${RESOLVED_PATIENT_ID}" "$ADMIN_TOKEN")"
|
||||
|
||||
if jq -e '.resourceType == "Patient"' <<<"$patient_json" >/dev/null 2>&1; then
|
||||
pass "GET /fhir/Patient/{id} returns Patient resource"
|
||||
else
|
||||
fail "GET /fhir/Patient/{id} returns Patient resource"
|
||||
fi
|
||||
|
||||
mrn="$(jq -er '.identifier[0].value // empty' <<<"$patient_json" 2>/dev/null || true)"
|
||||
if [[ "$mrn" == "VCR-000001" ]]; then
|
||||
pass "Patient read includes MRN identifier VCR-000001"
|
||||
else
|
||||
fail "Patient read includes MRN identifier VCR-000001 (got: ${mrn:-<empty>})"
|
||||
fi
|
||||
|
||||
gender="$(jq -er '.gender // empty' <<<"$patient_json" 2>/dev/null || true)"
|
||||
birth_date="$(jq -er '.birthDate // empty' <<<"$patient_json" 2>/dev/null || true)"
|
||||
if [[ "$gender" == "female" && "$birth_date" == "1978-03-15" ]]; then
|
||||
pass "Patient read includes gender and birthDate"
|
||||
else
|
||||
fail "Patient read includes gender and birthDate (gender=$gender birthDate=$birth_date)"
|
||||
fi
|
||||
|
||||
identifier_json="$(fhir_get "/Patient?identifier=VCR-000001" "$ADMIN_TOKEN")"
|
||||
if [[ "$(jq -er '.total // 0' <<<"$identifier_json")" -ge 1 ]]; then
|
||||
pass "GET /fhir/Patient?identifier=VCR-000001 returns matches"
|
||||
else
|
||||
fail "GET /fhir/Patient?identifier=VCR-000001 returns matches"
|
||||
fi
|
||||
}
|
||||
|
||||
test_fhir_encounter() {
|
||||
section "5. FHIR Encounter read & search"
|
||||
|
||||
local encounter_json status subject patient_search
|
||||
encounter_json="$(fhir_get "/Encounter/${ENCOUNTER1_ID}" "$ADMIN_TOKEN")"
|
||||
|
||||
if jq -e '.resourceType == "Encounter"' <<<"$encounter_json" >/dev/null 2>&1; then
|
||||
pass "GET /fhir/Encounter/{id} returns Encounter resource"
|
||||
else
|
||||
fail "GET /fhir/Encounter/{id} returns Encounter resource"
|
||||
fi
|
||||
|
||||
status="$(jq -er '.status // empty' <<<"$encounter_json" 2>/dev/null || true)"
|
||||
subject="$(jq -er '.subject.reference // empty' <<<"$encounter_json" 2>/dev/null || true)"
|
||||
if [[ "$status" == "in-progress" && "$subject" == "Patient/${RESOLVED_PATIENT_ID}" ]]; then
|
||||
pass "Encounter read has in-progress status and patient reference"
|
||||
else
|
||||
fail "Encounter read has in-progress status and patient reference"
|
||||
fi
|
||||
|
||||
patient_search="$(fhir_get "/Encounter?patient=${RESOLVED_PATIENT_ID}" "$ADMIN_TOKEN")"
|
||||
if [[ "$(jq -er '.total // 0' <<<"$patient_search")" -ge 1 ]]; then
|
||||
pass "GET /fhir/Encounter?patient={id} returns matches"
|
||||
else
|
||||
fail "GET /fhir/Encounter?patient={id} returns matches"
|
||||
fi
|
||||
}
|
||||
|
||||
test_fhir_observation() {
|
||||
section "6. FHIR Observation read, LOINC search, category & date"
|
||||
|
||||
local obs_json loinc_code loinc_unit loinc_value loinc_search category_search date_search
|
||||
obs_json="$(fhir_get "/Observation/${HEART_RATE_OBS_ID}" "$ADMIN_TOKEN")"
|
||||
|
||||
if jq -e '.resourceType == "Observation"' <<<"$obs_json" >/dev/null 2>&1; then
|
||||
pass "GET /fhir/Observation/{id} returns Observation resource"
|
||||
else
|
||||
fail "GET /fhir/Observation/{id} returns Observation resource"
|
||||
fi
|
||||
|
||||
loinc_code="$(jq -er '.code.coding[0].code // empty' <<<"$obs_json" 2>/dev/null || true)"
|
||||
loinc_unit="$(jq -er '.valueQuantity.unit // empty' <<<"$obs_json" 2>/dev/null || true)"
|
||||
loinc_value="$(jq -er '.valueQuantity.value // empty' <<<"$obs_json" 2>/dev/null || true)"
|
||||
if [[ "$loinc_code" == "8867-4" && "$loinc_unit" == "bpm" && "$loinc_value" == "88" ]]; then
|
||||
pass "Observation read maps HEART_RATE to LOINC 8867-4 with value 88 bpm"
|
||||
else
|
||||
fail "Observation read maps HEART_RATE to LOINC 8867-4 with value 88 bpm"
|
||||
fi
|
||||
|
||||
loinc_search="$(fhir_get "/Observation?patient=${RESOLVED_PATIENT_ID}&code=8867-4" "$ADMIN_TOKEN")"
|
||||
if [[ "$(jq -er '.entry | length' <<<"$loinc_search" 2>/dev/null || echo 0)" -ge 1 ]]; then
|
||||
pass "GET /fhir/Observation?code=8867-4 resolves LOINC to HEART_RATE"
|
||||
else
|
||||
fail "GET /fhir/Observation?code=8867-4 resolves LOINC to HEART_RATE"
|
||||
fi
|
||||
|
||||
category_search="$(fhir_get "/Observation?patient=${RESOLVED_PATIENT_ID}&category=vital-signs" "$ADMIN_TOKEN")"
|
||||
if [[ "$(jq -er '.entry | length' <<<"$category_search" 2>/dev/null || echo 0)" -ge 1 ]]; then
|
||||
pass "GET /fhir/Observation?category=vital-signs returns vital sign observations"
|
||||
else
|
||||
fail "GET /fhir/Observation?category=vital-signs returns vital sign observations"
|
||||
fi
|
||||
|
||||
date_search="$(fhir_get "/Observation?patient=${RESOLVED_PATIENT_ID}&date=ge2026-06-20" "$ADMIN_TOKEN")"
|
||||
if [[ "$(jq -er '.total // 0' <<<"$date_search" 2>/dev/null || echo 0)" -ge 1 ]]; then
|
||||
pass "GET /fhir/Observation?date=ge2026-06-20 filters by recordedAt"
|
||||
else
|
||||
fail "GET /fhir/Observation?date=ge2026-06-20 filters by recordedAt"
|
||||
fi
|
||||
}
|
||||
|
||||
test_fhir_patient_everything() {
|
||||
section "7. FHIR Patient \$everything"
|
||||
|
||||
local bundle_json total resource_types has_patient has_encounter has_observation
|
||||
bundle_json="$(fhir_get "/Patient/${RESOLVED_PATIENT_ID}/\$everything" "$ADMIN_TOKEN")"
|
||||
|
||||
total="$(jq -er '.total // 0' <<<"$bundle_json" 2>/dev/null || echo 0)"
|
||||
resource_types="$(jq -r '[.entry[]?.resource.resourceType] | join(",")' <<<"$bundle_json" 2>/dev/null || true)"
|
||||
|
||||
if [[ "$total" -gt 0 ]]; then
|
||||
pass "GET /fhir/Patient/{id}/\$everything returns non-empty Bundle"
|
||||
else
|
||||
fail "GET /fhir/Patient/{id}/\$everything returns non-empty Bundle"
|
||||
fi
|
||||
|
||||
has_patient="$(grep -c 'Patient' <<<"$resource_types" || true)"
|
||||
has_encounter="$(grep -c 'Encounter' <<<"$resource_types" || true)"
|
||||
has_observation="$(grep -c 'Observation' <<<"$resource_types" || true)"
|
||||
|
||||
if [[ "$has_patient" -ge 1 && "$has_encounter" -ge 1 && "$has_observation" -ge 1 ]]; then
|
||||
pass "\$everything Bundle contains Patient, Encounter, and Observation"
|
||||
else
|
||||
fail "\$everything Bundle contains Patient, Encounter, and Observation (types: $resource_types)"
|
||||
fi
|
||||
}
|
||||
|
||||
test_fhir_content_type() {
|
||||
section "8. Content-Type negotiation"
|
||||
|
||||
local content_type
|
||||
content_type="$(curl -sS -o /dev/null -w '%{content_type}' \
|
||||
"${FHIR_URL}/Patient/${RESOLVED_PATIENT_ID}" \
|
||||
-H "Authorization: Bearer $ADMIN_TOKEN" \
|
||||
-H 'Accept: application/fhir+json')"
|
||||
|
||||
if [[ "$content_type" == application/fhir+json* ]]; then
|
||||
pass "Patient read Content-Type is application/fhir+json"
|
||||
else
|
||||
fail "Patient read Content-Type is application/fhir+json (got: $content_type)"
|
||||
fi
|
||||
}
|
||||
|
||||
test_fhir_error_handling() {
|
||||
section "9. FHIR error handling — 404 OperationOutcome"
|
||||
|
||||
local response http_status issue_code issue_severity
|
||||
response="$(fhir_get_status "/Patient/00000000-0000-0000-0000-000000000000" "$ADMIN_TOKEN")"
|
||||
http_status="${response##*__HTTP_STATUS__:}"
|
||||
response="${response%__HTTP_STATUS__:*}"
|
||||
|
||||
issue_code="$(jq -er '.issue[0].code // empty' <<<"$response" 2>/dev/null || true)"
|
||||
issue_severity="$(jq -er '.issue[0].severity // empty' <<<"$response" 2>/dev/null || true)"
|
||||
|
||||
if [[ "$http_status" == "404" ]]; then
|
||||
pass "unknown Patient returns HTTP 404"
|
||||
else
|
||||
fail "unknown Patient returns HTTP 404 (got HTTP $http_status)"
|
||||
fi
|
||||
|
||||
if [[ "$issue_code" == "not-found" && "$issue_severity" == "error" ]]; then
|
||||
pass "404 response is OperationOutcome with not-found issue"
|
||||
else
|
||||
fail "404 response is OperationOutcome with not-found issue"
|
||||
fi
|
||||
}
|
||||
|
||||
test_bundle_pagination() {
|
||||
section "10. Bundle pagination links"
|
||||
|
||||
local bundle_json
|
||||
bundle_json="$(fhir_get "/Patient?_count=1&_offset=0" "$ADMIN_TOKEN")"
|
||||
|
||||
if [[ "$(jq -er '.total // 0' <<<"$bundle_json")" -ge 2 ]]; then
|
||||
pass "Patient search total >= 2 for pagination test"
|
||||
else
|
||||
fail "Patient search total >= 2 for pagination test"
|
||||
return
|
||||
fi
|
||||
|
||||
if jq -e '.link[] | select(.relation == "self")' <<<"$bundle_json" >/dev/null 2>&1; then
|
||||
pass "search Bundle includes self link"
|
||||
else
|
||||
fail "search Bundle includes self link"
|
||||
fi
|
||||
|
||||
if jq -e '.link[] | select(.relation == "next")' <<<"$bundle_json" >/dev/null 2>&1; then
|
||||
pass "search Bundle includes next link"
|
||||
else
|
||||
fail "search Bundle includes next link"
|
||||
fi
|
||||
}
|
||||
|
||||
test_integration_tests() {
|
||||
section "11. dotnet integration tests — FhirIntegrationTests"
|
||||
|
||||
if [[ "$SKIP_TEST_CHECKS" == "1" ]]; then
|
||||
log " SKIP: dotnet integration tests (VIGILCARE_SKIP_TEST_CHECKS=1)"
|
||||
return
|
||||
fi
|
||||
|
||||
if ! command -v dotnet >/dev/null 2>&1; then
|
||||
log " SKIP: dotnet not installed"
|
||||
return
|
||||
fi
|
||||
|
||||
if dotnet test "$REPO_ROOT/VigilCareRecordsAPI.Tests/VigilCareRecordsAPI.Tests.csproj" \
|
||||
--filter "FullyQualifiedName~FhirIntegrationTests" \
|
||||
--no-restore >/tmp/vigilcare-p11-tests.log 2>&1; then
|
||||
pass "FhirIntegrationTests passed"
|
||||
else
|
||||
fail "FhirIntegrationTests passed"
|
||||
log " see /tmp/vigilcare-p11-tests.log"
|
||||
fi
|
||||
}
|
||||
|
||||
print_manual_ui_checklist() {
|
||||
section "12. Manual Vue UI checks (plan §7)"
|
||||
log " Login as admin1 → http://localhost:3028/fhir-explorer"
|
||||
log " - Select Patient resource type; search by name Santos"
|
||||
log " - Results table shows matching patients"
|
||||
log " - Click a row to see full FHIR JSON"
|
||||
log " - Patient \$everything: select patient, Load All Data"
|
||||
log " - Grouped Patient summary, Encounters list, Observations timeline"
|
||||
log " - Open CapabilityStatement link opens metadata JSON in new tab"
|
||||
}
|
||||
|
||||
main() {
|
||||
require_cmd curl
|
||||
require_cmd jq
|
||||
|
||||
log "VigilCare Records — Phase 11 verification"
|
||||
log "API: $API_URL"
|
||||
log "FHIR: $FHIR_URL"
|
||||
|
||||
assert_api_reachable
|
||||
|
||||
test_authentication
|
||||
ensure_fhir_clinical_seed
|
||||
test_fhir_metadata
|
||||
resolve_patient_id
|
||||
test_fhir_patient_read
|
||||
test_fhir_encounter
|
||||
test_fhir_observation
|
||||
test_fhir_patient_everything
|
||||
test_fhir_content_type
|
||||
test_fhir_error_handling
|
||||
test_bundle_pagination
|
||||
test_integration_tests
|
||||
print_manual_ui_checklist
|
||||
|
||||
log ""
|
||||
log "Results: $PASS_COUNT passed, $FAIL_COUNT failed"
|
||||
if (( FAIL_COUNT > 0 )); then
|
||||
log "Failed checks:"
|
||||
for item in "${FAILED_TESTS[@]}"; do
|
||||
log " - $item"
|
||||
done
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log "All Phase 11 API verification checks passed."
|
||||
log "Complete the manual Vue FHIR Explorer checklist above if not already done."
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Reference in New Issue
Block a user