Files
vigilcare-records/scripts/run-vigilcare-records-phase-11-verification.sh
voltsrage c871dc4842
CI / backend (push) Failing after 2m26s
CI / frontend (push) Failing after 53s
Add deployment files
2026-08-11 20:17:53 +08:00

665 lines
22 KiB
Bash
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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, and 404 OperationOutcome.
#
# Prerequisites:
# docker compose up -d (PostgreSQL, Redis, MinIO)
# dotnet ef database update --project VigilCareRecordsAPI
# dotnet run --project VigilCareRecordsAPI
# Phase 110 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
#
# 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}"
# 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=""
RESOLVED_ENCOUNTER_ID=""
RESOLVED_HEART_RATE_OBS_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
}
verification_fixture_ready() {
local heart_rate_count
heart_rate_count="$(psql_query "
SELECT count(*) FROM clinical.observations o
JOIN clinical.patients p ON p.id = o.patient_id
WHERE p.mrn = 'VCR-000001' AND o.observation_code = 'HEART_RATE';
" 2>/dev/null || echo "0")"
[[ "${heart_rate_count:-0}" -ge 1 ]]
}
seed_verification_fixture() {
local patient_id encounter_id batch_id heart_rate_count
patient_id="$(psql_query "SELECT id FROM clinical.patients WHERE mrn = 'VCR-000001' LIMIT 1;" 2>/dev/null || true)"
if [[ -z "$patient_id" ]]; then
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;
" || return 1
patient_id="$PATIENT1_ID"
else
psql_exec "
UPDATE clinical.patients SET
full_name = 'MARIA SANTOS',
date_of_birth = '1978-03-15',
sex = 'female',
blood_type = 'A+',
emergency_contact = 'Juan Santos - 555-0101',
allergies_json = '[\"Penicillin\", \"Sulfa drugs\"]',
no_known_allergies = false,
updated_at = NOW()
WHERE id = '${patient_id}';
" || return 1
fi
encounter_id="$(psql_query "
SELECT id FROM clinical.encounters
WHERE patient_id = '${patient_id}'
ORDER BY created_at
LIMIT 1;
" 2>/dev/null || true)"
if [[ -z "$encounter_id" ]]; then
batch_id="$(psql_query "SELECT id FROM digitization_batches ORDER BY created_at LIMIT 1;" 2>/dev/null || true)"
batch_id="${batch_id:-$BATCH1_ID}"
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}', '${patient_id}', NOW() - interval '5 days',
'Internal Medicine', '2A-04', 'Pneumonia with elevated WBC',
'active', '${batch_id}', NOW() - interval '3 days', NOW() - interval '12 hours'
)
ON CONFLICT (id) DO NOTHING;
" || return 1
encounter_id="$ENCOUNTER1_ID"
fi
batch_id="$(psql_query "
SELECT source_batch_id FROM clinical.encounters
WHERE id = '${encounter_id}'
LIMIT 1;
" 2>/dev/null || true)"
batch_id="${batch_id:-$BATCH1_ID}"
heart_rate_count="$(psql_query "
SELECT count(*) FROM clinical.observations
WHERE patient_id = '${patient_id}' AND observation_code = 'HEART_RATE';
" 2>/dev/null || echo "0")"
if [[ "${heart_rate_count:-0}" -lt 1 ]]; then
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}', '${encounter_id}', '${patient_id}',
'HEART_RATE', 88.000, 'bpm', NOW() - interval '5 days',
'digitization_backfill', '${batch_id}', NOW() - interval '12 hours'),
('${WBC_OBS_ID}', '${encounter_id}', '${patient_id}',
'WBC_K_UL', 14.200, 'K/uL', NOW() - interval '5 days',
'digitization_backfill', '${batch_id}', NOW() - interval '12 hours')
ON CONFLICT (id) DO NOTHING;
" || return 1
fi
return 0
}
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
if verification_fixture_ready; then
pass "FHIR verification fixture (VCR-000001 + HEART_RATE) already present"
return
fi
log " Ensuring FHIR verification fixture for VCR-000001..."
if seed_verification_fixture; then
if verification_fixture_ready; then
pass "FHIR verification fixture ready for VCR-000001"
else
fail "FHIR verification fixture ready for VCR-000001"
fi
else
fail "seed FHIR verification fixture for VCR-000001"
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_clinical_ids() {
section "3. Resolve FHIR clinical resource IDs"
local search_json encounter_search obs_search
if psql_available; then
RESOLVED_PATIENT_ID="$(psql_query "
SELECT id FROM clinical.patients WHERE mrn = 'VCR-000001' LIMIT 1;
" 2>/dev/null || true)"
if [[ -n "$RESOLVED_PATIENT_ID" ]]; then
RESOLVED_ENCOUNTER_ID="$(psql_query "
SELECT id FROM clinical.encounters
WHERE patient_id = '${RESOLVED_PATIENT_ID}'
ORDER BY created_at
LIMIT 1;
" 2>/dev/null || true)"
RESOLVED_HEART_RATE_OBS_ID="$(psql_query "
SELECT id FROM clinical.observations
WHERE patient_id = '${RESOLVED_PATIENT_ID}'
AND observation_code = 'HEART_RATE'
ORDER BY recorded_at DESC
LIMIT 1;
" 2>/dev/null || true)"
fi
fi
if [[ -z "$RESOLVED_PATIENT_ID" ]]; then
search_json="$(fhir_get "/Patient?identifier=VCR-000001" "$ADMIN_TOKEN")"
RESOLVED_PATIENT_ID="$(jq -er '.entry[0].resource.id // empty' <<<"$search_json" 2>/dev/null || true)"
fi
if [[ -z "$RESOLVED_PATIENT_ID" ]]; then
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)"
fi
if [[ -z "$RESOLVED_ENCOUNTER_ID" && -n "$RESOLVED_PATIENT_ID" ]]; then
encounter_search="$(fhir_get "/Encounter?patient=${RESOLVED_PATIENT_ID}" "$ADMIN_TOKEN")"
RESOLVED_ENCOUNTER_ID="$(jq -er '.entry[0].resource.id // empty' <<<"$encounter_search" 2>/dev/null || true)"
fi
if [[ -z "$RESOLVED_HEART_RATE_OBS_ID" && -n "$RESOLVED_PATIENT_ID" ]]; then
obs_search="$(fhir_get "/Observation?patient=${RESOLVED_PATIENT_ID}&code=8867-4" "$ADMIN_TOKEN")"
RESOLVED_HEART_RATE_OBS_ID="$(jq -er '.entry[0].resource.id // empty' <<<"$obs_search" 2>/dev/null || true)"
fi
if [[ -n "$RESOLVED_PATIENT_ID" ]]; then
pass "resolved patient ID ($RESOLVED_PATIENT_ID)"
else
fail "resolved patient ID"
fi
if [[ -n "$RESOLVED_ENCOUNTER_ID" ]]; then
pass "resolved encounter ID ($RESOLVED_ENCOUNTER_ID)"
else
fail "resolved encounter ID"
fi
if [[ -n "$RESOLVED_HEART_RATE_OBS_ID" ]]; then
pass "resolved HEART_RATE observation ID ($RESOLVED_HEART_RATE_OBS_ID)"
else
fail "resolved HEART_RATE observation 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/${RESOLVED_ENCOUNTER_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_search category_search date_search date_filter
obs_json="$(fhir_get "/Observation/${RESOLVED_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)"
if [[ "$loinc_code" == "8867-4" && "$loinc_unit" == "bpm" ]] \
&& jq -e '.valueQuantity.value == 88' <<<"$obs_json" >/dev/null 2>&1; 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_filter="$(jq -er '.effectiveDateTime // empty' <<<"$obs_json" 2>/dev/null | cut -c1-10 || true)"
if [[ -n "$date_filter" ]]; then
date_filter="$(date -d "${date_filter} - 1 day" +%Y-%m-%d 2>/dev/null || echo "2026-06-20")"
else
date_filter="2026-06-20"
fi
date_search="$(fhir_get "/Observation?patient=${RESOLVED_PATIENT_ID}&date=ge${date_filter}" "$ADMIN_TOKEN")"
if [[ "$(jq -er '.total // 0' <<<"$date_search" 2>/dev/null || echo 0)" -ge 1 ]]; then
pass "GET /fhir/Observation?date=ge${date_filter} filters by recordedAt"
else
fail "GET /fhir/Observation?date=ge${date_filter} 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
}
print_manual_ui_checklist() {
section "11. 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_clinical_ids
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
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 "$@"