#!/usr/bin/env bash set -euo pipefail # VigilCare Records — End-to-End Verification Script # Tests the full digitization workflow: upload → entry → verify → approve → promote # # Prerequisites: # - API running on http://localhost:5217 # - docker compose up -d (PostgreSQL, Redis, MinIO, Seq) # - Seed data applied (dotnet run applies seed on startup) # - A test PDF file at ./scripts/test-scan.pdf (create with: echo "test" | enscript -o - | ps2pdf - scripts/test-scan.pdf) # # Usage: # chmod +x scripts/run-vigilcare-records-verification.sh # ./scripts/run-vigilcare-records-verification.sh API="http://localhost:5217/api/v1" PASS=0 FAIL=0 TEST_FILE="${1:-scripts/test-scan.pdf}" # Colors for terminal output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' NC='\033[0m' # No Color log_pass() { PASS=$((PASS + 1)); echo -e "${GREEN} PASS${NC} $1"; } log_fail() { FAIL=$((FAIL + 1)); echo -e "${RED} FAIL${NC} $1"; } log_step() { echo -e "\n${YELLOW}── $1 ──${NC}"; } # Create a minimal test PDF if it doesn't exist if [ ! -f "$TEST_FILE" ]; then echo "Creating test PDF at $TEST_FILE..." mkdir -p "$(dirname "$TEST_FILE")" echo "%PDF-1.0 1 0 obj<>endobj 2 0 obj<>endobj 3 0 obj<>>>endobj xref 0 4 0000000000 65535 f 0000000009 00000 n 0000000058 00000 n 0000000115 00000 n trailer<> startxref 206 %%EOF" > "$TEST_FILE" fi # ============================================================================ # Phase 1: Authentication # ============================================================================ log_step "1. Authentication" # Login as intake clerk INTAKE_LOGIN=$(curl -s -X POST "$API/auth/login" \ -H "Content-Type: application/json" \ -d '{"username":"intake1","password":"password"}') INTAKE_TOKEN=$(echo "$INTAKE_LOGIN" | jq -r '.data.token') INTAKE_REFRESH=$(echo "$INTAKE_LOGIN" | jq -r '.data.refreshToken') if [ "$INTAKE_TOKEN" != "null" ] && [ -n "$INTAKE_TOKEN" ]; then log_pass "Intake clerk login returned JWT" else log_fail "Intake clerk login failed" exit 1 fi if [ "$INTAKE_REFRESH" != "null" ] && [ -n "$INTAKE_REFRESH" ]; then log_pass "Intake clerk login returned refresh token" else log_fail "Intake clerk login missing refresh token" fi # Login as entry clerk ENTRY_TOKEN=$(curl -s -X POST "$API/auth/login" \ -H "Content-Type: application/json" \ -d '{"username":"entry1","password":"password"}' | jq -r '.data.token') if [ "$ENTRY_TOKEN" != "null" ] && [ -n "$ENTRY_TOKEN" ]; then log_pass "Entry clerk login returned JWT" else log_fail "Entry clerk login failed" fi # Login as verifier (must be different from entry clerk for separation of duties) VERIFIER_TOKEN=$(curl -s -X POST "$API/auth/login" \ -H "Content-Type: application/json" \ -d '{"username":"verifier1","password":"password"}' | jq -r '.data.token') if [ "$VERIFIER_TOKEN" != "null" ] && [ -n "$VERIFIER_TOKEN" ]; then log_pass "Verifier login returned JWT" else log_fail "Verifier login failed" fi # Login as approver APPROVER_TOKEN=$(curl -s -X POST "$API/auth/login" \ -H "Content-Type: application/json" \ -d '{"username":"approver1","password":"password"}' | jq -r '.data.token') if [ "$APPROVER_TOKEN" != "null" ] && [ -n "$APPROVER_TOKEN" ]; then log_pass "Approver login returned JWT" else log_fail "Approver login failed" fi # Verify /me endpoint ME_ROLE=$(curl -s "$API/auth/me" \ -H "Authorization: Bearer $INTAKE_TOKEN" | jq -r '.data.role') if [ "$ME_ROLE" = "INTAKE_CLERK" ]; then log_pass "/me returns correct role: INTAKE_CLERK" else log_fail "/me returned unexpected role: $ME_ROLE" fi # Verify 401 without token HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "$API/auth/me") if [ "$HTTP_CODE" = "401" ]; then log_pass "Unauthenticated /me returns 401" else log_fail "Unauthenticated /me returned $HTTP_CODE (expected 401)" fi # Refresh token rotation REFRESH_RESPONSE=$(curl -s -X POST "$API/auth/refresh" \ -H "Content-Type: application/json" \ -d "{\"refreshToken\":\"$INTAKE_REFRESH\"}") NEW_TOKEN=$(echo "$REFRESH_RESPONSE" | jq -r '.data.token') NEW_REFRESH=$(echo "$REFRESH_RESPONSE" | jq -r '.data.refreshToken') if [ "$NEW_TOKEN" != "null" ] && [ -n "$NEW_TOKEN" ] && [ "$NEW_REFRESH" != "$INTAKE_REFRESH" ]; then log_pass "Refresh rotated tokens successfully" INTAKE_TOKEN="$NEW_TOKEN" INTAKE_REFRESH="$NEW_REFRESH" else log_fail "Token refresh failed: $REFRESH_RESPONSE" fi # Logout revokes refresh token LOGOUT_CODE=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$API/auth/logout" \ -H "Content-Type: application/json" \ -d "{\"refreshToken\":\"$INTAKE_REFRESH\"}") if [ "$LOGOUT_CODE" = "204" ]; then log_pass "Logout revoked refresh token (204)" else log_fail "Logout returned $LOGOUT_CODE (expected 204)" fi # Re-login for subsequent test phases INTAKE_LOGIN=$(curl -s -X POST "$API/auth/login" \ -H "Content-Type: application/json" \ -d '{"username":"intake1","password":"password"}') INTAKE_TOKEN=$(echo "$INTAKE_LOGIN" | jq -r '.data.token') # ============================================================================ # Phase 2: Upload and Batch Creation # ============================================================================ log_step "2. Upload and Batch Creation" UPLOAD_RESPONSE=$(curl -s -X POST "$API/digitization-batches" \ -H "Authorization: Bearer $INTAKE_TOKEN" \ -F "file=@$TEST_FILE" \ -F "batchType=VITALS_SHEET" \ -F "track=BACKFILL") BATCH_ID=$(echo "$UPLOAD_RESPONSE" | jq -r '.data.id') BATCH_STATUS=$(echo "$UPLOAD_RESPONSE" | jq -r '.data.status') if [ "$BATCH_ID" != "null" ] && [ -n "$BATCH_ID" ]; then log_pass "Batch created: $BATCH_ID" else log_fail "Batch creation failed: $UPLOAD_RESPONSE" exit 1 fi if [ "$BATCH_STATUS" = "UPLOADED" ]; then log_pass "Initial status is UPLOADED" else log_fail "Initial status is $BATCH_STATUS (expected UPLOADED)" fi # Verify presigned URL DOC_URL=$(curl -s "$API/digitization-batches/$BATCH_ID" \ -H "Authorization: Bearer $INTAKE_TOKEN" | jq -r '.data.documentUrl') if [ "$DOC_URL" != "null" ] && [ -n "$DOC_URL" ]; then log_pass "Presigned URL returned for document" else log_fail "No presigned URL returned" fi # ============================================================================ # Phase 3: Assign and Begin Entry # ============================================================================ log_step "3. Assign and Begin Entry" # Get entry clerk user ID ENTRY_USER_ID=$(curl -s "$API/auth/me" \ -H "Authorization: Bearer $ENTRY_TOKEN" | jq -r '.data.id') # Assign batch to entry clerk ASSIGN_RESPONSE=$(curl -s -X PATCH "$API/digitization-batches/$BATCH_ID/assign" \ -H "Authorization: Bearer $INTAKE_TOKEN" \ -H "Content-Type: application/json" \ -d "{\"entryClerkUserId\":\"$ENTRY_USER_ID\"}") ASSIGN_SUCCESS=$(echo "$ASSIGN_RESPONSE" | jq -r '.success') if [ "$ASSIGN_SUCCESS" = "true" ]; then log_pass "Batch assigned to entry clerk" else log_fail "Batch assignment failed: $(echo "$ASSIGN_RESPONSE" | jq -r '.error.message')" fi # ============================================================================ # Phase 4: Draft Data Entry # ============================================================================ log_step "4. Draft Data Entry" # Save draft patient PATIENT_RESPONSE=$(curl -s -X PUT "$API/digitization-batches/$BATCH_ID/draft/patient" \ -H "Authorization: Bearer $ENTRY_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "fullName": "E2E Test Patient", "dateOfBirth": "1985-06-15", "sex": "male", "bloodType": "A+", "emergencyContact": "Test Contact - 555-9999", "noKnownAllergies": true }') if [ "$(echo "$PATIENT_RESPONSE" | jq -r '.success')" = "true" ]; then log_pass "Draft patient saved" else log_fail "Draft patient save failed: $(echo "$PATIENT_RESPONSE" | jq -r '.error.message')" fi # Save draft encounter ENCOUNTER_RESPONSE=$(curl -s -X PUT "$API/digitization-batches/$BATCH_ID/draft/encounter" \ -H "Authorization: Bearer $ENTRY_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "admissionDate": "2026-06-20T08:00:00Z", "department": "Emergency Department", "roomBed": "ED-12", "admissionReason": "Chest pain, rule out MI" }') if [ "$(echo "$ENCOUNTER_RESPONSE" | jq -r '.success')" = "true" ]; then log_pass "Draft encounter saved" else log_fail "Draft encounter save failed" fi # Add observations for OBS in \ '{"observationCode":"HEART_RATE","value":95,"unit":"bpm","recordedAt":"2026-06-20T08:15:00Z"}' \ '{"observationCode":"TEMP_C","value":37.1,"unit":"C","recordedAt":"2026-06-20T08:15:00Z"}' \ '{"observationCode":"BP_SYSTOLIC","value":142,"unit":"mmHg","recordedAt":"2026-06-20T08:15:00Z"}' \ '{"observationCode":"BP_DIASTOLIC","value":88,"unit":"mmHg","recordedAt":"2026-06-20T08:15:00Z"}' \ '{"observationCode":"RESP_RATE","value":20,"unit":"breaths/min","recordedAt":"2026-06-20T08:15:00Z"}' \ '{"observationCode":"SPO2","value":97,"unit":"%","recordedAt":"2026-06-20T08:15:00Z"}' do OBS_RESULT=$(curl -s -X POST "$API/digitization-batches/$BATCH_ID/draft/observations" \ -H "Authorization: Bearer $ENTRY_TOKEN" \ -H "Content-Type: application/json" \ -d "$OBS") OBS_CODE=$(echo "$OBS" | jq -r '.observationCode') if [ "$(echo "$OBS_RESULT" | jq -r '.success')" = "true" ]; then log_pass "Observation $OBS_CODE added" else log_fail "Observation $OBS_CODE failed: $(echo "$OBS_RESULT" | jq -r '.error.message')" fi done # Verify draft is complete DRAFT_RESPONSE=$(curl -s "$API/digitization-batches/$BATCH_ID/draft" \ -H "Authorization: Bearer $ENTRY_TOKEN") OBS_COUNT=$(echo "$DRAFT_RESPONSE" | jq -r '.data.observations | length') if [ "$OBS_COUNT" = "6" ]; then log_pass "Draft has 6 observations" else log_fail "Draft has $OBS_COUNT observations (expected 6)" fi # Submit for verification SUBMIT_RESPONSE=$(curl -s -X POST "$API/digitization-batches/$BATCH_ID/submit-for-verification" \ -H "Authorization: Bearer $ENTRY_TOKEN") if [ "$(echo "$SUBMIT_RESPONSE" | jq -r '.success')" = "true" ]; then log_pass "Submitted for verification" else log_fail "Submit failed: $(echo "$SUBMIT_RESPONSE" | jq -r '.error.message')" fi # Verify status is now PENDING_VERIFICATION CURRENT_STATUS=$(curl -s "$API/digitization-batches/$BATCH_ID" \ -H "Authorization: Bearer $ENTRY_TOKEN" | jq -r '.data.batch.status') if [ "$CURRENT_STATUS" = "PENDING_VERIFICATION" ]; then log_pass "Status is PENDING_VERIFICATION" else log_fail "Status is $CURRENT_STATUS (expected PENDING_VERIFICATION)" fi # ============================================================================ # Phase 5: Separation of Duties Check # ============================================================================ log_step "5. Separation of Duties" # Entry clerk tries to verify their own batch — should get 409 SOD_RESPONSE=$(curl -s -X POST "$API/digitization-batches/$BATCH_ID/verify" \ -H "Authorization: Bearer $ENTRY_TOKEN" \ -H "Content-Type: application/json" \ -d '{"fieldChecks":[],"passed":true}') SOD_CODE=$(echo "$SOD_RESPONSE" | jq -r '.error.code') if [ "$SOD_CODE" = "SEPARATION_OF_DUTIES_VIOLATION" ]; then log_pass "Entry clerk cannot verify own batch (409 SEPARATION_OF_DUTIES_VIOLATION)" else log_fail "Separation of duties not enforced: $SOD_CODE" fi # ============================================================================ # Phase 6: Verification # ============================================================================ log_step "6. Verification (by different user)" VERIFY_RESPONSE=$(curl -s -X POST "$API/digitization-batches/$BATCH_ID/verify" \ -H "Authorization: Bearer $VERIFIER_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "fieldChecks": [ {"fieldPath": "patient.fullName", "passed": true}, {"fieldPath": "patient.dateOfBirth", "passed": true}, {"fieldPath": "observations[0].value", "passed": true}, {"fieldPath": "observations[1].value", "passed": true}, {"fieldPath": "observations[2].value", "passed": true}, {"fieldPath": "observations[3].value", "passed": true}, {"fieldPath": "observations[4].value", "passed": true}, {"fieldPath": "observations[5].value", "passed": true} ], "passed": true }') if [ "$(echo "$VERIFY_RESPONSE" | jq -r '.success')" = "true" ]; then log_pass "Verification passed" else log_fail "Verification failed: $(echo "$VERIFY_RESPONSE" | jq -r '.error.message')" fi # Check status VERIFIED_STATUS=$(curl -s "$API/digitization-batches/$BATCH_ID" \ -H "Authorization: Bearer $VERIFIER_TOKEN" | jq -r '.data.batch.status') if [ "$VERIFIED_STATUS" = "VERIFIED" ] || [ "$VERIFIED_STATUS" = "AWAITING_CLINICAL_APPROVAL" ]; then log_pass "Status after verification: $VERIFIED_STATUS" else log_fail "Status after verification: $VERIFIED_STATUS (expected VERIFIED or AWAITING_CLINICAL_APPROVAL)" fi # ============================================================================ # Phase 7: Approval and Promotion # ============================================================================ log_step "7. Approval and Promotion" APPROVE_RESPONSE=$(curl -s -X POST "$API/digitization-batches/$BATCH_ID/approve" \ -H "Authorization: Bearer $APPROVER_TOKEN" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: e2e-test-$(date +%s)") if [ "$(echo "$APPROVE_RESPONSE" | jq -r '.success')" = "true" ]; then log_pass "Batch approved and promoted" else log_fail "Approval failed: $(echo "$APPROVE_RESPONSE" | jq -r '.error.message')" fi # Verify final status is PROMOTED FINAL_STATUS=$(curl -s "$API/digitization-batches/$BATCH_ID" \ -H "Authorization: Bearer $APPROVER_TOKEN" | jq -r '.data.batch.status') if [ "$FINAL_STATUS" = "PROMOTED" ]; then log_pass "Final status is PROMOTED" else log_fail "Final status is $FINAL_STATUS (expected PROMOTED)" fi # Verify promotedAt is set PROMOTED_AT=$(curl -s "$API/digitization-batches/$BATCH_ID" \ -H "Authorization: Bearer $APPROVER_TOKEN" | jq -r '.data.batch.promotedAt') if [ "$PROMOTED_AT" != "null" ] && [ -n "$PROMOTED_AT" ]; then log_pass "promotedAt timestamp set: $PROMOTED_AT" else log_fail "promotedAt is null" fi # ============================================================================ # Phase 8: Illegal Transition Check # ============================================================================ log_step "8. Illegal Transitions" # Try to re-approve a promoted batch — should get 409 ILLEGAL_RESPONSE=$(curl -s -X POST "$API/digitization-batches/$BATCH_ID/approve" \ -H "Authorization: Bearer $APPROVER_TOKEN" \ -H "Content-Type: application/json") ILLEGAL_CODE=$(echo "$ILLEGAL_RESPONSE" | jq -r '.error.code') if [ "$ILLEGAL_CODE" = "ILLEGAL_STATUS_TRANSITION" ]; then log_pass "Cannot approve promoted batch (409 ILLEGAL_STATUS_TRANSITION)" else log_fail "Illegal transition not blocked: $ILLEGAL_CODE" fi # ============================================================================ # Phase 9: Work Queue Overview # ============================================================================ log_step "9. Work Queue Overview" OVERVIEW_RESPONSE=$(curl -s "$API/work-queue/overview" \ -H "Authorization: Bearer $APPROVER_TOKEN") OVERVIEW_SUCCESS=$(echo "$OVERVIEW_RESPONSE" | jq -r '.success') if [ "$OVERVIEW_SUCCESS" = "true" ]; then log_pass "Work queue overview returned successfully" echo " Status counts: $(echo "$OVERVIEW_RESPONSE" | jq -c '.data.statusCounts')" echo " Reject rate: $(echo "$OVERVIEW_RESPONSE" | jq -r '.data.rejectRate')" else log_fail "Work queue overview failed" fi # ============================================================================ # Phase 10: Metrics Endpoint # ============================================================================ log_step "10. Prometheus Metrics" METRICS=$(curl -s http://localhost:5217/metrics) if echo "$METRICS" | grep -q "digitization_batches_by_status"; then log_pass "digitization_batches_by_status metric present" else log_fail "digitization_batches_by_status metric missing" fi if echo "$METRICS" | grep -q "digitization_promotion_duration_seconds"; then log_pass "digitization_promotion_duration_seconds metric present" else log_fail "digitization_promotion_duration_seconds metric missing" fi if echo "$METRICS" | grep -q "digitization_rejection_total"; then log_pass "digitization_rejection_total metric present" else log_fail "digitization_rejection_total metric missing" fi # ============================================================================ # Summary # ============================================================================ echo "" echo "============================================" echo " E2E Verification Complete" echo "============================================" echo -e " ${GREEN}Passed: $PASS${NC}" echo -e " ${RED}Failed: $FAIL${NC}" echo "" if [ "$FAIL" -eq 0 ]; then echo -e " ${GREEN}ALL TESTS PASSED${NC}" exit 0 else echo -e " ${RED}$FAIL TEST(S) FAILED${NC}" exit 1 fi