BROWSER=1 on e2e-camera-snapshot.sh signs in with the real Supabase test user, opens the fleet page and checks the snapshot view decodes, refreshes and has picture content.
216 lines
11 KiB
Bash
Executable File
216 lines
11 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# A P1/A1 camera, end to end: the real proxy reads a JPEG frame from the real
|
|
# printer's port 6000 (read-only - it only watches), the real backend relays
|
|
# it, and an authenticated viewer fetches it.
|
|
#
|
|
# Needs a printer in ../continuum-proxy/printers.toml whose camera is the
|
|
# P1/A1 kind (CAMERA_ID, default p1p) and that is switched on. Touches the
|
|
# printer's camera and MQTT status, nothing else; no command is sent.
|
|
set -euo pipefail
|
|
|
|
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
cd "$REPO_ROOT"
|
|
|
|
CAMERA_ID="${CAMERA_ID:-p1p}"
|
|
GO2RTC_BIN="${GO2RTC_BIN:-$HOME/.local/bin/go2rtc}"
|
|
PROXY_DIR="${PROXY_DIR:-$REPO_ROOT/../continuum-proxy}"
|
|
PORT="${PORT:-3050}"
|
|
AUTH_PORT="${AUTH_PORT:-3051}"
|
|
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
|
|
# Where to keep the last frame for a human to look at. Unset: not kept.
|
|
SAVE_FRAME="${SAVE_FRAME:-}"
|
|
# BROWSER=1: also open the real web console in real Chrome (needs the real
|
|
# Supabase test user, so source ../dev-secrets.sh).
|
|
BROWSER="${BROWSER:-0}"
|
|
APP_HOST="${APP_HOST:-127.0.0.1}"
|
|
APP_DIR="${APP_DIR:-$REPO_ROOT/../continuum-app}"
|
|
APP_PORT="${APP_PORT:-3052}"
|
|
CHROME_BIN="${CHROME_BIN:-/usr/bin/google-chrome}"
|
|
APP_PID=""
|
|
|
|
WORK="$(mktemp -d)"
|
|
BACKEND_PID=""; PROXY_PID=""; AUTH_PID=""
|
|
fail() { echo "FAIL: $*" >&2; exit 1; }
|
|
ok() { echo " ok: $*"; }
|
|
psql_q() { psql "$DATABASE_URL" -tAq -c "$1"; }
|
|
USER_ID="$(cat /proc/sys/kernel/random/uuid)"
|
|
|
|
cleanup() {
|
|
local rc=$?
|
|
for pid in "$PROXY_PID" "$BACKEND_PID" "$AUTH_PID"; do [ -n "$pid" ] && kill "$pid" 2>/dev/null || true; done
|
|
[ -n "$APP_PID" ] && kill -- "-$APP_PID" 2>/dev/null || true
|
|
for _ in $(seq 1 20); do [ -n "$PROXY_PID" ] && kill -0 "$PROXY_PID" 2>/dev/null || break; sleep 0.3; done
|
|
for pid in "$PROXY_PID" "$BACKEND_PID" "$AUTH_PID"; do [ -n "$pid" ] && kill -9 "$pid" 2>/dev/null || true; done
|
|
[ -n "$APP_PID" ] && kill -9 -- "-$APP_PID" 2>/dev/null || true
|
|
[ -n "${ORG_ID:-}" ] && psql_q "DELETE FROM organisations WHERE id = '$ORG_ID'" >/dev/null 2>&1 || true
|
|
if [ $rc -ne 0 ]; then
|
|
echo; echo "--- backend log (tail) ---"; tail -20 "$WORK/backend.log" 2>/dev/null || true
|
|
echo "--- proxy log (tail) ---"; tail -30 "$WORK/proxy.log" 2>/dev/null || true
|
|
fi
|
|
rm -rf "$WORK"
|
|
sleep 0.5
|
|
if ps -eo comm | grep -qx go2rtc; then
|
|
echo "WARNING: a go2rtc process is still running after cleanup" >&2; [ $rc -eq 0 ] && rc=1
|
|
fi
|
|
exit $rc
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
echo "==> checking preconditions"
|
|
psql_q "SELECT camera_snapshot FROM printers LIMIT 0" >/dev/null 2>&1 \
|
|
|| fail "database lacks printers.camera_snapshot - run scripts/dev-env.sh schema"
|
|
[ -x "$GO2RTC_BIN" ] || fail "no go2rtc at $GO2RTC_BIN"
|
|
[ -f "$PROXY_DIR/printers.toml" ] || fail "no $PROXY_DIR/printers.toml"
|
|
grep -q "^id *= *\"$CAMERA_ID\"" "$PROXY_DIR/printers.toml" || fail "printers.toml has no printer '$CAMERA_ID'"
|
|
ps -eo comm | grep -qx go2rtc && fail "a go2rtc is already running"
|
|
CAM_HOST="$(awk -v id="$CAMERA_ID" -F'"' '/^id *=/ {cur=$2} cur==id && /^(host|address|ip) *=/ {print $2; exit}' "$PROXY_DIR/printers.toml")"
|
|
if [ -n "$CAM_HOST" ]; then
|
|
timeout 4 bash -c "</dev/tcp/$CAM_HOST/6000" 2>/dev/null || fail "$CAMERA_ID ($CAM_HOST) refuses TCP 6000: no snapshot camera, or it is off"
|
|
ok "$CAMERA_ID accepts connections on 6000"
|
|
fi
|
|
|
|
if [ "$BROWSER" = "1" ]; then
|
|
for name in SUPABASE_URL SUPABASE_SECRET_KEY SUPABASE_PUBLISHABLE_KEY; do
|
|
[ -n "${!name:-}" ] || fail "$name is not set - source ../dev-secrets.sh (BROWSER=1)"
|
|
done
|
|
[ -x "$CHROME_BIN" ] || fail "no Chrome at $CHROME_BIN"
|
|
[ -d "$APP_DIR/apps/web/node_modules" ] || fail "$APP_DIR has no installed web app"
|
|
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)"; USER_TOKEN="$(sb_field accessToken)"; SB_EMAIL="$(sb_field email)"; SB_PASSWORD="$(sb_field password)"
|
|
unset SB_USER
|
|
ok "signed in as the real Supabase test user"
|
|
fi
|
|
|
|
echo "==> seeding an organisation and a farm claiming '$CAMERA_ID'"
|
|
EDGE_PROXY_TOKEN="e2e-$(date +%s)"
|
|
ORG_ID="$(psql_q "INSERT INTO organisations (name) VALUES ('e2e snapshot org') RETURNING id")"
|
|
psql_q "INSERT INTO org_members (org_id, user_id, role) VALUES ('$ORG_ID', '$USER_ID', 'viewer')" >/dev/null
|
|
FARM_ID="$(psql_q "INSERT INTO farms (name, org_id, edge_proxy_token) VALUES ('e2e snapshot farm', '$ORG_ID', '$EDGE_PROXY_TOKEN') RETURNING id")"
|
|
psql_q "INSERT INTO printers (farm_id, name, vendor, gateway_printer_id) VALUES ('$FARM_ID', 'snapshot under test', 'bambu', '$CAMERA_ID')" >/dev/null
|
|
CAM_UUID="$(psql_q "SELECT id FROM printers WHERE farm_id='$FARM_ID'")"
|
|
|
|
echo "==> starting the auth issuer and the backend"
|
|
if [ "$BROWSER" != "1" ]; then
|
|
bun scripts/dev-auth.ts --port "$AUTH_PORT" > "$WORK/auth.log" 2>&1 &
|
|
AUTH_PID=$!
|
|
AUTH_URL="http://localhost:$AUTH_PORT"
|
|
for _ in $(seq 1 40); do curl -sf "$AUTH_URL/auth/v1/.well-known/jwks.json" >/dev/null 2>&1 && break; sleep 0.25; done
|
|
export SUPABASE_URL="$AUTH_URL"
|
|
USER_TOKEN="$(curl -sf "$AUTH_URL/token?sub=$USER_ID" | python3 -c 'import json,sys; print(json.load(sys.stdin)["access_token"])')"
|
|
fi
|
|
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"
|
|
|
|
echo "==> starting the real continuum-proxy"
|
|
( cd "$PROXY_DIR" && cargo build --quiet 2>&1 | tail -3 ) || fail "proxy did not build"
|
|
env -C "$PROXY_DIR" \
|
|
CONTINUUM_GATEWAY_ID=e2e-snap-gw CONTINUUM_FARM_ID="$FARM_ID" CONTINUUM_GATEWAY_TOKEN="$EDGE_PROXY_TOKEN" \
|
|
CONTINUUM_UPLINK_URL="ws://localhost:$PORT/ws/edge/v1" \
|
|
CONTINUUM_GO2RTC_BIN="$GO2RTC_BIN" CONTINUUM_GO2RTC_CONFIG="$WORK/go2rtc.yaml" \
|
|
CONTINUUM_PRINTERS_PATH=printers.toml CONTINUUM_STORE_PATH="$WORK/continuum.db" RUST_LOG=info \
|
|
"$PROXY_DIR/target/debug/continuum-proxy" > "$WORK/proxy.log" 2>&1 &
|
|
PROXY_PID=$!
|
|
|
|
echo "==> waiting for the camera to be advertised as a snapshot camera"
|
|
STREAM=""
|
|
for _ in $(seq 1 45); do
|
|
STREAM="$(curl -sf -H "authorization: Bearer $USER_TOKEN" "$BASE/printers/$CAM_UUID/stream" 2>/dev/null || true)"
|
|
[ -n "$STREAM" ] && break
|
|
sleep 1
|
|
done
|
|
[ -n "$STREAM" ] || fail "GET /printers/$CAM_UUID/stream never answered 200"
|
|
printf '%s' "$STREAM" | python3 -c 'import json,sys; d=json.load(sys.stdin); assert d["kind"]=="snapshot" and "iceServers" not in d, d' \
|
|
|| fail "the stream answer is not a snapshot answer"
|
|
ok "the stream endpoint says kind=snapshot (no WebRTC session offered)"
|
|
|
|
echo "==> fetching frames"
|
|
check_frame() {
|
|
local file="$1" label="$2" size
|
|
size="$(stat -c %s "$file")"
|
|
[ "$size" -gt 1000 ] || fail "$label: only $size bytes"
|
|
[ "$(head -c2 "$file" | od -An -tx1 | tr -d ' \n')" = "ffd8" ] || fail "$label: does not start with a JPEG marker"
|
|
[ "$(tail -c2 "$file" | od -An -tx1 | tr -d ' \n')" = "ffd9" ] || fail "$label: does not end with a JPEG marker"
|
|
ok "$label: a $size-byte JPEG"
|
|
}
|
|
fetch() { curl -s -o "$1" -D "$1.h" -w '%{http_code}' -H "authorization: Bearer $USER_TOKEN" "$BASE/printers/$CAM_UUID/snapshot"; }
|
|
code="$(fetch "$WORK/f1.jpg")"
|
|
[ "$code" = 200 ] || fail "first frame: HTTP $code: $(head -c 300 "$WORK/f1.jpg")"
|
|
grep -qi '^content-type: image/jpeg' "$WORK/f1.jpg.h" || fail "wrong content-type"
|
|
grep -qi '^cache-control: no-store' "$WORK/f1.jpg.h" || fail "frames must be no-store"
|
|
check_frame "$WORK/f1.jpg" "first frame"
|
|
sleep 1.2
|
|
code="$(fetch "$WORK/f2.jpg")"; [ "$code" = 200 ] || fail "second frame: HTTP $code"
|
|
check_frame "$WORK/f2.jpg" "second frame (a fresh read after the cache window)"
|
|
cmp -s "$WORK/f1.jpg" "$WORK/f2.jpg" && fail "two frames a second apart are byte-identical - is the cache serving stale pictures?"
|
|
ok "consecutive frames differ"
|
|
|
|
echo "==> five viewers at once share the camera"
|
|
viewers=()
|
|
for i in 1 2 3 4 5; do fetch "$WORK/c$i.jpg" > "$WORK/c$i.code" & viewers+=($!); done
|
|
# Not a bare `wait`: that would also wait for the backend and the proxy.
|
|
for pid in "${viewers[@]}"; do wait "$pid"; done
|
|
for i in 1 2 3 4 5; do [ "$(cat "$WORK/c$i.code")" = 200 ] || fail "viewer $i: HTTP $(cat "$WORK/c$i.code")"; check_frame "$WORK/c$i.jpg" "viewer $i"; done
|
|
ok "all five got a picture"
|
|
|
|
echo "==> access control"
|
|
[ "$(curl -s -o /dev/null -w '%{http_code}' "$BASE/printers/$CAM_UUID/snapshot")" = 401 ] || fail "no token must be 401"
|
|
ok "no token: 401"
|
|
|
|
# The code is never read out of printers.toml here; the camera login happens
|
|
# on the gateway, and the only thing that should cross is the picture.
|
|
if grep -qi 'access_code\|accessCode' "$WORK/backend.log"; then
|
|
fail "the backend log mentions an access code"
|
|
fi
|
|
ok "the backend log never mentions an access code"
|
|
|
|
# Optional soak: the first real dispatch froze the proxy ~4.5 min in (an
|
|
# MQTT publish that never completed while the fleet lock was held). Staying
|
|
# connected well past that and still receiving telemetry is the regression test.
|
|
if [ -n "${SOAK_S:-}" ]; then
|
|
echo "==> soaking for ${SOAK_S}s: telemetry must keep arriving"
|
|
sleep "$SOAK_S"
|
|
AGE="$(psql_q "SELECT extract(epoch FROM now() - last_seen_at)::int FROM printers WHERE id='$CAM_UUID'")"
|
|
[ -n "$AGE" ] && [ "$AGE" -lt 20 ] || fail "telemetry is ${AGE:-never}s old after the soak - the proxy froze"
|
|
ok "telemetry still fresh (${AGE}s old) after ${SOAK_S}s"
|
|
code="$(fetch "$WORK/after.jpg")"; [ "$code" = 200 ] || fail "no camera frame after the soak: HTTP $code"
|
|
ok "camera still answers after the soak"
|
|
fi
|
|
|
|
if [ "$BROWSER" = "1" ]; then
|
|
echo "==> the real console in real Chrome"
|
|
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 "$APP_HOST" --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" || { tail -20 "$WORK/app.log" >&2; fail "the web console did not start"; }
|
|
OUT="$(env APP_URL="http://127.0.0.1:$APP_PORT" SUPABASE_E2E_EMAIL="$SB_EMAIL" SUPABASE_E2E_PASSWORD="$SB_PASSWORD" \
|
|
CHROME_BIN="$CHROME_BIN" timeout 240 bun scripts/e2e-camera-snapshot-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 8 ] && [ "$bad" -eq 0 ]; then bad=1; echo " FAILED: only $seen of 8 browser results" >&2; fi
|
|
if [ "$bad" -ne 0 ]; then
|
|
printf '%s\n' "$OUT" | sed 's/^/ | /' >&2; tail -15 "$WORK/app.log" >&2
|
|
fail "the real browser did not show the snapshot camera"
|
|
fi
|
|
fi
|
|
|
|
[ -n "$SAVE_FRAME" ] && cp "$WORK/f2.jpg" "$SAVE_FRAME" && ok "kept a frame at $SAVE_FRAME"
|
|
echo
|
|
echo "PASS - a P1/A1 camera frame travelled printer -> proxy -> backend -> an authenticated viewer."
|