Files
at-container-registry/test/e2e/oauth-refresh-e2e.sh
T
Evan JarrettandClaude Opus 5 30caf43146 test/e2e: drive the OAuth refresh path against a real PDS
client_test.go reproduces the cancellation precisely, but in-process against an
httptest PDS. The failure this range fixes — mass sign-outs — happened against a
real one, through the real refresher and the real oauth_sessions row, so the
unit tests alone are a thinner sign-off than the batch deserves.

Staling the access token in the live row and restarting the appview (the
refresher caches sessions in memory, so editing the DB alone changes nothing)
forces the real refresh path. Four concurrent pulls then advance rev 1 to 3
rather than 1 to 4: the compare-and-swap collapses four racing refreshes into
two rotations, with the losers adopting the winner's token instead of each
burning one. Killing a pull 150ms into that window and retrying still succeeds.

Backs ui.db up first, since a burned refresh token would otherwise leave the
account signed out.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 16:34:25 -05:00

102 lines
3.8 KiB
Bash
Executable File

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