feature: Barcode/QR Cover Sheet System
This commit is contained in:
+751
@@ -0,0 +1,751 @@
|
||||
#!/usr/bin/env bash
|
||||
# Runs Phase 10 verification checks from docs/plans/phase-10-plan.md.
|
||||
#
|
||||
# Covers cover sheet generation, lookup, list filters, PDF endpoints,
|
||||
# barcode-assisted batch upload, redeem/auto-assign, and reuse prevention.
|
||||
#
|
||||
# Prerequisites:
|
||||
# docker compose up -d (PostgreSQL, Redis, MinIO)
|
||||
# dotnet ef database update --project VigilCareRecordsAPI
|
||||
# dotnet run --project VigilCareRecordsAPI
|
||||
# Phase 1–9 seed data (intake1, entry1, 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 assertions
|
||||
# VIGILCARE_SKIP_TEST_CHECKS set to 1 to skip dotnet integration tests
|
||||
#
|
||||
# Usage:
|
||||
# chmod +x scripts/run-vigilcare-records-phase-10-verification.sh
|
||||
# ./scripts/run-vigilcare-records-phase-10-verification.sh
|
||||
|
||||
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}"
|
||||
SKIP_TEST_CHECKS="${VIGILCARE_SKIP_TEST_CHECKS:-0}"
|
||||
|
||||
# Resolved at runtime from the live API (seed IDs vary per environment)
|
||||
ENTRY_CLERK1_ID=""
|
||||
PATIENT1_ID=""
|
||||
|
||||
PASS_COUNT=0
|
||||
FAIL_COUNT=0
|
||||
FAILED_TESTS=()
|
||||
|
||||
# Shared state populated during the run
|
||||
INTAKE_TOKEN=""
|
||||
ADMIN_TOKEN=""
|
||||
ENTRY_TOKEN=""
|
||||
UPLOAD_CODE=""
|
||||
UPLOAD_COVER_SHEET_ID=""
|
||||
UPLOAD_BATCH_ID=""
|
||||
ASSIGN_CODE=""
|
||||
PDF_SHEET_IDS=()
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
ensure_fixture_pdf() {
|
||||
if [[ -f "$FIXTURE_PDF" ]]; then
|
||||
return 0
|
||||
fi
|
||||
mkdir -p "$(dirname "$FIXTURE_PDF")"
|
||||
cat >"$FIXTURE_PDF" <<'EOF'
|
||||
%PDF-1.0
|
||||
1 0 obj<</Type/Catalog/Pages 2 0 R>>endobj 2 0 obj<</Type/Pages/Kids[3 0 R]/Count 1>>endobj 3 0 obj<</Type/Page/MediaBox[0 0 612 792]/Parent 2 0 R/Resources<<>>>>endobj
|
||||
xref
|
||||
0 4
|
||||
0000000000 65535 f
|
||||
0000000009 00000 n
|
||||
0000000058 00000 n
|
||||
0000000115 00000 n
|
||||
trailer<</Size 4/Root 1 0 R>>
|
||||
startxref
|
||||
206
|
||||
%%EOF
|
||||
EOF
|
||||
}
|
||||
|
||||
unique_pdf_path() {
|
||||
local suffix="$1"
|
||||
local path="/tmp/vigilcare-p10-${suffix}-${RANDOM}.pdf"
|
||||
printf '%%PDF-1.4\nphase10-%s-%s\n%%%%EOF\n' "$suffix" "$(date +%s%N)" >"$path"
|
||||
printf '%s' "$path"
|
||||
}
|
||||
|
||||
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_get() {
|
||||
local url="$1"
|
||||
local token="$2"
|
||||
curl -sS "$url" -H "Authorization: Bearer $token"
|
||||
}
|
||||
|
||||
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_bool_field() {
|
||||
local json="$1"
|
||||
local field="$2"
|
||||
jq -er ".data.$field | if . == null then empty else tostring end" <<<"$json"
|
||||
}
|
||||
|
||||
resolve_directory_ids() {
|
||||
local users_json patients_json
|
||||
users_json="$(json_get "$API_URL/api/v1/users?role=DATA_ENTRY_CLERK" "$INTAKE_TOKEN")"
|
||||
patients_json="$(json_get "$API_URL/api/v1/patients/search?q=Patient" "$INTAKE_TOKEN")"
|
||||
|
||||
ENTRY_CLERK1_ID="$(jq -er '.data[0].id // empty' <<<"$users_json" 2>/dev/null || true)"
|
||||
PATIENT1_ID="$(jq -er '.data[0].id // empty' <<<"$patients_json" 2>/dev/null || true)"
|
||||
|
||||
if [[ -n "$ENTRY_CLERK1_ID" ]]; then
|
||||
pass "resolved entry clerk ID for auto-assign test"
|
||||
else
|
||||
fail "resolved entry clerk ID for auto-assign test"
|
||||
fi
|
||||
|
||||
if [[ -n "$PATIENT1_ID" ]]; then
|
||||
pass "resolved patient ID for patient-linked cover sheet test"
|
||||
else
|
||||
fail "resolved patient ID for patient-linked cover sheet test"
|
||||
fi
|
||||
}
|
||||
|
||||
extract_error_code() {
|
||||
local json="$1"
|
||||
jq -er '.error.code // empty' <<<"$json" 2>/dev/null || true
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
upload_with_cover_sheet() {
|
||||
local token="$1"
|
||||
local code="$2"
|
||||
local pdf="$3"
|
||||
curl -sS -X POST "$API_URL/api/v1/digitization-batches" \
|
||||
-H "Authorization: Bearer $token" \
|
||||
-F "file=@${pdf};type=application/pdf" \
|
||||
-F "coverSheetCode=$code"
|
||||
}
|
||||
|
||||
upload_with_cover_sheet_status() {
|
||||
local token="$1"
|
||||
local code="$2"
|
||||
local pdf="$3"
|
||||
local body_file http_code_val
|
||||
body_file="$(mktemp)"
|
||||
http_code_val="$(curl -sS -o "$body_file" -w '%{http_code}' -X POST "$API_URL/api/v1/digitization-batches" \
|
||||
-H "Authorization: Bearer $token" \
|
||||
-F "file=@${pdf};type=application/pdf" \
|
||||
-F "coverSheetCode=$code")"
|
||||
cat "$body_file"
|
||||
rm -f "$body_file"
|
||||
printf '\n__HTTP_STATUS__:%s' "$http_code_val"
|
||||
}
|
||||
|
||||
generate_cover_sheets() {
|
||||
local token="$1"
|
||||
local body="$2"
|
||||
json_post "$API_URL/api/v1/cover-sheets/generate" "$body" "$token"
|
||||
}
|
||||
|
||||
test_authentication() {
|
||||
section "0. Authentication"
|
||||
|
||||
local intake_json admin_json entry_json
|
||||
intake_json="$(login intake1)"
|
||||
admin_json="$(login admin1)"
|
||||
entry_json="$(login entry1)"
|
||||
|
||||
INTAKE_TOKEN="$(extract_data_field "$intake_json" token)"
|
||||
ADMIN_TOKEN="$(extract_data_field "$admin_json" token)"
|
||||
ENTRY_TOKEN="$(extract_data_field "$entry_json" token)"
|
||||
|
||||
if [[ -z "$INTAKE_TOKEN" ]]; then
|
||||
log "ERROR: intake1 login failed."
|
||||
log "Response: ${intake_json:-<empty>}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -n "$INTAKE_TOKEN" ]]; then
|
||||
pass "intake1 login returns JWT"
|
||||
else
|
||||
fail "intake1 login returns JWT"
|
||||
fi
|
||||
|
||||
if [[ -n "$ADMIN_TOKEN" ]]; then
|
||||
pass "admin1 login returns JWT"
|
||||
else
|
||||
fail "admin1 login returns JWT"
|
||||
fi
|
||||
|
||||
if [[ -n "$ENTRY_TOKEN" ]]; then
|
||||
pass "entry1 login returns JWT"
|
||||
else
|
||||
fail "entry1 login returns JWT"
|
||||
fi
|
||||
|
||||
resolve_directory_ids
|
||||
}
|
||||
|
||||
test_database_schema() {
|
||||
section "1. Database — cover_sheets table"
|
||||
|
||||
if ! psql_available; then
|
||||
log " SKIP: PostgreSQL checks (set VIGILCARE_SKIP_DB_CHECKS=0 and start postgres)"
|
||||
return
|
||||
fi
|
||||
|
||||
local table_exists index_code index_unused
|
||||
table_exists="$(psql_query "
|
||||
SELECT count(*)
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = 'public' AND table_name = 'cover_sheets';
|
||||
")"
|
||||
index_code="$(psql_query "
|
||||
SELECT count(*)
|
||||
FROM pg_indexes
|
||||
WHERE tablename = 'cover_sheets' AND indexname = 'ix_cover_sheets_code';
|
||||
")"
|
||||
index_unused="$(psql_query "
|
||||
SELECT count(*)
|
||||
FROM pg_indexes
|
||||
WHERE tablename = 'cover_sheets' AND indexname = 'ix_cover_sheets_unused';
|
||||
")"
|
||||
|
||||
if [[ "$table_exists" == "1" ]]; then
|
||||
pass "cover_sheets table exists"
|
||||
else
|
||||
fail "cover_sheets table exists"
|
||||
fi
|
||||
|
||||
if [[ "$index_code" == "1" ]]; then
|
||||
pass "unique index ix_cover_sheets_code exists"
|
||||
else
|
||||
fail "unique index ix_cover_sheets_code exists"
|
||||
fi
|
||||
|
||||
if [[ "$index_unused" == "1" ]]; then
|
||||
pass "filtered index ix_cover_sheets_unused exists"
|
||||
else
|
||||
fail "filtered index ix_cover_sheets_unused exists"
|
||||
fi
|
||||
}
|
||||
|
||||
test_cover_sheet_generation() {
|
||||
section "2. Cover sheet generation (plan §1)"
|
||||
|
||||
local gen_json count unique_count bad_format
|
||||
gen_json="$(generate_cover_sheets "$INTAKE_TOKEN" \
|
||||
'{"count":5,"batchType":"VITALS_SHEET","track":"BACKFILL"}')"
|
||||
|
||||
if [[ "$(jq -er '.success' <<<"$gen_json")" == "true" ]]; then
|
||||
pass "POST /cover-sheets/generate returns success for intake1"
|
||||
else
|
||||
fail "POST /cover-sheets/generate returns success for intake1"
|
||||
log " response: $gen_json"
|
||||
return
|
||||
fi
|
||||
|
||||
count="$(jq '.data | length' <<<"$gen_json")"
|
||||
unique_count="$(jq '[.data[].code] | unique | length' <<<"$gen_json")"
|
||||
if [[ "$count" == "5" && "$unique_count" == "5" ]]; then
|
||||
pass "generate creates 5 cover sheets with unique codes"
|
||||
else
|
||||
fail "generate creates 5 cover sheets with unique codes (count=$count unique=$unique_count)"
|
||||
fi
|
||||
|
||||
bad_format="$(jq -r '[.data[].code | test("^VCR-CS-[0-9A-F]{8}$")] | all' <<<"$gen_json")"
|
||||
if [[ "$bad_format" == "true" ]]; then
|
||||
pass "all codes match VCR-CS-{8-hex} format"
|
||||
else
|
||||
fail "all codes match VCR-CS-{8-hex} format"
|
||||
fi
|
||||
|
||||
UPLOAD_CODE="$(jq -r '.data[0].code' <<<"$gen_json")"
|
||||
UPLOAD_COVER_SHEET_ID="$(jq -r '.data[0].id' <<<"$gen_json")"
|
||||
PDF_SHEET_IDS=($(jq -r '.data[0].id, .data[1].id' <<<"$gen_json"))
|
||||
|
||||
local entry_json entry_token entry_code
|
||||
entry_json="$(login entry1)"
|
||||
entry_token="$(extract_data_field "$entry_json" token)"
|
||||
entry_code="$(http_code -X POST "$API_URL/api/v1/cover-sheets/generate" \
|
||||
-H "Authorization: Bearer $entry_token" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"count":1,"batchType":"VITALS_SHEET","track":"BACKFILL"}')"
|
||||
if [[ "$entry_code" == "403" ]]; then
|
||||
pass "generate denied for DATA_ENTRY_CLERK (403)"
|
||||
else
|
||||
fail "generate denied for DATA_ENTRY_CLERK (403) — got HTTP $entry_code"
|
||||
fi
|
||||
|
||||
local admin_json
|
||||
admin_json="$(generate_cover_sheets "$ADMIN_TOKEN" \
|
||||
'{"count":1,"batchType":"LAB_RESULTS","track":"BACKFILL"}')"
|
||||
if [[ "$(jq -er '.success' <<<"$admin_json")" == "true" ]]; then
|
||||
pass "generate allowed for ADMINISTRATOR"
|
||||
else
|
||||
fail "generate allowed for ADMINISTRATOR"
|
||||
fi
|
||||
}
|
||||
|
||||
test_cover_sheet_lookup_and_list() {
|
||||
section "3. Cover sheet lookup and list (plan §2)"
|
||||
|
||||
if [[ -z "$UPLOAD_CODE" ]]; then
|
||||
fail "lookup requires generated cover sheet code"
|
||||
return
|
||||
fi
|
||||
|
||||
local lookup_json is_used batch_type
|
||||
lookup_json="$(json_get "$API_URL/api/v1/cover-sheets/lookup/$UPLOAD_CODE" "$INTAKE_TOKEN")"
|
||||
|
||||
if [[ "$(jq -er '.success' <<<"$lookup_json")" == "true" ]]; then
|
||||
pass "GET /cover-sheets/lookup/{code} returns success"
|
||||
else
|
||||
fail "GET /cover-sheets/lookup/{code} returns success"
|
||||
return
|
||||
fi
|
||||
|
||||
is_used="$(extract_bool_field "$lookup_json" isUsed)"
|
||||
batch_type="$(extract_data_field "$lookup_json" batchType)"
|
||||
if [[ "$is_used" == "false" ]]; then
|
||||
pass "lookup shows isUsed=false before upload"
|
||||
else
|
||||
fail "lookup shows isUsed=false before upload (isUsed=$is_used)"
|
||||
fi
|
||||
|
||||
if [[ "$batch_type" == "VITALS_SHEET" ]]; then
|
||||
pass "lookup returns batchType VITALS_SHEET"
|
||||
else
|
||||
fail "lookup returns batchType VITALS_SHEET (got $batch_type)"
|
||||
fi
|
||||
|
||||
local unknown_json unknown_code
|
||||
unknown_json="$(json_get "$API_URL/api/v1/cover-sheets/lookup/VCR-CS-DEADBEEF" "$INTAKE_TOKEN")"
|
||||
unknown_code="$(extract_error_code "$unknown_json")"
|
||||
if [[ "$unknown_code" == "COVER_SHEET_NOT_FOUND" ]]; then
|
||||
pass "lookup unknown code returns COVER_SHEET_NOT_FOUND"
|
||||
else
|
||||
fail "lookup unknown code returns COVER_SHEET_NOT_FOUND (got ${unknown_code:-<none>})"
|
||||
fi
|
||||
|
||||
local list_json unused_count
|
||||
list_json="$(json_get "$API_URL/api/v1/cover-sheets?isUsed=false&page=1&pageSize=20" "$INTAKE_TOKEN")"
|
||||
unused_count="$(jq '.data | length' <<<"$list_json")"
|
||||
if [[ "$(jq -er '.success' <<<"$list_json")" == "true" && "$unused_count" -ge 5 ]]; then
|
||||
pass "GET /cover-sheets?isUsed=false lists unused sheets"
|
||||
else
|
||||
fail "GET /cover-sheets?isUsed=false lists unused sheets (count=$unused_count)"
|
||||
fi
|
||||
}
|
||||
|
||||
test_pdf_generation() {
|
||||
section "4. Cover sheet PDF generation (plan Step 5)"
|
||||
|
||||
if [[ -z "$UPLOAD_COVER_SHEET_ID" || ${#PDF_SHEET_IDS[@]} -lt 2 ]]; then
|
||||
fail "PDF tests require generated cover sheet IDs"
|
||||
return
|
||||
fi
|
||||
|
||||
local pdf_tmp headers_file pdf_code content_type pdf_header
|
||||
pdf_tmp="$(mktemp)"
|
||||
headers_file="$(mktemp)"
|
||||
pdf_code="$(curl -sS -D "$headers_file" -o "$pdf_tmp" -w '%{http_code}' -X POST \
|
||||
"$API_URL/api/v1/cover-sheets/${UPLOAD_COVER_SHEET_ID}/pdf" \
|
||||
-H "Authorization: Bearer $INTAKE_TOKEN")"
|
||||
content_type="$(awk -F': ' 'tolower($1)=="content-type"{print $2}' "$headers_file" | tr -d '\r' | head -1)"
|
||||
rm -f "$headers_file"
|
||||
|
||||
if [[ "$pdf_code" == "200" ]]; then
|
||||
pass "POST /cover-sheets/{id}/pdf returns 200"
|
||||
else
|
||||
fail "POST /cover-sheets/{id}/pdf returns 200 (got HTTP $pdf_code)"
|
||||
fi
|
||||
|
||||
if [[ "$content_type" == application/pdf* ]]; then
|
||||
pass "single PDF response Content-Type is application/pdf"
|
||||
else
|
||||
fail "single PDF response Content-Type is application/pdf (got ${content_type:-<none>})"
|
||||
fi
|
||||
|
||||
pdf_header="$(head -c 8 "$pdf_tmp" || true)"
|
||||
if [[ "$pdf_header" == %PDF-1.* ]]; then
|
||||
pass "single PDF body starts with %PDF header"
|
||||
else
|
||||
fail "single PDF body starts with %PDF header"
|
||||
fi
|
||||
|
||||
if grep -aq "$UPLOAD_CODE" "$pdf_tmp" 2>/dev/null; then
|
||||
pass "single PDF embeds cover sheet code text"
|
||||
else
|
||||
fail "single PDF embeds cover sheet code text"
|
||||
fi
|
||||
rm -f "$pdf_tmp"
|
||||
|
||||
local batch_pdf_tmp batch_pdf_code
|
||||
batch_pdf_tmp="$(mktemp)"
|
||||
batch_pdf_code="$(curl -sS -o "$batch_pdf_tmp" -w '%{http_code}' -X POST \
|
||||
"$API_URL/api/v1/cover-sheets/batch-pdf" \
|
||||
-H "Authorization: Bearer $INTAKE_TOKEN" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d "{\"coverSheetIds\":[\"${PDF_SHEET_IDS[0]}\",\"${PDF_SHEET_IDS[1]}\"]}")"
|
||||
|
||||
if [[ "$batch_pdf_code" == "200" ]]; then
|
||||
pass "POST /cover-sheets/batch-pdf returns 200"
|
||||
else
|
||||
fail "POST /cover-sheets/batch-pdf returns 200 (got HTTP $batch_pdf_code)"
|
||||
fi
|
||||
|
||||
if grep -aq '/Count 2' "$batch_pdf_tmp" 2>/dev/null || strings "$batch_pdf_tmp" | grep -q '/Count 2'; then
|
||||
pass "batch PDF contains two pages (/Count 2)"
|
||||
else
|
||||
fail "batch PDF contains two pages (/Count 2)"
|
||||
fi
|
||||
rm -f "$batch_pdf_tmp"
|
||||
}
|
||||
|
||||
test_barcode_assisted_upload() {
|
||||
section "5. Barcode-assisted upload (plan §3)"
|
||||
|
||||
if [[ -z "$UPLOAD_CODE" ]]; then
|
||||
fail "barcode upload requires generated cover sheet code"
|
||||
return
|
||||
fi
|
||||
|
||||
local pdf upload_json batch_type track status entered_by
|
||||
pdf="$(unique_pdf_path upload)"
|
||||
upload_json="$(upload_with_cover_sheet "$INTAKE_TOKEN" "$UPLOAD_CODE" "$pdf")"
|
||||
rm -f "$pdf"
|
||||
|
||||
if [[ "$(jq -er '.success' <<<"$upload_json")" == "true" ]]; then
|
||||
pass "POST /digitization-batches with coverSheetCode creates batch"
|
||||
else
|
||||
fail "POST /digitization-batches with coverSheetCode creates batch"
|
||||
log " response: $upload_json"
|
||||
return
|
||||
fi
|
||||
|
||||
batch_type="$(extract_data_field "$upload_json" batchType)"
|
||||
track="$(extract_data_field "$upload_json" track)"
|
||||
status="$(extract_data_field "$upload_json" status)"
|
||||
UPLOAD_BATCH_ID="$(extract_data_field "$upload_json" id)"
|
||||
|
||||
if [[ "$batch_type" == "VITALS_SHEET" && "$track" == "BACKFILL" ]]; then
|
||||
pass "batch inherits batchType and track from cover sheet"
|
||||
else
|
||||
fail "batch inherits batchType and track from cover sheet (type=$batch_type track=$track)"
|
||||
fi
|
||||
|
||||
local redeemed_json redeemed_used redeemed_batch_id
|
||||
redeemed_json="$(json_get "$API_URL/api/v1/cover-sheets/lookup/$UPLOAD_CODE" "$INTAKE_TOKEN")"
|
||||
redeemed_used="$(extract_bool_field "$redeemed_json" isUsed)"
|
||||
redeemed_batch_id="$(extract_data_field "$redeemed_json" batchId)"
|
||||
|
||||
if [[ "$redeemed_used" == "true" && "$redeemed_batch_id" == "$UPLOAD_BATCH_ID" ]]; then
|
||||
pass "cover sheet redeemed and linked to batch after upload"
|
||||
else
|
||||
fail "cover sheet redeemed and linked to batch after upload"
|
||||
fi
|
||||
|
||||
if psql_available && [[ -n "$UPLOAD_COVER_SHEET_ID" ]]; then
|
||||
local db_used db_batch
|
||||
db_used="$(psql_query "
|
||||
SELECT is_used FROM cover_sheets WHERE id = '$UPLOAD_COVER_SHEET_ID';
|
||||
")"
|
||||
db_batch="$(psql_query "
|
||||
SELECT batch_id FROM cover_sheets WHERE id = '$UPLOAD_COVER_SHEET_ID';
|
||||
")"
|
||||
if [[ "$db_used" == "t" && "$db_batch" == "$UPLOAD_BATCH_ID" ]]; then
|
||||
pass "database row shows is_used=true with batch_id"
|
||||
else
|
||||
fail "database row shows is_used=true with batch_id"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
test_cover_sheet_reuse_and_unknown() {
|
||||
section "6. Reuse prevention and unknown code (plan §4)"
|
||||
|
||||
if [[ -z "$UPLOAD_CODE" ]]; then
|
||||
fail "reuse test requires uploaded cover sheet code"
|
||||
return
|
||||
fi
|
||||
|
||||
local pdf reuse_response reuse_status reuse_code unknown_response unknown_status unknown_code
|
||||
pdf="$(unique_pdf_path reuse)"
|
||||
reuse_response="$(upload_with_cover_sheet_status "$INTAKE_TOKEN" "$UPLOAD_CODE" "$pdf")"
|
||||
rm -f "$pdf"
|
||||
|
||||
reuse_status="${reuse_response##*__HTTP_STATUS__:}"
|
||||
reuse_response="${reuse_response%$'\n'__HTTP_STATUS__:*}"
|
||||
reuse_code="$(extract_error_code "$reuse_response")"
|
||||
|
||||
if [[ "$reuse_status" == "409" && "$reuse_code" == "COVER_SHEET_ALREADY_USED" ]]; then
|
||||
pass "reused cover sheet code returns 409 COVER_SHEET_ALREADY_USED"
|
||||
else
|
||||
fail "reused cover sheet code returns 409 COVER_SHEET_ALREADY_USED (HTTP $reuse_status code=${reuse_code:-<none>})"
|
||||
fi
|
||||
|
||||
pdf="$(unique_pdf_path unknown)"
|
||||
unknown_response="$(upload_with_cover_sheet_status "$INTAKE_TOKEN" "VCR-CS-DEADBEEF" "$pdf")"
|
||||
rm -f "$pdf"
|
||||
|
||||
unknown_status="${unknown_response##*__HTTP_STATUS__:}"
|
||||
unknown_response="${unknown_response%$'\n'__HTTP_STATUS__:*}"
|
||||
unknown_code="$(extract_error_code "$unknown_response")"
|
||||
|
||||
if [[ "$unknown_status" == "404" && "$unknown_code" == "COVER_SHEET_NOT_FOUND" ]]; then
|
||||
pass "unknown cover sheet code on upload returns 404 COVER_SHEET_NOT_FOUND"
|
||||
else
|
||||
fail "unknown cover sheet code on upload returns 404 COVER_SHEET_NOT_FOUND (HTTP $unknown_status code=${unknown_code:-<none>})"
|
||||
fi
|
||||
}
|
||||
|
||||
test_auto_assign_cover_sheet() {
|
||||
section "7. Pre-assigned cover sheet auto-assigns batch"
|
||||
|
||||
if [[ -z "$ENTRY_CLERK1_ID" ]]; then
|
||||
fail "auto-assign test skipped — no entry clerk ID"
|
||||
return
|
||||
fi
|
||||
|
||||
local gen_json assign_code pdf upload_json status entered_by
|
||||
gen_json="$(generate_cover_sheets "$INTAKE_TOKEN" \
|
||||
"{\"count\":1,\"batchType\":\"LAB_RESULTS\",\"track\":\"BACKFILL\",\"assignToUserId\":\"$ENTRY_CLERK1_ID\"}")"
|
||||
|
||||
if [[ "$(jq -er '.success' <<<"$gen_json")" != "true" ]]; then
|
||||
fail "generate pre-assigned cover sheet"
|
||||
return
|
||||
fi
|
||||
|
||||
assign_code="$(jq -r '.data[0].code' <<<"$gen_json")"
|
||||
pdf="$(unique_pdf_path assign)"
|
||||
upload_json="$(upload_with_cover_sheet "$INTAKE_TOKEN" "$assign_code" "$pdf")"
|
||||
rm -f "$pdf"
|
||||
|
||||
status="$(extract_data_field "$upload_json" status)"
|
||||
entered_by="$(extract_data_field "$upload_json" enteredByUserId)"
|
||||
|
||||
if [[ "$status" == "IN_ENTRY" ]]; then
|
||||
pass "pre-assigned upload transitions batch to IN_ENTRY"
|
||||
else
|
||||
fail "pre-assigned upload transitions batch to IN_ENTRY (status=$status)"
|
||||
fi
|
||||
|
||||
if [[ "$entered_by" == "$ENTRY_CLERK1_ID" ]]; then
|
||||
pass "pre-assigned upload sets enteredByUserId to entry clerk"
|
||||
else
|
||||
fail "pre-assigned upload sets enteredByUserId to entry clerk (got $entered_by)"
|
||||
fi
|
||||
}
|
||||
|
||||
test_patient_linked_cover_sheet() {
|
||||
section "8. Patient-linked cover sheet populates batch patient"
|
||||
|
||||
if [[ -z "$PATIENT1_ID" ]]; then
|
||||
fail "patient-linked test skipped — no patient ID"
|
||||
return
|
||||
fi
|
||||
|
||||
local gen_json patient_code pdf upload_json patient_id
|
||||
gen_json="$(generate_cover_sheets "$INTAKE_TOKEN" \
|
||||
"{\"count\":1,\"batchType\":\"MEDICATION_LIST\",\"track\":\"BACKFILL\",\"patientId\":\"$PATIENT1_ID\"}")"
|
||||
|
||||
if [[ "$(jq -er '.success' <<<"$gen_json")" != "true" ]]; then
|
||||
fail "generate patient-linked cover sheet"
|
||||
return
|
||||
fi
|
||||
|
||||
patient_code="$(jq -r '.data[0].code' <<<"$gen_json")"
|
||||
pdf="$(unique_pdf_path patient)"
|
||||
upload_json="$(upload_with_cover_sheet "$INTAKE_TOKEN" "$patient_code" "$pdf")"
|
||||
rm -f "$pdf"
|
||||
|
||||
patient_id="$(extract_data_field "$upload_json" patientId)"
|
||||
if [[ "$patient_id" == "$PATIENT1_ID" ]]; then
|
||||
pass "patient-linked cover sheet sets batch patientId"
|
||||
else
|
||||
fail "patient-linked cover sheet sets batch patientId (got $patient_id)"
|
||||
fi
|
||||
}
|
||||
|
||||
test_integration_tests() {
|
||||
section "9. dotnet integration tests — CoverSheetBatchTests, CoverSheetPdfTests"
|
||||
|
||||
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~CoverSheet" \
|
||||
--no-restore >/tmp/vigilcare-p10-tests.log 2>&1; then
|
||||
pass "CoverSheet integration tests passed"
|
||||
else
|
||||
fail "CoverSheet integration tests passed"
|
||||
log " see /tmp/vigilcare-p10-tests.log"
|
||||
fi
|
||||
}
|
||||
|
||||
print_manual_ui_checklist() {
|
||||
section "10. Manual Vue UI checks (plan §5)"
|
||||
log " Login as intake1 → /cover-sheets"
|
||||
log " - Generate 5 VITALS_SHEET covers"
|
||||
log " - Print Cover Sheets opens PDF in new tab"
|
||||
log " - Cover sheet list shows unused sheets"
|
||||
log " Navigate to /intake"
|
||||
log " - Scan/type a cover sheet code (Enter triggers lookup)"
|
||||
log " - Lookup auto-fills batch type, track, patient"
|
||||
log " - Upload with Cover Sheet marks sheet as Used with linked batch ID"
|
||||
}
|
||||
|
||||
main() {
|
||||
require_cmd curl
|
||||
require_cmd jq
|
||||
ensure_fixture_pdf
|
||||
|
||||
log "VigilCare Records — Phase 10 verification"
|
||||
log "API: $API_URL"
|
||||
|
||||
assert_api_reachable
|
||||
|
||||
test_authentication
|
||||
test_database_schema
|
||||
test_cover_sheet_generation
|
||||
test_cover_sheet_lookup_and_list
|
||||
test_pdf_generation
|
||||
test_barcode_assisted_upload
|
||||
test_cover_sheet_reuse_and_unknown
|
||||
test_auto_assign_cover_sheet
|
||||
test_patient_linked_cover_sheet
|
||||
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 10 API verification checks passed."
|
||||
log "Complete the manual Vue UI checklist above if not already done."
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Reference in New Issue
Block a user