#!/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