diff --git a/.claude/skills/rtl-docker-fixture/SKILL.md b/.claude/skills/rtl-docker-fixture/SKILL.md index ac333923..a715d8db 100644 --- a/.claude/skills/rtl-docker-fixture/SKILL.md +++ b/.claude/skills/rtl-docker-fixture/SKILL.md @@ -33,23 +33,8 @@ bin/ln-cli bob fwdinghistory docker compose logs -f rtl ``` -Testing the **BTCPay Server integration** (RTL in single-sign-on mode behind a proxy) — -behind a compose profile, so a plain `up` does not start it: - -```bash -docker compose --profile sso up -d -./scripts/verify-sso.sh # 11 assertions over the whole entry path; non-zero on failure -open "$(bin/sso-url)" # the link BTCPay renders on its Services page -``` - Key facts when working with the fixture: -- **Run `scripts/verify-sso.sh` after touching authentication, CSRF or static serving.** - BTCPay reaches RTL over a path the standalone login never exercises — a rotating cookie - file, an unregistered `/rtl/api/authenticate/cookie` URL that falls through to the - catch-all in `server/utils/app.ts`, and a reverse proxy. Note `GET /rtl/` is served by - `express.static` and mints **no** `XSRF-TOKEN`; only the catch-all does, so a client - entering there 403s on its first POST. That is long-standing, not a regression. - **`scripts/seed.sh` is deterministic but not idempotent.** Every amount is fixed, so a fresh run always produces identical state (screenshots differ only by your change) — so **do not introduce randomness**. It refuses to run twice against an already-seeded diff --git a/CLAUDE.md b/CLAUDE.md index 6fbba349..2b869343 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -50,10 +50,7 @@ frequently do, since the controllers were written in parallel. `ERESOLVE` conflict from `@fortawesome/angular-fontawesome`. - **`npm run server` only works on Windows** — it sets `NODE_ENV` with `set X=Y&&` syntax. On macOS/Linux use `npm run serverUbuntu`. -- **`npm run lint` and `npm run test` must both be green before a PR.** Nothing will check - this for you: no build or test CI runs on an open PR. `checks.yml` fires on - `pull_request: closed` (i.e. on merge) and on tags/releases, and `rtlreviewbot` only on a - requested review or a comment — so running both locally is the only gate before merge. +- **`npm run lint` and `npm run test` must both be green before a PR.** - If lint reports hundreds of template "Parsing error" failures, look for a stale **`coverage/`** directory (git-ignored Karma output). The template linter walks its HTML report. Delete it and re-run. @@ -94,17 +91,6 @@ Eclair, wired to RTL — for end-to-end testing across all three implementations `docker/README.md`, or the `rtl-docker-fixture` skill in `.claude/skills/`. It is dev-only; every credential in it is throwaway. -The fixture also carries a **BTCPay Server SSO harness** behind a compose profile -(`docker compose --profile sso up -d`). BTCPay bundles RTL and reaches it over a path the -standalone login never exercises: a rotating cookie file, an unregistered -`/rtl/api/authenticate/cookie` URL that is not a route at all and falls through to the -catch-all in `server/utils/app.ts`, and a reverse proxy serving it under `/rtl`. Run -`docker/scripts/verify-sso.sh` (11 assertions, exits non-zero) after touching -authentication, CSRF or static serving — none of that path is covered by logging into the -fixture's own RTL. One trap it encodes: `GET /rtl/` is served by `express.static`, which -sits above the catch-all and mints no `XSRF-TOKEN`, so a client entering there gets a 403 -on its first POST. That is long-standing behaviour, not a regression. - Backend regression tests live in `test/backend/` (plain `node:test`, run against the compiled `backend/`). `npm run test` compiles the backend, then runs them (`npm run testbackend`) before the frontend Karma/Jasmine specs, so they never test stale diff --git a/docker/README.md b/docker/README.md index 7cf8ccb9..b0de051d 100644 --- a/docker/README.md +++ b/docker/README.md @@ -74,9 +74,6 @@ docker compose up -d # bitcoind, alice, bob, carol, cln, eclair, rtl ./scripts/seed.sh # fund, connect, open channels, make payments ``` -To also bring up the BTCPay single-sign-on harness, add `--profile sso` — see -[BTCPay SSO harness](#btcpay-sso-harness). - Then open — password `rtldev`. All five nodes (alice, bob, carol, cln, eclair) appear in the node switcher. @@ -121,7 +118,6 @@ bin/ln-cli bob listchannels bin/ln-cli bob fwdinghistory # forwarding history bin/e-cli getinfo # eclair-cli bin/e-cli channels -bin/sso-url # BTCPay-style SSO link (needs --profile sso) docker compose exec cln lightning-cli --network=regtest listpeerchannels # Core Lightning ``` @@ -132,94 +128,6 @@ docker compose logs -f rtl docker compose logs alice ``` -## BTCPay SSO harness - -BTCPay Server bundles RTL and runs it in single-sign-on mode, reached through a very -different entry path than the standalone login: no password, a rotating cookie, and a -reverse proxy in front. That path has broken before without the standalone flow -noticing, so the fixture can reproduce it. - -It is behind a compose profile, so a plain `docker compose up -d` does not start it: - -```bash -docker compose --profile sso up -d -./scripts/verify-sso.sh # 11 assertions over the whole entry path -open "$(bin/sso-url)" # or click through it yourself -``` - -`bin/sso-url` prints the link BTCPay renders on its Services page. Following it lands -you in RTL already authenticated, against the `alice` node. - -### How the flow works - -```mermaid -sequenceDiagram - participant B as Browser - participant P as rtl-sso-proxy
(stands in for traefik) - participant R as rtl-sso
(RTL_SSO=1) - participant C as .cookie
(shared volume) - - R->>C: writes 64 random bytes at startup - Note over B: bin/sso-url reads the cookie —
BTCPay reads the same file - B->>P: GET /rtl/api/authenticate/cookie?access-key= - P->>R: same URI, prefix passed through - R-->>B: not a registered route → catch-all:
mints XSRF-TOKEN, serves index.html - B->>P: POST /rtl/api/authenticate
{ PASSWORD, sha256(access-key) } - P->>R: - R->>C: matches → rotates the cookie - R-->>B: JWT -``` - -The three services are `rtl-sso-config-init` (stages `rtl/RTL-Config.sso.json`, same -copy-into-a-volume dance as the standalone RTL), `rtl-sso` (RTL with `RTL_SSO=1`, -`RTL_COOKIE_PATH` and `LOGOUT_REDIRECT_LINK` — the env block is lifted verbatim from -BTCPay's own compose fragment), and `rtl-sso-proxy` (nginx standing in for BTCPay's -traefik). `RTL_IMAGE` overrides both RTL containers at once, so a branch build gets -tested through both entry paths. - -It is a second RTL container rather than a flag on the first because RTL picks one -authentication mode at startup — SSO and the password login cannot coexist in one -instance. Both are up at the same time on different ports. - -### Things this makes visible - -**No prefix stripping anywhere.** RTL is built with `` and mounts -every route under `baseHref '/rtl'`, so BTCPay's traefik — and the nginx here — pass -`/rtl/…` through unmodified. The proxy deliberately 404s everything outside `/rtl`, so -a request escaping the prefix shows up as a failure instead of being quietly served. - -**The entry URL is not a real route.** `/rtl/api/authenticate/cookie` matches nothing in -`server/routes/shared/authenticate.ts`; it falls through to the catch-all in -`server/utils/app.ts`, which is what mints the `XSRF-TOKEN` cookie and serves the SPA. -The access-key is the raw cookie file content — the frontend sha256s it before posting -and the backend compares against `sha256(cookieValue)`. - -**`GET /rtl/` mints no CSRF token.** That path is served by `express.static`, which -sits *above* the catch-all, so a client entering there has no `XSRF-TOKEN` and its first -POST gets a 403. Only the catch-all mints one. This is long-standing behaviour, not a -regression — but it is why `verify-sso.sh` always seeds its cookie jar from the entry -URL, and worth remembering before concluding that CSRF is broken. - -**The cookie is effectively single-use.** Authenticating rotates it, so a stale -`bin/sso-url` link fails. BTCPay re-reads the file on every page render, which is why -this is invisible in normal use. - -### What it does not cover - -BTCPay itself is not here — no postgres, nbxplorer or btcpayserver container. So this -does not exercise BTCPay *generating* the link, its Services page, or its own upgrades. -For that, run BTCPay's own regtest stack and point it at a local image: - -```bash -# in a btcpayserver-docker checkout, after building an RTL image locally -docker build -t shahanafarooqui/rtl:dev /path/to/RTL -# then edit the rtl image tag in the generated docker-compose, or set it in -# docker-compose-generator/docker-fragments/bitcoin-lnd.yml before generating -``` - -That tests the real composition rather than this reconstruction of it; the harness here -is the fast everyday check. - ## Notes and gotchas **RTL's config.** `rtl/RTL-Config.regtest.json` is the tracked template. RTL rewrites @@ -272,7 +180,3 @@ one LND uses, eclair never sees new blocks and channels never confirm. ## Not included The Boltz swap service. - -BTCPay Server itself (postgres + nbxplorer + btcpayserver). The `sso` profile -reproduces the entry path BTCPay uses to reach RTL without running BTCPay — see -[BTCPay SSO harness](#btcpay-sso-harness) for what that covers and what it does not. diff --git a/docker/bin/sso-url b/docker/bin/sso-url deleted file mode 100755 index 08d92870..00000000 --- a/docker/bin/sso-url +++ /dev/null @@ -1,39 +0,0 @@ -#!/usr/bin/env bash -# -# Print the BTCPay-style single-sign-on entry URL for the SSO harness. -# -# docker compose --profile sso up -d -# bin/sso-url # print it -# open "$(bin/sso-url)" # or follow it straight into RTL -# -# This is the link BTCPay renders on its Services page. BTCPay builds it from -# BTCPAY_BTCEXTERNALRTL="server=/rtl/api/authenticate/cookie;cookiefile=..." -# by reading the cookie file RTL wrote and appending it as ?access-key=. The -# value is the raw file content: RTL's frontend sha256s it before posting -# (src/app/app.component.ts) and the backend compares against -# sha256(cookieValue), so no hashing happens here. -# -# Authenticating rotates the cookie (common.refreshCookie), so re-run this for -# each login -- exactly as BTCPay re-reads the file on every page render. - -set -euo pipefail - -cd "$(dirname "$0")/.." -# shellcheck disable=SC1091 -[ -f .env ] && source .env - -port="${RTL_SSO_PORT:-3001}" - -if ! docker compose --profile sso ps --status running --services 2>/dev/null | grep -qx rtl-sso; then - echo "rtl-sso is not running. Start it with: docker compose --profile sso up -d" >&2 - exit 1 -fi - -cookie="$(docker compose --profile sso exec -T rtl-sso cat /RTL/cookie/.cookie | tr -d '\r\n')" - -if [ -z "$cookie" ]; then - echo "The cookie file /RTL/cookie/.cookie is empty. Is RTL_SSO=1 set on rtl-sso?" >&2 - exit 1 -fi - -echo "http://localhost:${port}/rtl/api/authenticate/cookie?access-key=${cookie}" diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index e059ab3a..55672171 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -17,9 +17,6 @@ volumes: eclair_data: rtl_db: rtl_config: - rtl_sso_db: - rtl_sso_config: - rtl_sso_cookie: x-lnd: &lnd image: polarlightning/lnd:0.20.0-beta @@ -288,7 +285,7 @@ services: container_name: ${COMPOSE_PROJECT_NAME}_rtl # Defaults to the published image; override with RTL_IMAGE (e.g. a locally built # branch image) to test unreleased changes: RTL_IMAGE=rtl:pr1625 docker compose up. - image: ${RTL_IMAGE:-shahanafarooqui/rtl:v0.15.10} + image: ${RTL_IMAGE:-shahanafarooqui/rtl:v0.15.8} restart: unless-stopped depends_on: rtl-config-init: @@ -314,87 +311,3 @@ services: - carol_data:/lnd/carol:ro - cln_data:/cln:ro - rtl_db:/RTL/database - - # --------------------------------------------------------------------------- - # BTCPay Server SSO harness -- profile "sso", so a plain 'up' does not start it: - # - # docker compose --profile sso up -d - # bin/sso-url - # - # BTCPay bundles RTL as a service and runs it in single-sign-on mode. RTL - # writes a random cookie to RTL_COOKIE_PATH; BTCPay reads that file and renders - # a link to /rtl/api/authenticate/cookie?access-key= on its Services - # page. That path is not a registered route -- it falls through to RTL's - # catch-all, which mints the XSRF-TOKEN cookie and serves index.html, and the - # SPA then reads access-key from the query string and posts it as a password - # login. Reproducing that entry path is the whole point of this harness: it is - # where RTL's auth, CSRF and static-serving behaviour meet an external caller, - # and it has broken before without the standalone flow noticing. - # - # BTCPay itself (postgres + nbxplorer + btcpayserver) is deliberately not here. - # See README.md, "BTCPay SSO harness", for what that leaves untested and how to - # run against a real BTCPay when it matters. - # --------------------------------------------------------------------------- - - # Same copy-into-a-volume dance as rtl-config-init above, and for the same - # reason: RTL rewrites its config on startup. - rtl-sso-config-init: - container_name: ${COMPOSE_PROJECT_NAME}_rtl_sso_config_init - profiles: ["sso"] - image: busybox:1.36 - command: > - sh -c "cp /template/RTL-Config.sso.json /config/RTL-Config.json && - chmod 644 /config/RTL-Config.json && - echo 'sso config staged'" - volumes: - - ./rtl/RTL-Config.sso.json:/template/RTL-Config.sso.json:ro - - rtl_sso_config:/config - - # A second RTL rather than a flag on the first: RTL picks one authentication - # mode at startup, so SSO and the password login cannot coexist in one - # instance. Only alice is wired up, matching BTCPay's one-node-per-RTL layout. - # - # The environment block is copied from BTCPay's own compose fragment - # (docker-compose-generator/docker-fragments/bitcoin-lnd.yml in - # btcpayserver-docker), so this exercises the env-driven SSO path BTCPay - # actually uses rather than the config-file equivalent -- which is why - # RTL-Config.sso.json leaves its SSO block zeroed and carries no multiPass. - # - # Only 'expose'd, never published: all access goes through the proxy, as it - # does under BTCPay. - rtl-sso: - container_name: ${COMPOSE_PROJECT_NAME}_rtl_sso - profiles: ["sso"] - image: ${RTL_IMAGE:-shahanafarooqui/rtl:v0.15.10} - restart: unless-stopped - depends_on: - rtl-sso-config-init: - condition: service_completed_successfully - alice: - condition: service_started - environment: - RTL_CONFIG_PATH: /RTL/config - RTL_SSO: 1 - RTL_COOKIE_PATH: /RTL/cookie/.cookie - LOGOUT_REDIRECT_LINK: /server/services - expose: - - "3000" - volumes: - - rtl_sso_config:/RTL/config - - rtl_sso_cookie:/RTL/cookie - - alice_data:/lnd/alice:ro - - rtl_sso_db:/RTL/database - - # Stands in for BTCPay's traefik. Routes only /rtl and /rtl/*, mirroring the - # label BTCPay puts on its RTL container; see nginx/rtl-sso.conf. - rtl-sso-proxy: - container_name: ${COMPOSE_PROJECT_NAME}_rtl_sso_proxy - profiles: ["sso"] - image: nginx:1.27-alpine - restart: unless-stopped - depends_on: - - rtl-sso - ports: - - "${RTL_SSO_PORT:-3001}:80" - volumes: - - ./nginx/rtl-sso.conf:/etc/nginx/conf.d/default.conf:ro diff --git a/docker/nginx/rtl-sso.conf b/docker/nginx/rtl-sso.conf deleted file mode 100644 index 314aa605..00000000 --- a/docker/nginx/rtl-sso.conf +++ /dev/null @@ -1,46 +0,0 @@ -# Reverse proxy in front of RTL running in BTCPay Server's single-sign-on mode. -# -# Stands in for the traefik instance BTCPay puts in front of its bundled RTL. -# The location regex mirrors the router rule BTCPay labels that container with: -# -# Host(`${BTCPAY_HOST}`) && (Path(`/rtl`) || PathPrefix(`/rtl/`)) -# -# so only /rtl and /rtl/* are proxied and everything else 404s here. That -# strictness is deliberate: RTL is built with (angular.json) -# and mounts every route under baseHref '/rtl' (server/utils/common.ts), so a -# request that escapes the prefix is a bug the harness should surface rather -# than quietly serve. -# -# The prefix is passed through unmodified -- there is no strip-prefix step to -# get wrong, because RTL expects to see it. proxy_pass without a URI part -# preserves the original request URI. - -map $http_upgrade $connection_upgrade { - default upgrade; - '' close; -} - -server { - listen 80; - server_name _; - - location ~ ^/rtl(/|$) { - proxy_pass http://rtl-sso:3000; - - # RTL runs with Express 'trust proxy' enabled, so these are what it sees - # as the client address in its logs and rate limiting. - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - - # RTL's websocket lives at /rtl/api/ws and stays open for the life of the - # session. Without the upgrade headers it fails the handshake and the UI - # silently stops receiving live updates; without the long read timeout - # nginx drops it after 60s. - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $connection_upgrade; - proxy_read_timeout 3600s; - } -} diff --git a/docker/rtl/RTL-Config.sso.json b/docker/rtl/RTL-Config.sso.json deleted file mode 100644 index 59b7b1b4..00000000 --- a/docker/rtl/RTL-Config.sso.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "port": "3000", - "defaultNodeIndex": 1, - "dbDirectoryPath": "/RTL/database", - "SSO": { - "rtlSSO": 0, - "rtlCookiePath": "", - "logoutRedirectLink": "" - }, - "nodes": [ - { - "index": 1, - "lnNode": "alice", - "lnImplementation": "LND", - "authentication": { - "macaroonPath": "/lnd/alice/data/chain/bitcoin/regtest" - }, - "settings": { - "userPersona": "OPERATOR", - "themeMode": "DAY", - "themeColor": "PURPLE", - "logLevel": "ERROR", - "lnServerUrl": "https://alice:8080", - "fiatConversion": false, - "unannouncedChannels": false, - "blockExplorerUrl": "https://mempool.space" - } - } - ] -} diff --git a/docker/scripts/verify-sso.sh b/docker/scripts/verify-sso.sh deleted file mode 100755 index c9fe0e1f..00000000 --- a/docker/scripts/verify-sso.sh +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/env bash -# -# Check the BTCPay SSO harness end to end. -# -# Walks the exact path a browser takes when an operator clicks the RTL link on -# BTCPay Server's Services page, and asserts each step. Run it after any change -# to authentication, CSRF or static serving -- this is the entry path BTCPay -# uses, and none of it is covered by logging into the standalone fixture RTL. -# -# docker compose --profile sso up -d -# ./scripts/verify-sso.sh -# -# Usage: ./scripts/verify-sso.sh (from the docker/ directory) -# -# Exits non-zero if any check fails, so it can gate a PR. - -# Deliberately no -e: a failing assertion must record itself and let the rest of -# the checks run, rather than aborting on the first one. That means anything -# that would normally rely on -e needs its own guard. -set -uo pipefail - -cd "$(dirname "$0")/.." || exit 1 - -if [ -f .env ]; then - set -a - # shellcheck disable=SC1091 - source .env - set +a -fi - -BASE="http://localhost:${RTL_SSO_PORT:-3001}" -JAR="$(mktemp)" -JAR2="$(mktemp)" -trap 'rm -f "$JAR" "$JAR2"' EXIT - -pass=0 -fail=0 -# Both return 0 explicitly: the checks below are written as `test && ok || bad`, -# which would also run `bad` if `ok` itself ever returned non-zero. -ok() { echo " PASS: $1"; pass=$((pass + 1)); return 0; } -bad() { echo " FAIL: $1"; fail=$((fail + 1)); return 0; } - -if ! docker compose --profile sso ps --status running --services 2>/dev/null | grep -qx rtl-sso; then - echo "rtl-sso is not running. Start it with: docker compose --profile sso up -d" >&2 - exit 1 -fi - -cookie="$(docker compose --profile sso exec -T rtl-sso cat /RTL/cookie/.cookie | tr -d '\r\n')" -echo "cookie: ${cookie:0:16}... (${#cookie} chars)" - -echo -echo "1. the proxy routes only /rtl, mirroring BTCPay's traefik rule" -code=$(curl -s -o /dev/null -w '%{http_code}' "$BASE/") -[ "$code" = "404" ] && ok "GET / -> 404" || bad "GET / -> $code (want 404)" -code=$(curl -s -o /dev/null -w '%{http_code}' "$BASE/api/authenticate") -[ "$code" = "404" ] && ok "GET /api/authenticate -> 404" || bad "GET /api/authenticate -> $code (want 404)" -code=$(curl -s -o /dev/null -w '%{http_code}' "$BASE/rtl/") -[ "$code" = "200" ] && ok "GET /rtl/ -> 200" || bad "GET /rtl/ -> $code (want 200)" - -echo -echo "2. the entry URL falls through to the catch-all, which mints the CSRF token" -body=$(curl -s -c "$JAR" "$BASE/rtl/api/authenticate/cookie?access-key=$cookie") -echo "$body" | grep -q '' \ - && ok "entry URL serves the SPA shell with base href /rtl/" \ - || bad "entry URL did not serve index.html" -xsrf=$(awk '/XSRF-TOKEN/ {print $7}' "$JAR") -[ -n "$xsrf" ] && ok "XSRF-TOKEN cookie minted" || bad "no XSRF-TOKEN cookie" -grep -q '_csrf' "$JAR" && ok "_csrf cookie set" || bad "no _csrf cookie" - -echo -echo "3. the SPA posts sha256(access-key) as a password login" -hash=$(printf '%s' "$cookie" | shasum -a 256 | cut -d' ' -f1) -resp=$(curl -s -b "$JAR" -c "$JAR" -X POST "$BASE/rtl/api/authenticate" \ - -H 'Content-Type: application/json' -H "X-XSRF-TOKEN: $xsrf" \ - -d "{\"authenticateWith\":\"PASSWORD\",\"authenticationValue\":\"$hash\"}") -token=$(echo "$resp" | python3 -c 'import sys,json;print(json.load(sys.stdin).get("token",""))' 2>/dev/null) -[ -n "$token" ] && ok "authenticated, JWT issued" || bad "auth failed: $resp" - -echo -echo "4. the JWT reaches the node behind it" -info=$(curl -s -b "$JAR" "$BASE/rtl/api/lnd/getinfo" -H "Authorization: Bearer $token") -node_alias=$(echo "$info" | python3 -c 'import sys,json;print(json.load(sys.stdin).get("alias",""))' 2>/dev/null) -[ "$node_alias" = "alice" ] && ok "GET /rtl/api/lnd/getinfo -> alias '$node_alias'" || bad "getinfo returned: ${info:0:200}" - -echo -echo "5. the cookie rotates on login, so each BTCPay page render hands out a fresh one" -after=$(docker compose --profile sso exec -T rtl-sso cat /RTL/cookie/.cookie | tr -d '\r\n') -[ "$after" != "$cookie" ] && ok "cookie rotated after authentication" || bad "cookie did NOT rotate" - -echo -echo "6. a wrong access-key is refused" -# The token has to come from the catch-all: GET /rtl/ is served by express.static, -# which mints no XSRF-TOKEN, and the POST would then fail CSRF (403) before it -# ever reached the access-key comparison this step is checking. -curl -s -c "$JAR2" "$BASE/rtl/api/authenticate/cookie?access-key=x" > /dev/null -x2=$(awk '/XSRF-TOKEN/ {print $7}' "$JAR2") -badhash=$(printf '%s' "not-the-cookie-value-but-long-enough-to-pass-the-length-check" | shasum -a 256 | cut -d' ' -f1) -code=$(curl -s -o /dev/null -w '%{http_code}' -b "$JAR2" -X POST "$BASE/rtl/api/authenticate" \ - -H 'Content-Type: application/json' -H "X-XSRF-TOKEN: $x2" \ - -d "{\"authenticateWith\":\"PASSWORD\",\"authenticationValue\":\"$badhash\"}") -[ "$code" = "406" ] && ok "wrong access-key -> 406" || bad "wrong access-key -> $code (want 406)" - -echo -echo "7. the standalone fixture RTL is unaffected" -code=$(curl -s -o /dev/null -w '%{http_code}' "http://localhost:${RTL_PORT:-3000}/rtl/") -[ "$code" = "200" ] && ok "standalone RTL still serving on ${RTL_PORT:-3000}" || bad "standalone RTL -> $code" - -echo -echo "=== $pass passed, $fail failed ===" -[ "$fail" -eq 0 ]