#!/bin/sh # End-to-end HTTP tests against a running catcrafts-server. # # Why this exists separately from --selftest: that one covers the pure # functions (escaping, JSON, form validation) in-process. This covers the # things only a real request can show — status codes, headers, redirects, # form submission, and whether a page is actually complete without # JavaScript. Those are exactly the properties that matter at launch and the # ones a unit test cannot observe. # # Runs the server itself on a scratch port with a temporary orders file and the # FAKE payment rail, so it never touches real data, never dials Mollie, and needs # no setup. The fake rail makes the whole order lifecycle testable: it hands # out pretend payment links, and reports "paid" once the marker file exists — # which is how these tests simulate the customer paying. # # usage: tools/e2e.sh [path-to-catcrafts-server] # # Exits non-zero on the first failure, so it works as a CI gate. set -eu SERVER="${1:-}" if [ -z "$SERVER" ]; then SERVER=$(find bin -maxdepth 1 -type d -name 'Catcrafts.Server-*' | sort | head -n1)/catcrafts-server fi [ -x "$SERVER" ] || { echo "e2e: server binary not found or not executable: $SERVER" >&2; exit 1; } PORT="${E2E_PORT:-8199}" BASE="http://127.0.0.1:$PORT" WORK="$(mktemp -d)" ORDERS="$WORK/orders.jsonl" pass=0 fail=0 skipped=0 cleanup() { [ -n "${SRV_PID:-}" ] && kill "$SRV_PID" 2>/dev/null || true rm -rf "$WORK" } trap cleanup EXIT INT TERM # Deterministic environment: a developer shell that sourced the repo .env # must not leak real provider keys into the test server — live Sendcloud # rates would silently change the shipping totals asserted below. unset MOLLIE_API_KEY BUNQ_API_KEY SENDCLOUD_PUBLIC_KEY SENDCLOUD_SECRET_KEY SENDCLOUD_METHOD 2>/dev/null || true # An ephemeral GPG key so invoice signing runs the REAL signing path and the # suite can verify the signature. gpg is required (CI installs gnupg with the # base tools); a missing binary should fail loudly, not skip silently. export GNUPGHOME="$WORK/gnupg" mkdir -p "$GNUPGHOME"; chmod 700 "$GNUPGHOME" gpg --batch --passphrase '' --quick-gen-key 'Catcrafts e2e ' \ default default never >/dev/null 2>&1 \ || { echo "e2e: could not create a GPG key (is gnupg installed?)" >&2; exit 1; } export INVOICE_GPG_KEY='invoices@e2e.invalid' "$SERVER" --serve "$PORT" --orders="$ORDERS" --rail=fake >"$WORK/server.log" 2>&1 & SRV_PID=$! # Wait for the listener rather than sleeping a fixed amount: a fixed sleep is # either too short on a loaded machine or wasted time on a fast one. i=0 while [ "$i" -lt 100 ]; do if curl -s -o /dev/null "$BASE/api/healthz" 2>/dev/null; then break; fi i=$((i + 1)) sleep 0.1 done if [ "$i" -ge 100 ]; then echo "e2e: server did not come up on $PORT" >&2 cat "$WORK/server.log" >&2 exit 1 fi ok() { pass=$((pass + 1)); printf ' ok %s\n' "$1"; } bad() { fail=$((fail + 1)); printf ' FAIL %s\n %s\n' "$1" "$2"; } # Counted and reported separately, never as a pass: a check that silently did not # run is how a suite ends up reporting green over an untested code path. skip() { skipped=$((skipped + 1)); printf ' SKIP %s\n %s\n' "$1" "$2"; } # status [method] [data] status() { _p="$1"; _want="$2"; _m="${3:-GET}"; _d="${4:-}" if [ "$_m" = POST ]; then _got=$(curl -s -o /dev/null -w '%{http_code}' -X POST -d "$_d" "$BASE$_p") elif [ "$_m" = HEAD ]; then # --head, not -X HEAD: with -X curl still waits for a response body # that a correct HEAD reply never sends, and hangs until timeout. _got=$(curl -s -o /dev/null -w '%{http_code}' --head "$BASE$_p") else _got=$(curl -s -o /dev/null -w '%{http_code}' -X "$_m" "$BASE$_p") fi [ "$_got" = "$_want" ] && ok "$_m $_p -> $_want" \ || bad "$_m $_p" "expected $_want, got $_got" } # body_has