Nothing subscribed to channel:print_alerts and nothing watched a printer fail, so an operator away from the screen found out when they came back to a ruined plate. Alerts are now rows in printer_alerts, listed at GET /alerts (open=true for what still needs a person) and cleared with POST /alerts/:id/acknowledge, which is operator-only and keeps the first acknowledger on the record. Two sources. Telemetry: a change into error is a failed print if the printer was printing or paused, otherwise a printer error; a print the printer paused itself (how an AMS fault looks) is announced too. Transitions, not levels - a printer sitting in error is one alert, not one per frame - and a connection's first sight of a printer raises nothing, so a gateway that reconnects does not re-announce known faults. AI: the worker's Valkey publications are stored when confidence is at least ALERT_MIN_CONFIDENCE (default 0.6); the worker deliberately does not threshold, so the policy lives here. A bad message costs one message, not the listener. e2e-authz.sh gains the scoping/ack cases; new e2e-alerts.sh drives a stand-in gateway over the real /ws/edge/v1 and the real Valkey channel (checked to fail when the derivation is broken).
357 lines
17 KiB
Bash
Executable File
357 lines
17 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# End-to-end test of authentication and the org -> farm -> printer
|
|
# authorization chain, against a real database and the real HTTP routes.
|
|
#
|
|
# 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:-3020}"
|
|
AUTH_PORT="${AUTH_PORT:-3021}"
|
|
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=""
|
|
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")
|
|
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
|
|
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"])'; }
|
|
|
|
OWNER_A_ID="$(new_uuid)"; OWNER_A="$(mint "$OWNER_A_ID")"
|
|
OPERATOR_A_ID="$(new_uuid)"; OPERATOR_A="$(mint "$OPERATOR_A_ID")"
|
|
VIEWER_A_ID="$(new_uuid)"; VIEWER_A="$(mint "$VIEWER_A_ID")"
|
|
OUTSIDER_ID="$(new_uuid)"; OUTSIDER="$(mint "$OUTSIDER_ID")"
|
|
ok "minted four access tokens"
|
|
|
|
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
|
|
curl -sf "$BASE/health" >/dev/null 2>&1 || fail "backend did not become healthy: $(cat "$WORK/backend.log")"
|
|
ok "backend healthy"
|
|
|
|
# --- 2. anonymous access --------------------------------------------------
|
|
|
|
echo "==> nothing works without a token"
|
|
expect 200 GET /health - -- "GET /health is public"
|
|
expect 401 GET /orgs - -- "GET /orgs rejects anonymous"
|
|
expect 401 GET /farms - -- "GET /farms rejects anonymous"
|
|
expect 401 GET /printers - -- "GET /printers rejects anonymous"
|
|
expect 401 GET /jobs - -- "GET /jobs rejects anonymous"
|
|
expect 401 GET /auth/session - -- "GET /auth/session rejects anonymous"
|
|
expect 401 POST /farms - '{"name":"x","orgId":"00000000-0000-0000-0000-000000000000"}' -- "POST /farms rejects anonymous"
|
|
expect 401 GET /farms -H.invalid.token -- "a malformed token is rejected"
|
|
|
|
# --- 3. the seeded world --------------------------------------------------
|
|
|
|
echo "==> building org A (via the API, as owner_a)"
|
|
expect 201 POST /orgs "$OWNER_A" '{"name":"e2e org A"}' -- "owner_a creates org A"
|
|
ORG_A="$(json 'd["id"]')"
|
|
body_has '"role":"owner"'
|
|
ok "org A = $ORG_A, creator is its owner"
|
|
|
|
expect 200 GET /auth/session "$OWNER_A" -- "owner_a reads their session"
|
|
body_has "$ORG_A"
|
|
|
|
expect 200 PUT "/orgs/$ORG_A/members/$OPERATOR_A_ID" "$OWNER_A" '{"role":"operator"}' -- "owner_a adds operator_a"
|
|
expect 200 PUT "/orgs/$ORG_A/members/$VIEWER_A_ID" "$OWNER_A" '{"role":"viewer"}' -- "owner_a adds viewer_a"
|
|
|
|
expect 201 POST /farms "$OWNER_A" "{\"name\":\"e2e farm A\",\"orgId\":\"$ORG_A\"}" -- "owner_a creates farm A"
|
|
FARM_A="$(json 'd["id"]')"
|
|
body_lacks edgeProxyToken
|
|
ok "farm A = $FARM_A, and the create response carries no proxy token"
|
|
|
|
# `accessCode` is deliberately still sent, even though the column was removed
|
|
# (2026-10-19) and the field is now unknown to the create schema. That is the
|
|
# point: a client that believes it can hand the cloud a printer's LAN password
|
|
# must have it dropped and never see it again. The schema is open, so the
|
|
# field validates and Elysia cleans it away rather than rejecting the request
|
|
# - which is behaviour worth proving against a real server, not just against
|
|
# the schema (see the credentials block in tests/authz.test.ts).
|
|
expect 201 POST /printers "$OWNER_A" \
|
|
"{\"farmId\":\"$FARM_A\",\"name\":\"e2e printer A\",\"vendor\":\"bambu\",\"accessCode\":\"SECRET123\",\"serialNumber\":\"SN-SECRET\"}" \
|
|
-- "owner_a adds printer A, trying to set an access code"
|
|
PRINTER_A="$(json 'd["id"]')"
|
|
body_lacks SECRET123
|
|
ok "printer A = $PRINTER_A, and the submitted access code was dropped, not echoed"
|
|
|
|
echo "==> building org B (via the API, as the outsider)"
|
|
expect 201 POST /orgs "$OUTSIDER" '{"name":"e2e org B"}' -- "outsider creates their own org B"
|
|
ORG_B="$(json 'd["id"]')"
|
|
|
|
# --- 4. scoping: a list shows only your own ------------------------------
|
|
|
|
echo "==> lists are scoped to the caller's organisations"
|
|
expect 200 GET /farms "$VIEWER_A" -- "viewer_a lists farms"
|
|
body_has "$FARM_A"
|
|
expect 200 GET /farms "$OUTSIDER" -- "the outsider lists farms"
|
|
body_lacks "$FARM_A"
|
|
ok "the outsider's farm list does not contain farm A"
|
|
|
|
expect 200 GET /printers "$OUTSIDER" -- "the outsider lists printers"
|
|
body_lacks "$PRINTER_A"
|
|
ok "the outsider's printer list does not contain printer A"
|
|
|
|
expect 200 GET /orgs "$OUTSIDER" -- "the outsider lists organisations"
|
|
body_lacks "$ORG_A"
|
|
ok "the outsider cannot see org A exists"
|
|
|
|
# --- 5. a non-member gets 404, not 403 -----------------------------------
|
|
|
|
echo "==> a non-member cannot distinguish 'forbidden' from 'does not exist'"
|
|
expect 404 GET "/farms/$FARM_A" "$OUTSIDER" -- "GET another org's farm"
|
|
expect 404 GET "/printers/$PRINTER_A" "$OUTSIDER" -- "GET another org's printer"
|
|
expect 404 GET "/orgs/$ORG_A" "$OUTSIDER" -- "GET another org"
|
|
expect 404 GET "/orgs/$ORG_A/members" "$OUTSIDER" -- "GET another org's members"
|
|
expect 404 DELETE "/farms/$FARM_A" "$OUTSIDER" -- "DELETE another org's farm"
|
|
expect 404 PATCH "/printers/$PRINTER_A" "$OUTSIDER" '{"name":"hijacked"}' -- "PATCH another org's printer"
|
|
expect 404 PUT "/orgs/$ORG_A/members/$OUTSIDER_ID" "$OUTSIDER" '{"role":"owner"}' -- "add themselves to another org"
|
|
expect 404 POST "/farms/$FARM_A/edge-proxy-token" "$OUTSIDER" -- "rotate another farm's gateway credential"
|
|
expect 404 POST "/printers/$PRINTER_A/commands" "$OUTSIDER" '{"action":"set_fan_speed","percent":100}' \
|
|
-- "command another org's printer"
|
|
# The same id, as a member: proves the 404s above are about membership and not
|
|
# about the farm being missing or the route being broken.
|
|
expect 200 GET "/farms/$FARM_A" "$VIEWER_A" -- "the same farm id, as a member of its org"
|
|
|
|
# --- 6. the role ladder, over HTTP ---------------------------------------
|
|
|
|
echo "==> roles gate what a member can do"
|
|
expect 200 GET "/printers/$PRINTER_A" "$VIEWER_A" -- "a viewer reads a printer"
|
|
expect 403 POST "/printers/$PRINTER_A/commands" "$VIEWER_A" '{"action":"set_fan_speed","percent":100}' \
|
|
-- "a viewer cannot command a printer"
|
|
body_has FORBIDDEN
|
|
# 409 PRINTER_UNREACHABLE, not 403: no gateway is connected in this test, so
|
|
# authorization passing is exactly what reaching that error means.
|
|
expect 409 POST "/printers/$PRINTER_A/commands" "$OPERATOR_A" '{"action":"set_fan_speed","percent":100}' \
|
|
-- "an operator passes authorization and fails on delivery"
|
|
body_has PRINTER_UNREACHABLE
|
|
|
|
expect 403 POST "/jobs" "$VIEWER_A" "{\"farmId\":\"$FARM_A\",\"fileName\":\"v.3mf\"}" -- "a viewer cannot submit a job"
|
|
expect 201 POST "/jobs" "$OPERATOR_A" "{\"farmId\":\"$FARM_A\",\"fileName\":\"o.3mf\"}" -- "an operator submits a job"
|
|
JOB_A="$(json 'd["job"]["id"]')"
|
|
SUBMITTER="$(json 'd["job"]["submittedBy"]')"
|
|
[ "$SUBMITTER" = "$OPERATOR_A_ID" ] \
|
|
&& ok "submittedBy came from the token, not the body ($SUBMITTER)" \
|
|
|| bad "submittedBy is '$SUBMITTER', expected '$OPERATOR_A_ID'"
|
|
|
|
# The old route took `submittedBy` from the body. Sending it now must not
|
|
# change the recorded submitter - an ignored extra field, not an override.
|
|
expect 201 POST "/jobs" "$OPERATOR_A" \
|
|
"{\"farmId\":\"$FARM_A\",\"fileName\":\"spoof.3mf\",\"submittedBy\":\"$OWNER_A_ID\"}" \
|
|
-- "a job body claiming another submitter"
|
|
SPOOFED="$(json 'd["job"]["submittedBy"]')"
|
|
[ "$SPOOFED" = "$OPERATOR_A_ID" ] \
|
|
&& ok "a spoofed submittedBy in the body is ignored" \
|
|
|| bad "the body overrode submittedBy: got '$SPOOFED'"
|
|
|
|
expect 403 PATCH "/farms/$FARM_A" "$OPERATOR_A" '{"name":"renamed by operator"}' -- "an operator cannot rename a farm"
|
|
expect 200 PATCH "/farms/$FARM_A" "$OWNER_A" '{"name":"e2e farm A renamed"}' -- "an owner can rename a farm"
|
|
expect 403 POST "/printers" "$OPERATOR_A" "{\"farmId\":\"$FARM_A\",\"name\":\"nope\",\"vendor\":\"bambu\"}" \
|
|
-- "an operator cannot add a printer"
|
|
|
|
# --- 7. credentials stay on the server ------------------------------------
|
|
|
|
echo "==> credentials never appear in an ordinary response"
|
|
expect 200 GET "/farms/$FARM_A" "$OWNER_A" -- "an owner fetches their own farm"
|
|
body_lacks edgeProxyToken
|
|
expect 200 GET /farms "$OWNER_A" -- "an owner lists their farms"
|
|
body_lacks edgeProxyToken
|
|
expect 200 GET "/printers/$PRINTER_A" "$OWNER_A" -- "an owner fetches their own printer"
|
|
body_lacks SECRET123
|
|
body_lacks accessCode
|
|
ok "no proxy token and no access code in any read"
|
|
|
|
echo "==> rotation is owner-only, and returns the new value exactly once"
|
|
expect 403 POST "/farms/$FARM_A/edge-proxy-token" "$OPERATOR_A" -- "an operator cannot rotate"
|
|
expect 200 POST "/farms/$FARM_A/edge-proxy-token" "$OWNER_A" -- "an owner rotates"
|
|
ROTATED="$(json 'd["edgeProxyToken"]')"
|
|
[ -n "$ROTATED" ] || bad "rotation returned no token"
|
|
STORED="$(psql_q "SELECT edge_proxy_token FROM farms WHERE id='$FARM_A'")"
|
|
[ "$ROTATED" = "$STORED" ] && ok "the returned token is the one now stored" || bad "rotation did not persist"
|
|
|
|
# And it still works for what it is for.
|
|
CODE="$(req POST /auth/edge-token - "{\"farmId\":\"$FARM_A\",\"edgeProxyToken\":\"$ROTATED\"}")"
|
|
[ "$CODE" = "200" ] && ok "the rotated token still mints an edge JWT (no user token needed)" \
|
|
|| bad "POST /auth/edge-token returned $CODE for the rotated token"
|
|
expect 401 POST /auth/edge-token - "{\"farmId\":\"$FARM_A\",\"edgeProxyToken\":\"not-the-token\"}" \
|
|
-- "a wrong proxy token is refused"
|
|
|
|
# --- 7b. alerts ---------------------------------------------------------
|
|
|
|
echo "==> alerts are scoped, and acknowledging is an operator's decision"
|
|
ALERT_A="$(new_uuid)"
|
|
psql_q "INSERT INTO printer_alerts (id, farm_id, printer_id, kind, message) VALUES ('$ALERT_A', '$FARM_A', '$PRINTER_A', 'print_failed', 'e2e: the print failed')" >/dev/null
|
|
expect 200 GET "/alerts?open=true" "$VIEWER_A" -- "a viewer lists open alerts"
|
|
body_has "$ALERT_A"
|
|
expect 200 GET "/alerts?open=true" "$OUTSIDER" -- "the outsider lists open alerts"
|
|
body_lacks "$ALERT_A"
|
|
ok "the outsider's alert list does not contain farm A's alert"
|
|
expect 404 POST "/alerts/$ALERT_A/acknowledge" "$OUTSIDER" -- "the outsider cannot acknowledge another org's alert"
|
|
expect 403 POST "/alerts/$ALERT_A/acknowledge" "$VIEWER_A" -- "a viewer cannot acknowledge"
|
|
expect 200 POST "/alerts/$ALERT_A/acknowledge" "$OPERATOR_A" -- "an operator acknowledges"
|
|
WHO="$(json 'd["acknowledgedBy"]')"
|
|
[ "$WHO" = "$OPERATOR_A_ID" ] && ok "acknowledgedBy came from the token" || bad "acknowledgedBy is '$WHO'"
|
|
expect 200 POST "/alerts/$ALERT_A/acknowledge" "$OWNER_A" -- "acknowledging again is harmless"
|
|
WHO2="$(json 'd["acknowledgedBy"]')"
|
|
[ "$WHO2" = "$OPERATOR_A_ID" ] && ok "the first acknowledger stays on the record" || bad "second ack overwrote the record: '$WHO2'"
|
|
expect 200 GET "/alerts?open=true" "$VIEWER_A" -- "an acknowledged alert leaves the open list"
|
|
body_lacks "$ALERT_A"
|
|
expect 401 GET /alerts - -- "GET /alerts rejects anonymous"
|
|
|
|
# --- 8. the guards that stop an org becoming unadministrable --------------
|
|
|
|
echo "==> an organisation cannot be left without an owner"
|
|
expect 409 DELETE "/orgs/$ORG_A/members/$OWNER_A_ID" "$OWNER_A" -- "the only owner cannot remove themselves"
|
|
expect 409 PUT "/orgs/$ORG_A/members/$OWNER_A_ID" "$OWNER_A" '{"role":"viewer"}' -- "the only owner cannot demote themselves"
|
|
expect 409 DELETE "/orgs/$ORG_A" "$OWNER_A" -- "an org with farms attached cannot be deleted"
|
|
body_has CONFLICT
|
|
expect 200 PUT "/orgs/$ORG_A/members/$OPERATOR_A_ID" "$OWNER_A" '{"role":"owner"}' -- "promote a second owner"
|
|
expect 200 DELETE "/orgs/$ORG_A/members/$OWNER_A_ID" "$OWNER_A" -- "now the first owner may leave"
|
|
expect 404 GET "/orgs/$ORG_A" "$OWNER_A" -- "and immediately loses access to the org"
|
|
|
|
# --- 9. pagination is bounded --------------------------------------------
|
|
|
|
echo "==> list endpoints are bounded"
|
|
expect 422 GET "/jobs?limit=10000" "$OPERATOR_A" -- "an oversized limit is refused, not silently clamped"
|
|
expect 200 GET "/jobs?limit=1" "$OPERATOR_A" -- "a limit of 1 is honoured"
|
|
COUNT="$(json 'len(d)')"
|
|
[ "$COUNT" = "1" ] && ok "one row returned" || bad "limit=1 returned $COUNT rows"
|
|
|
|
echo
|
|
if [ "$FAILURES" -eq 0 ]; then
|
|
echo "PASS - authentication, org scoping, the role ladder and credential"
|
|
echo " containment all verified against a real database."
|
|
else
|
|
echo "FAIL - $FAILURES assertion(s) failed." >&2
|
|
exit 1
|
|
fi
|