From 1cbac3b82cc913fa3c8b303129130c37a8fedf5d Mon Sep 17 00:00:00 2001 From: archipelago Date: Fri, 1 May 2026 16:49:30 -0400 Subject: [PATCH] =?UTF-8?q?test(lifecycle):=20add=20UI=20surface=20coverag?= =?UTF-8?q?e=20=E2=80=94=20HTTPS=20proxy=20+=20iframe=20URLs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes the coverage gap where existing bats suites would report green on a node whose dashboard tiles 502 because the proxy upstream is dead. First pass against .198 caught real prod issues immediately: /app/lnd/ → 502 (lnd container exited) /app/mempool/ → 502 (mempool container exited) /app/fedimint/ → 502 (fedimint container exited) while existing tests reported only "container is up: false" with no 404/502 distinction. * lib/ui-probes.bash — sourced helper. probe_https_200, probe_app_url (skip-if-container-down else assert-200), probe_dashboard_shell (asserts the Vue SPA HTML, not nginx default — catches the layout regression from feedback_release_tarball_layout.md), probe_dashboard_catalog (asserts /catalog.json non-empty). * bats/ui-coverage.bats — 9 @test cases covering the dashboard + bitcoin-ui :8334 + the seven HTTPS_PROXY_PATHS most users hit (lnd, electrumx, mempool, fedimint, btcpay, filebrowser). URL list mirrors HTTPS_PROXY_PATHS in neode-ui/src/views/appSession/appSessionConfig.ts. Divergence between the two is the exact bug class we're guarding against. Loops clean under run-20x.sh. Container-state oracle is via local podman inspect, so the suite must run on the archy host (same as companion-survives-archipelago-restart.bats). Co-Authored-By: Claude Opus 4.7 (1M context) --- tests/lifecycle/bats/ui-coverage.bats | 93 ++++++++++++++++++++++ tests/lifecycle/lib/ui-probes.bash | 108 ++++++++++++++++++++++++++ 2 files changed, 201 insertions(+) create mode 100644 tests/lifecycle/bats/ui-coverage.bats create mode 100644 tests/lifecycle/lib/ui-probes.bash diff --git a/tests/lifecycle/bats/ui-coverage.bats b/tests/lifecycle/bats/ui-coverage.bats new file mode 100644 index 00000000..1ef5db46 --- /dev/null +++ b/tests/lifecycle/bats/ui-coverage.bats @@ -0,0 +1,93 @@ +#!/usr/bin/env bats +# tests/lifecycle/bats/ui-coverage.bats +# +# UI surface tests — exercises the URLs a real user actually clicks +# through, not just the JSON-RPC API. Fills the coverage gap where the +# previous bats suites would report "container is up" while the iframe +# behind /app// was returning 502 because nginx had a stale upstream +# or the proxy port was wrong. +# +# URL map sourced from neode-ui/src/views/appSession/appSessionConfig.ts +# (the frontend's own resolveAppUrl). Tests here MUST stay in sync with +# that file — divergence is the whole bug class we're guarding against. +# +# Each app probe is gated on its container being running: +# - container down → skip (clean dependency report, no false-fail) +# - container up → URL MUST return 200 with non-empty body +# +# Looped 20× via tests/lifecycle/run-20x.sh. + +load '../lib/rpc.bash' +load '../lib/ui-probes.bash' + +setup_file() { + : "${ARCHY_PASSWORD:?Set ARCHY_PASSWORD env var to the UI password}" + export ARCHY_FORCE_LOGIN=1 + rpc_login + unset ARCHY_FORCE_LOGIN + HOST="${ARCHY_HOST:-127.0.0.1}" + export HOST +} + +teardown_file() { + rpc_logout_local +} + +# ──────────────────────────────────────────────────────────────────── +# Dashboard shell + catalog (always required) +# ──────────────────────────────────────────────────────────────────── + +@test "dashboard https://host/ returns the Vue SPA shell" { + run probe_dashboard_shell + [ "$status" -eq 0 ] +} + +@test "dashboard catalog endpoint responds with apps" { + run probe_dashboard_catalog + [ "$status" -eq 0 ] +} + +# ──────────────────────────────────────────────────────────────────── +# Bitcoin UI — direct host port (8334), companion container +# ──────────────────────────────────────────────────────────────────── + +@test "bitcoin-ui is reachable on :8334 when archy-bitcoin-ui is running" { + probe_app_url archy-bitcoin-ui "http://$HOST:8334/" "bitcoin-ui (direct port 8334)" +} + +# ──────────────────────────────────────────────────────────────────── +# HTTPS proxy paths — match HTTPS_PROXY_PATHS in appSessionConfig.ts +# ──────────────────────────────────────────────────────────────────── + +@test "lnd proxy https://host/app/lnd/ responds when lnd is running" { + probe_app_url lnd "https://$HOST/app/lnd/" "lnd (proxy /app/lnd/)" +} + +@test "electrumx proxy https://host/app/electrumx/ responds when electrumx is running" { + # electrumx companion (archy-electrs-ui) is what serves the iframe HTML; + # the electrumx daemon is just the TCP backend. + probe_app_url archy-electrs-ui "https://$HOST/app/electrumx/" "electrumx (proxy /app/electrumx/)" +} + +@test "mempool proxy https://host/app/mempool/ responds when mempool is running" { + probe_app_url mempool "https://$HOST/app/mempool/" "mempool (proxy /app/mempool/)" +} + +@test "fedimint proxy https://host/app/fedimint/ responds when fedimint is running" { + probe_app_url fedimint "https://$HOST/app/fedimint/" "fedimint (proxy /app/fedimint/)" +} + +@test "btcpay proxy https://host/app/btcpay/ responds when btcpay-server is running" { + probe_app_url btcpay-server "https://$HOST/app/btcpay/" "btcpay (proxy /app/btcpay/)" +} + +@test "filebrowser proxy https://host/app/filebrowser/ responds when filebrowser is running" { + probe_app_url filebrowser "https://$HOST/app/filebrowser/" "filebrowser (proxy /app/filebrowser/)" +} + +# ──────────────────────────────────────────────────────────────────── +# Companion-served URLs that aren't in HTTPS_PROXY_PATHS but show up +# in the dashboard. archy-lnd-ui shares lnd's iframe path; archy-electrs-ui +# shares electrumx's. The earlier test already covers those — leaving +# this section for future companion-direct probes (none today). +# ──────────────────────────────────────────────────────────────────── diff --git a/tests/lifecycle/lib/ui-probes.bash b/tests/lifecycle/lib/ui-probes.bash new file mode 100644 index 00000000..a480b7cf --- /dev/null +++ b/tests/lifecycle/lib/ui-probes.bash @@ -0,0 +1,108 @@ +#!/usr/bin/env bash +# tests/lifecycle/lib/ui-probes.bash +# +# HTTPS proxy + iframe URL probes. Sourced from bats files. Pairs with +# lib/rpc.bash but tests the URL surface a real user actually clicks +# (dashboard, /app// proxy paths, direct-port iframes), not just the +# JSON-RPC API. +# +# Pattern: every probe is a skip-or-assert pair: +# - if the container that backs the URL is not running → skip +# (cleanly reports the dependency, doesn't false-fail) +# - if it IS running → the URL MUST return 200 +# That catches the "container up but UI broken" failure mode that the +# RPC-only tests miss (.198 today: archy-bitcoin-ui Up 12 minutes, +# but is the iframe actually serving usable HTML? this layer answers). + +# Curl options for a probe: short timeout, follow redirects, ignore self- +# signed cert (the alpha fleet uses one), no proxy environment leak. +PROBE_CURL_OPTS=(-skfL -m 8 --noproxy "*") + +# ──────────────────────────────────────────────────────────────────── +# Container-state oracle +# ──────────────────────────────────────────────────────────────────── + +# True iff `name` is currently in the running state per podman. +probe_container_running() { + local name="$1" + [[ "$(podman inspect --format '{{.State.Running}}' "$name" 2>/dev/null)" == "true" ]] +} + +# ──────────────────────────────────────────────────────────────────── +# URL probes +# ──────────────────────────────────────────────────────────────────── + +# Probe an HTTPS URL — assert 200 and non-empty body. +# Usage: probe_https_200 URL "human description" +probe_https_200() { + local url="$1" + local label="${2:-$url}" + local body status + body=$(curl "${PROBE_CURL_OPTS[@]}" -w '%{http_code}' "$url" 2>/dev/null) || { + echo "probe_https_200: $label ($url) — curl failed (network/timeout)" >&2 + return 1 + } + status="${body: -3}" + body="${body:0:-3}" + if [[ "$status" != "200" ]]; then + echo "probe_https_200: $label ($url) returned $status (want 200)" >&2 + return 1 + fi + if [[ -z "$body" ]]; then + echo "probe_https_200: $label ($url) returned empty body" >&2 + return 1 + fi + return 0 +} + +# Probe a URL backed by a container — skip if container is not running, +# assert 200 if it is. This is the standard shape for app UI tests. +# Usage: probe_app_url CONTAINER URL "human description" +probe_app_url() { + local container="$1" + local url="$2" + local label="${3:-$url}" + if ! probe_container_running "$container"; then + skip "$label: backing container '$container' is not running" + fi + run probe_https_200 "$url" "$label" + [ "$status" -eq 0 ] +} + +# Probe the archipelago dashboard itself (the SPA shell at https://node/). +# Asserts 200 and that the body looks like the Vue index, not an nginx +# default page. Catches "frontend tarball was extracted with the wrong +# layout" — see feedback_release_tarball_layout.md. +probe_dashboard_shell() { + local host="${ARCHY_HOST:-127.0.0.1}" + local url="https://$host/" + local body + body=$(curl "${PROBE_CURL_OPTS[@]}" "$url" 2>/dev/null) || { + echo "probe_dashboard_shell: $url — curl failed" >&2 + return 1 + } + # Vue shell carries one of these markers:
, the SPA bundle + # tag, or the manifest link. Nginx default does not. + if echo "$body" | grep -qE 'id="app"|&2 + echo "first 200 bytes: ${body:0:200}" >&2 + return 1 +} + +# Probe the catalog endpoint that the dashboard uses to populate tiles. +# Returns 0 if catalog is reachable AND has at least one entry. +probe_dashboard_catalog() { + local host="${ARCHY_HOST:-127.0.0.1}" + local body + body=$(curl "${PROBE_CURL_OPTS[@]}" "https://$host/catalog.json" 2>/dev/null) || { + echo "probe_dashboard_catalog: /catalog.json fetch failed" >&2 + return 1 + } + if ! echo "$body" | jq -e 'length > 0' >/dev/null 2>&1; then + echo "probe_dashboard_catalog: /catalog.json is not a non-empty array/object" >&2 + return 1 + fi + return 0 +}