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.
463 lines
23 KiB
Bash
Executable File
463 lines
23 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# End-to-end test of the /ws/edge/v1 contract against a real database and real
|
|
# printers: seed a farm, claim its printers, mint an edge token, run the actual
|
|
# continuum-proxy daemon, and assert that live telemetry reached Postgres and
|
|
# that a command sent over HTTP reached a printer.
|
|
#
|
|
# This is the test that the unit tests cannot be: tests/edge_protocol.test.ts
|
|
# only proves the two ends agree about the wire format. Nothing but this proves
|
|
# the printer-id translation, persistTelemetry and sendPrinterCommand actually
|
|
# work against a database.
|
|
#
|
|
# SAFETY: the only command it sends is a fan speed, and only to the printer
|
|
# named by PRINTER_ID (default p1p), which it refuses to send unless that
|
|
# printer reports IDLE. It never starts, pauses or cancels a print. Point it at
|
|
# a printer you can see.
|
|
#
|
|
# Usage:
|
|
# scripts/dev-env.sh up && scripts/dev-env.sh schema
|
|
# scripts/e2e-edge.sh
|
|
#
|
|
# PRINTER_ID=p1p which printer to send the fan command to
|
|
# PROXY_DIR=../continuum-proxy
|
|
# KEEP_DATA=1 leave the seeded farm behind for inspection
|
|
#
|
|
# The HTTP routes now require a verified Supabase access token, so this starts
|
|
# scripts/dev-auth.ts - a local RS256 issuer publishing a JWKS at the path the
|
|
# backend fetches - and mints a token for a seeded org `operator`. That is the
|
|
# real verification path, not a bypass: see the header of dev-auth.ts.
|
|
set -euo pipefail
|
|
|
|
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
cd "$REPO_ROOT"
|
|
|
|
PRINTER_ID="${PRINTER_ID:-p1p}"
|
|
PROXY_DIR="${PROXY_DIR:-$REPO_ROOT/../continuum-proxy}"
|
|
PORT="${PORT:-3010}"
|
|
AUTH_PORT="${AUTH_PORT:-3011}"
|
|
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}"
|
|
# The backend refuses to boot without object-storage credentials, which is
|
|
# deliberate - see src/s3/index.ts. Nothing in this test presigns or uploads
|
|
# anything, so these only have to exist.
|
|
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
|
|
export R2_ACCESS_KEY_ID R2_SECRET_ACCESS_KEY R2_ENDPOINT
|
|
# This script asserts TURN is unconfigured (below). Sourcing dev-secrets.sh
|
|
# turns it on, which used to fail the assertion - and print the live
|
|
# credentials in the failure message. Make the assumption true.
|
|
unset CLOUDFLARE_TURN_KEY_ID CLOUDFLARE_TURN_API_TOKEN
|
|
|
|
WORK="$(mktemp -d)"
|
|
BACKEND_PID=""
|
|
PROXY_PID=""
|
|
AUTH_PID=""
|
|
|
|
# `uuidgen` is not installed everywhere; the kernel's generator always is.
|
|
new_uuid() {
|
|
if [ -r /proc/sys/kernel/random/uuid ]; then
|
|
cat /proc/sys/kernel/random/uuid
|
|
else
|
|
python3 -c 'import uuid; print(uuid.uuid4())'
|
|
fi
|
|
}
|
|
|
|
# Uses DATABASE_URL rather than a hardcoded localhost. The backend under test
|
|
# reads that variable, so a psql pointed anywhere else seeds fixtures into a
|
|
# different database than the one being exercised - and the failure message
|
|
# below names DATABASE_URL, which would then be naming a URL nothing tried.
|
|
psql_q() { psql "$DATABASE_URL" -tAq -c "$1"; }
|
|
|
|
# The farm's operator. A real Supabase user id would come from the project;
|
|
# here it only has to be a uuid that the token's `sub` and the org_members row
|
|
# agree on.
|
|
USER_ID="$(new_uuid)"
|
|
|
|
cleanup() {
|
|
local rc=$?
|
|
for pid in "$PROXY_PID" "$BACKEND_PID" "$AUTH_PID"; do
|
|
[ -z "$pid" ] && continue
|
|
kill "$pid" 2>/dev/null || true
|
|
done
|
|
# A gateway that outlives this script keeps reconnecting with a token for a
|
|
# farm that is about to be deleted, and pollutes the next run's log.
|
|
for _ in $(seq 1 10); do
|
|
[ -n "$PROXY_PID" ] || break
|
|
kill -0 "$PROXY_PID" 2>/dev/null || break
|
|
sleep 0.3
|
|
done
|
|
# Never `kill $PID` with an unset PID: `kill 0` signals the whole process
|
|
# group, which includes this script and whatever launched it.
|
|
for pid in "$PROXY_PID" "$BACKEND_PID" "$AUTH_PID"; do
|
|
[ -n "$pid" ] && kill -9 "$pid" 2>/dev/null || true
|
|
done
|
|
if [ -z "${KEEP_DATA:-}" ] && [ -n "${ORG_ID:-}" ]; then
|
|
# Farms cascade from the organisation, printers from the farm.
|
|
psql_q "DELETE FROM organisations WHERE id = '$ORG_ID'" >/dev/null 2>&1 || true
|
|
fi
|
|
if [ $rc -ne 0 ]; then
|
|
echo
|
|
echo "--- backend log (tail) ---"; tail -30 "$WORK/backend.log" 2>/dev/null || true
|
|
echo "--- dev-auth log (tail) ---"; tail -10 "$WORK/auth.log" 2>/dev/null || true
|
|
echo "--- proxy log (tail) ---"; tail -30 "$WORK/proxy.log" 2>/dev/null || true
|
|
fi
|
|
rm -rf "$WORK"
|
|
exit $rc
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
fail() { echo "FAIL: $*" >&2; exit 1; }
|
|
ok() { echo " ok: $*"; }
|
|
|
|
# --- 0. preconditions ------------------------------------------------------
|
|
|
|
echo "==> checking the database is up and migrated"
|
|
psql_q "SELECT 1" >/dev/null 2>&1 || fail "no database at $DATABASE_URL - run scripts/dev-env.sh up"
|
|
psql_q "SELECT gateway_printer_id FROM printers LIMIT 0" >/dev/null 2>&1 \
|
|
|| fail "printers.gateway_printer_id is missing - run scripts/dev-env.sh schema"
|
|
psql_q "SELECT org_id FROM farms LIMIT 0" >/dev/null 2>&1 \
|
|
|| fail "farms.org_id is missing - run scripts/dev-env.sh schema"
|
|
ok "schema has the edge columns and the org layer"
|
|
|
|
[ -f "$PROXY_DIR/printers.toml" ] || fail "no $PROXY_DIR/printers.toml - the proxy needs real printers"
|
|
|
|
# --- 1. seed ---------------------------------------------------------------
|
|
|
|
echo "==> seeding an organisation, a farm and claiming its printers"
|
|
EDGE_PROXY_TOKEN="e2e-$(date +%s)"
|
|
ORG_ID="$(psql_q "INSERT INTO organisations (name) VALUES ('e2e org') RETURNING id")"
|
|
[ -n "$ORG_ID" ] || fail "could not create an organisation"
|
|
# `operator`, not `owner`: the command route requires operator, so seeding the
|
|
# weakest role that should work proves the boundary is where it claims to be
|
|
# rather than passing because the test user can do everything.
|
|
psql_q "INSERT INTO org_members (org_id, user_id, role)
|
|
VALUES ('$ORG_ID', '$USER_ID', 'operator')" >/dev/null
|
|
FARM_ID="$(psql_q "INSERT INTO farms (name, org_id, edge_proxy_token)
|
|
VALUES ('e2e farm', '$ORG_ID', '$EDGE_PROXY_TOKEN') RETURNING id")"
|
|
[ -n "$FARM_ID" ] || fail "could not create a farm"
|
|
ok "org $ORG_ID, farm $FARM_ID, operator $USER_ID"
|
|
|
|
# gateway_printer_id is the whole point: it maps the gateway's own ids from
|
|
# printers.toml onto these rows. Telemetry for an id nothing claims is skipped.
|
|
while read -r pid pname; do
|
|
[ -z "$pid" ] && continue
|
|
psql_q "INSERT INTO printers (farm_id, name, vendor, gateway_printer_id)
|
|
VALUES ('$FARM_ID', '$pname', 'bambu', '$pid')" >/dev/null
|
|
ok "claimed '$pid' as '$pname'"
|
|
done < <(awk -F'"' '/^id *=/ {id=$2} /^name *=/ {print id, $2}' "$PROXY_DIR/printers.toml")
|
|
|
|
# --- 2. backend ------------------------------------------------------------
|
|
|
|
echo "==> starting the dev-auth JWKS issuer on :$AUTH_PORT"
|
|
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
|
|
curl -sf "$AUTH_URL/auth/v1/.well-known/jwks.json" >/dev/null 2>&1 \
|
|
|| fail "the dev-auth issuer did not come up: $(cat "$WORK/auth.log")"
|
|
export SUPABASE_URL="$AUTH_URL"
|
|
ok "issuer up, SUPABASE_URL=$SUPABASE_URL"
|
|
|
|
USER_TOKEN="$(curl -sf "$AUTH_URL/token?sub=$USER_ID" \
|
|
| python3 -c 'import json,sys; print(json.load(sys.stdin)["access_token"])')"
|
|
[ -n "$USER_TOKEN" ] || fail "could not mint a user token"
|
|
AUTH_HEADER="authorization: Bearer $USER_TOKEN"
|
|
ok "minted an access token for the operator"
|
|
|
|
echo "==> starting continuum-backend on :$PORT"
|
|
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
|
|
HEALTH="$(curl -sf "$BASE/health")" || fail "backend did not become healthy"
|
|
echo "$HEALTH" | grep -q '"db":true' || fail "backend reports the database is down: $HEALTH"
|
|
echo "$HEALTH" | grep -q '"valkey":true' || fail "backend reports valkey is down: $HEALTH"
|
|
ok "health: $HEALTH"
|
|
|
|
# --- 3. edge token ---------------------------------------------------------
|
|
|
|
echo "==> minting an edge token"
|
|
TOKEN="$(curl -sf -X POST "$BASE/auth/edge-token" \
|
|
-H 'content-type: application/json' \
|
|
-d "{\"farmId\":\"$FARM_ID\",\"edgeProxyToken\":\"$EDGE_PROXY_TOKEN\"}" \
|
|
| python3 -c 'import json,sys; print(json.load(sys.stdin).get("token",""))')"
|
|
[ -n "$TOKEN" ] || fail "no token returned from POST /auth/edge-token"
|
|
ok "token minted (${#TOKEN} chars)"
|
|
|
|
# --- 4. the real gateway ---------------------------------------------------
|
|
|
|
echo "==> starting the real continuum-proxy against it"
|
|
( cd "$PROXY_DIR" && cargo build --quiet 2>&1 | tail -3 ) || fail "proxy did not build"
|
|
# env -C rather than a `( cd ... ) &` subshell: $! has to be the daemon itself,
|
|
# or cleanup kills only the wrapper and leaves an orphan reconnecting with a
|
|
# stale token. An earlier version did exactly that, and the orphan then
|
|
# reported a deleted farm's printers into the next run.
|
|
# STREAM_BASE_URL is deliberately set: it is what the gateway advertises in
|
|
# its hello and the only way GET /printers/:id/stream can answer 200, so
|
|
# leaving it unset would make the camera assertions below untestable rather
|
|
# than passing. It does not have to be reachable - nothing here plays video.
|
|
STREAM_BASE_URL="http://e2e-gateway.invalid:1984"
|
|
env -C "$PROXY_DIR" \
|
|
CONTINUUM_GATEWAY_ID=e2e-gw \
|
|
CONTINUUM_FARM_ID="$FARM_ID" \
|
|
CONTINUUM_GATEWAY_TOKEN="$EDGE_PROXY_TOKEN" \
|
|
CONTINUUM_UPLINK_URL="ws://localhost:$PORT/ws/edge/v1" \
|
|
`# Deliberately absent, not an oversight: what is under test is the config` \
|
|
`# the gateway writes and the camera_stream it advertises, both of which` \
|
|
`# happen without go2rtc. Spawning the real one would bind :1984 and dial` \
|
|
`# the printers on the LAN, so the test would depend on hardware it does` \
|
|
`# not need. The supervisor logs a spawn failure every 5s; that is expected.` \
|
|
CONTINUUM_GO2RTC_BIN=/nonexistent-go2rtc \
|
|
CONTINUUM_GO2RTC_CONFIG="$WORK/go2rtc.yaml" \
|
|
CONTINUUM_STREAM_BASE_URL="$STREAM_BASE_URL" \
|
|
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=$!
|
|
|
|
# Wait for *every* claimed printer, not just the first. Telemetry is on a 5s
|
|
# timer and a Bambu on protocol v1 reports one tick behind the rest (it only
|
|
# sends a full status when asked, so each tick reads the previous tick's
|
|
# answer) - stopping at the first row that appears is how an earlier version of
|
|
# this script silently tested only the v2 printer.
|
|
CLAIMED="$(psql_q "SELECT count(*) FROM printers WHERE farm_id='$FARM_ID' AND gateway_printer_id IS NOT NULL")"
|
|
echo "==> waiting for live telemetry from all $CLAIMED claimed printer(s)"
|
|
for _ in $(seq 1 45); do
|
|
COUNT="$(psql_q "SELECT count(*) FROM printers
|
|
WHERE farm_id='$FARM_ID' AND gateway_printer_id IS NOT NULL
|
|
AND last_seen_at IS NOT NULL AND vendor_state IS NOT NULL")"
|
|
[ "${COUNT:-0}" -ge "$CLAIMED" ] && break
|
|
sleep 1
|
|
done
|
|
|
|
[ "${COUNT:-0}" -ge 1 ] || fail "no telemetry was persisted at all (see the proxy log below)"
|
|
|
|
if [ "${COUNT:-0}" -lt "$CLAIMED" ]; then
|
|
# Not fatal: a printer that is genuinely switched off cannot report, and that
|
|
# should not fail a run against the ones that can. Name them so the
|
|
# difference between "off" and "broken" is visible.
|
|
echo " WARNING: only $COUNT of $CLAIMED printers reported. Silent:"
|
|
psql_q "SELECT ' - ' || gateway_printer_id FROM printers
|
|
WHERE farm_id='$FARM_ID' AND gateway_printer_id IS NOT NULL
|
|
AND (last_seen_at IS NULL OR vendor_state IS NULL)"
|
|
else
|
|
ok "all $COUNT claimed printer(s) reported live telemetry"
|
|
fi
|
|
|
|
if grep "reported unknown printer" "$WORK/backend.log" | grep -q "$FARM_ID"; then
|
|
fail "the backend could not map a gateway printer id for this farm - check gateway_printer_id"
|
|
fi
|
|
ok "every printer id this gateway reported mapped to a row"
|
|
|
|
echo "==> what landed in the database:"
|
|
psql "$DATABASE_URL" -c \
|
|
"SELECT gateway_printer_id AS gw, status, vendor_state, progress_percent AS pct,
|
|
current_layer || '/' || total_layers AS layers, remaining_minutes AS mins,
|
|
nozzle_temp_c AS nozzle, bed_temp_c AS bed, active_alerts AS alerts,
|
|
coalesce(current_job_name, '-') AS job
|
|
FROM printers WHERE farm_id = '$FARM_ID' ORDER BY gateway_printer_id"
|
|
|
|
# Proves the telemetry is real rather than a row touched with nulls.
|
|
psql_q "SELECT 1 FROM printers WHERE farm_id='$FARM_ID' AND vendor_state IS NOT NULL
|
|
AND nozzle_temp_c IS NOT NULL LIMIT 1" | grep -q 1 \
|
|
|| fail "rows were touched but carry no actual telemetry"
|
|
ok "rows carry real vendor state and temperatures"
|
|
|
|
# --- 5. a command, the whole way down --------------------------------------
|
|
|
|
TARGET_UUID="$(psql_q "SELECT id FROM printers WHERE farm_id='$FARM_ID' AND gateway_printer_id='$PRINTER_ID'")"
|
|
[ -n "$TARGET_UUID" ] || fail "no printer claimed as '$PRINTER_ID'"
|
|
|
|
GW="$(curl -sf -H "$AUTH_HEADER" "$BASE/printers/$TARGET_UUID/gateway")"
|
|
echo "$GW" | grep -q '"gatewayOnline":true' || fail "the gateway is not registered as online: $GW"
|
|
ok "gateway reports online for $PRINTER_ID"
|
|
|
|
COMMAND_TESTED=no
|
|
STATE="$(psql_q "SELECT vendor_state FROM printers WHERE id='$TARGET_UUID'")"
|
|
if [ "$STATE" != "IDLE" ] && [ "$STATE" != "FINISH" ]; then
|
|
echo " SKIPPED the command test: $PRINTER_ID reports '${STATE:-no telemetry yet}', not IDLE/FINISH."
|
|
echo " Refusing to touch a printer that may be mid-print."
|
|
else
|
|
echo "==> sending set_fan_speed to $PRINTER_ID ($STATE) over HTTP"
|
|
RESP="$(curl -s -o "$WORK/cmd.json" -w '%{http_code}' -X POST \
|
|
"$BASE/printers/$TARGET_UUID/commands" -H 'content-type: application/json' \
|
|
-H "$AUTH_HEADER" -d '{"action":"set_fan_speed","percent":40}')"
|
|
[ "$RESP" = "200" ] || fail "command returned HTTP $RESP: $(cat "$WORK/cmd.json")"
|
|
grep -q '"ok":true' "$WORK/cmd.json" || fail "command was not acknowledged: $(cat "$WORK/cmd.json")"
|
|
ok "command acknowledged by the printer: $(cat "$WORK/cmd.json")"
|
|
|
|
# Put it back, so this leaves the printer as it found it.
|
|
curl -s -X POST "$BASE/printers/$TARGET_UUID/commands" \
|
|
-H 'content-type: application/json' -H "$AUTH_HEADER" \
|
|
-d '{"action":"set_fan_speed","percent":0}' > /dev/null
|
|
ok "fan returned to 0"
|
|
COMMAND_TESTED=yes
|
|
fi
|
|
|
|
# --- 5b. the camera path ---------------------------------------------------
|
|
|
|
# Only the X1/H2 class publishes an rtsp_url - confirmed at the network layer:
|
|
# the H2C accepts TCP on 322, a P1P refuses it and listens on 6000 instead. So
|
|
# "some printers have a stream and most do not" is the expected state, and both
|
|
# halves are asserted. A run where no printer has a camera still passes, but
|
|
# says so rather than claiming the path works.
|
|
echo "==> the camera path: what the gateway reported, and what the API answers"
|
|
WITH_CAMERA="$(psql_q "SELECT gateway_printer_id FROM printers
|
|
WHERE farm_id='$FARM_ID' AND camera_stream IS NOT NULL
|
|
ORDER BY gateway_printer_id LIMIT 1")"
|
|
|
|
if [ -z "$WITH_CAMERA" ]; then
|
|
echo " SKIPPED: no printer reported a camera stream."
|
|
echo " Expected if only P1/A1-class printers are on: they publish no rtsp_url."
|
|
else
|
|
# The gateway sets camera_stream to its own printer id, so this also checks
|
|
# the two ends agree about the name a stream is served under.
|
|
STREAM_NAME="$(psql_q "SELECT camera_stream FROM printers
|
|
WHERE farm_id='$FARM_ID' AND gateway_printer_id='$WITH_CAMERA'")"
|
|
[ "$STREAM_NAME" = "$WITH_CAMERA" ] \
|
|
|| fail "camera_stream for '$WITH_CAMERA' is '$STREAM_NAME', expected its own gateway id"
|
|
ok "'$WITH_CAMERA' reported camera stream '$STREAM_NAME'"
|
|
|
|
CAM_UUID="$(psql_q "SELECT id FROM printers
|
|
WHERE farm_id='$FARM_ID' AND gateway_printer_id='$WITH_CAMERA'")"
|
|
STREAM="$(curl -sf -H "$AUTH_HEADER" "$BASE/printers/$CAM_UUID/stream")" \
|
|
|| fail "GET /printers/$CAM_UUID/stream failed for a printer that has a camera"
|
|
echo "$STREAM" | grep -q "\"cameraId\":\"$WITH_CAMERA\"" \
|
|
|| fail "wrong cameraId: $STREAM"
|
|
# Deliberately NOT a URL any more. A browser cannot play from a gateway's
|
|
# own address - go2rtc sends no CORS headers and 401s an unauthenticated
|
|
# preflight - so serving one offered a player that could never load. If
|
|
# this reappears, something has regressed to the pre-relay design.
|
|
echo "$STREAM" | grep -q '"streamUrl"' \
|
|
&& fail "the stream endpoint is serving a gateway URL again: $STREAM"
|
|
# No Cloudflare credentials in this test, so TURN is off and iceServers is
|
|
# empty. Both are asserted, because an empty list with turnConfigured true
|
|
# would mean minting silently failed.
|
|
echo "$STREAM" | grep -q '"turnConfigured":false' \
|
|
|| fail "expected TURN to be unconfigured in the test env: $STREAM"
|
|
echo "$STREAM" | grep -q '"iceServers":\[\]' \
|
|
|| fail "expected no ICE servers when TURN is unconfigured: $STREAM"
|
|
ok "GET .../stream: $STREAM"
|
|
|
|
# The signalling relay, end to end through the real gateway: an offer goes
|
|
# cloud -> uplink -> gateway -> its own go2rtc. go2rtc is not running in
|
|
# this test (see CONTINUUM_GO2RTC_BIN above), so the honest expectation is
|
|
# a 503 whose reason came back *from the gateway* rather than from a
|
|
# timeout - that is what proves the round trip happened at all.
|
|
OFFER_CODE="$(curl -s -o "$WORK/offer.json" -w '%{http_code}' -X POST \
|
|
-H "$AUTH_HEADER" -H 'Content-Type: application/json' \
|
|
-d '{"sdp":"v=0\r\no=- 0 0 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\n"}' \
|
|
"$BASE/printers/$CAM_UUID/webrtc")"
|
|
# go2rtc is deliberately not running here, so the expected answer is the
|
|
# gateway reporting that it could not reach it - which still proves the
|
|
# offer travelled cloud -> uplink -> gateway. A timeout would mean it did
|
|
# not. Verified once by hand with the real go2rtc and a valid offer, which
|
|
# returns a 200 and an answer SDP carrying go2rtc's own ICE candidates;
|
|
# that is not automated here because it needs a live camera and go2rtc
|
|
# installed, which would make this script depend on both.
|
|
if [ "$OFFER_CODE" = "503" ] && grep -q 'go2rtc' "$WORK/offer.json"; then
|
|
ok "the WebRTC offer round-tripped to the gateway: $(head -c 170 "$WORK/offer.json")"
|
|
elif [ "$OFFER_CODE" = "503" ] && grep -q 'did not answer within' "$WORK/offer.json"; then
|
|
fail "the gateway never answered the offer - the relay did not round-trip: $(cat "$WORK/offer.json")"
|
|
else
|
|
fail "unexpected answer to a WebRTC offer (HTTP $OFFER_CODE): $(cat "$WORK/offer.json")"
|
|
fi
|
|
fi
|
|
|
|
# A printer with no camera must 404, and say which half is missing. This is
|
|
# the case a frontend hits for most of a mixed fleet, so it matters that it is
|
|
# a clean refusal rather than a 200 carrying nulls.
|
|
NO_CAMERA="$(psql_q "SELECT id FROM printers
|
|
WHERE farm_id='$FARM_ID' AND camera_stream IS NULL
|
|
AND gateway_printer_id IS NOT NULL LIMIT 1")"
|
|
if [ -n "$NO_CAMERA" ]; then
|
|
CODE="$(curl -s -o "$WORK/nocam.json" -w '%{http_code}' -H "$AUTH_HEADER" \
|
|
"$BASE/printers/$NO_CAMERA/stream")"
|
|
[ "$CODE" = "404" ] || fail "expected 404 for a printer with no camera, got $CODE"
|
|
grep -q 'no camera' "$WORK/nocam.json" \
|
|
|| fail "the 404 should blame the printer's camera, not the gateway: $(cat "$WORK/nocam.json")"
|
|
ok "a printer with no camera is refused with 404, blaming the right half"
|
|
fi
|
|
|
|
# --- 5c. the operator console's live feed ----------------------------------
|
|
|
|
# The user-facing socket, driven for real. The unit tests can only mirror its
|
|
# composition, because the route imports ../../db; this is the only thing that
|
|
# proves it is wired into the app at all, that the Supabase token verifies on
|
|
# the upgrade, and that a gateway's telemetry actually reaches a console.
|
|
echo "==> /ws/telemetry: connecting as the operator and waiting for a push"
|
|
WS_OUT="$WORK/telemetry.json"
|
|
if bun scripts/watch-telemetry.ts "ws://localhost:$PORT" "$USER_TOKEN" 12 > "$WS_OUT" 2>"$WORK/ws.err"; then
|
|
python3 - "$WS_OUT" <<'PYCHK' || fail "the telemetry socket did not behave: $(cat "$WS_OUT" "$WORK/ws.err")"
|
|
import json, sys
|
|
d = json.load(open(sys.argv[1]))
|
|
# "bearer" and not the token: the runtime echoes the first offered subprotocol
|
|
# back in the response header, so the credential must not be in that position.
|
|
assert d["negotiated"] == "bearer", f'echoed subprotocol was {d["negotiated"]!r}, expected "bearer"'
|
|
kinds = [f.get("type") for f in d["frames"]]
|
|
assert "ready" in kinds, f"no ready frame; got {kinds}"
|
|
ready = next(f for f in d["frames"] if f.get("type") == "ready")
|
|
assert ready["visiblePrinters"] >= 1, f"ready says {ready['visiblePrinters']} visible printers"
|
|
pushes = [f for f in d["frames"] if f.get("type") == "printer.telemetry"]
|
|
assert pushes, f"no printer.telemetry push arrived; got {kinds}"
|
|
# A push must be the printer resource, not a telemetry-shaped subset, and must
|
|
# carry real readings rather than a touched row of nulls.
|
|
p = pushes[-1]["printer"]
|
|
for key in ("id", "farmId", "name", "vendor"):
|
|
assert key in p, f"push is missing {key}: {p}"
|
|
assert p.get("vendorState"), f"push carries no vendor state: {p}"
|
|
print(f' ok: {len(pushes)} push(es); last was {p["name"]} '
|
|
f'state={p["vendorState"]} nozzle={p.get("nozzleTempC")} camera={p.get("cameraStream")}')
|
|
PYCHK
|
|
else
|
|
fail "could not connect to /ws/telemetry: $(cat "$WORK/ws.err")"
|
|
fi
|
|
ok "the console socket authenticated and received live telemetry"
|
|
|
|
# A socket with no valid credential must not open at all - the whole point of
|
|
# checking at the upgrade rather than in a first message.
|
|
echo "==> an unauthenticated console socket must be refused"
|
|
if bun scripts/watch-telemetry.ts "ws://localhost:$PORT" "not-a-real-token" 5 \
|
|
> "$WORK/ws-bad.json" 2>&1; then
|
|
fail "a socket opened with a bogus token: $(cat "$WORK/ws-bad.json")"
|
|
fi
|
|
ok "a bogus token is refused before any socket exists"
|
|
|
|
# --- 6. the negative case --------------------------------------------------
|
|
|
|
echo "==> an unclaimed printer must be refused, not guessed at"
|
|
ORPHAN="$(psql_q "INSERT INTO printers (farm_id, name, vendor)
|
|
VALUES ('$FARM_ID', 'unclaimed', 'bambu') RETURNING id")"
|
|
CODE="$(curl -s -o "$WORK/orphan.json" -w '%{http_code}' -X POST \
|
|
"$BASE/printers/$ORPHAN/commands" -H 'content-type: application/json' \
|
|
-H "$AUTH_HEADER" -d '{"action":"set_fan_speed","percent":0}')"
|
|
[ "$CODE" = "409" ] || fail "expected HTTP 409 for an unclaimed printer, got $CODE"
|
|
grep -q 'PRINTER_UNREACHABLE' "$WORK/orphan.json" || fail "wrong error: $(cat "$WORK/orphan.json")"
|
|
ok "unclaimed printer refused with 409 PRINTER_UNREACHABLE"
|
|
|
|
echo
|
|
# Say what was actually exercised. Claiming commands work when the command test
|
|
# was skipped is worse than not running it.
|
|
if [ -n "${WITH_CAMERA:-}" ]; then
|
|
echo " Camera path verified: '$WITH_CAMERA' reported a stream and the API served it."
|
|
else
|
|
echo " Camera path NOT verified: no printer on this LAN publishes an rtsp_url."
|
|
fi
|
|
if [ "$COMMAND_TESTED" = yes ]; then
|
|
echo "PASS - telemetry and commands both verified against a real database and real printers."
|
|
else
|
|
echo "PARTIAL PASS - telemetry verified against a real database and real printers."
|
|
echo " The command path was NOT exercised: no printer was idle enough to send to."
|
|
echo " Rerun with PRINTER_ID=<an idle printer> once one is free."
|
|
fi
|