#!/usr/bin/env bash # oauth-e2e.sh — the end-to-end half of batch 04 that the unit tests cannot reach. # # client_test.go reproduces the cancellation precisely, but in-process against an # httptest PDS. This drives the real refresher, the real oauth_sessions row and a # real PDS (jarrett.app), which is where "mass sign-outs" actually happened. # # Covers both commits: # 37bab32 — a cancelled refresh must not burn the refresh token # e75b2e2 — concurrent refreshes must not delete a live session (CAS on rev) # # Safety: ui.db is snapshotted first. If the refresh path burns the token this # restores rather than leaving the account logged out. set -uo pipefail SCRATCH=/tmp/claude-1000/-home-data-atcr-io/3675ee68-5b8d-4bc1-adf1-1738577d3238/scratchpad IMG=127.0.0.1:5000/evan.jarrett.net/valtest:v4 DB=/var/lib/atcr/ui.db q() { docker exec atcr-appview sh -c "sqlite3 $DB \"$1\"" 2>/dev/null; } state() { q "select rev || '|' || substr(json_extract(session_data,'\$.access_token'),1,12) || '|' || substr(json_extract(session_data,'\$.refresh_token'),1,12) from oauth_sessions limit 1" } echo "==> backing up ui.db" docker exec atcr-appview sh -c "sqlite3 $DB \".backup /var/lib/atcr/ui.e2e-backup.db\"" || exit 1 BEFORE=$(state) echo " before: rev|access|refresh = $BEFORE" echo "==> staling the access token so the next call must refresh" q "select session_data from oauth_sessions limit 1" > "$SCRATCH/sd.json" python3 - "$SCRATCH/sd.json" <<'PY' import json, sys p = sys.argv[1] d = json.load(open(p)) d["access_token"] = "expired-by-validation-" + d["access_token"][-8:] json.dump(d, open(p, "w")) PY docker cp "$SCRATCH/sd.json" atcr-appview:/tmp/sd.json >/dev/null q "update oauth_sessions set session_data = readfile('/tmp/sd.json')" echo " staled: $(state)" echo "==> restarting appview (the refresher caches sessions in memory)" docker compose restart atcr-appview >/dev/null 2>&1 for i in $(seq 1 60); do [ "$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:5000/v2/)" = "401" ] && break sleep 1 done echo "==> firing 4 concurrent pulls (forces concurrent refresh — the e75b2e2 case)" pids=(); rc=0 for i in 1 2 3 4; do ( crane pull --insecure "$IMG" "$SCRATCH/e2e-$i.tar" >/dev/null 2>&1 ) & pids+=($!) done for p in "${pids[@]}"; do wait "$p" || rc=1; done echo " concurrent pulls all succeeded: $([ $rc -eq 0 ] && echo YES || echo NO)" AFTER=$(state) echo " after: rev|access|refresh = $AFTER" ROWS=$(q "select count(*) from oauth_sessions") echo " session rows still present: $ROWS" echo echo "==> cancellation case (37bab32): kill a pull mid-refresh, then retry" q "select session_data from oauth_sessions limit 1" > "$SCRATCH/sd2.json" python3 - "$SCRATCH/sd2.json" <<'PY' import json, sys p = sys.argv[1] d = json.load(open(p)) d["access_token"] = "expired-again-" + d["access_token"][-8:] json.dump(d, open(p, "w")) PY docker cp "$SCRATCH/sd2.json" atcr-appview:/tmp/sd2.json >/dev/null q "update oauth_sessions set session_data = readfile('/tmp/sd2.json')" docker compose restart atcr-appview >/dev/null 2>&1 for i in $(seq 1 60); do [ "$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:5000/v2/)" = "401" ] && break sleep 1 done crane pull --insecure "$IMG" "$SCRATCH/e2e-cancel.tar" >/dev/null 2>&1 & KILLPID=$! sleep 0.15 kill -9 $KILLPID 2>/dev/null wait $KILLPID 2>/dev/null echo " killed a pull 150ms in (mid-refresh window)" if crane pull --insecure "$IMG" "$SCRATCH/e2e-retry.tar" >/dev/null 2>&1; then echo " retry after cancellation SUCCEEDED" else echo " retry after cancellation FAILED — refresh token was burned" fi echo " final: rev|access|refresh = $(state)" echo " session rows: $(q 'select count(*) from oauth_sessions')" echo echo "restore if needed: docker exec atcr-appview sh -c 'cp /var/lib/atcr/ui.e2e-backup.db $DB'"