Files
octoturge 9e4b3e5315 Drive the first-run experience in a real browser
e2e-setup-browser.sh signs a user with no organisation into the real web
console, creates a farm, copies the gateway settings the page shows,
connects a stand-in gateway configured from exactly those settings, adds a
printer through the form, gets a failed-print alert and dismisses it, then
removes the printer. Afterwards it searches every table and the backend log
for the access code the browser typed. No printer is touched and dispatch
is off. The e2e scripts that start the real proxy also give it its own store
file in the scratch directory.
2026-10-29 15:15:06 +00:00

138 lines
6.4 KiB
Bash
Executable File

#!/usr/bin/env bash
# The first-run experience in the real web console, in a real Chrome: a user
# with no organisation signs in, creates a farm, copies the gateway settings,
# connects a gateway configured from exactly those settings, adds a printer
# through the form, gets an alert when it fails, and removes it.
#
# The gateway is a stand-in (scripts/fake-gateway-serve.ts), so NO printer and
# no network device is touched; e2e-provision-proxy.sh covers the real proxy.
#
# SAFETY: nothing here talks to a real printer, and dispatch is off.
#
# Needs: source ../dev-secrets.sh first (real Supabase, as e2e-camera.sh's
# BROWSER mode), a migrated database, Chrome, ../continuum-app installed, and
# port 3042 free. Uses the throwaway Supabase user continuum-e2e@example.invalid,
# which must have no organisation (it refuses to start otherwise, so it cannot
# trample a SERVE=1 stack) and removes what it creates.
#
# Usage:
# set -a; . ../dev-secrets.sh; set +a
# scripts/e2e-setup-browser.sh
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$REPO_ROOT"
PORT="${PORT:-3036}"
BASE="http://localhost:$PORT"
DATABASE_URL="${DATABASE_URL:-postgres://continuum:continuum@localhost:5432/continuum}"
VALKEY_URL="${VALKEY_URL:-redis://localhost:6379}"
EDGE_JWT_SECRET="${EDGE_JWT_SECRET:-e2e-edge-secret}"
R2_ACCESS_KEY_ID="${R2_ACCESS_KEY_ID:-e2e}"
R2_SECRET_ACCESS_KEY="${R2_SECRET_ACCESS_KEY:-e2e}"
R2_ENDPOINT="${R2_ENDPOINT:-http://localhost:9000}"
export DATABASE_URL VALKEY_URL EDGE_JWT_SECRET PORT R2_ACCESS_KEY_ID R2_SECRET_ACCESS_KEY R2_ENDPOINT
APP_DIR="${APP_DIR:-$REPO_ROOT/../continuum-app}"
APP_PORT="${APP_PORT:-3042}"
CHROME_BIN="${CHROME_BIN:-/usr/bin/google-chrome}"
WORK="$(mktemp -d)"
BACKEND_PID=""
APP_PID=""
USER_ID=""
fail() { echo "FAIL: $*" >&2; exit 1; }
ok() { echo " ok: $*"; }
psql_q() { psql "$DATABASE_URL" -tAq -c "$1"; }
cleanup() {
local rc=$?
[ -n "$BACKEND_PID" ] && kill "$BACKEND_PID" 2>/dev/null || true
[ -n "$APP_PID" ] && kill -- "-$APP_PID" 2>/dev/null || true
sleep 0.5
[ -n "$BACKEND_PID" ] && kill -9 "$BACKEND_PID" 2>/dev/null || true
[ -n "$APP_PID" ] && kill -9 -- "-$APP_PID" 2>/dev/null || true
# What this run created, and only that: the user began with no organisation.
if [ -n "$USER_ID" ] && [ -z "${KEEP_DATA:-}" ]; then
psql_q "DELETE FROM organisations WHERE id IN (SELECT org_id FROM org_members WHERE user_id='$USER_ID')" >/dev/null 2>&1 || true
fi
if [ $rc -ne 0 ]; then
echo; echo "--- backend log (tail) ---"; tail -20 "$WORK/backend.log" 2>/dev/null || true
echo "--- web console log (tail) ---"; tail -15 "$WORK/app.log" 2>/dev/null || true
fi
rm -rf "$WORK"
exit $rc
}
trap cleanup EXIT
for name in SUPABASE_URL SUPABASE_SECRET_KEY SUPABASE_PUBLISHABLE_KEY; do
[ -n "${!name:-}" ] || fail "$name is not set - source ../dev-secrets.sh"
done
psql_q "SELECT 1 FROM printer_alerts LIMIT 0" >/dev/null 2>&1 || fail "no migrated database - run scripts/dev-env.sh up && schema"
[ -x "$CHROME_BIN" ] || fail "no Chrome at $CHROME_BIN"
[ -d "$APP_DIR/apps/web/node_modules" ] || fail "$APP_DIR has no installed web app"
# bash's own /dev/tcp rather than `ss`, which is not installed everywhere.
if (exec 3<>"/dev/tcp/127.0.0.1/$APP_PORT") 2>/dev/null; then fail "port $APP_PORT is already in use (a SERVE=1 stack running?)"; fi
SB_USER="$(bun scripts/e2e-supabase-user.ts)" || fail "could not provision the Supabase test user"
sb_field() { printf '%s' "$SB_USER" | python3 -c 'import json,sys; print(json.load(sys.stdin)[sys.argv[1]])' "$1"; }
USER_ID="$(sb_field id)"; SB_EMAIL="$(sb_field email)"; SB_PASSWORD="$(sb_field password)"
unset SB_USER
[ "$(psql_q "SELECT count(*) FROM org_members WHERE user_id='$USER_ID'")" = "0" ] \
|| fail "the test user already belongs to an organisation - stop any SERVE=1 stack first"
ok "a Supabase user with no organisation"
echo "==> starting the backend on :$PORT"
DISPATCH_ENABLED=false bun run src/index.ts > "$WORK/backend.log" 2>&1 &
BACKEND_PID=$!
for _ in $(seq 1 60); do curl -sf "$BASE/health" >/dev/null 2>&1 && break; sleep 0.5; done
curl -sf "$BASE/health" >/dev/null || fail "backend did not become healthy"
ok "backend healthy"
echo "==> starting the web console on :$APP_PORT"
setsid env -C "$APP_DIR/apps/web" \
NUXT_PUBLIC_API_BASE="$BASE" \
NUXT_PUBLIC_WS_BASE="ws://localhost:$PORT" \
SUPABASE_URL="$SUPABASE_URL" \
SUPABASE_KEY="$SUPABASE_PUBLISHABLE_KEY" \
NUXT_PUBLIC_PASSWORD_LOGIN=true \
pnpm exec nuxt dev --host 127.0.0.1 --port "$APP_PORT" > "$WORK/app.log" 2>&1 &
APP_PID=$!
for _ in $(seq 1 120); do curl -sf -o /dev/null "http://127.0.0.1:$APP_PORT/login" 2>/dev/null && break; sleep 1; done
curl -sf -o /dev/null "http://127.0.0.1:$APP_PORT/login" || fail "the web console did not start"
ok "the web console is up"
ACCESS_CODE="BROWSERCODE$(cat /proc/sys/kernel/random/uuid | cut -c1-6)"
FRAMES="$WORK/frames.jsonl"; : > "$FRAMES"
echo "==> first run, in real Chrome"
OUT="$(env APP_URL="http://127.0.0.1:$APP_PORT" API_BASE="$BASE" WS_BASE="ws://localhost:$PORT" \
SUPABASE_E2E_EMAIL="$SB_EMAIL" SUPABASE_E2E_PASSWORD="$SB_PASSWORD" CHROME_BIN="$CHROME_BIN" \
FRAMES_FILE="$FRAMES" ACCESS_CODE="$ACCESS_CODE" \
timeout 280 bun scripts/e2e-setup-browser.ts 2>&1 || true)"
seen=0; bad=0
while IFS=$'\t' read -r verdict label; do
case "$verdict" in
ok) ok "$label"; seen=$((seen + 1)) ;;
FAILED) echo " FAILED: $label" >&2; seen=$((seen + 1)); bad=$((bad + 1)) ;;
esac
done < <(printf '%s\n' "$OUT" | grep -E "^(ok|FAILED)$(printf '\t')")
if [ "$seen" -lt 18 ] && [ "$bad" -eq 0 ]; then
bad=1; echo " FAILED: the browser run produced only $seen of 18 results" >&2
fi
if [ "$bad" -ne 0 ]; then
printf '%s\n' "$OUT" | sed 's/^/ | /' >&2
fail "the first-run experience did not work"
fi
# The credential the browser typed must rest nowhere in the cloud.
grep -q "$ACCESS_CODE" "$WORK/backend.log" && fail "the access code is in the backend log"
LEAK="$(psql "$DATABASE_URL" -tAq -c "SELECT string_agg(table_name, ' ') FROM information_schema.tables WHERE table_schema='public'" \
| tr ' ' '\n' | while read -r tbl; do [ -n "$tbl" ] && psql_q "SELECT count(*) FROM \"$tbl\" t WHERE row_to_json(t)::text LIKE '%$ACCESS_CODE%'" | sed "s/^/$tbl /"; done | awk '$2>0')"
[ -z "$LEAK" ] || fail "the access code is stored in: $LEAK"
ok "the access code is in no table and no log"
echo
echo "PASS - a new user went from nothing to a farm, a gateway, a printer, an alert and back."