Files
archy/scripts/run-e2e-tests.sh
Dorian 0cf71c4115 fix: zero-amount invoices, identity.verify DID extraction, tor service permissions
- Allow zero-amount Lightning invoices (BOLT11 "any amount") by changing
  validation from amount_sats < 1 to amount_sats < 0
- identity.verify now extracts pubkey directly from did:key format instead
  of requiring the DID to belong to a local identity
- tor.create-service writes config to data_dir/tor-config/ instead of
  /var/lib/archipelago/tor/ (owned by debian-tor, not archipelago user)
- Add E2E test script (scripts/run-e2e-tests.sh) covering 47 RPC endpoints
- Add testing plan with results (loop/testing.md)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 09:53:36 +00:00

252 lines
9.6 KiB
Bash

#!/usr/bin/env bash
# E2E test suite for all Archipelago RPC endpoints.
# Uses correct method names from the dispatch table.
# Run on the server: bash run-e2e-tests.sh
set -u
BASE="http://127.0.0.1:5678"
JAR="/tmp/test-cookies.txt"
rm -f "$JAR"
PC=0; FC=0; SC=0
pass() { PC=$((PC + 1)); printf "\033[32m✓ %s\033[0m\n" "$1"; }
fail() { FC=$((FC + 1)); printf "\033[31m✗ %s\033[0m\n" "$1"; }
skip() { SC=$((SC + 1)); printf "\033[33m⊘ %s\033[0m\n" "$1"; }
rpc() {
sleep 0.3
local method="$1"
local params="${2:-"{}"}"
curl -s -b "$JAR" -c "$JAR" \
-H "Content-Type: application/json" \
-d "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"$method\",\"params\":$params}" \
"${BASE}/rpc/v1" 2>/dev/null
}
# Check if RPC response is successful (error field is null or absent)
rpc_ok() {
local resp="$1"
[ -z "$resp" ] && return 1
echo "$resp" | grep -q '"error":null' && return 0
echo "$resp" | grep -q '"error"' && return 1
return 0
}
echo ""
echo "━━━ Auth ━━━"
# Warmup: first request after server restart may get empty response
curl -s "${BASE}/health" > /dev/null 2>&1
sleep 1
# Login with retry
LOGIN=""
for attempt in 1 2 3; do
LOGIN=$(rpc "auth.login" '{"password":"password123"}')
if [ -n "$LOGIN" ]; then break; fi
sleep 0.5
done
rpc_ok "$LOGIN" && pass "auth.login" || fail "auth.login: $LOGIN"
echo ""
echo "━━━ Identity ━━━"
ID_LIST=$(rpc "identity.list")
rpc_ok "$ID_LIST" && pass "identity.list" || fail "identity.list: $ID_LIST"
FIRST_ID=$(echo "$ID_LIST" | python3 -c "import sys,json; r=json.load(sys.stdin); ids=r.get('result',{}).get('identities',[]); print(ids[0]['id'] if ids else '')" 2>/dev/null)
if [ -n "$FIRST_ID" ]; then
# sign
SIGN=$(rpc "identity.sign" "{\"id\":\"$FIRST_ID\",\"message\":\"hello\"}")
rpc_ok "$SIGN" && pass "identity.sign" || fail "identity.sign: $SIGN"
DID=$(echo "$SIGN" | python3 -c "import sys,json; print(json.load(sys.stdin)['result']['did'])" 2>/dev/null)
SIG_HEX=$(echo "$SIGN" | python3 -c "import sys,json; print(json.load(sys.stdin)['result']['signature'])" 2>/dev/null)
# verify (valid)
VER=$(rpc "identity.verify" "{\"did\":\"$DID\",\"message\":\"hello\",\"signature\":\"$SIG_HEX\"}")
echo "$VER" | python3 -c "import sys,json; r=json.load(sys.stdin); assert r['result']['valid']" 2>/dev/null && pass "identity.verify (valid)" || fail "identity.verify: $VER"
# verify (bad)
VER_BAD=$(rpc "identity.verify" "{\"did\":\"$DID\",\"message\":\"nope\",\"signature\":\"$SIG_HEX\"}")
echo "$VER_BAD" | python3 -c "import sys,json; r=json.load(sys.stdin); assert not r['result']['valid']" 2>/dev/null && pass "identity.verify (bad rejected)" || fail "identity.verify bad: $VER_BAD"
# get
R=$(rpc "identity.get" "{\"id\":\"$FIRST_ID\"}")
rpc_ok "$R" && pass "identity.get" || fail "identity.get: $R"
# set-default
R=$(rpc "identity.set-default" "{\"id\":\"$FIRST_ID\"}")
rpc_ok "$R" && pass "identity.set-default" || fail "identity.set-default: $R"
# nostr key
NOSTR=$(rpc "identity.create-nostr-key" "{\"id\":\"$FIRST_ID\"}")
rpc_ok "$NOSTR" && pass "identity.create-nostr-key" || {
echo "$NOSTR" | grep -q "already exists" && pass "identity.create-nostr-key (exists)" || fail "nostr-key: $NOSTR"
}
# nostr sign
HASH=$(python3 -c "import hashlib; print(hashlib.sha256(b'test').hexdigest())")
R=$(rpc "identity.nostr-sign" "{\"id\":\"$FIRST_ID\",\"event_hash\":\"$HASH\"}")
rpc_ok "$R" && pass "identity.nostr-sign" || fail "identity.nostr-sign: $R"
else
fail "no identity found"
fi
# Create + nostr + delete
CREATE=$(rpc "identity.create" '{"name":"TmpTest","purpose":"anonymous"}')
rpc_ok "$CREATE" && pass "identity.create" || fail "identity.create: $CREATE"
TEMP_ID=$(echo "$CREATE" | python3 -c "import sys,json; print(json.load(sys.stdin).get('result',{}).get('id',''))" 2>/dev/null)
if [ -n "$TEMP_ID" ]; then
R=$(rpc "identity.create-nostr-key" "{\"id\":\"$TEMP_ID\"}")
rpc_ok "$R" && pass "nostr-key (new identity)" || fail "nostr-key (new): $R"
R=$(rpc "identity.delete" "{\"id\":\"$TEMP_ID\"}")
rpc_ok "$R" && pass "identity.delete" || fail "identity.delete: $R"
fi
echo ""
echo "━━━ Names (identity.*-name) ━━━"
R=$(rpc "identity.list-names")
rpc_ok "$R" && pass "identity.list-names" || fail "identity.list-names: $(echo $R | head -c 120)"
if [ -n "$FIRST_ID" ]; then
R=$(rpc "identity.register-name" "{\"name\":\"e2e\",\"domain\":\"archipelago.local\",\"identity_id\":\"$FIRST_ID\",\"did\":\"$DID\"}")
rpc_ok "$R" && pass "identity.register-name" || fail "identity.register-name: $(echo $R | head -c 120)"
REG_NAME_ID=$(echo "$R" | python3 -c "import sys,json; print(json.load(sys.stdin).get('result',{}).get('id',''))" 2>/dev/null)
R=$(rpc "identity.resolve-name" '{"identifier":"e2e@archipelago.local"}')
rpc_ok "$R" && pass "identity.resolve-name" || fail "identity.resolve-name: $(echo $R | head -c 120)"
if [ -n "$REG_NAME_ID" ]; then
R=$(rpc "identity.remove-name" "{\"id\":\"$REG_NAME_ID\"}")
rpc_ok "$R" && pass "identity.remove-name" || fail "identity.remove-name: $(echo $R | head -c 120)"
fi
fi
echo ""
echo "━━━ Credentials (identity.*-credential) ━━━"
R=$(rpc "identity.list-credentials")
rpc_ok "$R" && pass "identity.list-credentials" || fail "identity.list-credentials: $(echo $R | head -c 120)"
if [ -n "$FIRST_ID" ]; then
R=$(rpc "identity.issue-credential" "{\"issuer_id\":\"$FIRST_ID\",\"subject_did\":\"did:key:z6MkTest\",\"type\":\"TestCred\",\"claims\":{\"name\":\"E2E\"}}")
rpc_ok "$R" && pass "identity.issue-credential" || fail "identity.issue-credential: $(echo $R | head -c 120)"
fi
echo ""
echo "━━━ Lightning ━━━"
R=$(rpc "lnd.getinfo")
rpc_ok "$R" && pass "lnd.getinfo" || fail "lnd.getinfo: $R"
R=$(rpc "lnd.listchannels")
rpc_ok "$R" && pass "lnd.listchannels" || fail "lnd.listchannels: $R"
R=$(rpc "lnd.newaddress")
rpc_ok "$R" && pass "lnd.newaddress" || fail "lnd.newaddress: $R"
R=$(rpc "lnd.createinvoice" '{"amount_sats":0,"memo":"zero amount test"}')
rpc_ok "$R" && pass "lnd.createinvoice (0 sats)" || fail "lnd.createinvoice (0): $R"
R=$(rpc "lnd.createinvoice" '{"amount_sats":1000,"memo":"test"}')
rpc_ok "$R" && pass "lnd.createinvoice (1000 sats)" || fail "lnd.createinvoice (1000): $R"
R=$(rpc "bitcoin.getinfo")
rpc_ok "$R" && pass "bitcoin.getinfo" || fail "bitcoin.getinfo: $R"
echo ""
echo "━━━ Tor ━━━"
R=$(rpc "tor.list-services")
rpc_ok "$R" && pass "tor.list-services" || fail "tor.list-services: $R"
R=$(rpc "tor.create-service" '{"name":"test-e2e","local_port":9999}')
rpc_ok "$R" && pass "tor.create-service" || fail "tor.create-service: $(echo $R | head -c 150)"
R=$(rpc "tor.delete-service" '{"name":"test-e2e"}')
rpc_ok "$R" && pass "tor.delete-service" || fail "tor.delete-service: $R"
R=$(rpc "tor.get-onion-address" '{"name":"archipelago"}')
rpc_ok "$R" && pass "tor.get-onion-address" || fail "tor.get-onion-address: $R"
echo ""
echo "━━━ Ecash Wallet ━━━"
R=$(rpc "wallet.ecash-balance")
rpc_ok "$R" && pass "wallet.ecash-balance" || skip "wallet.ecash-balance"
R=$(rpc "wallet.ecash-history")
rpc_ok "$R" && pass "wallet.ecash-history" || skip "wallet.ecash-history"
R=$(rpc "wallet.networking-profits")
rpc_ok "$R" && pass "wallet.networking-profits" || skip "wallet.networking-profits"
echo ""
echo "━━━ Content ━━━"
R=$(rpc "content.list-mine")
rpc_ok "$R" && pass "content.list-mine" || fail "content.list-mine: $R"
echo ""
echo "━━━ Network ━━━"
R=$(rpc "network.get-visibility")
rpc_ok "$R" && pass "network.get-visibility" || fail "network.get-visibility: $R"
R=$(rpc "network.diagnostics")
rpc_ok "$R" && pass "network.diagnostics" || fail "network.diagnostics: $R"
R=$(rpc "network.list-requests")
rpc_ok "$R" && pass "network.list-requests" || fail "network.list-requests: $R"
R=$(rpc "node-list-peers")
rpc_ok "$R" && pass "node-list-peers" || fail "node-list-peers: $R"
echo ""
echo "━━━ Nostr Relays ━━━"
R=$(rpc "nostr.list-relays")
rpc_ok "$R" && pass "nostr.list-relays" || fail "nostr.list-relays: $R"
R=$(rpc "nostr.get-stats")
rpc_ok "$R" && pass "nostr.get-stats" || fail "nostr.get-stats: $R"
echo ""
echo "━━━ DWN ━━━"
R=$(rpc "dwn.status")
rpc_ok "$R" && pass "dwn.status" || fail "dwn.status: $R"
echo ""
echo "━━━ Update ━━━"
R=$(rpc "update.status")
rpc_ok "$R" && pass "update.status" || fail "update.status: $R"
R=$(rpc "update.check")
rpc_ok "$R" && pass "update.check" || skip "update.check"
echo ""
echo "━━━ Router ━━━"
R=$(rpc "router.info")
rpc_ok "$R" && pass "router.info" || skip "router.info"
R=$(rpc "router.list-forwards")
rpc_ok "$R" && pass "router.list-forwards" || skip "router.list-forwards"
echo ""
echo "━━━ Health & HTTP endpoints ━━━"
HC=$(curl -s -o /dev/null -w "%{http_code}" "${BASE}/health")
[ "$HC" = "200" ] && pass "/health (200)" || fail "/health ($HC)"
EC=$(curl -s -o /dev/null -w "%{http_code}" "${BASE}/electrs-status")
[ "$EC" = "200" ] && pass "/electrs-status (200)" || fail "/electrs-status ($EC)"
echo ""
echo "━━━ Container Management ━━━"
R=$(rpc "container-list")
rpc_ok "$R" && pass "container-list" || fail "container-list: $R"
R=$(rpc "container-status" '{"app_id":"bitcoin-knots"}')
rpc_ok "$R" && pass "container-status (bitcoin-knots)" || fail "container-status: $R"
echo ""
echo "━━━━━━━━━━━━━ RESULTS ━━━━━━━━━━━━━"
printf "\033[32m Passed: %d\033[0m\n" "$PC"
printf "\033[31m Failed: %d\033[0m\n" "$FC"
printf "\033[33m Skipped: %d\033[0m\n" "$SC"
T=$((PC + FC + SC))
if [ "$FC" -eq 0 ]; then
printf "\n\033[1;32m🎉 ALL %d PASSED (%d skipped)\033[0m\n" "$PC" "$SC"
else
printf "\n\033[1;31m⚠ %d/%d FAILED\033[0m\n" "$FC" "$T"
fi
rm -f "$JAR"