Files
NetForge/scripts/uat-test.sh
kevin-asprec dcffed4a0d feat: add Docker deployment, UAT test suites, and fix signup middleware bug
- Add Dockerfile, docker-entrypoint.sh, and .dockerignore for containerized deployment
- Fix middleware to exclude /api/tenants/signup from auth (P0 signup bug)
- Add Playwright E2E tests (16 browser tests) and curl-based API test script (80 tests)
- Add playwright config and dev dependency
- Update .gitignore with proper exclusions
- Add v1 milestone audit report and ISP system PRD

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 08:20:54 +08:00

1244 lines
37 KiB
Bash

#!/bin/bash
# =============================================================================
# NetForge v1.0 — Comprehensive UAT Test Script
# =============================================================================
# Tests all API endpoints and critical workflows end-to-end via curl.
# Requires: Docker containers running (docker compose up)
#
# Usage: bash scripts/uat-test.sh
# =============================================================================
set -uo pipefail
BASE_URL="http://localhost:3000"
COOKIE_JAR="/tmp/nf_uat_cookies.txt"
COOKIE_JAR_SUPER="/tmp/nf_uat_cookies_super.txt"
COOKIE_JAR_TENANT2="/tmp/nf_uat_cookies_t2.txt"
PASS=0
FAIL=0
SKIP=0
FAILURES=""
# Unique suffix per run to avoid duplicate-name errors
RUN_ID=$(date +%s | tail -c 6)
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
BOLD='\033[1m'
NC='\033[0m' # No Color
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
log_section() {
echo ""
echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo -e "${BOLD} $1${NC}"
echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
}
log_test() {
echo -ne " ${BOLD}TEST:${NC} $1 ... "
}
pass() {
echo -e "${GREEN}PASS${NC}"
PASS=$((PASS + 1))
}
fail() {
echo -e "${RED}FAIL${NC}$1"
FAIL=$((FAIL + 1))
FAILURES="${FAILURES}\n - $2: $1"
}
skip() {
echo -e "${YELLOW}SKIP${NC}$1"
SKIP=$((SKIP + 1))
}
# Make an authenticated API call and return the body
api_get() {
local url="$1"
local jar="${2:-$COOKIE_JAR}"
curl -s -b "$jar" "${BASE_URL}${url}"
}
api_post() {
local url="$1"
local data="$2"
local jar="${3:-$COOKIE_JAR}"
curl -s -b "$jar" -X POST "${BASE_URL}${url}" \
-H "Content-Type: application/json" \
-d "$data"
}
api_put() {
local url="$1"
local data="$2"
local jar="${3:-$COOKIE_JAR}"
curl -s -b "$jar" -X PUT "${BASE_URL}${url}" \
-H "Content-Type: application/json" \
-d "$data"
}
api_delete() {
local url="$1"
local jar="${2:-$COOKIE_JAR}"
curl -s -b "$jar" -X DELETE "${BASE_URL}${url}"
}
# Get HTTP status code only
api_status() {
local method="${1}"
local url="${2}"
local jar="${3:-$COOKIE_JAR}"
curl -s -o /dev/null -w "%{http_code}" -b "$jar" -X "$method" "${BASE_URL}${url}"
}
# Extract JSON field (simple jq-like with grep)
json_field() {
echo "$1" | grep -o "\"$2\":[^,}]*" | head -1 | sed "s/\"$2\"://" | tr -d '"' | tr -d ' '
}
# Extract JSON string field
json_str() {
echo "$1" | grep -o "\"$2\":\"[^\"]*\"" | head -1 | sed "s/\"$2\":\"//" | tr -d '"'
}
login() {
local email="$1"
local password="$2"
local jar="$3"
rm -f "$jar"
local csrf=$(curl -s -c "$jar" "${BASE_URL}/api/auth/csrf" | grep -o '"csrfToken":"[^"]*"' | cut -d'"' -f4)
curl -s -b "$jar" -c "$jar" -X POST "${BASE_URL}/api/auth/callback/credentials" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "email=${email}&password=${password}&csrfToken=${csrf}" \
-o /dev/null --max-redirs 5 -L 2>/dev/null || true
}
# =============================================================================
# PHASE 0: Connectivity Check
# =============================================================================
log_section "PHASE 0: Connectivity"
log_test "App is reachable at ${BASE_URL}"
STATUS=$(curl -s -o /dev/null -w "%{http_code}" "${BASE_URL}/login" 2>/dev/null || echo "000")
if [ "$STATUS" = "200" ]; then
pass
else
fail "HTTP $STATUS — is Docker running?" "connectivity"
echo -e "\n${RED}Cannot reach the app. Run 'docker compose up -d' first.${NC}"
exit 1
fi
# =============================================================================
# PHASE 1: Authentication & Authorization
# =============================================================================
log_section "PHASE 1: Authentication & Authorization"
# --- Login as Admin ---
log_test "Admin login (admin@demo.com)"
login "admin@demo.com" "admin123" "$COOKIE_JAR"
SESSION=$(api_get "/api/auth/session")
USER_EMAIL=$(json_str "$SESSION" "email")
if [ "$USER_EMAIL" = "admin@demo.com" ]; then
pass
else
fail "Session email='$USER_EMAIL'" "admin-login"
fi
# --- Login as Super-admin ---
log_test "Super-admin login (superadmin@netforge.com)"
login "superadmin@netforge.com" "super123" "$COOKIE_JAR_SUPER"
SESSION=$(api_get "/api/auth/session" "$COOKIE_JAR_SUPER")
SA_EMAIL=$(json_str "$SESSION" "email")
if [ "$SA_EMAIL" = "superadmin@netforge.com" ]; then
pass
else
fail "Session email='$SA_EMAIL'" "superadmin-login"
fi
# --- Login as Tenant 2 Admin ---
log_test "Tenant 2 admin login (admin2@demo.com)"
login "admin2@demo.com" "admin123" "$COOKIE_JAR_TENANT2"
SESSION=$(api_get "/api/auth/session" "$COOKIE_JAR_TENANT2")
T2_EMAIL=$(json_str "$SESSION" "email")
if [ "$T2_EMAIL" = "admin2@demo.com" ]; then
pass
else
fail "Session email='$T2_EMAIL'" "tenant2-login"
fi
# --- Invalid login ---
log_test "Invalid login returns error"
rm -f /tmp/nf_bad_cookies.txt
CSRF=$(curl -s -c /tmp/nf_bad_cookies.txt "${BASE_URL}/api/auth/csrf" | grep -o '"csrfToken":"[^"]*"' | cut -d'"' -f4)
RESULT=$(curl -s -b /tmp/nf_bad_cookies.txt -c /tmp/nf_bad_cookies.txt -X POST "${BASE_URL}/api/auth/callback/credentials" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "email=admin@demo.com&password=wrongpassword&csrfToken=${CSRF}" \
-w "%{url_effective}" -L 2>/dev/null)
if echo "$RESULT" | grep -qi "error\|signin"; then
pass
else
fail "Expected error in redirect URL, got: $RESULT" "invalid-login"
fi
# --- Unauthenticated API access ---
log_test "Unauthenticated API returns 401 or 307 redirect"
STATUS=$(curl -s -o /dev/null -w "%{http_code}" "${BASE_URL}/api/subscribers")
if [ "$STATUS" = "401" ] || [ "$STATUS" = "307" ]; then
pass
else
fail "Expected 401/307, got $STATUS" "unauth-api"
fi
# --- Super-admin tenant list ---
log_test "Super-admin can list all tenants"
TENANTS=$(api_get "/api/admin/tenants" "$COOKIE_JAR_SUPER")
if echo "$TENANTS" | grep -q "Demo ISP" && echo "$TENANTS" | grep -q "Test ISP 2"; then
pass
else
fail "Missing tenants in response" "superadmin-tenants"
fi
# --- Regular admin cannot access super-admin API ---
log_test "Admin cannot access /api/admin/tenants"
STATUS=$(api_status "GET" "/api/admin/tenants")
if [ "$STATUS" = "403" ] || [ "$STATUS" = "401" ]; then
pass
else
fail "Expected 401/403, got $STATUS" "admin-superadmin-deny"
fi
# =============================================================================
# PHASE 2: Service Plans & Subscribers
# =============================================================================
log_section "PHASE 2: Service Plans & Subscribers"
# --- Create service plan ---
log_test "Create POSTPAID service plan"
PLAN_RESP=$(api_post "/api/service-plans" "{
\"name\": \"UAT Basic 25Mbps ${RUN_ID}\",
\"speed\": \"25 Mbps\",
\"monthlyPrice\": 999,
\"billingType\": \"POSTPAID\",
\"description\": \"UAT test plan\"
}")
PLAN_ID=$(json_str "$PLAN_RESP" "id")
if [ -n "$PLAN_ID" ] && [ "$PLAN_ID" != "null" ]; then
pass
else
fail "No plan ID returned: $PLAN_RESP" "create-plan"
PLAN_ID=""
fi
# --- Create PREPAID plan ---
log_test "Create PREPAID service plan"
PREPAID_RESP=$(api_post "/api/service-plans" "{
\"name\": \"UAT Prepaid 50Mbps ${RUN_ID}\",
\"speed\": \"50 Mbps\",
\"monthlyPrice\": 1499,
\"billingType\": \"PREPAID\",
\"description\": \"UAT prepaid plan\"
}")
PREPAID_PLAN_ID=$(json_str "$PREPAID_RESP" "id")
if [ -n "$PREPAID_PLAN_ID" ] && [ "$PREPAID_PLAN_ID" != "null" ]; then
pass
else
fail "No plan ID: $PREPAID_RESP" "create-prepaid-plan"
PREPAID_PLAN_ID=""
fi
# --- List service plans ---
log_test "List service plans"
PLANS=$(api_get "/api/service-plans")
if echo "$PLANS" | grep -q "UAT Basic 25Mbps\|UAT"; then
pass
else
fail "Plan not found in list: $PLANS" "list-plans"
fi
# --- Create subscriber ---
log_test "Create subscriber"
if [ -n "$PLAN_ID" ]; then
SUB_RESP=$(api_post "/api/subscribers" "{
\"firstName\": \"Juan\",
\"lastName\": \"Dela Cruz\",
\"email\": \"juan@example.com\",
\"phone\": \"+639171234567\",
\"address\": \"123 Main St, Barangay 1\",
\"servicePlanId\": \"$PLAN_ID\"
}")
SUB_ID=$(json_str "$SUB_RESP" "id")
ACCT_NUM=$(json_str "$SUB_RESP" "accountNumber")
if [ -n "$SUB_ID" ] && [ "$SUB_ID" != "null" ]; then
pass
echo " → Subscriber ID: $SUB_ID, Account: $ACCT_NUM"
else
fail "No subscriber ID: $SUB_RESP" "create-subscriber"
SUB_ID=""
fi
else
skip "No plan ID from previous step"
SUB_ID=""
fi
# --- Create second subscriber ---
log_test "Create second subscriber"
if [ -n "$PREPAID_PLAN_ID" ]; then
SUB2_RESP=$(api_post "/api/subscribers" "{
\"firstName\": \"Maria\",
\"lastName\": \"Santos\",
\"email\": \"maria@example.com\",
\"phone\": \"+639181234567\",
\"address\": \"456 Oak Ave, Barangay 2\",
\"servicePlanId\": \"$PREPAID_PLAN_ID\"
}")
SUB2_ID=$(json_str "$SUB2_RESP" "id")
if [ -n "$SUB2_ID" ] && [ "$SUB2_ID" != "null" ]; then
pass
else
fail "No subscriber ID: $SUB2_RESP" "create-subscriber-2"
SUB2_ID=""
fi
else
skip "No prepaid plan ID"
SUB2_ID=""
fi
# --- Search subscribers ---
log_test "Search subscribers by name"
SEARCH=$(api_get "/api/subscribers?search=Juan")
SEARCH_TOTAL=$(json_field "$SEARCH" "total")
if [ "$SEARCH_TOTAL" -ge 1 ] 2>/dev/null; then
pass
else
fail "Expected total=1, got $SEARCH_TOTAL" "search-subscribers"
fi
# --- Filter by status ---
log_test "Filter subscribers by ACTIVE status"
ACTIVE=$(api_get "/api/subscribers?status=ACTIVE")
ACTIVE_TOTAL=$(json_field "$ACTIVE" "total")
if [ "$ACTIVE_TOTAL" -ge 2 ] 2>/dev/null; then
pass
else
fail "Expected >=2 active, got $ACTIVE_TOTAL" "filter-active"
fi
# --- Get single subscriber ---
log_test "Get subscriber detail"
if [ -n "$SUB_ID" ]; then
DETAIL=$(api_get "/api/subscribers/$SUB_ID")
DETAIL_NAME=$(json_str "$DETAIL" "firstName")
if [ "$DETAIL_NAME" = "Juan" ]; then
pass
else
fail "Expected firstName=Juan, got $DETAIL_NAME" "get-subscriber"
fi
else
skip "No subscriber ID"
fi
# --- Subscriber balance ---
log_test "Get subscriber balance"
if [ -n "$SUB_ID" ]; then
BAL=$(api_get "/api/subscribers/$SUB_ID/balance")
if echo "$BAL" | grep -q "balance\|outstanding\|total"; then
pass
else
fail "Unexpected balance response: $BAL" "subscriber-balance"
fi
else
skip "No subscriber ID"
fi
# =============================================================================
# PHASE 2b: Billing & Payments
# =============================================================================
log_section "PHASE 2b: Billing & Payments"
# --- Generate invoices ---
log_test "Generate billing cycle"
BILLING_RESP=$(api_post "/api/billing/generate" '{}')
if echo "$BILLING_RESP" | grep -q "generated\|skipped\|results"; then
pass
GENERATED=$(json_field "$BILLING_RESP" "generated")
echo " → Generated: $GENERATED invoices"
else
fail "Unexpected billing response: $BILLING_RESP" "generate-billing"
fi
# --- List invoices ---
log_test "List invoices"
INVOICES=$(api_get "/api/invoices")
if echo "$INVOICES" | grep -q "invoices\|total\|\[\]"; then
pass
else
fail "Unexpected invoices response: $INVOICES" "list-invoices"
fi
# Extract first invoice ID if available
INVOICE_ID=$(echo "$INVOICES" | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4)
# --- Record payment ---
log_test "Record payment against invoice"
if [ -n "$INVOICE_ID" ] && [ -n "$SUB_ID" ]; then
PAY_RESP=$(api_post "/api/payments" "{
\"subscriberId\": \"$SUB_ID\",
\"invoiceId\": \"$INVOICE_ID\",
\"amount\": 500,
\"method\": \"CASH\",
\"reference\": \"UAT-PAY-001\"
}")
PAY_ID=$(json_str "$PAY_RESP" "id")
if [ -n "$PAY_ID" ] && [ "$PAY_ID" != "null" ]; then
pass
echo " → Payment ID: $PAY_ID (partial ₱500)"
else
fail "No payment ID: $PAY_RESP" "record-payment"
PAY_ID=""
fi
else
skip "No invoice or subscriber ID"
PAY_ID=""
fi
# --- Record second payment (full remaining) ---
log_test "Record second payment (remaining balance)"
if [ -n "$INVOICE_ID" ] && [ -n "$SUB_ID" ]; then
PAY2_RESP=$(api_post "/api/payments" "{
\"subscriberId\": \"$SUB_ID\",
\"invoiceId\": \"$INVOICE_ID\",
\"amount\": 499,
\"method\": \"BANK_TRANSFER\",
\"reference\": \"UAT-PAY-002\"
}")
PAY2_ID=$(json_str "$PAY2_RESP" "id")
if [ -n "$PAY2_ID" ] && [ "$PAY2_ID" != "null" ]; then
pass
else
fail "No payment ID: $PAY2_RESP" "record-payment-2"
fi
else
skip "No invoice or subscriber"
fi
# --- Payment history ---
log_test "Get subscriber payment history"
if [ -n "$SUB_ID" ]; then
PAYMENTS=$(api_get "/api/subscribers/$SUB_ID/payments")
if echo "$PAYMENTS" | grep -q "UAT-PAY\|payments\|\[\]"; then
pass
else
fail "Unexpected: $PAYMENTS" "payment-history"
fi
else
skip "No subscriber ID"
fi
# --- Outstanding report ---
log_test "Outstanding report"
OUTSTANDING=$(api_get "/api/reports/outstanding")
if echo "$OUTSTANDING" | grep -q "total\|results\|invoices\|\[\]"; then
pass
else
fail "Unexpected: $OUTSTANDING" "outstanding-report"
fi
# =============================================================================
# PHASE 2c: Accounting Foundation
# =============================================================================
log_section "PHASE 2c: Accounting Foundation"
# --- Chart of Accounts ---
log_test "List Chart of Accounts"
ACCOUNTS=$(api_get "/api/accounting/accounts")
ACCT_COUNT=$(echo "$ACCOUNTS" | grep -o '"code"' | wc -l)
if [ "$ACCT_COUNT" -gt 0 ] 2>/dev/null; then
pass
echo "$ACCT_COUNT accounts found"
else
fail "COA is empty — seed.ts does not call seedChartOfAccounts() [APP BUG]" "chart-of-accounts"
echo " → This blocks: journal entries, expenses, financial reports with real data"
fi
# --- Journal entries ---
log_test "List journal entries"
JE_LIST=$(api_get "/api/accounting/journal-entries")
if echo "$JE_LIST" | grep -q "entries\|journalEntries\|total\|\[\]"; then
pass
else
fail "Unexpected: $JE_LIST" "list-journal-entries"
fi
# --- Manual journal entry ---
log_test "Create manual journal entry"
# Get account IDs for Cash on Hand (1010) and Petty Cash
ACCT_1010_ID=$(echo "$ACCOUNTS" | grep -o '"id":"[^"]*","code":"1010"' | head -1 | grep -o '"id":"[^"]*"' | cut -d'"' -f4)
ACCT_1020_ID=$(echo "$ACCOUNTS" | grep -o '"id":"[^"]*","code":"1020"' | head -1 | grep -o '"id":"[^"]*"' | cut -d'"' -f4)
if [ -n "$ACCT_1010_ID" ] && [ -n "$ACCT_1020_ID" ]; then
JE_RESP=$(api_post "/api/accounting/journal-entries" "{
\"description\": \"UAT: Transfer cash to bank\",
\"lines\": [
{ \"accountId\": \"$ACCT_1020_ID\", \"debit\": 1000, \"credit\": 0, \"description\": \"Deposit to bank\" },
{ \"accountId\": \"$ACCT_1010_ID\", \"debit\": 0, \"credit\": 1000, \"description\": \"From cash on hand\" }
]
}")
JE_ID=$(json_str "$JE_RESP" "id")
if [ -n "$JE_ID" ] && [ "$JE_ID" != "null" ]; then
pass
echo " → JE ID: $JE_ID"
else
fail "No JE ID: $JE_RESP" "create-journal-entry"
JE_ID=""
fi
else
skip "Could not find account IDs for 1010/1020"
JE_ID=""
fi
# --- Approve journal entry ---
log_test "Approve manual journal entry"
if [ -n "$JE_ID" ]; then
APPROVE_RESP=$(api_post "/api/accounting/journal-entries/$JE_ID/approve" '{}')
APPROVE_STATUS=$(json_str "$APPROVE_RESP" "status")
if [ "$APPROVE_STATUS" = "POSTED" ]; then
pass
else
fail "Expected POSTED, got $APPROVE_STATUS: $APPROVE_RESP" "approve-je"
fi
else
skip "No JE ID"
fi
# --- Account balance ---
log_test "Get account balance"
if [ -n "$ACCT_1010_ID" ]; then
BAL_RESP=$(api_get "/api/accounting/accounts/$ACCT_1010_ID/balance")
if echo "$BAL_RESP" | grep -q "balance\|debit\|credit"; then
pass
else
fail "Unexpected: $BAL_RESP" "account-balance"
fi
else
skip "No account ID"
fi
# --- Accounting periods ---
log_test "List accounting periods"
PERIODS=$(api_get "/api/accounting/periods")
if echo "$PERIODS" | grep -q "periods\|\[\]"; then
pass
else
# May return array directly
if echo "$PERIODS" | grep -q "\["; then
pass
else
fail "Unexpected: $PERIODS" "accounting-periods"
fi
fi
# =============================================================================
# PHASE 3a: Zones & Collectors
# =============================================================================
log_section "PHASE 3a: Zones & Collectors"
# --- Create zone ---
log_test "Create zone"
ZONE_RESP=$(api_post "/api/zones" "{
\"name\": \"UAT Zone North ${RUN_ID}\",
\"description\": \"Northern coverage area\"
}")
ZONE_ID=$(json_str "$ZONE_RESP" "id")
if [ -n "$ZONE_ID" ] && [ "$ZONE_ID" != "null" ]; then
pass
echo " → Zone ID: $ZONE_ID"
else
fail "No zone ID: $ZONE_RESP" "create-zone"
ZONE_ID=""
fi
# --- List zones ---
log_test "List zones"
ZONES=$(api_get "/api/zones")
if echo "$ZONES" | grep -q "UAT Zone North"; then
pass
else
fail "Zone not in list: $ZONES" "list-zones"
fi
# --- Assign subscriber to zone ---
log_test "Assign subscriber to zone"
if [ -n "$ZONE_ID" ] && [ -n "$SUB_ID" ]; then
ASSIGN_RESP=$(api_post "/api/zones/$ZONE_ID/subscribers" "{\"subscriberId\": \"$SUB_ID\"}")
if echo "$ASSIGN_RESP" | grep -q "$SUB_ID\|success\|zoneId"; then
pass
else
fail "Unexpected: $ASSIGN_RESP" "assign-sub-zone"
fi
else
skip "No zone or subscriber ID"
fi
# =============================================================================
# PHASE 3b: Ticketing System
# =============================================================================
log_section "PHASE 3b: Ticketing System"
# --- List ticket categories ---
log_test "List ticket categories"
CATS=$(api_get "/api/ticket-categories")
if echo "$CATS" | grep -q "\[\]\|categories\|Installation\|id"; then
pass
else
fail "Unexpected: $CATS" "list-ticket-categories"
fi
# --- Create ticket category ---
log_test "Create ticket category"
CAT_RESP=$(api_post "/api/ticket-categories" "{
\"name\": \"UAT Test Category ${RUN_ID}\",
\"description\": \"Category for UAT testing\"
}")
CAT_ID=$(json_str "$CAT_RESP" "id")
if [ -n "$CAT_ID" ] && [ "$CAT_ID" != "null" ]; then
pass
else
fail "No category ID: $CAT_RESP" "create-ticket-category"
# Try to find any existing category
CAT_ID=$(echo "$CATS" | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4)
fi
# --- Create ticket ---
log_test "Create support ticket"
if [ -n "$CAT_ID" ]; then
TICK_RESP=$(api_post "/api/tickets" "{
\"subject\": \"No internet connection\",
\"description\": \"Subscriber reports no connectivity since this morning\",
\"categoryId\": \"$CAT_ID\",
\"priority\": \"HIGH\"
}")
TICK_ID=$(json_str "$TICK_RESP" "id")
if [ -n "$TICK_ID" ] && [ "$TICK_ID" != "null" ]; then
pass
echo " → Ticket ID: $TICK_ID"
else
fail "No ticket ID: $TICK_RESP" "create-ticket"
TICK_ID=""
fi
else
skip "No category ID"
TICK_ID=""
fi
# --- List tickets ---
log_test "List tickets"
TICKETS=$(api_get "/api/tickets")
if echo "$TICKETS" | grep -q "No internet\|tickets\|total"; then
pass
else
fail "Unexpected: $TICKETS" "list-tickets"
fi
# --- Create second ticket ---
log_test "Create second ticket"
if [ -n "$CAT_ID" ]; then
TICK2_RESP=$(api_post "/api/tickets" "{
\"subject\": \"Slow connection speed\",
\"description\": \"Subscriber experiencing 2Mbps instead of 25Mbps\",
\"categoryId\": \"$CAT_ID\",
\"priority\": \"MEDIUM\"
}")
TICK2_ID=$(json_str "$TICK2_RESP" "id")
if [ -n "$TICK2_ID" ] && [ "$TICK2_ID" != "null" ]; then
pass
else
fail "No ticket ID: $TICK2_RESP" "create-ticket-2"
TICK2_ID=""
fi
else
skip "No category ID"
TICK2_ID=""
fi
# =============================================================================
# PHASE 3c: Technicians & Job Orders
# =============================================================================
log_section "PHASE 3c: Technicians & Job Orders"
# --- Create technician profile ---
log_test "Create technician profile"
# First we need a user with TECHNICIAN role — check if one exists or create via notes
TECH_RESP=$(api_post "/api/technicians" '{
"phone": "+639191234567",
"skills": ["Fiber Splicing", "Router Config"],
"compensationModel": "PER_JOB"
}')
TECH_ID=$(json_str "$TECH_RESP" "id")
if [ -n "$TECH_ID" ] && [ "$TECH_ID" != "null" ]; then
pass
else
# May need a TECHNICIAN user — this is expected to fail without one
skip "No TECHNICIAN user in seed data: $TECH_RESP"
TECH_ID=""
fi
# --- Job type rates ---
log_test "List job type rates"
JTR=$(api_get "/api/job-type-rates")
if echo "$JTR" | grep -q "\[\]\|rates\|jobType"; then
pass
else
fail "Unexpected: $JTR" "list-job-type-rates"
fi
# --- Create job order from ticket ---
log_test "Create job order from ticket"
if [ -n "$TICK_ID" ]; then
# Job orders require assignedToId (a TECHNICIAN user). Seed data has no technician users.
skip "Requires TECHNICIAN user — seed data only has ADMIN users [APP GAP]"
JO_ID=""
else
skip "No ticket ID"
JO_ID=""
fi
# --- List job orders ---
log_test "List job orders"
JO_LIST=$(api_get "/api/job-orders")
if echo "$JO_LIST" | grep -q "jobOrders\|\[\]\|total\|id"; then
pass
else
fail "Unexpected: $JO_LIST" "list-job-orders"
fi
# =============================================================================
# PHASE 4a: Inventory & Assets
# =============================================================================
log_section "PHASE 4a: Inventory & Assets"
# --- Register inventory item ---
log_test "Register inventory item (SERIALIZED)"
INV_RESP=$(api_post "/api/inventory/items" '{
"name": "TP-Link ONU",
"itemType": "ONU",
"model": "XN020-G3v",
"serialNumber": "UAT-ONU-'"${RUN_ID}"'",
"trackingType": "SERIALIZED",
"purchaseCost": 1500
}')
INV_ID=$(json_str "$INV_RESP" "id")
if [ -n "$INV_ID" ] && [ "$INV_ID" != "null" ]; then
pass
echo " → Item ID: $INV_ID"
else
fail "No item ID: $INV_RESP" "register-item"
INV_ID=""
fi
# --- Register batch items ---
log_test "Register second inventory item (BATCH)"
INV2_RESP=$(api_post "/api/inventory/items" '{
"name": "Fiber Patch Cord",
"itemType": "CABLE",
"model": "SC-APC 3m",
"trackingType": "BATCH",
"purchaseCost": 150
}')
INV2_ID=$(json_str "$INV2_RESP" "id")
if [ -n "$INV2_ID" ] && [ "$INV2_ID" != "null" ]; then
pass
else
fail "No item ID: $INV2_RESP" "register-item-2"
fi
# --- Record stock movement (RECEIVED) ---
log_test "Record RECEIVED stock movement"
if [ -n "$INV_ID" ]; then
MOV_RESP=$(api_post "/api/inventory/items/$INV_ID/movements" '{
"type": "RECEIVED",
"quantity": 10,
"notes": "Initial batch from supplier"
}')
if echo "$MOV_RESP" | grep -q "id\|movement"; then
pass
else
fail "Unexpected: $MOV_RESP" "stock-received"
fi
else
skip "No item ID"
fi
# --- Stock levels ---
log_test "Get stock levels"
STOCK=$(api_get "/api/inventory/stock-levels")
if echo "$STOCK" | grep -q "stock\|levels\|items\|\[\]"; then
pass
else
fail "Unexpected: $STOCK" "stock-levels"
fi
# --- Assign asset to subscriber ---
log_test "Assign asset to subscriber"
if [ -n "$INV_ID" ] && [ -n "$SUB_ID" ]; then
ASSIGN_RESP=$(api_post "/api/inventory/items/$INV_ID/assign" "{
\"assigneeType\": \"SUBSCRIBER\",
\"assigneeId\": \"$SUB_ID\",
\"condition\": \"NEW\",
\"notes\": \"ONU deployed at subscriber location\"
}")
if echo "$ASSIGN_RESP" | grep -q "id\|assigned\|success"; then
pass
else
fail "Unexpected: $ASSIGN_RESP" "assign-asset"
fi
else
skip "No item or subscriber ID"
fi
# --- Asset history ---
log_test "Get asset history"
if [ -n "$INV_ID" ]; then
HIST=$(api_get "/api/inventory/items/$INV_ID/history")
if echo "$HIST" | grep -q "history\|movements\|timeline\|\[\]"; then
pass
else
fail "Unexpected: $HIST" "asset-history"
fi
else
skip "No item ID"
fi
# =============================================================================
# PHASE 4b: Expenses
# =============================================================================
log_section "PHASE 4b: Expenses"
# --- Create expense category ---
log_test "Create expense category"
if [ "$ACCT_COUNT" -gt 0 ] 2>/dev/null; then
ECAT_RESP=$(api_post "/api/expenses/categories" '{
"name": "Bandwidth",
"accountCode": "5010"
}')
ECAT_ID=$(json_str "$ECAT_RESP" "id")
if [ -n "$ECAT_ID" ] && [ "$ECAT_ID" != "null" ]; then
pass
else
fail "No category ID: $ECAT_RESP" "create-expense-cat"
ECAT_ID=""
fi
else
skip "No COA seeded — expense categories require valid account codes"
ECAT_ID=""
fi
# --- Create vendor ---
log_test "Create vendor"
VENDOR_RESP=$(api_post "/api/vendors" "{
\"name\": \"PLDT Enterprise ${RUN_ID}\",
\"contactPerson\": \"Sales Team\",
\"email\": \"sales@pldt.com\",
\"phone\": \"+6328888\"
}")
VENDOR_ID=$(json_str "$VENDOR_RESP" "id")
if [ -n "$VENDOR_ID" ] && [ "$VENDOR_ID" != "null" ]; then
pass
else
fail "No vendor ID: $VENDOR_RESP" "create-vendor"
VENDOR_ID=""
fi
# --- List vendors ---
log_test "List vendors"
VENDORS=$(api_get "/api/vendors")
if echo "$VENDORS" | grep -q "PLDT\|vendors\|\[\]"; then
pass
else
fail "Unexpected: $VENDORS" "list-vendors"
fi
# --- Create expense ---
log_test "Create expense"
if [ -n "$ECAT_ID" ] && [ -n "$VENDOR_ID" ]; then
EXP_RESP=$(api_post "/api/expenses" "{
\"amount\": 25000,
\"description\": \"Monthly bandwidth fee\",
\"categoryId\": \"$ECAT_ID\",
\"vendorId\": \"$VENDOR_ID\",
\"paymentMethod\": \"BANK_TRANSFER\",
\"expenseDate\": \"2026-03-06\"
}")
EXP_ID=$(json_str "$EXP_RESP" "id")
if [ -n "$EXP_ID" ] && [ "$EXP_ID" != "null" ]; then
pass
echo " → Expense ID: $EXP_ID"
else
fail "No expense ID: $EXP_RESP" "create-expense"
EXP_ID=""
fi
else
skip "No expense category or vendor ID"
EXP_ID=""
fi
# --- Expense reports ---
log_test "Expense report by category"
EXP_RPT=$(api_get "/api/reports/expenses?startDate=2026-01-01&endDate=2026-12-31")
if echo "$EXP_RPT" | grep -q "total\|expenses\|categories\|\[\]"; then
pass
else
fail "Unexpected: $EXP_RPT" "expense-report"
fi
log_test "Expense report by vendor"
EXP_RPT_V=$(api_get "/api/reports/expenses/by-vendor?startDate=2026-01-01&endDate=2026-12-31")
if echo "$EXP_RPT_V" | grep -q "total\|vendors\|expenses\|\[\]"; then
pass
else
fail "Unexpected: $EXP_RPT_V" "expense-by-vendor"
fi
# =============================================================================
# PHASE 4c: Financial Reports
# =============================================================================
log_section "PHASE 4c: Financial Reports"
# --- Trial Balance ---
log_test "Trial Balance"
TB=$(api_get "/api/reports/trial-balance")
if echo "$TB" | grep -q "isBalanced\|totalDebits\|accounts"; then
IS_BALANCED=$(json_field "$TB" "isBalanced")
if [ "$IS_BALANCED" = "true" ]; then
pass
echo " → Books are balanced"
else
fail "Trial balance NOT balanced: $TB" "trial-balance"
fi
else
fail "Unexpected: $TB" "trial-balance"
fi
# --- Income Statement ---
log_test "Income Statement"
IS_RPT=$(api_get "/api/reports/income-statement?startDate=2026-01-01&endDate=2026-12-31")
if echo "$IS_RPT" | grep -q "revenue\|expenses\|netIncome\|income"; then
pass
else
fail "Unexpected: $IS_RPT" "income-statement"
fi
# --- Balance Sheet ---
log_test "Balance Sheet"
BS=$(api_get "/api/reports/balance-sheet?asOfDate=2026-03-06")
if echo "$BS" | grep -q "assets\|liabilities\|equity\|balance"; then
pass
else
fail "Unexpected: $BS" "balance-sheet"
fi
# =============================================================================
# PHASE 5a: Dashboard
# =============================================================================
log_section "PHASE 5a: Dashboard"
log_test "Dashboard summary"
DASH=$(api_get "/api/dashboard")
if echo "$DASH" | grep -q "revenue" && echo "$DASH" | grep -q "subscribers" && echo "$DASH" | grep -q "cashFlow"; then
pass
REV_TODAY=$(json_str "$DASH" "revenueToday")
echo " → Revenue today: ₱$REV_TODAY"
else
fail "Incomplete dashboard: $DASH" "dashboard"
fi
# =============================================================================
# PHASE 5b: Client Portal
# =============================================================================
log_section "PHASE 5b: Client Portal (Subscriber Login)"
# Portal login requires a subscriber with a password set
# The seed data doesn't set subscriber passwords, so we test the API endpoints directly
log_test "Portal account endpoint (unauthenticated blocks access)"
STATUS=$(curl -s -o /dev/null -w "%{http_code}" "${BASE_URL}/api/portal/account")
if [ "$STATUS" = "401" ] || [ "$STATUS" = "307" ]; then
pass
echo " → Returns $STATUS (middleware redirect to login)"
else
fail "Expected 401/307, got $STATUS" "portal-unauth"
fi
log_test "Portal invoices endpoint (unauthenticated blocks access)"
STATUS=$(curl -s -o /dev/null -w "%{http_code}" "${BASE_URL}/api/portal/invoices")
if [ "$STATUS" = "401" ] || [ "$STATUS" = "307" ]; then
pass
else
fail "Expected 401/307, got $STATUS" "portal-invoices-unauth"
fi
log_test "Portal tickets endpoint (unauthenticated blocks access)"
STATUS=$(curl -s -o /dev/null -w "%{http_code}" "${BASE_URL}/api/portal/tickets")
if [ "$STATUS" = "401" ] || [ "$STATUS" = "307" ]; then
pass
else
fail "Expected 401/307, got $STATUS" "portal-tickets-unauth"
fi
log_test "Portal payments coming-soon endpoint"
STATUS=$(curl -s -o /dev/null -w "%{http_code}" "${BASE_URL}/api/portal/payments/coming-soon")
if [ "$STATUS" = "200" ] || [ "$STATUS" = "401" ] || [ "$STATUS" = "307" ]; then
pass
else
fail "Expected 200/401/307, got $STATUS" "portal-coming-soon"
fi
# =============================================================================
# PHASE 6: Tenant Isolation
# =============================================================================
log_section "PHASE 6: Tenant Isolation"
# --- Tenant 2 should NOT see Tenant 1 data ---
log_test "Tenant 2 cannot see Tenant 1 subscribers"
T2_SUBS=$(api_get "/api/subscribers" "$COOKIE_JAR_TENANT2")
T2_TOTAL=$(json_field "$T2_SUBS" "total")
if [ "$T2_TOTAL" = "0" ]; then
pass
else
fail "Tenant 2 sees $T2_TOTAL subscribers (should be 0)" "tenant-isolation-subs"
fi
log_test "Tenant 2 cannot see Tenant 1 service plans"
T2_PLANS=$(api_get "/api/service-plans" "$COOKIE_JAR_TENANT2")
if echo "$T2_PLANS" | grep -q "UAT.*Mbps"; then
fail "Tenant 2 can see Tenant 1 plans!" "tenant-isolation-plans"
else
pass
fi
log_test "Tenant 2 cannot see Tenant 1 tickets"
T2_TICKETS=$(api_get "/api/tickets" "$COOKIE_JAR_TENANT2")
if echo "$T2_TICKETS" | grep -q "No internet"; then
fail "Tenant 2 can see Tenant 1 tickets!" "tenant-isolation-tickets"
else
pass
fi
log_test "Tenant 2 cannot see Tenant 1 zones"
T2_ZONES=$(api_get "/api/zones" "$COOKIE_JAR_TENANT2")
if echo "$T2_ZONES" | grep -q "UAT Zone"; then
fail "Tenant 2 can see Tenant 1 zones!" "tenant-isolation-zones"
else
pass
fi
log_test "Tenant 2 cannot see Tenant 1 inventory"
T2_INV=$(api_get "/api/inventory/items" "$COOKIE_JAR_TENANT2")
if echo "$T2_INV" | grep -q "TP-Link"; then
fail "Tenant 2 can see Tenant 1 inventory!" "tenant-isolation-inv"
else
pass
fi
log_test "Tenant 2 dashboard shows zeros"
T2_DASH=$(api_get "/api/dashboard" "$COOKIE_JAR_TENANT2")
T2_SUBS_TOTAL=$(echo "$T2_DASH" | grep -o '"total":[0-9]*' | head -1 | cut -d: -f2)
if [ "$T2_SUBS_TOTAL" = "0" ]; then
pass
else
fail "Tenant 2 dashboard shows total=$T2_SUBS_TOTAL" "tenant-isolation-dash"
fi
# =============================================================================
# PHASE 7: UI Pages
# =============================================================================
log_section "PHASE 7: UI Pages"
log_test "Login page renders"
STATUS=$(curl -s -o /dev/null -w "%{http_code}" "${BASE_URL}/login")
if [ "$STATUS" = "200" ]; then
pass
else
fail "HTTP $STATUS" "login-page"
fi
log_test "Signup page renders"
STATUS=$(curl -s -o /dev/null -w "%{http_code}" "${BASE_URL}/signup")
if [ "$STATUS" = "200" ]; then
pass
else
fail "HTTP $STATUS" "signup-page"
fi
log_test "Dashboard page (authenticated)"
STATUS=$(curl -s -o /dev/null -w "%{http_code}" -b "$COOKIE_JAR" "${BASE_URL}/dashboard")
if [ "$STATUS" = "200" ]; then
pass
else
fail "HTTP $STATUS" "dashboard-page"
fi
log_test "Super-admin tenants page"
STATUS=$(curl -s -o /dev/null -w "%{http_code}" -b "$COOKIE_JAR_SUPER" "${BASE_URL}/admin/tenants")
if [ "$STATUS" = "200" ]; then
pass
else
fail "HTTP $STATUS" "admin-tenants-page"
fi
log_test "Dashboard page redirects when unauthenticated"
STATUS=$(curl -s -o /dev/null -w "%{http_code}" "${BASE_URL}/dashboard" --max-redirs 5 -L 2>/dev/null || true)
if [ "$STATUS" = "302" ] || [ "$STATUS" = "307" ] || [ "$STATUS" = "200" ]; then
pass
else
fail "HTTP $STATUS (expected redirect)" "dashboard-redirect"
fi
# =============================================================================
# PHASE 8: Data Integrity — Subscriber Status Lifecycle
# =============================================================================
log_section "PHASE 8: Data Integrity"
# --- Suspend subscriber ---
log_test "Suspend subscriber (PATCH)"
if [ -n "$SUB_ID" ]; then
SUSP_RESP=$(curl -s -b "$COOKIE_JAR" -X PATCH "${BASE_URL}/api/subscribers/$SUB_ID/status" \
-H "Content-Type: application/json" -d '{"status": "SUSPENDED"}')
NEW_STATUS=$(json_str "$SUSP_RESP" "status")
if [ "$NEW_STATUS" = "SUSPENDED" ]; then
pass
else
fail "Expected SUSPENDED, got $NEW_STATUS: $SUSP_RESP" "suspend-subscriber"
fi
else
skip "No subscriber ID"
fi
# --- Reactivate subscriber ---
log_test "Reactivate subscriber (PATCH)"
if [ -n "$SUB_ID" ]; then
REACT_RESP=$(curl -s -b "$COOKIE_JAR" -X PATCH "${BASE_URL}/api/subscribers/$SUB_ID/status" \
-H "Content-Type: application/json" -d '{"status": "ACTIVE"}')
NEW_STATUS=$(json_str "$REACT_RESP" "status")
if [ "$NEW_STATUS" = "ACTIVE" ]; then
pass
else
fail "Expected ACTIVE, got $NEW_STATUS: $REACT_RESP" "reactivate-subscriber"
fi
else
skip "No subscriber ID"
fi
# --- Void payment ---
log_test "Void payment"
if [ -n "$PAY_ID" ]; then
VOID_RESP=$(api_post "/api/payments/$PAY_ID/void" '{"reason": "UAT test void"}')
VOID_STATUS=$(json_str "$VOID_RESP" "status")
if [ "$VOID_STATUS" = "VOIDED" ]; then
pass
else
fail "Expected VOIDED, got $VOID_STATUS: $VOID_RESP" "void-payment"
fi
else
skip "No payment ID"
fi
# --- Trial Balance still balanced after all operations ---
log_test "Trial Balance still balanced after all operations"
TB_FINAL=$(api_get "/api/reports/trial-balance")
IS_BALANCED=$(json_field "$TB_FINAL" "isBalanced")
if [ "$IS_BALANCED" = "true" ]; then
pass
echo " → Books still balanced after all test operations"
else
fail "Trial balance NOT balanced after operations!" "final-trial-balance"
fi
# =============================================================================
# PHASE 9: Collections & Remittances
# =============================================================================
log_section "PHASE 9: Collections & Remittances"
# These require a COLLECTOR user which isn't in seed data
# Test the endpoints respond correctly with admin access
log_test "Collections endpoint responds"
COLL=$(api_get "/api/collections")
if echo "$COLL" | grep -q "collections\|\[\]\|error\|Forbidden"; then
pass
else
fail "Unexpected: $COLL" "collections-endpoint"
fi
log_test "Remittances endpoint responds"
REM=$(api_get "/api/remittances")
if echo "$REM" | grep -q "remittances\|\[\]\|error\|Forbidden"; then
pass
else
fail "Unexpected: $REM" "remittances-endpoint"
fi
log_test "Collection report endpoint responds"
COLL_RPT=$(api_get "/api/reports/collections")
if echo "$COLL_RPT" | grep -q "summary\|collectors\|\[\]\|error\|collections"; then
pass
else
fail "Unexpected: $COLL_RPT" "collections-report"
fi
# =============================================================================
# PHASE 10: Compensation Reports
# =============================================================================
log_section "PHASE 10: Compensation Reports"
log_test "Compensation report endpoint"
COMP=$(api_get "/api/reports/compensation")
if echo "$COMP" | grep -q "technicians\|\[\]\|compensation\|error"; then
pass
else
fail "Unexpected: $COMP" "compensation-report"
fi
# =============================================================================
# RESULTS
# =============================================================================
echo ""
echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo -e "${BOLD} UAT TEST RESULTS${NC}"
echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo ""
TOTAL=$((PASS + FAIL + SKIP))
echo -e " ${GREEN}PASSED:${NC} $PASS"
echo -e " ${RED}FAILED:${NC} $FAIL"
echo -e " ${YELLOW}SKIPPED:${NC} $SKIP"
echo -e " ${BOLD}TOTAL:${NC} $TOTAL"
echo ""
if [ $FAIL -gt 0 ]; then
echo -e " ${RED}${BOLD}Failures:${NC}"
echo -e "$FAILURES"
echo ""
fi
if [ $FAIL -eq 0 ]; then
echo -e " ${GREEN}${BOLD}All tests passed!${NC}"
else
echo -e " ${YELLOW}Review failures above and test manually in the browser.${NC}"
fi
echo ""
# Cleanup
rm -f /tmp/nf_bad_cookies.txt
exit $FAIL