#!/usr/bin/env bash # Test suite for SX HTTP server app config handling. # Starts the server with different __app-config values and verifies behavior. set -euo pipefail cd "$(dirname "$0")/.." PROJECT_DIR="$(pwd)" SERVER="hosts/ocaml/_build/default/bin/sx_server.exe" BASE_PORT=8090 PASS=0 FAIL=0 ERRORS="" SERVER_PID="" CURRENT_PORT="" # ── Helpers ────────────────────────────────────────────────────────── TMP_DIR=$(mktemp -d) cleanup() { if [ -n "${SERVER_PID:-}" ] && kill -0 "$SERVER_PID" 2>/dev/null; then kill "$SERVER_PID" 2>/dev/null wait "$SERVER_PID" 2>/dev/null || true fi rm -rf "$TMP_DIR" } trap cleanup EXIT start_server() { local port="$1" local extra_env="${2:-}" CURRENT_PORT="$port" # Kill anything on this port kill $(lsof -ti :"$port") 2>/dev/null || true sleep 1 # Start server eval "$extra_env SX_PROJECT_DIR=$PROJECT_DIR $SERVER --http $port" \ > "$TMP_DIR/stdout" 2> "$TMP_DIR/stderr" & SERVER_PID=$! # Wait for "Listening" message local tries=0 while ! grep -q "Listening" "$TMP_DIR/stderr" 2>/dev/null; do sleep 1 tries=$((tries + 1)) if [ $tries -gt 120 ]; then echo "TIMEOUT waiting for server on port $port" cat "$TMP_DIR/stderr" return 1 fi if ! kill -0 "$SERVER_PID" 2>/dev/null; then echo "Server died during startup on port $port" cat "$TMP_DIR/stderr" return 1 fi done echo " (server ready on port $port)" } stop_server() { if [ -n "${SERVER_PID:-}" ] && kill -0 "$SERVER_PID" 2>/dev/null; then kill "$SERVER_PID" 2>/dev/null wait "$SERVER_PID" 2>/dev/null || true sleep 1 fi SERVER_PID="" } url() { echo "http://localhost:$CURRENT_PORT$1"; } assert_status() { local desc="$1" path="$2" expected="$3" shift 3 local actual actual=$(curl -s -o /dev/null -w "%{http_code}" "$@" "$(url "$path")" 2>/dev/null) if [ "$actual" = "$expected" ]; then PASS=$((PASS + 1)) echo " PASS: $desc" else FAIL=$((FAIL + 1)) ERRORS="$ERRORS\n FAIL: $desc — expected $expected, got $actual" echo " FAIL: $desc — expected $expected, got $actual" fi } assert_redirect() { local desc="$1" path="$2" expected_path="$3" local actual actual=$(curl -s -o /dev/null -w "%{redirect_url}" "$(url "$path")" 2>/dev/null) local expected="http://localhost:$CURRENT_PORT$expected_path" if [ "$actual" = "$expected" ]; then PASS=$((PASS + 1)) echo " PASS: $desc" else FAIL=$((FAIL + 1)) ERRORS="$ERRORS\n FAIL: $desc — expected → $expected, got → $actual" echo " FAIL: $desc — expected → $expected_path, got → $actual" fi } assert_body_contains() { local desc="$1" path="$2" expected="$3" shift 3 curl -s "$@" "$(url "$path")" > "$TMP_DIR/body" 2>/dev/null if grep -qF "$expected" "$TMP_DIR/body"; then PASS=$((PASS + 1)) echo " PASS: $desc" else FAIL=$((FAIL + 1)) local size=$(wc -c < "$TMP_DIR/body") ERRORS="$ERRORS\n FAIL: $desc — body ($size bytes) missing '$expected'" echo " FAIL: $desc — body ($size bytes) missing '$expected'" fi } assert_stderr_contains() { local desc="$1" expected="$2" if grep -qF "$expected" "$TMP_DIR/stderr" 2>/dev/null; then PASS=$((PASS + 1)) echo " PASS: $desc" else FAIL=$((FAIL + 1)) ERRORS="$ERRORS\n FAIL: $desc — stderr missing '$expected'" echo " FAIL: $desc — stderr missing '$expected'" fi } assert_stderr_not_contains() { local desc="$1" expected="$2" if grep -qF "$expected" "$TMP_DIR/stderr" 2>/dev/null; then FAIL=$((FAIL + 1)) ERRORS="$ERRORS\n FAIL: $desc — stderr should NOT contain '$expected'" echo " FAIL: $desc — stderr should NOT contain '$expected'" else PASS=$((PASS + 1)) echo " PASS: $desc" fi } # ── Setup ──────────────────────────────────────────────────────────── if [ ! -x "$SERVER" ]; then echo "ERROR: Server binary not found at $SERVER" echo "Run: cd hosts/ocaml && eval \$(opam env) && dune build" exit 1 fi echo "=== SX HTTP Server Config Tests ===" echo "" # ── Test Group 1: Default config (standard app-config.sx) ─────────── echo "── Group 1: Standard config ──" start_server $((BASE_PORT + 1)) assert_stderr_contains "config loaded" "App config loaded: title=SX prefix=/sx/" assert_stderr_contains "warmup ran 9 pages" "Pre-warmed 9 pages" assert_stderr_not_contains "no stepper cookie refs" "sx-home-stepper" assert_status "homepage 200" "/sx/" 200 assert_status "AJAX homepage 200" "/sx/" 200 -H "sx-request: true" assert_status "geography 200" "/sx/(geography)" 200 assert_status "language 200" "/sx/(language)" 200 assert_status "applications 200" "/sx/(applications)" 200 assert_redirect "root → /sx/" "/" "/sx/" assert_status "handler 200" "/sx/(api.spec-detail)?name=render-to-html" 200 assert_status "static 200" "/static/styles/tw.css" 200 assert_status "debug 200" "/sx/_debug/eval?expr=(%2B%201%202)" 200 assert_status "unknown 404" "/nope" 404 assert_body_contains "title is SX" "/sx/" "SX" assert_body_contains "AJAX returns SX wire format" "/sx/" "sx-swap-oob" -H "sx-request: true" assert_body_contains "debug eval result" "/sx/_debug/eval?expr=(%2B%201%202)" "3" stop_server echo "" # ── Test Group 2: Custom title + minimal warmup ───────────────────── echo "── Group 2: Custom title ──" mkdir -p "$TMP_DIR/custom-sx" cp -r "$PROJECT_DIR/sx/sx/"* "$TMP_DIR/custom-sx/" cat > "$TMP_DIR/custom-sx/app-config.sx" << 'SXEOF' (define __app-config {:title "My Custom App" :path-prefix "/sx/" :home-path "/sx/" :inner-layout "~layouts/doc" :outer-layout "~shared:layout/app-body" :shell "~shared:shell/sx-page-shell" :client-libs (list "tw-layout.sx" "tw-type.sx" "tw.sx") :css-files (list "basics.css" "tw.css") :batchable-helpers (list "highlight" "component-source") :handler-prefixes (list "handler:ex-" "handler:reactive-" "handler:") :warmup-paths (list "/sx/") :init-script :default}) SXEOF start_server $((BASE_PORT + 2)) "SX_COMPONENTS_DIR=$TMP_DIR/custom-sx" assert_stderr_contains "custom title in log" "App config loaded: title=My Custom App" assert_stderr_contains "warmup 1 page" "Pre-warmed 1 pages" assert_status "homepage works" "/sx/" 200 assert_body_contains "custom title in HTML" "/sx/" "My Custom App" stop_server echo "" # ── Test Group 3: No config (missing __app-config) ────────────────── echo "── Group 3: No app config (defaults) ──" mkdir -p "$TMP_DIR/noconfig-sx" cp -r "$PROJECT_DIR/sx/sx/"* "$TMP_DIR/noconfig-sx/" rm -f "$TMP_DIR/noconfig-sx/app-config.sx" start_server $((BASE_PORT + 3)) "SX_COMPONENTS_DIR=$TMP_DIR/noconfig-sx" assert_stderr_contains "defaults fallback" "No __app-config found, using defaults" assert_status "homepage without config" "/sx/" 200 assert_redirect "root redirect without config" "/" "/sx/" assert_body_contains "default title" "/sx/" "SX" stop_server echo "" # ── Test Group 4: Minimal config (only title) ─────────────────────── echo "── Group 4: Minimal config ──" mkdir -p "$TMP_DIR/minimal-sx" cp -r "$PROJECT_DIR/sx/sx/"* "$TMP_DIR/minimal-sx/" cat > "$TMP_DIR/minimal-sx/app-config.sx" << 'SXEOF' (define __app-config {:title "Bare"}) SXEOF start_server $((BASE_PORT + 4)) "SX_COMPONENTS_DIR=$TMP_DIR/minimal-sx" assert_stderr_contains "minimal config log" "App config loaded: title=Bare" assert_status "homepage with minimal config" "/sx/" 200 assert_body_contains "bare title" "/sx/" "Bare" # Defaults should still work for everything not specified assert_redirect "default redirect" "/" "/sx/" assert_status "default debug" "/sx/_debug/eval?expr=1" 200 stop_server echo "" # ── Summary ────────────────────────────────────────────────────────── echo "=== Results: $PASS passed, $FAIL failed ===" if [ $FAIL -gt 0 ]; then echo -e "\nFailures:$ERRORS" exit 1 else echo "All tests passed." exit 0 fi