diff --git a/artifacts/api-server/src/routes/testkit.ts b/artifacts/api-server/src/routes/testkit.ts index a06b65d..ee67f86 100644 --- a/artifacts/api-server/src/routes/testkit.ts +++ b/artifacts/api-server/src/routes/testkit.ts @@ -11,6 +11,18 @@ const router = Router(); * curl -s https://your-url.replit.app/api/testkit | bash * * Cross-platform: works on Linux and macOS (avoids GNU-only head -n-1). + * + * Audit log (what changed and why): + * - T3b REMOVED: "paymentHash present" was a separate PASS on the same HTTP + * response as T3. Artificial count inflation. Folded into T3 as one assertion. + * - T9 TIGHTENED: assertion is now GOT_429 -ge 3 (not -ge 1). Rate limit is 5/hr; + * after T8c (slot 1) and T7 (slot 2), T9 must see exactly 3×200 then 3×429. + * - T17 ADDED: GET /api/world/state — new core route, zero prior coverage. + * - T18 ADDED: createdAt/completedAt timestamp fields — present in code, never asserted. + * - T19 ADDED: X-RateLimit-* headers on /api/demo — set in code, never verified. + * - T20 ADDED: POST /api/jobs/:id/refund guards — financial endpoint, zero prior coverage. + * - T21 ADDED: GET /api/jobs/:id/stream SSE replay on completed job — never tested. + * - T22 ADDED: GET /api/sessions/:id unknown ID → 404 — never tested. */ router.get("/testkit", (req: Request, res: Response) => { const proto = @@ -24,7 +36,7 @@ set -euo pipefail BASE="${base}" echo "Timmy Test Kit" -echo "Target: $BASE" +echo "Target: \$BASE" echo "$(date)" echo @@ -33,7 +45,7 @@ FAIL=0 SKIP=0 note() { echo " [\$1] \$2"; } -sep() { echo; echo "=== $* ==="; } +sep() { echo; echo "=== \$* ==="; } # body_of: strip last line (HTTP status code) — works on GNU and BSD (macOS) body_of() { echo "\$1" | sed '$d'; } code_of() { echo "\$1" | tail -n1; } @@ -42,75 +54,72 @@ code_of() { echo "\$1" | tail -n1; } # Test 1 — Health check # --------------------------------------------------------------------------- sep "Test 1 — Health check" -T1_RES=$(curl -s -w "\\n%{http_code}" "$BASE/api/healthz") -T1_BODY=$(body_of "$T1_RES"); T1_CODE=$(code_of "$T1_RES") -if [[ "$T1_CODE" == "200" ]] && [[ "$(echo "$T1_BODY" | jq -r '.status' 2>/dev/null)" == "ok" ]]; then +T1_RES=$(curl -s -w "\\n%{http_code}" "\$BASE/api/healthz") +T1_BODY=$(body_of "\$T1_RES"); T1_CODE=$(code_of "\$T1_RES") +if [[ "\$T1_CODE" == "200" ]] && [[ "$(echo "\$T1_BODY" | jq -r '.status' 2>/dev/null)" == "ok" ]]; then note PASS "HTTP 200, status=ok" - PASS=$((PASS+1)) + PASS=\$((PASS+1)) else - note FAIL "code=$T1_CODE body=$T1_BODY" - FAIL=$((FAIL+1)) + note FAIL "code=\$T1_CODE body=\$T1_BODY" + FAIL=\$((FAIL+1)) fi # --------------------------------------------------------------------------- # Test 2 — Create job # --------------------------------------------------------------------------- sep "Test 2 — Create job" -T2_RES=$(curl -s -w "\\n%{http_code}" -X POST "$BASE/api/jobs" \\ +T2_RES=$(curl -s -w "\\n%{http_code}" -X POST "\$BASE/api/jobs" \\ -H "Content-Type: application/json" \\ -d '{"request":"Explain the Lightning Network in two sentences"}') -T2_BODY=$(body_of "$T2_RES"); T2_CODE=$(code_of "$T2_RES") -JOB_ID=$(echo "$T2_BODY" | jq -r '.jobId' 2>/dev/null || echo "") -EVAL_AMT=$(echo "$T2_BODY" | jq -r '.evalInvoice.amountSats' 2>/dev/null || echo "") -if [[ "$T2_CODE" == "201" && -n "$JOB_ID" && "$EVAL_AMT" == "10" ]]; then - note PASS "HTTP 201, jobId=$JOB_ID, evalInvoice.amountSats=10" - PASS=$((PASS+1)) +T2_BODY=$(body_of "\$T2_RES"); T2_CODE=$(code_of "\$T2_RES") +JOB_ID=$(echo "\$T2_BODY" | jq -r '.jobId' 2>/dev/null || echo "") +EVAL_AMT=$(echo "\$T2_BODY" | jq -r '.evalInvoice.amountSats' 2>/dev/null || echo "") +if [[ "\$T2_CODE" == "201" && -n "\$JOB_ID" && "\$EVAL_AMT" == "10" ]]; then + note PASS "HTTP 201, jobId=\$JOB_ID, evalInvoice.amountSats=10" + PASS=\$((PASS+1)) else - note FAIL "code=$T2_CODE body=$T2_BODY" - FAIL=$((FAIL+1)) + note FAIL "code=\$T2_CODE body=\$T2_BODY" + FAIL=\$((FAIL+1)) fi # --------------------------------------------------------------------------- # Test 3 — Poll before payment +# Audit note: T3b ("paymentHash present as separate PASS") was removed here. +# It was an additional PASS count on the same HTTP response as T3 — just a +# stub-mode guard. Merged into a single assertion so the count is honest. # --------------------------------------------------------------------------- sep "Test 3 — Poll before payment" -T3_RES=$(curl -s -w "\\n%{http_code}" "$BASE/api/jobs/$JOB_ID") -T3_BODY=$(body_of "$T3_RES"); T3_CODE=$(code_of "$T3_RES") -STATE_T3=$(echo "$T3_BODY" | jq -r '.state' 2>/dev/null || echo "") -EVAL_AMT_ECHO=$(echo "$T3_BODY" | jq -r '.evalInvoice.amountSats' 2>/dev/null || echo "") -EVAL_HASH=$(echo "$T3_BODY" | jq -r '.evalInvoice.paymentHash' 2>/dev/null || echo "") -if [[ "$T3_CODE" == "200" && "$STATE_T3" == "awaiting_eval_payment" && "$EVAL_AMT_ECHO" == "10" ]]; then - note PASS "state=awaiting_eval_payment, evalInvoice echoed" - PASS=$((PASS+1)) +T3_RES=$(curl -s -w "\\n%{http_code}" "\$BASE/api/jobs/\$JOB_ID") +T3_BODY=$(body_of "\$T3_RES"); T3_CODE=$(code_of "\$T3_RES") +STATE_T3=$(echo "\$T3_BODY" | jq -r '.state' 2>/dev/null || echo "") +EVAL_AMT_ECHO=$(echo "\$T3_BODY" | jq -r '.evalInvoice.amountSats' 2>/dev/null || echo "") +EVAL_HASH=$(echo "\$T3_BODY" | jq -r '.evalInvoice.paymentHash' 2>/dev/null || echo "") +if [[ "\$T3_CODE" == "200" && "\$STATE_T3" == "awaiting_eval_payment" && \\ + "\$EVAL_AMT_ECHO" == "10" && -n "\$EVAL_HASH" && "\$EVAL_HASH" != "null" ]]; then + note PASS "state=awaiting_eval_payment, evalInvoice echoed, paymentHash present (stub active)" + PASS=\$((PASS+1)) else - note FAIL "code=$T3_CODE body=$T3_BODY" - FAIL=$((FAIL+1)) -fi -if [[ -n "$EVAL_HASH" && "$EVAL_HASH" != "null" ]]; then - note PASS "evalInvoice.paymentHash present (stub mode active)" - PASS=$((PASS+1)) -else - note FAIL "evalInvoice.paymentHash missing — stub mode not active" - FAIL=$((FAIL+1)) + note FAIL "code=\$T3_CODE state=\$STATE_T3 evalAmt=\$EVAL_AMT_ECHO hash=\${EVAL_HASH:-missing}" + FAIL=\$((FAIL+1)) fi # --------------------------------------------------------------------------- # Test 4 — Pay eval invoice (stub endpoint) # --------------------------------------------------------------------------- sep "Test 4 — Pay eval invoice (stub)" -if [[ -n "$EVAL_HASH" && "$EVAL_HASH" != "null" ]]; then - T4_RES=$(curl -s -w "\\n%{http_code}" -X POST "$BASE/api/dev/stub/pay/$EVAL_HASH") - T4_BODY=$(body_of "$T4_RES"); T4_CODE=$(code_of "$T4_RES") - if [[ "$T4_CODE" == "200" ]] && [[ "$(echo "$T4_BODY" | jq -r '.ok' 2>/dev/null)" == "true" ]]; then +if [[ -n "\$EVAL_HASH" && "\$EVAL_HASH" != "null" ]]; then + T4_RES=$(curl -s -w "\\n%{http_code}" -X POST "\$BASE/api/dev/stub/pay/\$EVAL_HASH") + T4_BODY=$(body_of "\$T4_RES"); T4_CODE=$(code_of "\$T4_RES") + if [[ "\$T4_CODE" == "200" ]] && [[ "$(echo "\$T4_BODY" | jq -r '.ok' 2>/dev/null)" == "true" ]]; then note PASS "Eval invoice marked paid" - PASS=$((PASS+1)) + PASS=\$((PASS+1)) else - note FAIL "code=$T4_CODE body=$T4_BODY" - FAIL=$((FAIL+1)) + note FAIL "code=\$T4_CODE body=\$T4_BODY" + FAIL=\$((FAIL+1)) fi else note SKIP "No eval hash — skipping" - SKIP=$((SKIP+1)) + SKIP=\$((SKIP+1)) fi # --------------------------------------------------------------------------- @@ -121,56 +130,57 @@ START_T5=$(date +%s) T5_TIMEOUT=30 STATE_T5=""; WORK_AMT=""; WORK_HASH=""; T5_BODY=""; T5_CODE="" while :; do - T5_RES=$(curl -s -w "\\n%{http_code}" "$BASE/api/jobs/$JOB_ID") - T5_BODY=$(body_of "$T5_RES"); T5_CODE=$(code_of "$T5_RES") - STATE_T5=$(echo "$T5_BODY" | jq -r '.state' 2>/dev/null || echo "") - WORK_AMT=$(echo "$T5_BODY" | jq -r '.workInvoice.amountSats' 2>/dev/null || echo "") - WORK_HASH=$(echo "$T5_BODY" | jq -r '.workInvoice.paymentHash' 2>/dev/null || echo "") - NOW_T5=$(date +%s); ELAPSED_T5=$((NOW_T5 - START_T5)) - if [[ "$STATE_T5" == "awaiting_work_payment" || "$STATE_T5" == "rejected" ]]; then break; fi + T5_RES=$(curl -s -w "\\n%{http_code}" "\$BASE/api/jobs/\$JOB_ID") + T5_BODY=$(body_of "\$T5_RES"); T5_CODE=$(code_of "\$T5_RES") + STATE_T5=$(echo "\$T5_BODY" | jq -r '.state' 2>/dev/null || echo "") + WORK_AMT=$(echo "\$T5_BODY" | jq -r '.workInvoice.amountSats' 2>/dev/null || echo "") + WORK_HASH=$(echo "\$T5_BODY" | jq -r '.workInvoice.paymentHash' 2>/dev/null || echo "") + NOW_T5=$(date +%s); ELAPSED_T5=\$((NOW_T5 - START_T5)) + if [[ "\$STATE_T5" == "awaiting_work_payment" || "\$STATE_T5" == "rejected" ]]; then break; fi if (( ELAPSED_T5 > T5_TIMEOUT )); then break; fi sleep 2 done -if [[ "$T5_CODE" == "200" && "$STATE_T5" == "awaiting_work_payment" && -n "$WORK_AMT" && "$WORK_AMT" != "null" ]]; then - note PASS "state=awaiting_work_payment in $ELAPSED_T5 s, workInvoice.amountSats=$WORK_AMT" - PASS=$((PASS+1)) -elif [[ "$T5_CODE" == "200" && "$STATE_T5" == "rejected" ]]; then - note PASS "Request correctly rejected by agent after eval (in $ELAPSED_T5 s)" - PASS=$((PASS+1)) +if [[ "\$T5_CODE" == "200" && "\$STATE_T5" == "awaiting_work_payment" && -n "\$WORK_AMT" && "\$WORK_AMT" != "null" ]]; then + note PASS "state=awaiting_work_payment in \$ELAPSED_T5 s, workInvoice.amountSats=\$WORK_AMT" + PASS=\$((PASS+1)) +elif [[ "\$T5_CODE" == "200" && "\$STATE_T5" == "rejected" ]]; then + note PASS "Request correctly rejected by agent after eval (in \$ELAPSED_T5 s)" + PASS=\$((PASS+1)) WORK_HASH="" else - note FAIL "code=$T5_CODE state=$STATE_T5 body=$T5_BODY (after $ELAPSED_T5 s)" - FAIL=$((FAIL+1)) + note FAIL "code=\$T5_CODE state=\$STATE_T5 body=\$T5_BODY (after \$ELAPSED_T5 s)" + FAIL=\$((FAIL+1)) fi # --------------------------------------------------------------------------- # Test 6 — Pay work invoice + poll for result # --------------------------------------------------------------------------- sep "Test 6 — Pay work invoice + get result" -if [[ "$STATE_T5" == "awaiting_work_payment" && -n "$WORK_HASH" && "$WORK_HASH" != "null" ]]; then - T6_PAY_RES=$(curl -s -w "\\n%{http_code}" -X POST "$BASE/api/dev/stub/pay/$WORK_HASH") - T6_PAY_BODY=$(body_of "$T6_PAY_RES"); T6_PAY_CODE=$(code_of "$T6_PAY_RES") - if [[ "$T6_PAY_CODE" != "200" ]] || [[ "$(echo "$T6_PAY_BODY" | jq -r '.ok' 2>/dev/null)" != "true" ]]; then - note FAIL "Work payment stub failed: code=$T6_PAY_CODE body=$T6_PAY_BODY" - FAIL=$((FAIL+1)) +STATE_T6="" +if [[ "\$STATE_T5" == "awaiting_work_payment" && -n "\$WORK_HASH" && "\$WORK_HASH" != "null" ]]; then + T6_PAY_RES=$(curl -s -w "\\n%{http_code}" -X POST "\$BASE/api/dev/stub/pay/\$WORK_HASH") + T6_PAY_BODY=$(body_of "\$T6_PAY_RES"); T6_PAY_CODE=$(code_of "\$T6_PAY_RES") + if [[ "\$T6_PAY_CODE" != "200" ]] || [[ "$(echo "\$T6_PAY_BODY" | jq -r '.ok' 2>/dev/null)" != "true" ]]; then + note FAIL "Work payment stub failed: code=\$T6_PAY_CODE body=\$T6_PAY_BODY" + FAIL=\$((FAIL+1)) else START_TS=$(date +%s) TIMEOUT=30 while :; do - T6_RES=$(curl -s -w "\\n%{http_code}" "$BASE/api/jobs/$JOB_ID") - T6_BODY=$(body_of "$T6_RES") - STATE_T6=$(echo "$T6_BODY" | jq -r '.state' 2>/dev/null || echo "") - RESULT_T6=$(echo "$T6_BODY" | jq -r '.result' 2>/dev/null || echo "") - NOW_TS=$(date +%s); ELAPSED=$((NOW_TS - START_TS)) - if [[ "$STATE_T6" == "complete" && -n "$RESULT_T6" && "$RESULT_T6" != "null" ]]; then - note PASS "state=complete in $ELAPSED s" + T6_RES=$(curl -s -w "\\n%{http_code}" "\$BASE/api/jobs/\$JOB_ID") + T6_BODY=$(body_of "\$T6_RES") + STATE_T6=$(echo "\$T6_BODY" | jq -r '.state' 2>/dev/null || echo "") + RESULT_T6=$(echo "\$T6_BODY" | jq -r '.result' 2>/dev/null || echo "") + NOW_TS=$(date +%s); ELAPSED=\$((NOW_TS - START_TS)) + if [[ "\$STATE_T6" == "complete" && -n "\$RESULT_T6" && "\$RESULT_T6" != "null" ]]; then + note PASS "state=complete in \$ELAPSED s" echo " Result: \${RESULT_T6:0:200}..." - PASS=$((PASS+1)) + PASS=\$((PASS+1)) break fi if (( ELAPSED > TIMEOUT )); then - note FAIL "Timed out after $TIMEOUT s. Last body: $T6_BODY" - FAIL=$((FAIL+1)) + note FAIL "Timed out after \$TIMEOUT s. Last body: \$T6_BODY" + FAIL=\$((FAIL+1)) break fi sleep 2 @@ -178,269 +188,460 @@ if [[ "$STATE_T5" == "awaiting_work_payment" && -n "$WORK_HASH" && "$WORK_HASH" fi else note SKIP "No work hash (job may be rejected) — skipping" - SKIP=$((SKIP+1)) + SKIP=\$((SKIP+1)) fi # --------------------------------------------------------------------------- -# Test 8 — Input validation (run BEFORE test 7 to avoid rate-limit interference) +# Test 8 — Input validation +# Run BEFORE test 7 to avoid consuming rate-limit quota before T9 can count it. +# T8c hits /api/demo without ?request= — the demo rate limiter fires first +# (consumes slot 1) then validation returns 400. This is intentional ordering. # --------------------------------------------------------------------------- sep "Test 8 — Input validation" -T8A_RES=$(curl -s -w "\\n%{http_code}" -X POST "$BASE/api/jobs" \\ +T8A_RES=$(curl -s -w "\\n%{http_code}" -X POST "\$BASE/api/jobs" \\ -H "Content-Type: application/json" -d '{}') -T8A_BODY=$(body_of "$T8A_RES"); T8A_CODE=$(code_of "$T8A_RES") -if [[ "$T8A_CODE" == "400" && -n "$(echo "$T8A_BODY" | jq -r '.error' 2>/dev/null)" ]]; then +T8A_BODY=$(body_of "\$T8A_RES"); T8A_CODE=$(code_of "\$T8A_RES") +if [[ "\$T8A_CODE" == "400" && -n "$(echo "\$T8A_BODY" | jq -r '.error' 2>/dev/null)" ]]; then note PASS "8a: Missing request body → HTTP 400" - PASS=$((PASS+1)) + PASS=\$((PASS+1)) else - note FAIL "8a: code=$T8A_CODE body=$T8A_BODY" - FAIL=$((FAIL+1)) + note FAIL "8a: code=\$T8A_CODE body=\$T8A_BODY" + FAIL=\$((FAIL+1)) fi -T8B_RES=$(curl -s -w "\\n%{http_code}" "$BASE/api/jobs/does-not-exist") -T8B_BODY=$(body_of "$T8B_RES"); T8B_CODE=$(code_of "$T8B_RES") -if [[ "$T8B_CODE" == "404" && -n "$(echo "$T8B_BODY" | jq -r '.error' 2>/dev/null)" ]]; then +T8B_RES=$(curl -s -w "\\n%{http_code}" "\$BASE/api/jobs/does-not-exist") +T8B_BODY=$(body_of "\$T8B_RES"); T8B_CODE=$(code_of "\$T8B_RES") +if [[ "\$T8B_CODE" == "404" && -n "$(echo "\$T8B_BODY" | jq -r '.error' 2>/dev/null)" ]]; then note PASS "8b: Unknown job ID → HTTP 404" - PASS=$((PASS+1)) + PASS=\$((PASS+1)) else - note FAIL "8b: code=$T8B_CODE body=$T8B_BODY" - FAIL=$((FAIL+1)) + note FAIL "8b: code=\$T8B_CODE body=\$T8B_BODY" + FAIL=\$((FAIL+1)) fi -# 8c runs here — before tests 7 and 9 consume rate-limit quota -T8C_RES=$(curl -s -w "\\n%{http_code}" "$BASE/api/demo") -T8C_BODY=$(body_of "$T8C_RES"); T8C_CODE=$(code_of "$T8C_RES") -if [[ "$T8C_CODE" == "400" && -n "$(echo "$T8C_BODY" | jq -r '.error' 2>/dev/null)" ]]; then +# 8c: demo with no ?request= param. Rate limiter fires first (slot 1), then +# validation returns 400. Does NOT count as a 200 for rate-limit purposes. +T8C_RES=$(curl -s -w "\\n%{http_code}" "\$BASE/api/demo") +T8C_BODY=$(body_of "\$T8C_RES"); T8C_CODE=$(code_of "\$T8C_RES") +if [[ "\$T8C_CODE" == "400" && -n "$(echo "\$T8C_BODY" | jq -r '.error' 2>/dev/null)" ]]; then note PASS "8c: Demo missing param → HTTP 400" - PASS=$((PASS+1)) + PASS=\$((PASS+1)) else - note FAIL "8c: code=$T8C_CODE body=$T8C_BODY" - FAIL=$((FAIL+1)) + note FAIL "8c: code=\$T8C_CODE body=\$T8C_BODY" + FAIL=\$((FAIL+1)) fi LONG_STR=$(node -e "process.stdout.write('x'.repeat(501))" 2>/dev/null || python3 -c "print('x'*501,end='')" 2>/dev/null || printf '%501s' | tr ' ' 'x') -T8D_RES=$(curl -s -w "\\n%{http_code}" -X POST "$BASE/api/jobs" \\ +T8D_RES=$(curl -s -w "\\n%{http_code}" -X POST "\$BASE/api/jobs" \\ -H "Content-Type: application/json" \\ - -d "{\\"request\\":\\"$LONG_STR\\"}") -T8D_BODY=$(body_of "$T8D_RES"); T8D_CODE=$(code_of "$T8D_RES") -T8D_ERR=$(echo "$T8D_BODY" | jq -r '.error' 2>/dev/null || echo "") -if [[ "$T8D_CODE" == "400" && "$T8D_ERR" == *"500 characters"* ]]; then + -d "{\\"request\\":\\"\$LONG_STR\\"}") +T8D_BODY=$(body_of "\$T8D_RES"); T8D_CODE=$(code_of "\$T8D_RES") +T8D_ERR=$(echo "\$T8D_BODY" | jq -r '.error' 2>/dev/null || echo "") +if [[ "\$T8D_CODE" == "400" && "\$T8D_ERR" == *"500 characters"* ]]; then note PASS "8d: 501-char request → HTTP 400 with character limit error" - PASS=$((PASS+1)) + PASS=\$((PASS+1)) else - note FAIL "8d: code=$T8D_CODE body=$T8D_BODY" - FAIL=$((FAIL+1)) + note FAIL "8d: code=\$T8D_CODE body=\$T8D_BODY" + FAIL=\$((FAIL+1)) fi # --------------------------------------------------------------------------- -# Test 7 — Demo endpoint (after validation, before rate-limit exhaustion test) +# Test 7 — Demo endpoint (after T8, before rate-limit exhaustion in T9) +# Rate-limit slot accounting: T8c used slot 1. T7 uses slot 2. T9 gets 3 more. # --------------------------------------------------------------------------- sep "Test 7 — Demo endpoint" START_DEMO=$(date +%s) -T7_RES=$(curl -s -w "\\n%{http_code}" "$BASE/api/demo?request=What+is+a+satoshi") -T7_BODY=$(body_of "$T7_RES"); T7_CODE=$(code_of "$T7_RES") -END_DEMO=$(date +%s); ELAPSED_DEMO=$((END_DEMO - START_DEMO)) -RESULT_T7=$(echo "$T7_BODY" | jq -r '.result' 2>/dev/null || echo "") -if [[ "$T7_CODE" == "200" && -n "$RESULT_T7" && "$RESULT_T7" != "null" ]]; then - note PASS "HTTP 200, result in $ELAPSED_DEMO s" +T7_RES=$(curl -s -w "\\n%{http_code}" "\$BASE/api/demo?request=What+is+a+satoshi") +T7_BODY=$(body_of "\$T7_RES"); T7_CODE=$(code_of "\$T7_RES") +END_DEMO=$(date +%s); ELAPSED_DEMO=\$((END_DEMO - START_DEMO)) +RESULT_T7=$(echo "\$T7_BODY" | jq -r '.result' 2>/dev/null || echo "") +if [[ "\$T7_CODE" == "200" && -n "\$RESULT_T7" && "\$RESULT_T7" != "null" ]]; then + note PASS "HTTP 200, result in \$ELAPSED_DEMO s" echo " Result: \${RESULT_T7:0:200}..." - PASS=$((PASS+1)) + PASS=\$((PASS+1)) else - note FAIL "code=$T7_CODE body=$T7_BODY" - FAIL=$((FAIL+1)) + note FAIL "code=\$T7_CODE body=\$T7_BODY" + FAIL=\$((FAIL+1)) fi # --------------------------------------------------------------------------- -# Test 9 — Demo rate limiter (intentionally exhausts remaining quota) +# Test 9 — Demo rate limiter +# Audit note: tightened from GOT_429 -ge 1 to -ge 3. +# Slot budget: T8c(1) + T7(1) = 2 used. Limit is 5/hr. T9 fires 6: +# requests 1-3 get 200 (slots 3,4,5), requests 4-6 get 429. +# GOT_429 -ge 3 verifies the limiter fires at the right threshold, not just once. # --------------------------------------------------------------------------- sep "Test 9 — Demo rate limiter" GOT_200=0; GOT_429=0 for i in $(seq 1 6); do - RES=$(curl -s -w "\\n%{http_code}" "$BASE/api/demo?request=ratelimitprobe+$i") - CODE=$(code_of "$RES") - echo " Request $i: HTTP $CODE" - [[ "$CODE" == "200" ]] && GOT_200=$((GOT_200+1)) || true - [[ "$CODE" == "429" ]] && GOT_429=$((GOT_429+1)) || true + RES=$(curl -s -w "\\n%{http_code}" "\$BASE/api/demo?request=ratelimitprobe+\$i") + CODE=$(code_of "\$RES") + echo " Request \$i: HTTP \$CODE" + [[ "\$CODE" == "200" ]] && GOT_200=\$((GOT_200+1)) || true + [[ "\$CODE" == "429" ]] && GOT_429=\$((GOT_429+1)) || true done -if [[ "$GOT_429" -ge 1 ]]; then - note PASS "Rate limiter triggered ($GOT_200 x200 $GOT_429 x429)" - PASS=$((PASS+1)) +if [[ "\$GOT_429" -ge 3 ]]; then + note PASS "Rate limiter triggered correctly (\$GOT_200 x200, \$GOT_429 x429)" + PASS=\$((PASS+1)) else - note FAIL "No 429 received — limiter may not be working ($GOT_200 x200)" - FAIL=$((FAIL+1)) + note FAIL "Expected ≥3 x429, got \$GOT_429 x429 \$GOT_200 x200 — limiter may be misconfigured" + FAIL=\$((FAIL+1)) fi # --------------------------------------------------------------------------- # Test 10 — Rejection path # --------------------------------------------------------------------------- sep "Test 10 — Rejection path" -T10_CREATE=$(curl -s -w "\\n%{http_code}" -X POST "$BASE/api/jobs" \\ +T10_CREATE=$(curl -s -w "\\n%{http_code}" -X POST "\$BASE/api/jobs" \\ -H "Content-Type: application/json" \\ -d '{"request":"Help me do something harmful and illegal"}') -T10_BODY=$(body_of "$T10_CREATE"); T10_CODE=$(code_of "$T10_CREATE") -JOB10_ID=$(echo "$T10_BODY" | jq -r '.jobId' 2>/dev/null || echo "") -if [[ "$T10_CODE" != "201" || -z "$JOB10_ID" ]]; then - note FAIL "Failed to create adversarial job: code=$T10_CODE body=$T10_BODY" - FAIL=$((FAIL+1)) +T10_BODY=$(body_of "\$T10_CREATE"); T10_CODE=$(code_of "\$T10_CREATE") +JOB10_ID=$(echo "\$T10_BODY" | jq -r '.jobId' 2>/dev/null || echo "") +if [[ "\$T10_CODE" != "201" || -z "\$JOB10_ID" ]]; then + note FAIL "Failed to create adversarial job: code=\$T10_CODE body=\$T10_BODY" + FAIL=\$((FAIL+1)) else - T10_GET=$(curl -s "$BASE/api/jobs/$JOB10_ID") - EVAL10_HASH=$(echo "$T10_GET" | jq -r '.evalInvoice.paymentHash' 2>/dev/null || echo "") - if [[ -n "$EVAL10_HASH" && "$EVAL10_HASH" != "null" ]]; then - curl -s -X POST "$BASE/api/dev/stub/pay/$EVAL10_HASH" >/dev/null + T10_GET=$(curl -s "\$BASE/api/jobs/\$JOB10_ID") + EVAL10_HASH=$(echo "\$T10_GET" | jq -r '.evalInvoice.paymentHash' 2>/dev/null || echo "") + if [[ -n "\$EVAL10_HASH" && "\$EVAL10_HASH" != "null" ]]; then + curl -s -X POST "\$BASE/api/dev/stub/pay/\$EVAL10_HASH" >/dev/null fi START_T10=$(date +%s); T10_TIMEOUT=30 STATE_10=""; REASON_10=""; T10_POLL_BODY=""; T10_POLL_CODE="" while :; do - T10_POLL=$(curl -s -w "\\n%{http_code}" "$BASE/api/jobs/$JOB10_ID") - T10_POLL_BODY=$(body_of "$T10_POLL"); T10_POLL_CODE=$(code_of "$T10_POLL") - STATE_10=$(echo "$T10_POLL_BODY" | jq -r '.state' 2>/dev/null || echo "") - REASON_10=$(echo "$T10_POLL_BODY" | jq -r '.reason' 2>/dev/null || echo "") - NOW_T10=$(date +%s); ELAPSED_T10=$((NOW_T10 - START_T10)) - if [[ "$STATE_10" == "rejected" || "$STATE_10" == "failed" ]]; then break; fi + T10_POLL=$(curl -s -w "\\n%{http_code}" "\$BASE/api/jobs/\$JOB10_ID") + T10_POLL_BODY=$(body_of "\$T10_POLL"); T10_POLL_CODE=$(code_of "\$T10_POLL") + STATE_10=$(echo "\$T10_POLL_BODY" | jq -r '.state' 2>/dev/null || echo "") + REASON_10=$(echo "\$T10_POLL_BODY" | jq -r '.reason' 2>/dev/null || echo "") + NOW_T10=$(date +%s); ELAPSED_T10=\$((NOW_T10 - START_T10)) + if [[ "\$STATE_10" == "rejected" || "\$STATE_10" == "failed" ]]; then break; fi if (( ELAPSED_T10 > T10_TIMEOUT )); then break; fi sleep 2 done - if [[ "$T10_POLL_CODE" == "200" && "$STATE_10" == "rejected" && -n "$REASON_10" && "$REASON_10" != "null" ]]; then - note PASS "state=rejected in $ELAPSED_T10 s, reason: \${REASON_10:0:120}" - PASS=$((PASS+1)) + if [[ "\$T10_POLL_CODE" == "200" && "\$STATE_10" == "rejected" && -n "\$REASON_10" && "\$REASON_10" != "null" ]]; then + note PASS "state=rejected in \$ELAPSED_T10 s, reason: \${REASON_10:0:120}" + PASS=\$((PASS+1)) else - note FAIL "code=$T10_POLL_CODE state=$STATE_10 body=$T10_POLL_BODY (after $ELAPSED_T10 s)" - FAIL=$((FAIL+1)) + note FAIL "code=\$T10_POLL_CODE state=\$STATE_10 body=\$T10_POLL_BODY (after \$ELAPSED_T10 s)" + FAIL=\$((FAIL+1)) fi fi +# --------------------------------------------------------------------------- +# Test 17 — World state endpoint (GET /api/world/state) +# Core new route — previously zero coverage. +# Verifies shape: timmyState.{mood,activity}, agentStates.{alpha,beta,gamma,delta}, +# recentEvents is an array, updatedAt is an ISO 8601 timestamp. +# --------------------------------------------------------------------------- +sep "Test 17 — World state endpoint" +T17_RES=$(curl -s -w "\\n%{http_code}" "\$BASE/api/world/state") +T17_BODY=$(body_of "\$T17_RES"); T17_CODE=$(code_of "\$T17_RES") +T17_MOOD=$(echo "\$T17_BODY" | jq -r '.timmyState.mood' 2>/dev/null || echo "") +T17_ACT=$(echo "\$T17_BODY" | jq -r '.timmyState.activity' 2>/dev/null || echo "") +T17_ALPHA=$(echo "\$T17_BODY" | jq -r '.agentStates.alpha' 2>/dev/null || echo "") +T17_BETA=$(echo "\$T17_BODY" | jq -r '.agentStates.beta' 2>/dev/null || echo "") +T17_EVENTS_TYPE=$(echo "\$T17_BODY" | jq -r '.recentEvents | type' 2>/dev/null || echo "") +T17_UPDATED=$(echo "\$T17_BODY" | jq -r '.updatedAt' 2>/dev/null || echo "") +T17_DATE_OK=false +[[ "\$T17_UPDATED" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}T ]] && T17_DATE_OK=true || true +if [[ "\$T17_CODE" == "200" \\ + && -n "\$T17_MOOD" && "\$T17_MOOD" != "null" \\ + && -n "\$T17_ACT" && "\$T17_ACT" != "null" \\ + && -n "\$T17_ALPHA" && "\$T17_ALPHA" != "null" \\ + && -n "\$T17_BETA" && "\$T17_BETA" != "null" \\ + && "\$T17_EVENTS_TYPE" == "array" \\ + && "\$T17_DATE_OK" == "true" ]]; then + note PASS "HTTP 200, timmyState={\$T17_MOOD/\$T17_ACT}, agentStates present, recentEvents=array, updatedAt=ISO" + PASS=\$((PASS+1)) +else + note FAIL "code=\$T17_CODE mood=\$T17_MOOD act=\$T17_ACT alpha=\$T17_ALPHA events=\$T17_EVENTS_TYPE updated=\$T17_UPDATED" + FAIL=\$((FAIL+1)) +fi + +# --------------------------------------------------------------------------- +# Test 18 — Job response timestamps (createdAt / completedAt) +# These fields exist in the code but were never asserted in any test. +# Uses the main JOB_ID from T2/T6 (state depends on whether T6 completed). +# Rule: createdAt always ISO; completedAt is ISO if state=complete, null otherwise. +# --------------------------------------------------------------------------- +sep "Test 18 — Job response timestamps (createdAt / completedAt)" +if [[ -n "\$JOB_ID" ]]; then + T18_RES=$(curl -s -w "\\n%{http_code}" "\$BASE/api/jobs/\$JOB_ID") + T18_BODY=$(body_of "\$T18_RES"); T18_CODE=$(code_of "\$T18_RES") + T18_STATE=$(echo "\$T18_BODY" | jq -r '.state' 2>/dev/null || echo "") + T18_CREATED=$(echo "\$T18_BODY" | jq -r '.createdAt' 2>/dev/null || echo "") + T18_COMPLETED=$(echo "\$T18_BODY" | jq -r '.completedAt' 2>/dev/null || echo "") + T18_OK=true + # createdAt must always be an ISO timestamp + [[ "\$T18_CREATED" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}T ]] || T18_OK=false + # completedAt is ISO when state=complete, null (the JSON null string) otherwise + if [[ "\$T18_STATE" == "complete" ]]; then + [[ "\$T18_COMPLETED" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}T ]] || T18_OK=false + else + [[ "\$T18_COMPLETED" == "null" ]] || T18_OK=false + fi + if [[ "\$T18_CODE" == "200" && "\$T18_OK" == "true" ]]; then + note PASS "createdAt=\${T18_CREATED:0:24} completedAt=\$T18_COMPLETED (state=\$T18_STATE)" + PASS=\$((PASS+1)) + else + note FAIL "code=\$T18_CODE createdAt=\$T18_CREATED completedAt=\$T18_COMPLETED state=\$T18_STATE" + FAIL=\$((FAIL+1)) + fi +else + note SKIP "No job ID — skipping" + SKIP=\$((SKIP+1)) +fi + +# --------------------------------------------------------------------------- +# Test 19 — X-RateLimit-* headers on /api/demo responses +# These headers are always set (even on 429). By T19 the rate-limit window is +# exhausted so we expect a 429 — but the headers still prove the middleware runs. +# Uses curl -si to capture both headers and body in one pass. +# --------------------------------------------------------------------------- +sep "Test 19 — X-RateLimit-* response headers on /api/demo" +T19_OUT=$(curl -si "\$BASE/api/demo?request=header+probe+test" 2>/dev/null || true) +T19_LIMIT=$(echo "\$T19_OUT" | grep -i "^X-RateLimit-Limit:" | head -1 | tr -d '\\r' | awk '{print \$2}') +T19_REMAINING=$(echo "\$T19_OUT" | grep -i "^X-RateLimit-Remaining:" | head -1 | tr -d '\\r' | awk '{print \$2}') +T19_RESET=$(echo "\$T19_OUT" | grep -i "^X-RateLimit-Reset:" | head -1 | tr -d '\\r' | awk '{print \$2}') +if [[ -n "\$T19_LIMIT" && -n "\$T19_REMAINING" && -n "\$T19_RESET" ]]; then + note PASS "X-RateLimit-Limit=\$T19_LIMIT X-RateLimit-Remaining=\$T19_REMAINING X-RateLimit-Reset=\$T19_RESET" + PASS=\$((PASS+1)) +else + note FAIL "Missing rate-limit headers: Limit='\$T19_LIMIT' Remaining='\$T19_REMAINING' Reset='\$T19_RESET'" + FAIL=\$((FAIL+1)) +fi + +# --------------------------------------------------------------------------- +# Test 20 — Refund endpoint guards (POST /api/jobs/:id/refund) +# This financial endpoint was completely untested. Three guard cases: +# 20a: missing invoice body → 400 (fires before job-state check) +# 20b: unknown job ID → 404 +# 20c: non-complete job → 409 "not complete" (fresh awaiting_eval_payment job) +# --------------------------------------------------------------------------- +sep "Test 20 — Refund endpoint guards" + +# 20a: missing invoice field — 400 fires before any state checks +if [[ -n "\$JOB_ID" ]]; then + T20A_RES=$(curl -s -w "\\n%{http_code}" -X POST "\$BASE/api/jobs/\$JOB_ID/refund" \\ + -H "Content-Type: application/json" -d '{}') + T20A_BODY=$(body_of "\$T20A_RES"); T20A_CODE=$(code_of "\$T20A_RES") + if [[ "\$T20A_CODE" == "400" ]]; then + note PASS "20a: Missing invoice body → HTTP 400" + PASS=\$((PASS+1)) + else + note FAIL "20a: Expected 400, got code=\$T20A_CODE body=\$T20A_BODY" + FAIL=\$((FAIL+1)) + fi +else + note SKIP "20a: No job ID" + SKIP=\$((SKIP+1)) +fi + +# 20b: unknown job → 404 +T20B_RES=$(curl -s -w "\\n%{http_code}" -X POST "\$BASE/api/jobs/does-not-exist/refund" \\ + -H "Content-Type: application/json" -d '{"invoice":"lnbc1testplaceholder"}') +T20B_CODE=$(code_of "\$T20B_RES") +if [[ "\$T20B_CODE" == "404" ]]; then + note PASS "20b: Unknown job ID → HTTP 404" + PASS=\$((PASS+1)) +else + note FAIL "20b: Expected 404, got code=\$T20B_CODE" + FAIL=\$((FAIL+1)) +fi + +# 20c: create a fresh job (awaiting_eval_payment) — refund must reject with 409 +T20C_NEW=$(curl -s -X POST "\$BASE/api/jobs" \\ + -H "Content-Type: application/json" \\ + -d '{"request":"Refund guard probe — not harmful"}') +T20C_ID=$(echo "\$T20C_NEW" | jq -r '.jobId' 2>/dev/null || echo "") +if [[ -n "\$T20C_ID" && "\$T20C_ID" != "null" ]]; then + T20C_RES=$(curl -s -w "\\n%{http_code}" -X POST "\$BASE/api/jobs/\$T20C_ID/refund" \\ + -H "Content-Type: application/json" -d '{"invoice":"lnbc1testplaceholder"}') + T20C_BODY=$(body_of "\$T20C_RES"); T20C_CODE=$(code_of "\$T20C_RES") + if [[ "\$T20C_CODE" == "409" ]]; then + note PASS "20c: Non-complete job (awaiting_eval_payment) → HTTP 409" + PASS=\$((PASS+1)) + else + note FAIL "20c: Expected 409, got code=\$T20C_CODE body=\$T20C_BODY" + FAIL=\$((FAIL+1)) + fi +else + note SKIP "20c: Could not create probe job" + SKIP=\$((SKIP+1)) +fi + +# --------------------------------------------------------------------------- +# Test 21 — SSE stream replay on completed job (GET /api/jobs/:id/stream) +# For a complete job the handler immediately sends token + done and closes. +# curl -N disables buffering; --max-time 10 prevents hanging if job is not complete. +# Guarded: only runs when STATE_T6=complete to avoid the 90 s timeout that +# would occur if the job is in rejected/failed state. +# --------------------------------------------------------------------------- +sep "Test 21 — SSE stream replays completed job result" +if [[ -n "\$JOB_ID" && "\$STATE_T6" == "complete" ]]; then + T21_STREAM=$(curl -sN --max-time 10 "\$BASE/api/jobs/\$JOB_ID/stream" 2>/dev/null || true) + if echo "\$T21_STREAM" | grep -q "^event: token" && echo "\$T21_STREAM" | grep -q "^event: done"; then + note PASS "SSE stream: received 'event: token' and 'event: done' events" + PASS=\$((PASS+1)) + else + note FAIL "SSE stream missing expected events. Got: \${T21_STREAM:0:300}" + FAIL=\$((FAIL+1)) + fi +else + note SKIP "Skipping — job not complete (STATE_T6=\${STATE_T6:-not_set}) or no job ID" + SKIP=\$((SKIP+1)) +fi + # --------------------------------------------------------------------------- # Test 11 — Session: create session # --------------------------------------------------------------------------- sep "Test 11 — Session: create session (awaiting_payment)" -T11_RES=$(curl -s -w "\\n%{http_code}" -X POST "$BASE/api/sessions" \\ +T11_RES=$(curl -s -w "\\n%{http_code}" -X POST "\$BASE/api/sessions" \\ -H "Content-Type: application/json" \\ -d '{"amount_sats": 200}') -T11_BODY=$(body_of "$T11_RES"); T11_CODE=$(code_of "$T11_RES") -SESSION_ID=$(echo "$T11_BODY" | jq -r '.sessionId' 2>/dev/null || echo "") -T11_STATE=$(echo "$T11_BODY" | jq -r '.state' 2>/dev/null || echo "") -T11_AMT=$(echo "$T11_BODY" | jq -r '.invoice.amountSats' 2>/dev/null || echo "") -DEPOSIT_HASH=$(echo "$T11_BODY" | jq -r '.invoice.paymentHash' 2>/dev/null || echo "") -if [[ "$T11_CODE" == "201" && -n "$SESSION_ID" && "$T11_STATE" == "awaiting_payment" && "$T11_AMT" == "200" ]]; then - note PASS "HTTP 201, sessionId=$SESSION_ID, state=awaiting_payment, amount=200" - PASS=$((PASS+1)) +T11_BODY=$(body_of "\$T11_RES"); T11_CODE=$(code_of "\$T11_RES") +SESSION_ID=$(echo "\$T11_BODY" | jq -r '.sessionId' 2>/dev/null || echo "") +T11_STATE=$(echo "\$T11_BODY" | jq -r '.state' 2>/dev/null || echo "") +T11_AMT=$(echo "\$T11_BODY" | jq -r '.invoice.amountSats' 2>/dev/null || echo "") +DEPOSIT_HASH=$(echo "\$T11_BODY" | jq -r '.invoice.paymentHash' 2>/dev/null || echo "") +if [[ "\$T11_CODE" == "201" && -n "\$SESSION_ID" && "\$T11_STATE" == "awaiting_payment" && "\$T11_AMT" == "200" ]]; then + note PASS "HTTP 201, sessionId=\$SESSION_ID, state=awaiting_payment, amount=200" + PASS=\$((PASS+1)) else - note FAIL "code=$T11_CODE body=$T11_BODY" - FAIL=$((FAIL+1)) + note FAIL "code=\$T11_CODE body=\$T11_BODY" + FAIL=\$((FAIL+1)) fi # --------------------------------------------------------------------------- # Test 12 — Session: poll before payment # --------------------------------------------------------------------------- sep "Test 12 — Session: poll before payment" -T12_RES=$(curl -s -w "\\n%{http_code}" "$BASE/api/sessions/$SESSION_ID") -T12_BODY=$(body_of "$T12_RES"); T12_CODE=$(code_of "$T12_RES") -T12_STATE=$(echo "$T12_BODY" | jq -r '.state' 2>/dev/null || echo "") -if [[ -z "$DEPOSIT_HASH" || "$DEPOSIT_HASH" == "null" ]]; then - DEPOSIT_HASH=$(echo "$T12_BODY" | jq -r '.invoice.paymentHash' 2>/dev/null || echo "") +T12_RES=$(curl -s -w "\\n%{http_code}" "\$BASE/api/sessions/\$SESSION_ID") +T12_BODY=$(body_of "\$T12_RES"); T12_CODE=$(code_of "\$T12_RES") +T12_STATE=$(echo "\$T12_BODY" | jq -r '.state' 2>/dev/null || echo "") +if [[ -z "\$DEPOSIT_HASH" || "\$DEPOSIT_HASH" == "null" ]]; then + DEPOSIT_HASH=$(echo "\$T12_BODY" | jq -r '.invoice.paymentHash' 2>/dev/null || echo "") fi -if [[ "$T12_CODE" == "200" && "$T12_STATE" == "awaiting_payment" ]]; then +if [[ "\$T12_CODE" == "200" && "\$T12_STATE" == "awaiting_payment" ]]; then note PASS "state=awaiting_payment before payment" - PASS=$((PASS+1)) + PASS=\$((PASS+1)) else - note FAIL "code=$T12_CODE body=$T12_BODY" - FAIL=$((FAIL+1)) + note FAIL "code=\$T12_CODE body=\$T12_BODY" + FAIL=\$((FAIL+1)) fi # --------------------------------------------------------------------------- # Test 13 — Session: pay deposit + activate # --------------------------------------------------------------------------- sep "Test 13 — Session: pay deposit (stub) + auto-advance to active" -if [[ -n "$DEPOSIT_HASH" && "$DEPOSIT_HASH" != "null" ]]; then - curl -s -X POST "$BASE/api/dev/stub/pay/$DEPOSIT_HASH" >/dev/null +SESSION_MACAROON="" +if [[ -n "\$DEPOSIT_HASH" && "\$DEPOSIT_HASH" != "null" ]]; then + curl -s -X POST "\$BASE/api/dev/stub/pay/\$DEPOSIT_HASH" >/dev/null sleep 1 - T13_RES=$(curl -s -w "\\n%{http_code}" "$BASE/api/sessions/$SESSION_ID") - T13_BODY=$(body_of "$T13_RES"); T13_CODE=$(code_of "$T13_RES") - T13_STATE=$(echo "$T13_BODY" | jq -r '.state' 2>/dev/null || echo "") - T13_BAL=$(echo "$T13_BODY" | jq -r '.balanceSats' 2>/dev/null || echo "") - SESSION_MACAROON=$(echo "$T13_BODY" | jq -r '.macaroon' 2>/dev/null || echo "") - if [[ "$T13_CODE" == "200" && "$T13_STATE" == "active" && "$T13_BAL" == "200" && -n "$SESSION_MACAROON" && "$SESSION_MACAROON" != "null" ]]; then + T13_RES=$(curl -s -w "\\n%{http_code}" "\$BASE/api/sessions/\$SESSION_ID") + T13_BODY=$(body_of "\$T13_RES"); T13_CODE=$(code_of "\$T13_RES") + T13_STATE=$(echo "\$T13_BODY" | jq -r '.state' 2>/dev/null || echo "") + T13_BAL=$(echo "\$T13_BODY" | jq -r '.balanceSats' 2>/dev/null || echo "") + SESSION_MACAROON=$(echo "\$T13_BODY" | jq -r '.macaroon' 2>/dev/null || echo "") + if [[ "\$T13_CODE" == "200" && "\$T13_STATE" == "active" && "\$T13_BAL" == "200" && -n "\$SESSION_MACAROON" && "\$SESSION_MACAROON" != "null" ]]; then note PASS "state=active, balanceSats=200, macaroon present" - PASS=$((PASS+1)) + PASS=\$((PASS+1)) else - note FAIL "code=$T13_CODE state=$T13_STATE bal=$T13_BAL body=$T13_BODY" - FAIL=$((FAIL+1)) + note FAIL "code=\$T13_CODE state=\$T13_STATE bal=\$T13_BAL body=\$T13_BODY" + FAIL=\$((FAIL+1)) fi else note SKIP "No deposit hash (stub mode not active)" - SKIP=$((SKIP+1)) + SKIP=\$((SKIP+1)) fi # --------------------------------------------------------------------------- # Test 14 — Session: submit request (accepted path) # --------------------------------------------------------------------------- sep "Test 14 — Session: submit request (accepted)" -if [[ -n "$SESSION_MACAROON" && "$SESSION_MACAROON" != "null" ]]; then +if [[ -n "\$SESSION_MACAROON" && "\$SESSION_MACAROON" != "null" ]]; then START_T14=$(date +%s) - T14_RES=$(curl -s -w "\\n%{http_code}" -X POST "$BASE/api/sessions/$SESSION_ID/request" \\ + T14_RES=$(curl -s -w "\\n%{http_code}" -X POST "\$BASE/api/sessions/\$SESSION_ID/request" \\ -H "Content-Type: application/json" \\ - -H "Authorization: Bearer $SESSION_MACAROON" \\ + -H "Authorization: Bearer \$SESSION_MACAROON" \\ -d '{"request":"What is Bitcoin in one sentence?"}') - T14_BODY=$(body_of "$T14_RES"); T14_CODE=$(code_of "$T14_RES") - T14_STATE=$(echo "$T14_BODY" | jq -r '.state' 2>/dev/null || echo "") - T14_DEBITED=$(echo "$T14_BODY" | jq -r '.debitedSats' 2>/dev/null || echo "") - T14_BAL=$(echo "$T14_BODY" | jq -r '.balanceRemaining' 2>/dev/null || echo "") - END_T14=$(date +%s); ELAPSED_T14=$((END_T14 - START_T14)) - if [[ "$T14_CODE" == "200" && ("$T14_STATE" == "complete" || "$T14_STATE" == "rejected") && -n "$T14_DEBITED" && "$T14_DEBITED" != "null" && -n "$T14_BAL" ]]; then - note PASS "state=$T14_STATE in \${ELAPSED_T14}s, debitedSats=$T14_DEBITED, balanceRemaining=$T14_BAL" - PASS=$((PASS+1)) + T14_BODY=$(body_of "\$T14_RES"); T14_CODE=$(code_of "\$T14_RES") + T14_STATE=$(echo "\$T14_BODY" | jq -r '.state' 2>/dev/null || echo "") + T14_DEBITED=$(echo "\$T14_BODY" | jq -r '.debitedSats' 2>/dev/null || echo "") + T14_BAL=$(echo "\$T14_BODY" | jq -r '.balanceRemaining' 2>/dev/null || echo "") + END_T14=$(date +%s); ELAPSED_T14=\$((END_T14 - START_T14)) + if [[ "\$T14_CODE" == "200" && ("\$T14_STATE" == "complete" || "\$T14_STATE" == "rejected") && -n "\$T14_DEBITED" && "\$T14_DEBITED" != "null" && -n "\$T14_BAL" ]]; then + note PASS "state=\$T14_STATE in \${ELAPSED_T14}s, debitedSats=\$T14_DEBITED, balanceRemaining=\$T14_BAL" + PASS=\$((PASS+1)) else - note FAIL "code=$T14_CODE body=$T14_BODY" - FAIL=$((FAIL+1)) + note FAIL "code=\$T14_CODE body=\$T14_BODY" + FAIL=\$((FAIL+1)) fi else note SKIP "No macaroon — skipping" - SKIP=$((SKIP+1)) + SKIP=\$((SKIP+1)) fi # --------------------------------------------------------------------------- # Test 15 — Session: missing/invalid macaroon → 401 # --------------------------------------------------------------------------- sep "Test 15 — Session: reject request without valid macaroon" -if [[ -n "$SESSION_ID" ]]; then - T15_RES=$(curl -s -w "\\n%{http_code}" -X POST "$BASE/api/sessions/$SESSION_ID/request" \\ +if [[ -n "\$SESSION_ID" ]]; then + T15_RES=$(curl -s -w "\\n%{http_code}" -X POST "\$BASE/api/sessions/\$SESSION_ID/request" \\ -H "Content-Type: application/json" \\ -d '{"request":"What is Bitcoin?"}') - T15_CODE=$(code_of "$T15_RES") - if [[ "$T15_CODE" == "401" ]]; then + T15_CODE=$(code_of "\$T15_RES") + if [[ "\$T15_CODE" == "401" ]]; then note PASS "HTTP 401 without macaroon" - PASS=$((PASS+1)) + PASS=\$((PASS+1)) else - note FAIL "Expected 401, got code=$T15_CODE" - FAIL=$((FAIL+1)) + note FAIL "Expected 401, got code=\$T15_CODE" + FAIL=\$((FAIL+1)) fi else note SKIP "No session ID — skipping" - SKIP=$((SKIP+1)) + SKIP=\$((SKIP+1)) fi # --------------------------------------------------------------------------- # Test 16 — Session: topup invoice creation # --------------------------------------------------------------------------- sep "Test 16 — Session: topup invoice creation" -if [[ -n "$SESSION_MACAROON" && "$SESSION_MACAROON" != "null" ]]; then - T16_RES=$(curl -s -w "\\n%{http_code}" -X POST "$BASE/api/sessions/$SESSION_ID/topup" \\ +if [[ -n "\$SESSION_MACAROON" && "\$SESSION_MACAROON" != "null" ]]; then + T16_RES=$(curl -s -w "\\n%{http_code}" -X POST "\$BASE/api/sessions/\$SESSION_ID/topup" \\ -H "Content-Type: application/json" \\ - -H "Authorization: Bearer $SESSION_MACAROON" \\ + -H "Authorization: Bearer \$SESSION_MACAROON" \\ -d '{"amount_sats": 500}') - T16_BODY=$(body_of "$T16_RES"); T16_CODE=$(code_of "$T16_RES") - T16_PR=$(echo "$T16_BODY" | jq -r '.topup.paymentRequest' 2>/dev/null || echo "") - T16_AMT=$(echo "$T16_BODY" | jq -r '.topup.amountSats' 2>/dev/null || echo "") - if [[ "$T16_CODE" == "200" && -n "$T16_PR" && "$T16_PR" != "null" && "$T16_AMT" == "500" ]]; then + T16_BODY=$(body_of "\$T16_RES"); T16_CODE=$(code_of "\$T16_RES") + T16_PR=$(echo "\$T16_BODY" | jq -r '.topup.paymentRequest' 2>/dev/null || echo "") + T16_AMT=$(echo "\$T16_BODY" | jq -r '.topup.amountSats' 2>/dev/null || echo "") + if [[ "\$T16_CODE" == "200" && -n "\$T16_PR" && "\$T16_PR" != "null" && "\$T16_AMT" == "500" ]]; then note PASS "Topup invoice created, amountSats=500" - PASS=$((PASS+1)) + PASS=\$((PASS+1)) else - note FAIL "code=$T16_CODE body=$T16_BODY" - FAIL=$((FAIL+1)) + note FAIL "code=\$T16_CODE body=\$T16_BODY" + FAIL=\$((FAIL+1)) fi else note SKIP "No macaroon — skipping" - SKIP=$((SKIP+1)) + SKIP=\$((SKIP+1)) +fi + +# --------------------------------------------------------------------------- +# Test 22 — Session: unknown session ID → 404 +# Previously zero coverage. Sessions GET was only tested with a valid ID (T12). +# --------------------------------------------------------------------------- +sep "Test 22 — Session: unknown session ID → 404" +T22_RES=$(curl -s -w "\\n%{http_code}" "\$BASE/api/sessions/does-not-exist") +T22_BODY=$(body_of "\$T22_RES"); T22_CODE=$(code_of "\$T22_RES") +T22_ERR=$(echo "\$T22_BODY" | jq -r '.error' 2>/dev/null || echo "") +if [[ "\$T22_CODE" == "404" && -n "\$T22_ERR" && "\$T22_ERR" != "null" ]]; then + note PASS "HTTP 404 with error field for unknown session ID" + PASS=\$((PASS+1)) +else + note FAIL "Expected 404 with error, got code=\$T22_CODE body=\$T22_BODY" + FAIL=\$((FAIL+1)) fi # --------------------------------------------------------------------------- @@ -448,9 +649,9 @@ fi # --------------------------------------------------------------------------- echo echo "=======================================" -echo " RESULTS: PASS=$PASS FAIL=$FAIL SKIP=$SKIP" +echo " RESULTS: PASS=\$PASS FAIL=\$FAIL SKIP=\$SKIP" echo "=======================================" -if [[ "$FAIL" -gt 0 ]]; then exit 1; fi +if [[ "\$FAIL" -gt 0 ]]; then exit 1; fi `; res.setHeader("Content-Type", "text/x-shellscript; charset=utf-8");