Files
vigilcare-clinical/scripts/run-api-redis-tests.sh
voltsrage 2a3ef62a7d
CI / frontend (push) Failing after 57s
CI / backend (push) Failing after 6m27s
Add deployment
2026-08-05 00:26:20 +08:00

267 lines
9.2 KiB
Bash

#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
COMPOSE_FILE="${COMPOSE_FILE:-${SCRIPT_DIR}/../docker-compose.yml}"
BASE_URL="${BASE_URL:-http://localhost:5270}"
REDIS_PORT="${REDIS_PORT:-6382}"
REDIS_KEY="${REDIS_KEY:-threshold:HEART_RATE}"
RECORDED_AT="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
TMP_FILES=()
cleanup() {
local f
for f in "${TMP_FILES[@]}"; do
rm -f "${f}" "${f}.status"
done
}
trap cleanup EXIT
if ! command -v curl >/dev/null 2>&1; then
echo "Missing dependency: curl"
exit 1
fi
if ! command -v jq >/dev/null 2>&1; then
echo "Missing dependency: jq"
exit 1
fi
redis_cmd() {
if command -v redis-cli >/dev/null 2>&1; then
redis-cli -p "${REDIS_PORT}" "$@"
elif command -v docker >/dev/null 2>&1 && [[ -f "${COMPOSE_FILE}" ]]; then
docker compose -f "${COMPOSE_FILE}" exec -T redis redis-cli "$@"
else
echo "Missing dependency: redis-cli (or docker compose with redis service)"
exit 1
fi
}
request() {
local method="$1"
local url="$2"
local body="${3:-}"
local tmp_body
tmp_body="$(mktemp)"
TMP_FILES+=("${tmp_body}")
local status
if [[ -n "${body}" ]]; then
status="$(curl -sS -o "${tmp_body}" -w "%{http_code}" -X "${method}" "${url}" \
-H "Content-Type: application/json" -d "${body}")"
else
status="$(curl -sS -o "${tmp_body}" -w "%{http_code}" -X "${method}" "${url}")"
fi
echo "${status}" > "${tmp_body}.status"
echo "${tmp_body}"
}
assert_status() {
local expected="$1"
local body_file="$2"
local status
status="$(cat "${body_file}.status")"
if [[ "${status}" != "${expected}" ]]; then
echo "Expected HTTP ${expected}, got ${status}"
echo "Response body:"
cat "${body_file}"
echo
return 1
fi
}
assert_json() {
local body_file="$1"
local jq_expr="$2"
local expected="$3"
local actual
actual="$(jq -r "${jq_expr}" "${body_file}")"
if [[ "${actual}" != "${expected}" ]]; then
echo "Expected ${jq_expr} = ${expected}, got ${actual}"
echo "Response body:"
cat "${body_file}"
echo
return 1
fi
}
TOTAL_STEPS=16
echo "Running API + Redis verification against ${BASE_URL}"
echo "Recorded-at timestamp: ${RECORDED_AT}"
echo ""
echo "[0/${TOTAL_STEPS}] Preflight — API reachable"
preflight_status="$(curl -sS -o /dev/null -w "%{http_code}" "${BASE_URL}/api/v1/alert-thresholds" || true)"
if [[ "${preflight_status}" != "200" ]]; then
echo "API not reachable at ${BASE_URL} (HTTP ${preflight_status})."
echo "Start the API with: dotnet run --project VigilCareClinicalAPI"
exit 1
fi
echo "OK: API is up"
echo ""
echo "[1/${TOTAL_STEPS}] Listing thresholds"
resp="$(request GET "${BASE_URL}/api/v1/alert-thresholds")"
assert_status "200" "${resp}"
threshold_count="$(jq -r '.data | length' "${resp}")"
if [[ "${threshold_count}" -lt 12 ]]; then
echo "Expected at least 12 alert thresholds, found ${threshold_count}."
exit 1
fi
echo "OK: ${threshold_count} threshold(s) configured"
threshold_id="$(jq -r '.data[] | select(.observationCode=="HEART_RATE") | .id' "${resp}" | head -n 1)"
if [[ -z "${threshold_id}" || "${threshold_id}" == "null" ]]; then
echo "Could not find HEART_RATE threshold id."
exit 1
fi
echo "OK: HEART_RATE threshold id = ${threshold_id}"
echo ""
echo "[2/${TOTAL_STEPS}] Registering patient"
patient_payload='{"firstName":"Test","lastName":"Runner","dateOfBirth":"1988-01-10","gender":"F"}'
resp="$(request POST "${BASE_URL}/api/v1/patients" "${patient_payload}")"
assert_status "201" "${resp}"
patient_id="$(jq -r '.data.id' "${resp}")"
if [[ -z "${patient_id}" || "${patient_id}" == "null" ]]; then
echo "Could not parse patient id."
exit 1
fi
echo "OK: patient id = ${patient_id}"
echo ""
echo "[3/${TOTAL_STEPS}] Opening encounter"
enc_payload='{"encounterType":"Inpatient","department":"ICU","attendingPhysician":"Dr. Script"}'
resp="$(request POST "${BASE_URL}/api/v1/patients/${patient_id}/encounters" "${enc_payload}")"
assert_status "201" "${resp}"
encounter_id="$(jq -r '.data.id' "${resp}")"
if [[ -z "${encounter_id}" || "${encounter_id}" == "null" ]]; then
echo "Could not parse encounter id."
exit 1
fi
echo "OK: encounter id = ${encounter_id}"
echo ""
echo "[4/${TOTAL_STEPS}] Verifying illegal status transition returns 409"
transition_payload='{"status":"Scheduled"}'
resp="$(request PATCH "${BASE_URL}/api/v1/encounters/${encounter_id}/status" "${transition_payload}")"
assert_status "409" "${resp}"
error_code="$(jq -r '.error.code' "${resp}")"
echo "OK: illegal transition blocked (${error_code})"
echo ""
echo "[5/${TOTAL_STEPS}] Updating HEART_RATE threshold"
update_payload='{"observationCode":"HEART_RATE","displayName":"Heart Rate","unit":"bpm","criticalLow":30,"warningLow":50,"warningHigh":110,"criticalHigh":160}'
resp="$(request PUT "${BASE_URL}/api/v1/alert-thresholds/${threshold_id}" "${update_payload}")"
assert_status "200" "${resp}"
echo "OK: threshold update accepted"
echo ""
echo "[6/${TOTAL_STEPS}] Verifying Redis invalidation"
redis_value="$(redis_cmd GET "${REDIS_KEY}" | tr -d '\r')"
if [[ "${redis_value}" != "(nil)" && -n "${redis_value}" ]]; then
echo "Expected Redis key ${REDIS_KEY} to be invalidated, but found value."
echo "Value: ${redis_value}"
exit 1
fi
echo "OK: Redis key invalidated (${REDIS_KEY})"
echo ""
echo "[7/${TOTAL_STEPS}] Listing patient"
resp="$(request GET "${BASE_URL}/api/v1/patients/${patient_id}")"
assert_status "200" "${resp}"
echo "OK: patient lookup succeeds"
echo ""
echo "[8/${TOTAL_STEPS}] Ingesting normal observation (no alert)"
normal_obs_payload="$(jq -nc \
--arg recordedAt "${RECORDED_AT}" \
'{observations:[{observationCode:"HEART_RATE",value:78,unit:"bpm",source:"DEVICE",recordedAt:$recordedAt}]}')"
resp="$(request POST "${BASE_URL}/api/v1/encounters/${encounter_id}/observations" "${normal_obs_payload}")"
assert_status "201" "${resp}"
assert_json "${resp}" '.data.alertGenerated' 'false'
echo "OK: normal observation ingested without alert"
echo ""
echo "[9/${TOTAL_STEPS}] Ingesting critical potassium (alert expected)"
critical_obs_payload="$(jq -nc \
--arg recordedAt "${RECORDED_AT}" \
'{observations:[{observationCode:"POTASSIUM_MEQ_L",value:2.1,unit:"mEq/L",source:"LAB",recordedAt:$recordedAt,idempotencyKey:"script-critical-001"}]}')"
resp="$(request POST "${BASE_URL}/api/v1/encounters/${encounter_id}/observations" "${critical_obs_payload}")"
assert_status "201" "${resp}"
assert_json "${resp}" '.data.alertGenerated' 'true'
alert_id="$(jq -r '.data.alertId' "${resp}")"
if [[ -z "${alert_id}" || "${alert_id}" == "null" ]]; then
echo "Could not parse alert id from critical ingest."
exit 1
fi
echo "OK: critical alert generated (alert id = ${alert_id})"
echo ""
echo "[10/${TOTAL_STEPS}] Fetching observation history"
resp="$(request GET "${BASE_URL}/api/v1/encounters/${encounter_id}/observations?code=HEART_RATE&limit=10")"
assert_status "200" "${resp}"
history_count="$(jq -r '.data.items | length' "${resp}")"
if [[ "${history_count}" -lt 1 ]]; then
echo "Expected at least one HEART_RATE observation in history, got ${history_count}"
exit 1
fi
echo "OK: observation history returned ${history_count} item(s)"
echo ""
echo "[11/${TOTAL_STEPS}] Resolving unacknowledged alert returns 409"
resp="$(request POST "${BASE_URL}/api/v1/alerts/${alert_id}/resolve")"
assert_status "409" "${resp}"
assert_json "${resp}" '.error.code' 'ALERT_NOT_ACKNOWLEDGED'
echo "OK: resolve blocked before acknowledge"
echo ""
echo "[12/${TOTAL_STEPS}] Acknowledging alert"
ack_payload='{"clinicianId":"DR-SCRIPT","note":"Reviewing from verification script."}'
resp="$(request POST "${BASE_URL}/api/v1/alerts/${alert_id}/acknowledge" "${ack_payload}")"
assert_status "200" "${resp}"
assert_json "${resp}" '.data.status' 'Acknowledged'
echo "OK: alert acknowledged"
echo ""
echo "[13/${TOTAL_STEPS}] Resolving acknowledged alert"
resp="$(request POST "${BASE_URL}/api/v1/alerts/${alert_id}/resolve")"
assert_status "200" "${resp}"
assert_json "${resp}" '.data.status' 'Resolved'
echo "OK: alert resolved"
echo ""
echo "[14/${TOTAL_STEPS}] Rejecting implausible observation with 422"
implausible_payload="$(jq -nc \
--arg recordedAt "${RECORDED_AT}" \
'{observations:[{observationCode:"HEART_RATE",value:350,unit:"bpm",source:"DEVICE",recordedAt:$recordedAt}]}')"
resp="$(request POST "${BASE_URL}/api/v1/encounters/${encounter_id}/observations" "${implausible_payload}")"
assert_status "422" "${resp}"
assert_json "${resp}" '.error.code' 'OBSERVATION_OUT_OF_PLAUSIBLE_RANGE'
echo "OK: implausible value rejected"
echo ""
echo "[15/${TOTAL_STEPS}] Discharging encounter"
discharge_payload='{"status":"Discharged"}'
resp="$(request PATCH "${BASE_URL}/api/v1/encounters/${encounter_id}/status" "${discharge_payload}")"
assert_status "200" "${resp}"
echo "OK: encounter discharged"
echo ""
echo "[16/${TOTAL_STEPS}] Ingest against discharged encounter returns 409"
post_discharge_payload="$(jq -nc \
--arg recordedAt "${RECORDED_AT}" \
'{observations:[{observationCode:"HEART_RATE",value:78,unit:"bpm",source:"DEVICE",recordedAt:$recordedAt}]}')"
resp="$(request POST "${BASE_URL}/api/v1/encounters/${encounter_id}/observations" "${post_discharge_payload}")"
assert_status "409" "${resp}"
assert_json "${resp}" '.error.code' 'ENCOUNTER_NOT_ACTIVE'
echo "OK: ingest blocked for discharged encounter"
echo ""
echo "All ${TOTAL_STEPS} API + Redis checks passed."