#!/usr/bin/env bash # OIDC integration-test runner. # # Brings up the test DB, a Node-based fake IdP (panva/node-oidc-provider # under tests/oidc/fake_idp/), and OxiCloud configured to talk to that # IdP, then runs the Hurl suite and tears everything down. # # Why a separate runner from tests/api/run.sh: # * the OxiCloud server here is launched with # `--config tests/common/server-with-oidc.env` (OIDC enabled) — the # default api run uses server.env with OIDC off, and we don't want # to flip flags mid-suite; # * the IdP is a Node process this script owns, distinct from the # postgres-test container that lives in spawn-db.sh. # # Invocation: # * locally: chained from `just api-test` after the api + webdav # suites, or directly via `bash tests/oidc/run.sh` # * in CI: chained from the `api-test` job in # .github/workflows/ci.yml — same shell call, same env. # # Prerequisites: docker, cargo, node ≥ 20, npm, hurl ≥ 4.0. set -euo pipefail REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)" COMMON="$REPO_ROOT/tests/common" OIDC_DIR="$REPO_ROOT/tests/oidc" FAKE_IDP_DIR="$OIDC_DIR/fake_idp" # Test variables (base_url, admin creds, oidc_issuer, oidc_authorize_endpoint). # shellcheck source=test.env source "$OIDC_DIR/test.env" SERVER_PORT="${base_url##*:}" # Derive the IdP port from oidc_issuer the same way (e.g. localhost:1080 → 1080). # Keeps run.sh and test.env in lockstep — change one, the other follows. IDP_PORT="${oidc_issuer##*:}" # ── Helpers ──────────────────────────────────────────────────────────────── log() { echo "[oidc-test] $*"; } die() { echo "[oidc-test] ERROR: $*" >&2; exit 1; } wait_for_http() { local url="$1" timeout="${2:-60}" local deadline=$(( $(date +%s) + timeout )) until curl -sf "$url" >/dev/null 2>&1; do [[ $(date +%s) -ge $deadline ]] && die "Timeout waiting for $url" sleep 0.5 done } # ── Fake-IdP process management ──────────────────────────────────────────── # All cleanup paths funnel through this helper so an exit at ANY phase # (early failure during npm install, hurl assertion fail, Ctrl-C, …) # always reaps the node process. The earlier subshell+setsid pattern # leaked daemons whenever the subshell exited before the trap fired, # leading to the "no change after rerunning" failure mode: an old # fake-idp from a prior failed run was still bound to port 1080, so # the new spawn either failed silently (EADDRINUSE) or the tests hit # the stale config. # # Belt-and-braces orphan reaping: kill by script-path pattern AND by # port. The pattern match misses processes started from a different # absolute path (e.g. via a symlinked checkout, or from a `node # server.js` started from inside tests/oidc/fake_idp/ where the # command line is just `node server.js`). The port-based fallback # catches anything bound to 1080 regardless of how it was launched — # the original case that caused the "stale daemon" debugging session. kill_fake_idp() { pkill -f "tests/oidc/fake_idp/server.js" 2>/dev/null || true pkill -f "node.*server.js" 2>/dev/null || true if command -v lsof >/dev/null 2>&1; then local pids pids=$(lsof -ti :"$IDP_PORT" 2>/dev/null || true) if [[ -n "$pids" ]]; then # shellcheck disable=SC2086 kill -9 $pids 2>/dev/null || true fi fi } # ── Teardown (always runs on exit) ───────────────────────────────────────── SERVER_PID="" cleanup() { if [[ -n "$SERVER_PID" ]]; then log "Stopping OxiCloud server (pid $SERVER_PID)..." kill "$SERVER_PID" 2>/dev/null || true wait "$SERVER_PID" 2>/dev/null || true fi log "Stopping fake-idp..." kill_fake_idp bash "$COMMON/stop-db.sh" || true } trap cleanup EXIT # ── 1. Postgres ──────────────────────────────────────────────────────────── bash "$COMMON/spawn-db.sh" # ── 2. Fake IdP (Node) ───────────────────────────────────────────────────── log "Installing fake-idp dependencies..." if [[ -f "$FAKE_IDP_DIR/package-lock.json" ]]; then # `npm ci` is faster + deterministic when the lockfile is present. (cd "$FAKE_IDP_DIR" && npm ci --silent --no-audit --no-fund) else (cd "$FAKE_IDP_DIR" && npm install --silent --no-audit --no-fund) fi # Sweep any orphaned fake-idp processes from a prior crashed/aborted # run BEFORE starting the new one. Without this, a stale daemon # bound to port 1080 would either swallow new spawns silently # (EADDRINUSE buried in /tmp/fake-idp.log) or serve every request # with its old config — producing the maddening "I changed the # config but nothing changed" failure mode. log "Sweeping any orphan fake-idp processes from prior runs..." kill_fake_idp # Brief moment for the OS to actually release the listener; without # this the new node call can race the just-killed process and lose # the bind. sleep 0.3 log "Starting fake-idp on port $IDP_PORT..." # Background the node process directly (no setsid / subshell wrapper). # pkill-by-path in cleanup means we don't need a process-group dance # to reap the child; the simpler launch keeps $IDP_PID correct (it's # the actual node PID, not a wrapping subshell) for any future call # site that wants to wait on it. FAKE_IDP_ISSUER="$oidc_issuer" FAKE_IDP_PORT="$IDP_PORT" \ node "$FAKE_IDP_DIR/server.js" > /tmp/fake-idp.log 2>&1 & log "Waiting for fake-idp discovery endpoint..." wait_for_http "$oidc_issuer/.well-known/openid-configuration" 30 log "fake-idp is ready (logs: /tmp/fake-idp.log)" # ── 3. Load shared server env (port + storage path) ──────────────────────── set -a # shellcheck source=../common/server-with-oidc.env source "$COMMON/server-with-oidc.env" OXICLOUD_SERVER_PORT=$SERVER_PORT OXICLOUD_STORAGE_PATH="$REPO_ROOT/tests/oidc/storage" set +a # shellcheck source=../common/wipe-storage.sh source "$COMMON/wipe-storage.sh" wipe_storage "$OXICLOUD_STORAGE_PATH" # ── 3.5. Ensure the SPA is built (static-dist/) ──────────────────────────── # The OIDC suite's Step 4 + Step 9 walk the full redirect chain and assert # they land on `/login?oidc_code=…` with HTTP 200 — the production contract, # where the container ships `static-dist/login.html`. Without that bundle # `resolve_static_path` falls back to `OXICLOUD_STATIC_PATH=./static`, which # was removed in commit 54639d46 — so ServeDir 404s the route and Step 4 # fails. Build here so local + CI both exercise the production layout. DIST_DIR="$REPO_ROOT/static-dist" if [[ ! -f "$DIST_DIR/login.html" ]]; then log "Building SvelteKit SPA (static-dist/login.html missing)..." (cd "$REPO_ROOT/frontend" \ && npm ci --silent --no-audit --no-fund \ && npm run build) || die "Frontend build failed; static-dist/ is required for the OIDC tests" fi # ── 4. Start OxiCloud server with OIDC enabled ───────────────────────────── BUILD_TARGET="${BUILD_TARGET:-debug}" OXICLOUD_BIN="$REPO_ROOT/target/$BUILD_TARGET/oxicloud" if [[ ! -x "$OXICLOUD_BIN" ]]; then log "Building OxiCloud server ($BUILD_TARGET)..." case "$BUILD_TARGET" in debug) (cd "$REPO_ROOT" && cargo build 2>&1 | tail -n 20) || die "cargo build failed" ;; release) (cd "$REPO_ROOT" && cargo build --release 2>&1 | tail -n 20) || die "cargo build --release failed" ;; *) die "Unsupported BUILD_TARGET='$BUILD_TARGET' (expected 'debug' or 'release')" ;; esac fi log "Starting OxiCloud server with OIDC config on port $SERVER_PORT..." "$OXICLOUD_BIN" --config "$COMMON/server-with-oidc.env" & SERVER_PID=$! log "Waiting for server at $base_url..." wait_for_http "$base_url/ready" 120 log "Server is ready." # ── 5. Run the OIDC Hurl suite ───────────────────────────────────────────── log "Running OIDC Hurl tests..." hurl --variables-file "$OIDC_DIR/test.env" \ --file-root "$REPO_ROOT/tests" \ --test --jobs 1 \ "$OIDC_DIR/oidc.hurl" log "OIDC tests passed."