The e2e scripts handed the proxy a pre-minted edge JWT, which hid that the proxy could not exchange its configured token for one. It now does, so the scripts configure it the way a real gateway is configured: with the farm's edge proxy token.
462 lines
23 KiB
Bash
Executable File
462 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 \
|
|
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
|