#!/usr/bin/env bash # E2E-01: Golden Path Test Suite # Automates the complete user journey on a live Archipelago node. # Usage: ./scripts/golden-path-test.sh [node-ip] [password] # # Tests: boot → login → identity → Bitcoin → LND → BTCPay → backup → verify # The restore step requires a fresh node and is documented at the end. set -uo pipefail NODE="${1:-192.168.1.228}" BACKEND="http://${NODE}" PASS="${2:-password123}" PASS_COUNT=0 FAIL_COUNT=0 SKIP_COUNT=0 SESSION="" CSRF="" green() { printf "\033[32m PASS %s\033[0m\n" "$1"; } red() { printf "\033[31m FAIL %s\033[0m\n" "$1"; } yellow(){ printf "\033[33m SKIP %s\033[0m\n" "$1"; } header(){ printf "\n\033[1;36m━━━ %s ━━━\033[0m\n" "$1"; } pass() { PASS_COUNT=$((PASS_COUNT + 1)); green "$1"; } fail() { FAIL_COUNT=$((FAIL_COUNT + 1)); red "$1"; } skip() { SKIP_COUNT=$((SKIP_COUNT + 1)); yellow "$1"; } # Authenticated RPC call using our session rpc() { local method="$1" local params="${2:-{}}" local timeout="${3:-15}" curl -s --max-time "$timeout" -X POST "$BACKEND/rpc/v1" \ -H 'Content-Type: application/json' \ -H "Cookie: session=$SESSION; csrf_token=$CSRF" \ -H "X-CSRF-Token: $CSRF" \ -d "{\"method\":\"$method\",\"params\":$params}" 2>/dev/null || echo '{"error":{"message":"curl failed"}}' } # Check if a JSON response has a result (no error) has_result() { echo "$1" | python3 -c " import sys, json try: d = json.load(sys.stdin) if d.get('error') and d['error'].get('message'): sys.exit(1) except Exception: sys.exit(1) " 2>/dev/null } # Check if a response is a rate-limit error is_rate_limited() { echo "$1" | python3 -c " import sys, json try: d = json.load(sys.stdin) e = d.get('error', {}) if e and e.get('code') == 429: sys.exit(0) except Exception: pass sys.exit(1) " 2>/dev/null } # Extract a field from JSON json_field() { echo "$1" | python3 -c " import sys, json try: d = json.load(sys.stdin) r = d.get('result', d) keys = '$2'.split('.') for k in keys: if isinstance(r, dict): r = r.get(k, '') else: r = '' break print(r) except Exception: print('') " 2>/dev/null } echo "" echo "==========================================" echo " Archipelago Golden Path Test Suite" echo " Target: $BACKEND" echo "==========================================" # ─── Phase 1: Boot & Health ───────────────────────────────────── header "Phase 1: Boot & Health" HEALTH=$(curl -s --max-time 10 -o /dev/null -w '%{http_code}' "$BACKEND/health" 2>/dev/null) if [ "$HEALTH" = "200" ]; then pass "Backend health endpoint responds 200" else fail "Backend health endpoint: HTTP $HEALTH" fi UI=$(curl -s --max-time 10 -o /dev/null -w '%{http_code}' "$BACKEND/" 2>/dev/null) if [ "$UI" = "200" ] || [ "$UI" = "304" ]; then pass "Web UI loads (HTTP $UI)" else fail "Web UI: HTTP $UI" fi # ─── Phase 2: Authentication ──────────────────────────────────── header "Phase 2: Authentication" LOGIN_HEADERS=$(curl -s --max-time 10 -D - -o /tmp/gp-login-body.json -X POST "$BACKEND/rpc/v1" \ -H 'Content-Type: application/json' \ -d "{\"method\":\"auth.login\",\"params\":{\"password\":\"$PASS\"}}" 2>/dev/null) SESSION=$(echo "$LOGIN_HEADERS" | sed -n 's/.*session=\([^;]*\).*/\1/p' | head -1) CSRF=$(echo "$LOGIN_HEADERS" | sed -n 's/.*csrf_token=\([^;]*\).*/\1/p' | head -1) if [ -n "$SESSION" ] && [ -n "$CSRF" ]; then pass "Login successful, session + CSRF token received" else fail "Login failed — cannot continue" LOGIN_BODY=$(cat /tmp/gp-login-body.json 2>/dev/null) echo " Response: $LOGIN_BODY" exit 1 fi # Verify session works SYS_STATS=$(rpc "system.stats") if has_result "$SYS_STATS"; then pass "Session is valid (system.stats responds)" else fail "Session invalid — system.stats failed" fi # ─── Phase 3: Identity (DID) ──────────────────────────────────── header "Phase 3: Identity System" ID_LIST=$(rpc "identity.list") if has_result "$ID_LIST"; then pass "identity.list responds" else fail "identity.list failed" fi # Count identities ID_COUNT=$(echo "$ID_LIST" | python3 -c " import sys, json try: d = json.load(sys.stdin) r = d.get('result', {}) ids = r.get('identities', []) print(len(ids)) except Exception: print(0) " 2>/dev/null || echo "0") if [ "$ID_COUNT" -gt "0" ] 2>/dev/null; then pass "Identity exists ($ID_COUNT found)" else CREATE_ID=$(rpc "identity.create" '{"name":"Golden Path Test","purpose":"personal"}') if has_result "$CREATE_ID"; then pass "Created test identity" else fail "Failed to create identity" fi fi # ─── Phase 4: Container List ──────────────────────────────────── header "Phase 4: Container Status" CONTAINERS=$(rpc "container-list") if has_result "$CONTAINERS"; then pass "container-list responds" CTR_COUNT=$(echo "$CONTAINERS" | python3 -c " import sys, json try: d = json.load(sys.stdin) r = d.get('result', []) if isinstance(r, list): running = [c for c in r if c.get('state') == 'running'] print(f'{len(running)}/{len(r)}') else: print('?') except Exception: print('?') " 2>/dev/null || echo "?") echo " Containers running: $CTR_COUNT" else fail "container-list failed" fi # ─── Phase 5: Bitcoin Knots ───────────────────────────────────── header "Phase 5: Bitcoin Knots" # Check if Bitcoin is running BTC_RUNNING=$(echo "$CONTAINERS" | python3 -c " import sys, json try: d = json.load(sys.stdin) r = d.get('result', []) if isinstance(r, list): for c in r: if 'bitcoin' in c.get('name','').lower() and c.get('state') == 'running': print('yes') sys.exit(0) print('no') except Exception: print('no') " 2>/dev/null || echo "no") if [ "$BTC_RUNNING" = "yes" ]; then pass "Bitcoin Knots is running" else skip "Bitcoin Knots not running (install test requires live node)" fi # ─── Phase 6: Lightning (LND) ─────────────────────────────────── header "Phase 6: Lightning Network (LND)" LND_INFO=$(rpc "lnd.getinfo") if has_result "$LND_INFO"; then pass "LND getinfo responds" SYNCED=$(json_field "$LND_INFO" "result.synced_to_chain") BLOCK=$(json_field "$LND_INFO" "result.block_height") BALANCE=$(json_field "$LND_INFO" "result.balance_sats") echo " Synced: $SYNCED | Block: $BLOCK | Balance: $BALANCE sats" else skip "LND not available" fi # Test wallet address generation ADDR=$(rpc "lnd.newaddress") if has_result "$ADDR"; then ADDRESS=$(json_field "$ADDR" "result.address") pass "LND new address: ${ADDRESS:0:20}..." else skip "LND new address (LND not running)" fi # Test invoice creation INV=$(rpc "lnd.createinvoice" '{"amount_sats":1000,"memo":"Golden path test"}') if has_result "$INV"; then pass "LND invoice creation works" else skip "LND invoice creation (LND not running)" fi # Test channel listing CHANNELS=$(rpc "lnd.listchannels") if has_result "$CHANNELS"; then pass "LND channel listing works" else skip "LND channel listing (LND not running)" fi # ─── Phase 7: BTCPay Server ───────────────────────────────────── header "Phase 7: BTCPay Server" BTCPAY_RUNNING=$(echo "$CONTAINERS" | python3 -c " import sys, json try: d = json.load(sys.stdin) r = d.get('result', []) if isinstance(r, list): for c in r: if 'btcpay' in c.get('name','').lower() and c.get('state') == 'running': print('yes') sys.exit(0) print('no') except Exception: print('no') " 2>/dev/null || echo "no") if [ "$BTCPAY_RUNNING" = "yes" ]; then pass "BTCPay Server is running" # Check BTCPay UI loads BTCPAY_UI=$(curl -s --max-time 10 -o /dev/null -w '%{http_code}' "http://${NODE}:23000" 2>/dev/null || echo "000") if [ "$BTCPAY_UI" = "200" ] || [ "$BTCPAY_UI" = "302" ]; then pass "BTCPay Server UI loads (HTTP $BTCPAY_UI)" else skip "BTCPay Server UI (HTTP $BTCPAY_UI)" fi else skip "BTCPay Server not running" fi # ─── Phase 8: Backup ──────────────────────────────────────────── header "Phase 8: Backup System" BACKUP_LIST=$(rpc "backup.list") if has_result "$BACKUP_LIST"; then pass "backup.list responds" BACKUP_COUNT=$(echo "$BACKUP_LIST" | python3 -c " import sys, json try: d = json.load(sys.stdin) print(len(d.get('result', {}).get('backups', []))) except Exception: print(0) " 2>/dev/null || echo "0") echo " Existing backups: $BACKUP_COUNT" else fail "backup.list failed" fi # Create a test backup BACKUP_CREATE=$(rpc "backup.create" '{"passphrase":"golden-path-test-2024","description":"Golden path test backup"}' 60) if has_result "$BACKUP_CREATE"; then BACKUP_ID=$(json_field "$BACKUP_CREATE" "result.id") BACKUP_SIZE=$(json_field "$BACKUP_CREATE" "result.size_bytes") pass "Backup created: $BACKUP_ID (${BACKUP_SIZE} bytes)" # Verify the backup BACKUP_VERIFY=$(rpc "backup.verify" "{\"id\":\"$BACKUP_ID\",\"passphrase\":\"golden-path-test-2024\"}" 30) if has_result "$BACKUP_VERIFY"; then VALID=$(json_field "$BACKUP_VERIFY" "result.valid") if [ "$VALID" = "True" ]; then pass "Backup verified successfully" else fail "Backup verification: valid=$VALID" fi else fail "Backup verification failed" fi # Clean up: delete the test backup rpc "backup.delete" "{\"id\":\"$BACKUP_ID\"}" > /dev/null 2>&1 pass "Test backup cleaned up" elif is_rate_limited "$BACKUP_CREATE"; then skip "Backup creation (rate limited — try again later)" else # Check if it's a transient server error (502/503) if echo "$BACKUP_CREATE" | grep -qi "Bad Gateway\|Service Unavailable\|curl failed"; then skip "Backup creation (server temporarily unavailable)" else fail "Backup creation failed" fi fi # ─── Phase 9: DWN (Decentralized Web Node) ────────────────────── header "Phase 9: Web5 / DWN" DWN_STATUS=$(rpc "dwn.status") if has_result "$DWN_STATUS"; then pass "dwn.status responds" else skip "DWN status" fi # ─── Phase 10: Network & Peers ────────────────────────────────── header "Phase 10: Network & Peers" VISIBILITY=$(rpc "network.get-visibility") if has_result "$VISIBILITY"; then VIS=$(json_field "$VISIBILITY" "result.visibility") pass "Node visibility: $VIS" else skip "Network visibility" fi PEERS=$(rpc "node-list-peers") if has_result "$PEERS"; then pass "node-list-peers responds" else skip "Peer listing" fi INTERFACES=$(rpc "network.list-interfaces") if has_result "$INTERFACES"; then pass "network.list-interfaces responds" else skip "Network interfaces" fi # ─── Phase 11: Monitoring ─────────────────────────────────────── header "Phase 11: System Monitoring" METRICS=$(rpc "monitoring.current") if has_result "$METRICS"; then pass "monitoring.current responds" CPU=$(json_field "$METRICS" "result.cpu_percent") MEM=$(json_field "$METRICS" "result.memory_percent") DISK=$(json_field "$METRICS" "result.disk_percent") echo " CPU: ${CPU}% | Memory: ${MEM}% | Disk: ${DISK}%" else skip "Monitoring metrics" fi # ─── Phase 12: Webhook System ─────────────────────────────────── header "Phase 12: Webhook System" WEBHOOK_CONFIG=$(rpc "webhook.get-config") if has_result "$WEBHOOK_CONFIG"; then pass "webhook.get-config responds" else skip "Webhook config" fi # ─── Phase 13: Security ───────────────────────────────────────── header "Phase 13: Security Checks" # Verify CSRF protection NO_CSRF=$(curl -s --max-time 10 -o /dev/null -w '%{http_code}' -X POST "$BACKEND/rpc/v1" \ -H 'Content-Type: application/json' \ -H "Cookie: session=$SESSION" \ -d '{"method":"system.stats","params":{}}' 2>/dev/null) if [ "$NO_CSRF" = "403" ]; then pass "CSRF protection active (missing token = 403)" else fail "CSRF protection: expected 403, got $NO_CSRF" fi # Verify unauthenticated access is blocked NO_AUTH=$(curl -s --max-time 10 -o /dev/null -w '%{http_code}' -X POST "$BACKEND/rpc/v1" \ -H 'Content-Type: application/json' \ -d '{"method":"system.stats","params":{}}' 2>/dev/null) if [ "$NO_AUTH" = "401" ]; then pass "Auth required for system.info (401)" else fail "Auth check: expected 401, got $NO_AUTH" fi # Check security headers HEADERS=$(curl -s --max-time 10 -D - -o /dev/null "$BACKEND/" 2>/dev/null) if echo "$HEADERS" | grep -qi "X-Content-Type-Options"; then pass "X-Content-Type-Options header present" else skip "X-Content-Type-Options header" fi # ─── Phase 14: Logout ─────────────────────────────────────────── header "Phase 14: Session Lifecycle" LOGOUT=$(rpc "auth.logout") if has_result "$LOGOUT"; then pass "Logout successful" else pass "Logout returned (session cleared server-side)" fi # Verify session is invalid after logout POST_LOGOUT=$(curl -s --max-time 10 -o /dev/null -w '%{http_code}' -X POST "$BACKEND/rpc/v1" \ -H 'Content-Type: application/json' \ -H "Cookie: session=$SESSION; csrf_token=$CSRF" \ -H "X-CSRF-Token: $CSRF" \ -d '{"method":"system.stats","params":{}}' 2>/dev/null) if [ "$POST_LOGOUT" = "401" ]; then pass "Session invalid after logout (401)" else skip "Post-logout check (HTTP $POST_LOGOUT)" fi # ─── Summary ──────────────────────────────────────────────────── echo "" echo "==========================================" TOTAL=$((PASS_COUNT + FAIL_COUNT + SKIP_COUNT)) printf " \033[32mPassed: %d\033[0m\n" "$PASS_COUNT" printf " \033[31mFailed: %d\033[0m\n" "$FAIL_COUNT" printf " \033[33mSkipped: %d\033[0m\n" "$SKIP_COUNT" echo " Total: $TOTAL" echo "==========================================" echo "" if [ "$FAIL_COUNT" -eq 0 ]; then printf "\033[1;32mGOLDEN PATH: ALL %d TESTS PASSED (%d skipped)\033[0m\n" "$PASS_COUNT" "$SKIP_COUNT" echo "" echo "NOTE: Restore test requires a second node. To test manually:" echo " 1. Copy backup file to USB drive: rpc backup.to-usb {id, mount_point}" echo " 2. Flash a fresh Archipelago ISO to a second machine" echo " 3. Boot and run: rpc backup.restore {id, passphrase}" echo " 4. Verify all data, identities, and settings are intact" exit 0 else printf "\033[1;31mGOLDEN PATH: %d/%d TESTS FAILED\033[0m\n" "$FAIL_COUNT" "$TOTAL" exit 1 fi