POST /farms/:id/printers now takes any of the four vendors the gateway can drive. Credentials depend on the vendor: Bambu needs an access code and serial, OctoPrint and PrusaLink an API key, Klipper nothing. A missing one is a 422 naming what is needed. The API key is relayed to the gateway and kept out of logs and errors like the access code. The printer_vendor enum gains octoprint (migration 0006); PrusaLink is stored as prusa, the name the enum already had. scripts/e2e-vendors.sh adds each kind through the API against a fake REST printer (scripts/fake-http-printer.ts) and runs a job through the real scheduler to completion. The fake shares its author's reading of the docs with the driver, so this proves the plumbing, not the protocols.
138 lines
7.7 KiB
Bash
Executable File
138 lines
7.7 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# The non-Bambu vendors end to end, against FAKE printers: add each one through
|
|
# the API (so the provision frame, the real proxy's driver and its store are
|
|
# all exercised), then run a job through the real scheduler and watch it go
|
|
# queued -> uploading -> printing -> completed from the fake's own telemetry.
|
|
#
|
|
# What this proves is the plumbing around the drivers. It does NOT prove the
|
|
# drivers speak the real Klipper / OctoPrint / PrusaLink protocols: the fake
|
|
# (scripts/fake-http-printer.ts) is written from the same reading of the docs
|
|
# as the driver, so a shared misreading passes here and fails on hardware.
|
|
#
|
|
# Needs the dev stack (scripts/dev-env.sh up && schema) and the object store
|
|
# credentials in ../dev-secrets.sh (the job file goes through the real
|
|
# presigned URLs). No real printer is touched.
|
|
set -euo pipefail
|
|
|
|
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
cd "$REPO_ROOT"
|
|
[ -f "$REPO_ROOT/../dev-secrets.sh" ] && . "$REPO_ROOT/../dev-secrets.sh"
|
|
|
|
KINDS="${KINDS:-klipper octoprint prusa_link}"
|
|
PROXY_DIR="${PROXY_DIR:-$REPO_ROOT/../continuum-proxy}"
|
|
PORT="${PORT:-3062}"; AUTH_PORT="${AUTH_PORT:-3063}"; FAKE_PORT="${FAKE_PORT:-7141}"
|
|
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}"
|
|
export DATABASE_URL VALKEY_URL EDGE_JWT_SECRET PORT R2_ACCESS_KEY_ID R2_SECRET_ACCESS_KEY R2_ENDPOINT
|
|
WORK="$(mktemp -d)"
|
|
BACKEND_PID=""; PROXY_PID=""; AUTH_PID=""; FAKE_PID=""; ORGS=""
|
|
fail() { echo "FAIL: $*" >&2; exit 1; }
|
|
ok() { echo " ok: $*"; }
|
|
psql_q() { psql "$DATABASE_URL" -tAq -c "$1"; }
|
|
new_uuid() { cat /proc/sys/kernel/random/uuid; }
|
|
json() { python3 -c "import json,sys; d=json.load(sys.stdin); print($1)"; }
|
|
|
|
stop() { for pid in "$@"; do [ -n "$pid" ] && kill "$pid" 2>/dev/null || true; done; sleep 0.5; for pid in "$@"; do [ -n "$pid" ] && kill -9 "$pid" 2>/dev/null || true; done; }
|
|
cleanup() {
|
|
local rc=$?
|
|
stop "$PROXY_PID" "$FAKE_PID" "$BACKEND_PID" "$AUTH_PID"
|
|
for org in $ORGS; do psql_q "DELETE FROM organisations WHERE id = '$org'" >/dev/null 2>&1 || true; done
|
|
if [ $rc -ne 0 ]; then
|
|
echo "--- backend log (tail) ---"; tail -25 "$WORK/backend.log" 2>/dev/null || true
|
|
echo "--- proxy log (tail) ---"; tail -30 "$WORK/proxy.log" 2>/dev/null || true
|
|
echo "--- fake printer log (tail) ---"; tail -15 "$WORK/fake.log" 2>/dev/null || true
|
|
fi
|
|
rm -rf "$WORK"; exit $rc
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
[ -n "${R2_ENDPOINT:-}" ] || fail "no object store credentials - ../dev-secrets.sh"
|
|
( cd "$PROXY_DIR" && cargo build --quiet 2>&1 | tail -3 ) || fail "proxy did not build"
|
|
|
|
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_ID="$(new_uuid)"
|
|
TOKEN="$(curl -sf "$AUTH_URL/token?sub=$USER_ID" | json 'd["access_token"]')"
|
|
auth=(-H "authorization: Bearer $TOKEN")
|
|
|
|
DISPATCH_ENABLED=true 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"
|
|
|
|
for KIND in $KINDS; do
|
|
echo "==> $KIND"
|
|
KEY="key-$(new_uuid | cut -c1-8)"
|
|
bun scripts/fake-http-printer.ts "$KIND" "$FAKE_PORT" "$KEY" > "$WORK/fake.log" 2>&1 &
|
|
FAKE_PID=$!
|
|
for _ in $(seq 1 40); do grep -q listening "$WORK/fake.log" && break; sleep 0.25; done
|
|
|
|
ORG_ID="$(curl -sf "${auth[@]}" -H 'content-type: application/json' -d "{\"name\":\"e2e vendors $KIND\"}" "$BASE/orgs" | json 'd["id"]')"
|
|
ORGS="$ORGS $ORG_ID"
|
|
FARM_ID="$(curl -sf "${auth[@]}" -H 'content-type: application/json' -d "{\"name\":\"farm $KIND\",\"orgId\":\"$ORG_ID\"}" "$BASE/farms" | json 'd["id"]')"
|
|
GW_TOKEN="$(curl -sf "${auth[@]}" -X POST "$BASE/farms/$FARM_ID/edge-proxy-token" | json 'd["edgeProxyToken"]')"
|
|
|
|
env -C "$PROXY_DIR" \
|
|
CONTINUUM_ALLOW_INSECURE_LOCAL_DOWNLOADS=true CONTINUUM_GATEWAY_ID=e2e-vendors-gw CONTINUUM_FARM_ID="$FARM_ID" CONTINUUM_GATEWAY_TOKEN="$GW_TOKEN" \
|
|
CONTINUUM_UPLINK_URL="ws://localhost:$PORT/ws/edge/v1" CONTINUUM_GO2RTC_BIN=/nonexistent-go2rtc CONTINUUM_GO2RTC_CONFIG="$WORK/go2rtc.yaml" \
|
|
CONTINUUM_PRINTERS_PATH="$WORK/no-such-printers.toml" CONTINUUM_STORE_PATH="$WORK/store-$KIND/continuum.db" RUST_LOG=info \
|
|
"$PROXY_DIR/target/debug/continuum-proxy" > "$WORK/proxy.log" 2>&1 &
|
|
PROXY_PID=$!
|
|
# This farm's own connect line: the log is shared across the loop.
|
|
for _ in $(seq 1 60); do grep -q "connected for farm $FARM_ID" "$WORK/backend.log" && break; sleep 0.5; done
|
|
grep -q "connected for farm $FARM_ID" "$WORK/backend.log" || fail "the proxy never connected"
|
|
|
|
# Klipper needs no credential; the others take an API key.
|
|
CRED=""; [ "$KIND" != klipper ] && CRED=",\"apiKey\":\"$KEY\""
|
|
CODE="$(curl -s -o "$WORK/add.json" -w '%{http_code}' "${auth[@]}" -H 'content-type: application/json' \
|
|
-d "{\"name\":\"Fake $KIND\",\"vendor\":\"$KIND\",\"host\":\"127.0.0.1:$FAKE_PORT\"$CRED}" "$BASE/farms/$FARM_ID/printers")"
|
|
[ "$CODE" = 201 ] || fail "adding the $KIND printer returned $CODE: $(head -c 300 "$WORK/add.json")"
|
|
PRN_UUID="$(json 'd["id"]' < "$WORK/add.json")"
|
|
ok "added through the API and connected by the proxy"
|
|
grep -q "$KEY" "$WORK/proxy.log" "$WORK/backend.log" && fail "the API key is in a log"
|
|
ok "the API key is in neither log"
|
|
|
|
if [ "$KIND" != klipper ]; then
|
|
CODE="$(curl -s -o /dev/null -w '%{http_code}' "${auth[@]}" -H 'content-type: application/json' \
|
|
-d "{\"name\":\"No key\",\"vendor\":\"$KIND\",\"host\":\"127.0.0.1:$FAKE_PORT\"}" "$BASE/farms/$FARM_ID/printers")"
|
|
[ "$CODE" = 422 ] && ok "a missing API key is refused (422)" || fail "a keyless add returned $CODE"
|
|
fi
|
|
|
|
for _ in $(seq 1 30); do [ "$(psql_q "SELECT status FROM printers WHERE id='$PRN_UUID'")" = idle ] && break; sleep 1; done
|
|
[ "$(psql_q "SELECT status FROM printers WHERE id='$PRN_UUID'")" = idle ] || fail "the cloud never saw the printer idle"
|
|
ok "the cloud sees it idle (telemetry from the driver's poller)"
|
|
|
|
J="$(curl -sf "${auth[@]}" -H 'content-type: application/json' \
|
|
-d "{\"farmId\":\"$FARM_ID\",\"fileName\":\"e2e-part.gcode\",\"contentType\":\"application/octet-stream\"}" "$BASE/jobs")"
|
|
JOB_ID="$(printf '%s' "$J" | json 'd["job"]["id"]')"
|
|
OBJECT_KEY="$(printf '%s' "$J" | json 'd["job"]["r2ObjectKey"]')"
|
|
UPLOAD_URL="$(printf '%s' "$J" | json 'd["uploadUrl"]')"
|
|
printf 'G28\nG1 X10 Y10 F3000\nM104 S0\n' > "$WORK/part.gcode"
|
|
CODE="$(curl -s -o /dev/null -w '%{http_code}' -X PUT -H 'Content-Type: application/octet-stream' --data-binary @"$WORK/part.gcode" "$UPLOAD_URL")"
|
|
[ "$CODE" = 200 ] || fail "job file upload returned $CODE"
|
|
|
|
SEEN=""; START=$SECONDS; STATUS=""
|
|
while [ $((SECONDS - START)) -lt 90 ]; do
|
|
STATUS="$(psql_q "SELECT status FROM print_jobs WHERE id='$JOB_ID'")"
|
|
case " $SEEN " in *" $STATUS "*) ;; *) SEEN="$SEEN $STATUS" ;; esac
|
|
[ "$STATUS" = completed ] || [ "$STATUS" = failed ] && break
|
|
sleep 1
|
|
done
|
|
echo " job status history:$SEEN"
|
|
[ "$STATUS" = completed ] || fail "the $KIND job ended as '$STATUS'"
|
|
ok "$KIND: queued, scheduled, uploaded, printed and completed (fake printer)"
|
|
|
|
bun -e "
|
|
import { s3 } from './src/s3';
|
|
import { DeleteObjectCommand } from '@aws-sdk/client-s3';
|
|
await s3.send(new DeleteObjectCommand({ Bucket: process.env.R2_BUCKET ?? 'continuum', Key: process.argv[1] }));
|
|
" "$OBJECT_KEY" >/dev/null 2>&1 || true
|
|
stop "$PROXY_PID" "$FAKE_PID"; PROXY_PID=""; FAKE_PID=""
|
|
done
|
|
echo "PASS - $KINDS: added from the API, scheduled and completed against fake printers."
|