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