Prove provisioning with the real proxy, and a real printer when one is named
e2e-provision.sh used a stand-in gateway; this drives the real continuum-proxy over the real socket, which is what catches the two ends disagreeing about the provision_printer frame. With no printer named it proves the failure path (a useful error returns, no row, nothing stored on the gateway, the code in neither process's log) and that the proxy starts and connects with no printers file at all. With REAL_HOST/REAL_SERIAL/ REAL_ACCESS_CODE it also proves the success path - telemetry under the new id, an owner-only store file, and the printer returning after a restart from the store alone. It only connects and listens; it sends no command.
This commit is contained in:
Executable
+277
@@ -0,0 +1,277 @@
|
||||
#!/usr/bin/env bash
|
||||
# End-to-end test of adding a printer with the REAL continuum-proxy as the
|
||||
# gateway (e2e-provision.sh uses a stand-in): the provision frame must parse on
|
||||
# the proxy, the proxy must try the printer, and the answer must come back.
|
||||
#
|
||||
# Without REAL_HOST/REAL_SERIAL/REAL_ACCESS_CODE it points the add at a closed
|
||||
# port on this machine and proves the failure path across the real wire: a
|
||||
# useful error comes back, no printer row is created, no store entry is made,
|
||||
# and the proxy's log never contains the access code.
|
||||
#
|
||||
# WITH them (a printer you are willing to connect to - this only READS, it
|
||||
# sends no command) it proves the success path as well: 201, telemetry flows
|
||||
# under the new id, the proxy's store file is owner-only, and after the proxy
|
||||
# is restarted with NO printers file the printer comes back from the store.
|
||||
# REAL_HOST=192.168.0.20 REAL_SERIAL=... REAL_ACCESS_CODE=... scripts/e2e-provision-proxy.sh
|
||||
#
|
||||
# This is the test that the unit tests cannot be. tests/authz.test.ts proves
|
||||
# tokens are verified and that the role ladder orders correctly; only this
|
||||
# proves that each route actually asks, that scoping is applied in the query
|
||||
# rather than after it, and that credentials stay out of responses - the three
|
||||
# things that were wrong when every route was unauthenticated.
|
||||
#
|
||||
# Four users, two organisations, no printers or gateways needed:
|
||||
# owner_a, operator_a, viewer_a in org A, which owns farm A and printer A
|
||||
# outsider_b in org B, which owns nothing
|
||||
#
|
||||
# Usage:
|
||||
# scripts/dev-env.sh up && scripts/dev-env.sh schema
|
||||
# scripts/e2e-authz.sh
|
||||
#
|
||||
# KEEP_DATA=1 leave the seeded organisations behind for inspection
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "$REPO_ROOT"
|
||||
|
||||
PORT="${PORT:-3034}"
|
||||
AUTH_PORT="${AUTH_PORT:-3035}"
|
||||
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}"
|
||||
# Presigning a PUT is local HMAC with no network call, so POST /jobs works
|
||||
# offline - but the SDK refuses to sign with empty credentials.
|
||||
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
|
||||
|
||||
WORK="$(mktemp -d)"
|
||||
BACKEND_PID=""
|
||||
PROXY_PID=""
|
||||
PROXY_DIR="${PROXY_DIR:-$REPO_ROOT/../continuum-proxy}"
|
||||
AUTH_PID=""
|
||||
FAILURES=0
|
||||
|
||||
# `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"; }
|
||||
|
||||
cleanup() {
|
||||
local rc=$?
|
||||
# Never `kill $PID` with an unset PID: `kill 0` signals the whole process
|
||||
# group, which includes this script and whatever launched it.
|
||||
local pids=()
|
||||
[ -n "$BACKEND_PID" ] && pids+=("$BACKEND_PID")
|
||||
[ -n "$AUTH_PID" ] && pids+=("$AUTH_PID")
|
||||
[ -n "$PROXY_PID" ] && pids+=("$PROXY_PID")
|
||||
if [ ${#pids[@]} -gt 0 ]; then
|
||||
kill "${pids[@]}" 2>/dev/null || true
|
||||
sleep 0.3
|
||||
kill -9 "${pids[@]}" 2>/dev/null || true
|
||||
fi
|
||||
if [ -z "${KEEP_DATA:-}" ]; then
|
||||
for org in "${ORG_A:-}" "${ORG_B:-}"; do
|
||||
[ -n "$org" ] && psql_q "DELETE FROM organisations WHERE id = '$org'" >/dev/null 2>&1 || true
|
||||
done
|
||||
fi
|
||||
if [ $rc -ne 0 ]; then
|
||||
echo
|
||||
echo "--- backend log (tail) ---"; tail -30 "$WORK/backend.log" 2>/dev/null || true
|
||||
echo "--- proxy log (tail, credentials are never logged) ---"; 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: $*"; }
|
||||
bad() { echo " FAILED: $*" >&2; FAILURES=$((FAILURES + 1)); }
|
||||
|
||||
# `req <method> <path> <token|-> [json body]` -> prints the status code,
|
||||
# leaves the response body in $WORK/last.json.
|
||||
req() {
|
||||
local method="$1" path="$2" token="$3" body="${4:-}"
|
||||
local args=(-s -o "$WORK/last.json" -w '%{http_code}' -X "$method" "$BASE$path")
|
||||
[ "$token" != "-" ] && args+=(-H "authorization: Bearer $token")
|
||||
[ -n "$body" ] && args+=(-H 'content-type: application/json' -d "$body")
|
||||
curl "${args[@]}"
|
||||
}
|
||||
|
||||
# `expect <code> <method> <path> <token> [body] -- <description>`
|
||||
expect() {
|
||||
local want="$1"; shift
|
||||
local method="$1" path="$2" token="$3"; shift 3
|
||||
local body=""
|
||||
if [ "${1:-}" != "--" ]; then body="$1"; shift; fi
|
||||
shift # the --
|
||||
local what="$*"
|
||||
local got; got="$(req "$method" "$path" "$token" "$body")"
|
||||
if [ "$got" = "$want" ]; then
|
||||
ok "$what ($got)"
|
||||
else
|
||||
bad "$what: expected $want, got $got - $(head -c 300 "$WORK/last.json")"
|
||||
fi
|
||||
}
|
||||
|
||||
body_has() { grep -q "$1" "$WORK/last.json" || bad "expected '$1' in: $(head -c 300 "$WORK/last.json")"; }
|
||||
body_lacks() { grep -q "$1" "$WORK/last.json" && bad "'$1' leaked into: $(head -c 300 "$WORK/last.json")" || true; }
|
||||
json() { python3 -c "import json,sys; d=json.load(sys.stdin); print($1)" < "$WORK/last.json"; }
|
||||
|
||||
# --- 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 org_id FROM farms LIMIT 0" >/dev/null 2>&1 \
|
||||
|| fail "farms.org_id is missing - run scripts/dev-env.sh schema"
|
||||
psql_q "SELECT role FROM org_members LIMIT 0" >/dev/null 2>&1 \
|
||||
|| fail "org_members is missing - run scripts/dev-env.sh schema"
|
||||
ok "schema has the org layer"
|
||||
|
||||
# --- 1. the issuer and the 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"
|
||||
|
||||
mint() { curl -sf "$AUTH_URL/token?sub=$1" | python3 -c 'import json,sys; print(json.load(sys.stdin)["access_token"])'; }
|
||||
|
||||
|
||||
echo "==> starting continuum-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 2>&1 || fail "backend did not become healthy: $(cat "$WORK/backend.log")"
|
||||
ok "backend healthy"
|
||||
|
||||
# --- 2. a farm and a real gateway ----------------------------------------
|
||||
|
||||
OWNER_ID="$(new_uuid)"; OWNER="$(mint "$OWNER_ID")"
|
||||
expect 201 POST /orgs "$OWNER" '{"name":"e2e provision-proxy org"}' -- "create an org"
|
||||
ORG_A="$(json 'd["id"]')"
|
||||
expect 201 POST /farms "$OWNER" "{\"name\":\"e2e provision-proxy farm\",\"orgId\":\"$ORG_A\"}" -- "create a farm"
|
||||
FARM_A="$(json 'd["id"]')"
|
||||
expect 200 POST "/farms/$FARM_A/edge-proxy-token" "$OWNER" -- "mint the gateway token"
|
||||
PROXY_TOKEN="$(json 'd["edgeProxyToken"]')"
|
||||
expect 200 POST /auth/edge-token - "{\"farmId\":\"$FARM_A\",\"edgeProxyToken\":\"$PROXY_TOKEN\"}" -- "trade it for an edge JWT"
|
||||
EDGE_JWT="$(json 'd["token"]')"
|
||||
|
||||
echo "==> building and starting the real continuum-proxy with NO printers"
|
||||
( cd "$PROXY_DIR" && cargo build --quiet 2>&1 | tail -3 ) || fail "proxy did not build"
|
||||
STORE="$WORK/proxy-store/continuum.db"
|
||||
: > "$WORK/proxy.log"
|
||||
start_proxy() {
|
||||
# env -C so $! is the daemon itself and cleanup does not orphan it.
|
||||
env -C "$PROXY_DIR" \
|
||||
CONTINUUM_GATEWAY_ID=e2e-gw \
|
||||
CONTINUUM_FARM_ID="$FARM_A" \
|
||||
CONTINUUM_GATEWAY_TOKEN="$EDGE_JWT" \
|
||||
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="$STORE" \
|
||||
RUST_LOG=info \
|
||||
"$PROXY_DIR/target/debug/continuum-proxy" >> "$WORK/proxy.log" 2>&1 &
|
||||
PROXY_PID=$!
|
||||
}
|
||||
stop_proxy() {
|
||||
[ -n "$PROXY_PID" ] && kill "$PROXY_PID" 2>/dev/null || true
|
||||
sleep 1
|
||||
[ -n "$PROXY_PID" ] && kill -9 "$PROXY_PID" 2>/dev/null || true
|
||||
PROXY_PID=""
|
||||
}
|
||||
wait_for_gateway() {
|
||||
for _ in $(seq 1 60); do
|
||||
grep -q "gateway e2e-gw connected" "$WORK/backend.log" && return 0
|
||||
sleep 0.5
|
||||
done
|
||||
return 1
|
||||
}
|
||||
start_proxy
|
||||
wait_for_gateway || fail "the proxy did not connect to the backend: $(tail -5 "$WORK/proxy.log")"
|
||||
ok "the proxy connected with no printers file at all, from its store alone"
|
||||
[ -f "$STORE" ] && ok "and created its store at $STORE" || bad "no store file was created"
|
||||
|
||||
# --- 3. the failure path across the real wire ------------------------------
|
||||
|
||||
echo "==> adding a printer that cannot be reached"
|
||||
DEAD_CODE="DEADCODE-$(new_uuid | cut -c1-8)"
|
||||
expect 502 POST "/farms/$FARM_A/printers" "$OWNER" \
|
||||
"{\"name\":\"Nowhere\",\"vendor\":\"bambu\",\"host\":\"127.0.0.1\",\"accessCode\":\"$DEAD_CODE\",\"serialNumber\":\"01P00A000000099\"}" \
|
||||
-- "the proxy reports it could not connect"
|
||||
body_has GATEWAY_REFUSED
|
||||
body_has "could not connect"
|
||||
body_lacks "$DEAD_CODE"
|
||||
N="$(psql_q "SELECT count(*) FROM printers WHERE farm_id='$FARM_A'")"
|
||||
[ "$N" = "0" ] && ok "no printer row was created" || bad "a row exists after a failed add: $N"
|
||||
grep -q "$DEAD_CODE" "$WORK/proxy.log" "$WORK/backend.log" && bad "the access code is in a log" || ok "the code is in neither the proxy's nor the backend's log"
|
||||
if command -v sqlite3 >/dev/null 2>&1; then
|
||||
[ "$(sqlite3 "$STORE" 'select count(*) from printers')" = "0" ] && ok "nothing was stored on the gateway" || bad "the proxy stored a printer it could not reach"
|
||||
fi
|
||||
|
||||
# --- 4. the success path, only with a printer you name ---------------------
|
||||
|
||||
if [ -n "${REAL_HOST:-}" ] && [ -n "${REAL_SERIAL:-}" ] && [ -n "${REAL_ACCESS_CODE:-}" ]; then
|
||||
echo "==> adding the real printer at $REAL_HOST (read-only: connect and listen)"
|
||||
expect 201 POST "/farms/$FARM_A/printers" "$OWNER" \
|
||||
"{\"name\":\"Real printer\",\"vendor\":\"bambu\",\"host\":\"$REAL_HOST\",\"accessCode\":\"$REAL_ACCESS_CODE\",\"serialNumber\":\"$REAL_SERIAL\"}" \
|
||||
-- "the proxy connected to the printer and the cloud created the row"
|
||||
PRINTER_ID="$(json 'd["id"]')"
|
||||
body_lacks "$REAL_ACCESS_CODE"
|
||||
MODE="$(stat -c %a "$STORE")"
|
||||
[ "$MODE" = "600" ] && ok "the store file is owner-only (600)" || bad "the store file mode is $MODE"
|
||||
|
||||
wait_telemetry() {
|
||||
for _ in $(seq 1 40); do
|
||||
[ -n "$(psql_q "SELECT vendor_state FROM printers WHERE id='$PRINTER_ID' AND vendor_state IS NOT NULL")" ] && return 0
|
||||
sleep 0.5
|
||||
done
|
||||
return 1
|
||||
}
|
||||
wait_telemetry && ok "telemetry arrives under the new id" || bad "no telemetry for the added printer"
|
||||
|
||||
echo "==> restart the proxy with no printers file: the printer must come back from the store"
|
||||
psql_q "UPDATE printers SET vendor_state=NULL, last_seen_at=NULL WHERE id='$PRINTER_ID'" >/dev/null
|
||||
stop_proxy
|
||||
# The edge JWT is short-lived but outlives this test.
|
||||
start_proxy
|
||||
wait_telemetry && ok "after a restart the printer reports again, from the store alone" || bad "the printer did not come back after a restart"
|
||||
grep -q "$REAL_ACCESS_CODE" "$WORK/proxy.log" "$WORK/backend.log" && bad "the access code is in a log" || ok "the access code is in no log"
|
||||
else
|
||||
echo " (skipping the success path: set REAL_HOST, REAL_SERIAL and REAL_ACCESS_CODE to run it against a real printer)"
|
||||
fi
|
||||
|
||||
echo
|
||||
if [ "$FAILURES" -eq 0 ]; then
|
||||
echo "PASS - the real proxy took provisioning requests over the real wire."
|
||||
else
|
||||
echo "FAIL - $FAILURES assertion(s) failed." >&2
|
||||
exit 1
|
||||
fi
|
||||
Reference in New Issue
Block a user