619 lines
21 KiB
Bash
619 lines
21 KiB
Bash
#!/usr/bin/env bash
|
|
# Runs Phase 2 verification checks from docs/plans/phase-2-plan.md.
|
|
#
|
|
# Prerequisites:
|
|
# docker compose up -d (PostgreSQL + Redis)
|
|
# dotnet run --project VigilCareRecordsAPI
|
|
# Phase 1 seed data (entry1, entry2, intake1 users)
|
|
#
|
|
# Environment overrides (same defaults as Phase 1 script):
|
|
# 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 assertions
|
|
|
|
set -uo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
FIXTURE_PDF="$SCRIPT_DIR/fixtures/test-scan.pdf"
|
|
|
|
API_URL="${VIGILCARE_API_URL:-http://localhost:5217}"
|
|
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}"
|
|
|
|
RECORDED_AT="${VIGILCARE_RECORDED_AT:-2025-01-01T10:00:00Z}"
|
|
|
|
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
|
|
}
|
|
|
|
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
|
|
command -v psql >/dev/null 2>&1 && return 0
|
|
compose_service_running postgres
|
|
}
|
|
|
|
psql_query() {
|
|
if 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"
|
|
elif compose_service_running postgres; then
|
|
"${COMPOSE[@]}" exec -T postgres \
|
|
psql -U "$PG_USER" -d "$PG_DB" -Atqc "$1"
|
|
else
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
http_code() {
|
|
curl -sS -o /dev/null -w '%{http_code}' "$@"
|
|
}
|
|
|
|
json_post() {
|
|
local url="$1"
|
|
local body="$2"
|
|
local token="${3:-}"
|
|
if [[ -n "$token" ]]; then
|
|
curl -sS -X POST "$url" \
|
|
-H "Authorization: Bearer $token" \
|
|
-H 'Content-Type: application/json' \
|
|
-d "$body"
|
|
else
|
|
curl -sS -X POST "$url" \
|
|
-H 'Content-Type: application/json' \
|
|
-d "$body"
|
|
fi
|
|
}
|
|
|
|
json_put() {
|
|
local url="$1"
|
|
local body="$2"
|
|
local token="$3"
|
|
curl -sS -X PUT "$url" \
|
|
-H "Authorization: Bearer $token" \
|
|
-H 'Content-Type: application/json' \
|
|
-d "$body"
|
|
}
|
|
|
|
login() {
|
|
local username="$1"
|
|
local password="${2:-password}"
|
|
json_post "$API_URL/api/v1/auth/login" \
|
|
"{\"username\":\"$username\",\"password\":\"$password\"}"
|
|
}
|
|
|
|
extract_data_field() {
|
|
local json="$1"
|
|
local field="$2"
|
|
jq -er ".data.$field // empty" <<<"$json"
|
|
}
|
|
|
|
extract_error_code() {
|
|
local json="$1"
|
|
jq -er '.error.code // empty' <<<"$json"
|
|
}
|
|
|
|
extract_error_message() {
|
|
local json="$1"
|
|
jq -er '.error.message // empty' <<<"$json"
|
|
}
|
|
|
|
upload_batch() {
|
|
local token="$1"
|
|
local file_path="${2:-$FIXTURE_PDF}"
|
|
|
|
curl -sS -X POST "$API_URL/api/v1/digitization-batches" \
|
|
-H "Authorization: Bearer $token" \
|
|
-F "file=@${file_path};type=application/pdf" \
|
|
-F "batchType=VITALS_SHEET"
|
|
}
|
|
|
|
assign_batch() {
|
|
local token="$1"
|
|
local batch_id="$2"
|
|
local entry_clerk_id="$3"
|
|
|
|
curl -sS -X PATCH "$API_URL/api/v1/digitization-batches/$batch_id/assign" \
|
|
-H "Authorization: Bearer $token" \
|
|
-H 'Content-Type: application/json' \
|
|
-d "{\"entryClerkUserId\":\"$entry_clerk_id\"}"
|
|
}
|
|
|
|
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 "Start API with: dotnet run --project VigilCareRecordsAPI"
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
# Upload a VITALS_SHEET batch and assign it to entry1.
|
|
# Prints the batch id to stdout.
|
|
create_assigned_vitals_batch() {
|
|
local intake_token="$1"
|
|
local upload_json batch_id entry_json entry_id
|
|
|
|
upload_json="$(upload_batch "$intake_token")"
|
|
if [[ "$(jq -er '.success' <<<"$upload_json")" != "true" ]]; then
|
|
log "ERROR: failed to upload batch for test setup"
|
|
return 1
|
|
fi
|
|
|
|
batch_id="$(extract_data_field "$upload_json" id)"
|
|
entry_json="$(login entry1)"
|
|
entry_id="$(extract_data_field "$entry_json" userId)"
|
|
assign_batch "$intake_token" "$batch_id" "$entry_id" >/dev/null
|
|
|
|
printf '%s' "$batch_id"
|
|
}
|
|
|
|
test_draft_patient_upsert() {
|
|
section "1. Draft CRUD — patient upsert creates and updates"
|
|
|
|
local intake_json intake_token entry_json entry_token batch_id
|
|
local first_json second_json blood_type
|
|
|
|
intake_json="$(login intake1)"
|
|
intake_token="$(extract_data_field "$intake_json" token)"
|
|
batch_id="$(create_assigned_vitals_batch "$intake_token")" || return
|
|
|
|
entry_json="$(login entry1)"
|
|
entry_token="$(extract_data_field "$entry_json" token)"
|
|
|
|
first_json="$(json_put \
|
|
"$API_URL/api/v1/digitization-batches/$batch_id/draft/patient" \
|
|
'{"fullName":"Chen Wei-Lin","dateOfBirth":"1985-03-15","sex":"M","noKnownAllergies":true,"noActiveMedications":true}' \
|
|
"$entry_token")"
|
|
if [[ "$(jq -er '.success' <<<"$first_json")" == "true" ]]; then
|
|
pass "first patient PUT creates draft patient (200)"
|
|
else
|
|
fail "first patient PUT creates draft patient (200)"
|
|
return
|
|
fi
|
|
|
|
second_json="$(json_put \
|
|
"$API_URL/api/v1/digitization-batches/$batch_id/draft/patient" \
|
|
'{"fullName":"Chen Wei-Lin","dateOfBirth":"1985-03-15","sex":"M","bloodType":"O+","noKnownAllergies":true,"noActiveMedications":true}' \
|
|
"$entry_token")"
|
|
blood_type="$(extract_data_field "$second_json" bloodType)"
|
|
if [[ "$(jq -er '.success' <<<"$second_json")" == "true" && "$blood_type" == "O+" ]]; then
|
|
pass "second patient PUT updates draft patient (bloodType added)"
|
|
else
|
|
fail "second patient PUT updates draft patient (bloodType added) (got: ${blood_type:-<none>})"
|
|
fi
|
|
}
|
|
|
|
test_status_transition_to_in_entry() {
|
|
section "2. Status transitions — first draft save moves to IN_ENTRY"
|
|
|
|
local intake_json intake_token entry_json entry_token batch_id status_before status_after
|
|
|
|
intake_json="$(login intake1)"
|
|
intake_token="$(extract_data_field "$intake_json" token)"
|
|
batch_id="$(create_assigned_vitals_batch "$intake_token")" || return
|
|
|
|
entry_json="$(login entry1)"
|
|
entry_token="$(extract_data_field "$entry_json" token)"
|
|
|
|
status_before="$(curl -sS "$API_URL/api/v1/digitization-batches/$batch_id" \
|
|
-H "Authorization: Bearer $entry_token" | jq -er '.data.status')"
|
|
if [[ "$status_before" == "UPLOADED" ]]; then
|
|
pass "batch status is UPLOADED before first draft save"
|
|
else
|
|
fail "batch status is UPLOADED before first draft save (got: $status_before)"
|
|
fi
|
|
|
|
json_put \
|
|
"$API_URL/api/v1/digitization-batches/$batch_id/draft/patient" \
|
|
'{"fullName":"Test Patient","noKnownAllergies":true,"noActiveMedications":true}' \
|
|
"$entry_token" >/dev/null
|
|
|
|
status_after="$(curl -sS "$API_URL/api/v1/digitization-batches/$batch_id" \
|
|
-H "Authorization: Bearer $entry_token" | jq -er '.data.status')"
|
|
if [[ "$status_after" == "IN_ENTRY" ]]; then
|
|
pass "batch status transitions to IN_ENTRY after first draft save"
|
|
else
|
|
fail "batch status transitions to IN_ENTRY after first draft save (got: $status_after)"
|
|
fi
|
|
}
|
|
|
|
test_plausibility_validation() {
|
|
section "3. Plausibility validation — implausible values rejected"
|
|
|
|
local intake_json intake_token entry_json entry_token batch_id
|
|
local bad_json bad_code good_code
|
|
|
|
intake_json="$(login intake1)"
|
|
intake_token="$(extract_data_field "$intake_json" token)"
|
|
batch_id="$(create_assigned_vitals_batch "$intake_token")" || return
|
|
|
|
entry_json="$(login entry1)"
|
|
entry_token="$(extract_data_field "$entry_json" token)"
|
|
|
|
bad_json="$(json_post \
|
|
"$API_URL/api/v1/digitization-batches/$batch_id/draft/observations" \
|
|
"{\"observationCode\":\"HEART_RATE\",\"value\":350,\"unit\":\"bpm\",\"recordedAt\":\"$RECORDED_AT\"}" \
|
|
"$entry_token")"
|
|
bad_code="$(extract_error_code "$bad_json")"
|
|
if [[ "$bad_code" == "OBSERVATION_OUT_OF_PLAUSIBLE_RANGE" ]]; then
|
|
pass "heart rate 350 bpm returns OBSERVATION_OUT_OF_PLAUSIBLE_RANGE"
|
|
else
|
|
fail "heart rate 350 bpm returns OBSERVATION_OUT_OF_PLAUSIBLE_RANGE (got: ${bad_code:-<none>})"
|
|
fi
|
|
|
|
good_code="$(http_code -X POST \
|
|
"$API_URL/api/v1/digitization-batches/$batch_id/draft/observations" \
|
|
-H "Authorization: Bearer $entry_token" \
|
|
-H 'Content-Type: application/json' \
|
|
-d "{\"observationCode\":\"HEART_RATE\",\"value\":78,\"unit\":\"bpm\",\"recordedAt\":\"$RECORDED_AT\"}")"
|
|
if [[ "$good_code" == "201" ]]; then
|
|
pass "heart rate 78 bpm saves successfully (201)"
|
|
else
|
|
fail "heart rate 78 bpm saves successfully (201) (got: $good_code)"
|
|
fi
|
|
}
|
|
|
|
test_incomplete_submit_rejected() {
|
|
section "4. Submit-for-verification — incomplete batch rejected"
|
|
|
|
local intake_json intake_token entry_json entry_token batch_id
|
|
local submit_body submit_code error_code error_msg
|
|
local submit_tmp
|
|
|
|
intake_json="$(login intake1)"
|
|
intake_token="$(extract_data_field "$intake_json" token)"
|
|
batch_id="$(create_assigned_vitals_batch "$intake_token")" || return
|
|
|
|
entry_json="$(login entry1)"
|
|
entry_token="$(extract_data_field "$entry_json" token)"
|
|
|
|
json_put \
|
|
"$API_URL/api/v1/digitization-batches/$batch_id/draft/patient" \
|
|
'{"fullName":"Test Patient","noKnownAllergies":true,"noActiveMedications":true}' \
|
|
"$entry_token" >/dev/null
|
|
|
|
submit_tmp="$(mktemp)"
|
|
submit_code="$(curl -sS -o "$submit_tmp" -w '%{http_code}' -X POST \
|
|
"$API_URL/api/v1/digitization-batches/$batch_id/submit-for-verification" \
|
|
-H "Authorization: Bearer $entry_token")"
|
|
submit_body="$(cat "$submit_tmp")"
|
|
rm -f "$submit_tmp"
|
|
error_code="$(extract_error_code "$submit_body")"
|
|
error_msg="$(extract_error_message "$submit_body")"
|
|
|
|
if [[ "$submit_code" == "422" && "$error_code" == "BATCH_INCOMPLETE" ]]; then
|
|
pass "incomplete vitals batch submit returns 422 BATCH_INCOMPLETE"
|
|
else
|
|
fail "incomplete vitals batch submit returns 422 BATCH_INCOMPLETE (http=$submit_code code=${error_code:-<none>})"
|
|
return
|
|
fi
|
|
|
|
if [[ "$error_msg" == *"Encounter context is required"* &&
|
|
"$error_msg" == *"At least one observation"* ]]; then
|
|
pass "BATCH_INCOMPLETE message lists missing encounter and observations"
|
|
else
|
|
fail "BATCH_INCOMPLETE message lists missing encounter and observations"
|
|
fi
|
|
}
|
|
|
|
test_get_draft_payload() {
|
|
section "5. Full draft payload — GET returns patient, encounter, observations"
|
|
|
|
local intake_json intake_token entry_json entry_token batch_id draft_json
|
|
local patient_name department obs_count
|
|
|
|
intake_json="$(login intake1)"
|
|
intake_token="$(extract_data_field "$intake_json" token)"
|
|
batch_id="$(create_assigned_vitals_batch "$intake_token")" || return
|
|
|
|
entry_json="$(login entry1)"
|
|
entry_token="$(extract_data_field "$entry_json" token)"
|
|
|
|
json_put \
|
|
"$API_URL/api/v1/digitization-batches/$batch_id/draft/patient" \
|
|
'{"fullName":"Chen Wei-Lin","dateOfBirth":"1985-03-15","sex":"M","noKnownAllergies":true,"noActiveMedications":true}' \
|
|
"$entry_token" >/dev/null
|
|
|
|
json_put \
|
|
"$API_URL/api/v1/digitization-batches/$batch_id/draft/encounter" \
|
|
'{"admissionDate":"2025-01-01T08:00:00Z","department":"ICU","roomBed":"ICU-3B","admissionReason":"Chest pain"}' \
|
|
"$entry_token" >/dev/null
|
|
|
|
json_post \
|
|
"$API_URL/api/v1/digitization-batches/$batch_id/draft/observations" \
|
|
"{\"observationCode\":\"HEART_RATE\",\"value\":92,\"unit\":\"bpm\",\"recordedAt\":\"$RECORDED_AT\"}" \
|
|
"$entry_token" >/dev/null
|
|
|
|
draft_json="$(curl -sS "$API_URL/api/v1/digitization-batches/$batch_id/draft" \
|
|
-H "Authorization: Bearer $entry_token")"
|
|
patient_name="$(jq -er '.data.patient.fullName // empty' <<<"$draft_json")"
|
|
department="$(jq -er '.data.encounter.department // empty' <<<"$draft_json")"
|
|
obs_count="$(jq -er '.data.observations | length' <<<"$draft_json")"
|
|
|
|
if [[ "$patient_name" == "Chen Wei-Lin" && "$department" == "ICU" && "$obs_count" -ge 1 ]]; then
|
|
pass "GET /draft returns patient, encounter, and observations"
|
|
else
|
|
fail "GET /draft returns patient, encounter, and observations (patient=$patient_name dept=$department obs=$obs_count)"
|
|
fi
|
|
}
|
|
|
|
test_complete_submit_succeeds() {
|
|
section "6. Full lifecycle — complete vitals batch submits to PENDING_VERIFICATION"
|
|
|
|
local intake_json intake_token entry_json entry_token batch_id submit_json status
|
|
|
|
intake_json="$(login intake1)"
|
|
intake_token="$(extract_data_field "$intake_json" token)"
|
|
batch_id="$(create_assigned_vitals_batch "$intake_token")" || return
|
|
|
|
entry_json="$(login entry1)"
|
|
entry_token="$(extract_data_field "$entry_json" token)"
|
|
|
|
json_put \
|
|
"$API_URL/api/v1/digitization-batches/$batch_id/draft/patient" \
|
|
'{"fullName":"Chen Wei-Lin","dateOfBirth":"1985-03-15","sex":"M","noKnownAllergies":true,"noActiveMedications":true}' \
|
|
"$entry_token" >/dev/null
|
|
|
|
json_put \
|
|
"$API_URL/api/v1/digitization-batches/$batch_id/draft/encounter" \
|
|
'{"admissionDate":"2025-01-01T08:00:00Z","department":"ICU","admissionReason":"Chest pain"}' \
|
|
"$entry_token" >/dev/null
|
|
|
|
json_post \
|
|
"$API_URL/api/v1/digitization-batches/$batch_id/draft/observations" \
|
|
"{\"observationCode\":\"HEART_RATE\",\"value\":92,\"unit\":\"bpm\",\"recordedAt\":\"$RECORDED_AT\"}" \
|
|
"$entry_token" >/dev/null
|
|
|
|
submit_json="$(curl -sS -X POST \
|
|
"$API_URL/api/v1/digitization-batches/$batch_id/submit-for-verification" \
|
|
-H "Authorization: Bearer $entry_token")"
|
|
status="$(extract_data_field "$submit_json" status)"
|
|
|
|
if [[ "$(jq -er '.success' <<<"$submit_json")" == "true" && "$status" == "PENDING_VERIFICATION" ]]; then
|
|
pass "complete vitals batch submits successfully (PENDING_VERIFICATION)"
|
|
else
|
|
fail "complete vitals batch submits successfully (PENDING_VERIFICATION) (status=${status:-<none>})"
|
|
fi
|
|
}
|
|
|
|
test_observation_crud() {
|
|
section "7. Observation CRUD — add, update, delete"
|
|
|
|
local intake_json intake_token entry_json entry_token batch_id
|
|
local add_json obs_id update_json update_value delete_code
|
|
|
|
intake_json="$(login intake1)"
|
|
intake_token="$(extract_data_field "$intake_json" token)"
|
|
batch_id="$(create_assigned_vitals_batch "$intake_token")" || return
|
|
|
|
entry_json="$(login entry1)"
|
|
entry_token="$(extract_data_field "$entry_json" token)"
|
|
|
|
add_json="$(json_post \
|
|
"$API_URL/api/v1/digitization-batches/$batch_id/draft/observations" \
|
|
"{\"observationCode\":\"TEMP_C\",\"value\":37.2,\"unit\":\"C\",\"recordedAt\":\"$RECORDED_AT\",\"note\":\"Oral temperature\"}" \
|
|
"$entry_token")"
|
|
obs_id="$(extract_data_field "$add_json" id)"
|
|
if [[ "$(jq -er '.success' <<<"$add_json")" == "true" && -n "$obs_id" ]]; then
|
|
pass "POST observation returns 201 with id"
|
|
else
|
|
fail "POST observation returns 201 with id"
|
|
return
|
|
fi
|
|
|
|
update_json="$(json_put \
|
|
"$API_URL/api/v1/digitization-batches/$batch_id/draft/observations/$obs_id" \
|
|
"{\"observationCode\":\"TEMP_C\",\"value\":38.1,\"unit\":\"C\",\"recordedAt\":\"$RECORDED_AT\",\"note\":\"Corrected — misread decimal\"}" \
|
|
"$entry_token")"
|
|
update_value="$(extract_data_field "$update_json" value)"
|
|
if [[ "$(jq -er '.success' <<<"$update_json")" == "true" && "$update_value" == "38.1" ]]; then
|
|
pass "PUT observation updates value (200)"
|
|
else
|
|
fail "PUT observation updates value (200) (got: ${update_value:-<none>})"
|
|
fi
|
|
|
|
delete_code="$(http_code -X DELETE \
|
|
"$API_URL/api/v1/digitization-batches/$batch_id/draft/observations/$obs_id" \
|
|
-H "Authorization: Bearer $entry_token")"
|
|
if [[ "$delete_code" == "204" ]]; then
|
|
pass "DELETE observation returns 204"
|
|
else
|
|
fail "DELETE observation returns 204 (got: $delete_code)"
|
|
fi
|
|
}
|
|
|
|
test_rejected_batch_reentry() {
|
|
section "8. Rejected batch — draft save transitions back to IN_ENTRY"
|
|
|
|
if ! psql_available; then
|
|
if [[ "$SKIP_DB_CHECKS" == "1" ]]; then
|
|
log " SKIP: rejected batch re-entry (VIGILCARE_SKIP_DB_CHECKS=1)"
|
|
else
|
|
log " SKIP: rejected batch re-entry (postgres not reachable)"
|
|
fi
|
|
return
|
|
fi
|
|
|
|
local intake_json intake_token entry_json entry_token batch_id status event_meta
|
|
|
|
intake_json="$(login intake1)"
|
|
intake_token="$(extract_data_field "$intake_json" token)"
|
|
batch_id="$(create_assigned_vitals_batch "$intake_token")" || return
|
|
|
|
entry_json="$(login entry1)"
|
|
entry_token="$(extract_data_field "$entry_json" token)"
|
|
|
|
psql_query "UPDATE digitization_batches SET status = 'REJECTED', rejection_reason = 'Missing encounter details' WHERE id = '$batch_id';" >/dev/null
|
|
|
|
json_put \
|
|
"$API_URL/api/v1/digitization-batches/$batch_id/draft/encounter" \
|
|
'{"admissionDate":"2025-01-01T08:00:00Z","department":"Emergency Department","roomBed":"ER-7"}' \
|
|
"$entry_token" >/dev/null
|
|
|
|
status="$(curl -sS "$API_URL/api/v1/digitization-batches/$batch_id" \
|
|
-H "Authorization: Bearer $entry_token" | jq -er '.data.status')"
|
|
if [[ "$status" == "IN_ENTRY" ]]; then
|
|
pass "REJECTED batch transitions to IN_ENTRY on draft save"
|
|
else
|
|
fail "REJECTED batch transitions to IN_ENTRY on draft save (got: $status)"
|
|
fi
|
|
|
|
event_meta="$(psql_query "SELECT metadata_json FROM digitization_events WHERE batch_id = '$batch_id' AND event_type = 'entry_started' ORDER BY occurred_at DESC LIMIT 1;")"
|
|
if [[ "$event_meta" == *"REJECTED"* ]]; then
|
|
pass "entry_started event metadata records previous REJECTED status"
|
|
else
|
|
fail "entry_started event metadata records previous REJECTED status"
|
|
fi
|
|
}
|
|
|
|
test_verified_batch_blocks_entry() {
|
|
section "9. Verified batch — data entry blocked with ENTRY_NOT_ALLOWED"
|
|
|
|
if ! psql_available; then
|
|
if [[ "$SKIP_DB_CHECKS" == "1" ]]; then
|
|
log " SKIP: verified batch entry guard (VIGILCARE_SKIP_DB_CHECKS=1)"
|
|
else
|
|
log " SKIP: verified batch entry guard (postgres not reachable)"
|
|
fi
|
|
return
|
|
fi
|
|
|
|
local intake_json intake_token entry_json entry_token batch_id resp_json error_code
|
|
|
|
intake_json="$(login intake1)"
|
|
intake_token="$(extract_data_field "$intake_json" token)"
|
|
batch_id="$(create_assigned_vitals_batch "$intake_token")" || return
|
|
|
|
entry_json="$(login entry1)"
|
|
entry_token="$(extract_data_field "$entry_json" token)"
|
|
|
|
psql_query "UPDATE digitization_batches SET status = 'VERIFIED' WHERE id = '$batch_id';" >/dev/null
|
|
|
|
resp_json="$(json_post \
|
|
"$API_URL/api/v1/digitization-batches/$batch_id/draft/observations" \
|
|
"{\"observationCode\":\"HEART_RATE\",\"value\":80,\"unit\":\"bpm\",\"recordedAt\":\"$RECORDED_AT\"}" \
|
|
"$entry_token")"
|
|
error_code="$(extract_error_code "$resp_json")"
|
|
|
|
if [[ "$error_code" == "ENTRY_NOT_ALLOWED" ]]; then
|
|
pass "verified batch blocks observation add (ENTRY_NOT_ALLOWED)"
|
|
else
|
|
fail "verified batch blocks observation add (ENTRY_NOT_ALLOWED) (got: ${error_code:-<none>})"
|
|
fi
|
|
}
|
|
|
|
test_batch_not_assigned() {
|
|
section "10. Assignment guard — non-assigned clerk receives BATCH_NOT_ASSIGNED"
|
|
|
|
local intake_json intake_token entry1_json entry2_json entry2_token batch_id entry1_id resp_json error_code
|
|
|
|
intake_json="$(login intake1)"
|
|
intake_token="$(extract_data_field "$intake_json" token)"
|
|
batch_id="$(create_assigned_vitals_batch "$intake_token")" || return
|
|
|
|
entry1_json="$(login entry1)"
|
|
entry1_id="$(extract_data_field "$entry1_json" userId)"
|
|
entry2_json="$(login entry2)"
|
|
entry2_token="$(extract_data_field "$entry2_json" token)"
|
|
|
|
assign_batch "$intake_token" "$batch_id" "$entry1_id" >/dev/null
|
|
|
|
resp_json="$(json_put \
|
|
"$API_URL/api/v1/digitization-batches/$batch_id/draft/patient" \
|
|
'{"fullName":"Blocked Clerk","noKnownAllergies":true,"noActiveMedications":true}' \
|
|
"$entry2_token")"
|
|
error_code="$(extract_error_code "$resp_json")"
|
|
|
|
if [[ "$error_code" == "BATCH_NOT_ASSIGNED" ]]; then
|
|
pass "entry2 cannot save draft on batch assigned to entry1 (BATCH_NOT_ASSIGNED)"
|
|
else
|
|
fail "entry2 cannot save draft on batch assigned to entry1 (BATCH_NOT_ASSIGNED) (got: ${error_code:-<none>})"
|
|
fi
|
|
}
|
|
|
|
main() {
|
|
require_cmd curl
|
|
require_cmd jq
|
|
|
|
if [[ ! -f "$FIXTURE_PDF" ]]; then
|
|
log "ERROR: missing fixture PDF at $FIXTURE_PDF"
|
|
exit 1
|
|
fi
|
|
|
|
log "VigilCare Records — Phase 2 verification"
|
|
log "API: $API_URL"
|
|
|
|
assert_api_reachable
|
|
|
|
test_draft_patient_upsert
|
|
test_status_transition_to_in_entry
|
|
test_plausibility_validation
|
|
test_incomplete_submit_rejected
|
|
test_get_draft_payload
|
|
test_complete_submit_succeeds
|
|
test_observation_crud
|
|
test_rejected_batch_reentry
|
|
test_verified_batch_blocks_entry
|
|
test_batch_not_assigned
|
|
|
|
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 2 verification checks passed."
|
|
}
|
|
|
|
main "$@"
|