#!/usr/bin/env bash # The app's own tests: start the compiled binary, drive it with curl, and # check the contract's wire-level rules (status codes, redirects, cookies, # escaping, Content-Length, static files, isolation, Origin, the unix # socket and a clean SIGTERM exit). The browser-level behaviour is the # shared suite's job (scripts/test-app.sh at the repo root). # # test/run.sh dist/app set -uo pipefail bin=$(cd "$(dirname "${1:?usage: test/run.sh }")" && pwd)/$(basename "$1") here=$(cd "$(dirname "$0")/.." && pwd) work=$(mktemp -d -p "${TMPDIR:-/tmp}") pid="" cleanup() { [ -n "$pid" ] && kill "$pid" 2>/dev/null rm -rf "$work" } trap cleanup EXIT pass=0 fail=0 ok() { pass=$((pass + 1)); } bad() { fail=$((fail + 1)); echo "FAIL: $*"; } check() { # check if [ "$2" == "$3" ]; then ok; else bad "$1: expected [$2], got [$3]"; fi } contains() { # contains if [[ "$3" == *"$2"* ]]; then ok; else bad "$1: [$2] not found"; fi } lacks() { if [[ "$3" != *"$2"* ]]; then ok; else bad "$1: [$2] found"; fi } port=$(python3 -c 'import socket; s=socket.socket(); s.bind(("127.0.0.1", 0)); print(s.getsockname()[1])') base="http://127.0.0.1:$port" "$bin" --listen "127.0.0.1:$port" --db "$work/app.db" >"$work/app.log" 2>&1 & pid=$! for _ in $(seq 50); do curl -sf "$base/healthz" >/dev/null && break; sleep 0.1; done status() { curl -s -o /dev/null -w '%{http_code}' "$@"; } jar="$work/jar" other="$work/other" # --- platform routes check "healthz" 200 "$(status "$base/healthz")" check "version" 200 "$(status "$base/api/version")" check "version body" "$(cat "$here/version.rb" | sed -n 's/^APP_VERSION = "\(.*\)"$/\1/p')" "$(curl -s "$base/api/version")" check "unknown path" 404 "$(status "$base/nope")" # --- listens only where it was told to listeners=$(ss -ltnH "sport = :$port" | awk '{print $4}') check "bound to 127.0.0.1 only" "127.0.0.1:$port" "$listeners" # --- first visit: cookie, empty list, no main/footer headers=$(curl -s -D - -o "$work/page" -c "$jar" "$base/") contains "cookie set" "Set-Cookie: tid=" "$headers" contains "cookie HttpOnly" "HttpOnly" "$headers" contains "cookie SameSite" "SameSite=Lax" "$headers" contains "cookie Path" "Path=/" "$headers" lacks "no Secure over http" "Secure" "$headers" token=$(awk '$6 == "tid" {print $7}' "$jar") check "token is 22 base64url chars" 1 "$([[ "$token" =~ ^[A-Za-z0-9_-]{22}$ ]] && echo 1 || echo 0)" page=$(cat "$work/page") lacks "empty list hides main" 'class="main"' "$page" lacks "empty list hides footer" 'class="footer"' "$page" contains "language in footer" "the CARLOS bakeoff · Spinel" "$page" # --- add: trimmed, blank ignored, 303 to the filter post() { curl -s -o /dev/null -w '%{http_code} %{redirect_url}' -b "$jar" -c "$jar" "$@"; } check "add" "303 $base/" "$(post --data-urlencode 'title= buy milk ' -d filter=all "$base/todos")" check "add from active" "303 $base/active" "$(post --data-urlencode 'title=walk the dog' -d filter=active "$base/todos")" check "blank add" "303 $base/completed" "$(post --data-urlencode 'title= ' -d filter=completed "$base/todos")" check "unknown filter" "303 $base/" "$(post --data-urlencode 'title=write' -d filter=bogus "$base/todos")" page=$(curl -s -b "$jar" "$base/") contains "trimmed title" "" "$page" check "three items" 3 "$(grep -c '
  • " "$page" contains "singular counter" "2 items left" "$page" contains "clear-completed shown" 'class="clear-completed"' "$page" active=$(curl -s -b "$jar" "$base/active") lacks "active filter hides completed" "data-id=\"$id1\"" "$active" contains "active link selected" '' "$active" completed=$(curl -s -b "$jar" "$base/completed") check "completed filter" 1 "$(grep -c '
  • /dev/null check "toggle-all again clears" 0 "$(curl -s -b "$jar" "$base/" | grep -c 'class="completed"')" post -d filter=all "$base/todos/$id1/toggle" >/dev/null check "clear-completed" "303 $base/" "$(post -d filter=all "$base/todos/clear-completed")" lacks "completed item gone" "data-id=\"$id1\"" "$(curl -s -b "$jar" "$base/")" # --- edit view, rename, empty rename deletes edit=$(curl -s -b "$jar" "$base/active?edit=$id2") contains "editing class" "
  • " "$edit" contains "edit form" "action=\"/todos/$id2/edit\"" "$edit" contains "edit link keeps filter" "href=\"/active?edit=$id2\"" "$edit" check "rename" "303 $base/active" "$(post --data-urlencode 'title= walk the cat ' -d filter=active "$base/todos/$id2/edit")" contains "renamed" "" "$(curl -s -b "$jar" "$base/")" check "empty rename" "303 $base/" "$(post --data-urlencode 'title= ' -d filter=all "$base/todos/$id2/edit")" lacks "empty rename deletes" "data-id=\"$id2\"" "$(curl -s -b "$jar" "$base/")" # --- escaping, UTF-8, Content-Length, long titles post --data-urlencode 'title= & "q" '"'"'s' -d filter=all "$base/todos" >/dev/null post --data-urlencode 'title=naïve 🚀 日本語' -d filter=all "$base/todos" >/dev/null headers=$(curl -s -D - -o "$work/page" -b "$jar" "$base/") page=$(cat "$work/page") contains "escaped title" "<img src=x onerror=alert(1)> & "q" 's" "$page" lacks "no raw img" "/dev/null contains "10,000-char title" "" "$(curl -s -b "$jar" "$base/")" # --- isolation, foreign ids, garbled cookies, Origin check "foreign id from another visitor" 404 "$(status -c "$other" -b "$other" -d filter=all "$base/todos/$id1/toggle")" own=$(curl -s -b "$jar" "$base/" | grep -o '
  • ' "$(head -1 <<<"$frag")" lacks "fragment is not a page" "' "$frag" # --- static files, byte for byte for f in base.css index.css; do curl -s "$base/static/$f" -o "$work/$f" if cmp -s "$work/$f" "$here/../../shared/$f"; then ok; else bad "/static/$f differs from shared/$f"; fi done contains "css type" "Content-Type: text/css" "$(curl -s -D - -o /dev/null "$base/static/index.css")" # --- cookies must be ones this server issued made_up="AAAAAAAAAAAAAAAAAAAAAA" hdr=$(curl -s -D - -o /dev/null -H "Cookie: tid=$made_up" -d filter=all --data-urlencode title=shared "$base/todos") contains "made-up token replaced" "Set-Cookie: tid=" "$hdr" lacks "made-up token not kept" "tid=$made_up" "$hdr" lacks "made-up token holds no list" "" "$(curl -s -H "Cookie: tid=$made_up" "$base/")" # --- long bodies arrive whole; short, chunked and oversized ones change nothing rockets=$(python3 -c 'print("\U0001F680" * 10000, end="")') check "10,000-emoji title" 303 "$(status -b "$jar" --data-urlencode "title=$rockets" -d filter=all "$base/todos")" contains "10,000 emoji kept whole" "" "$(curl -s -b "$jar" "$base/")" before=$(curl -s -b "$jar" "$base/" | grep -c '
  • /dev/null 2>&1 took=$(( $(date +%s) - start )) check "trickling client cut off within 12s" 1 "$([ "$took" -le 12 ] && echo 1 || echo 0)" check "server still answers after a trickler" 200 "$(status "$base/healthz")" # --- only loopback listeners, and no silent every-interface fallback timeout 5 "$bin" --listen "0.0.0.0:$((port + 1))" --db "$work/x.db" >/dev/null 2>&1 check "--listen 0.0.0.0 refused" 2 "$?" timeout 5 "$bin" --listen "10.1.2.3:$((port + 1))" --db "$work/x.db" >/dev/null 2>&1 check "--listen 10.1.2.3 refused" 2 "$?" timeout 5 "$bin" --db "$work/x.db" -p "$((port + 1))" >/dev/null 2>&1 check "no listener refused" 2 "$?" # --- clean exit on SIGTERM, and the data survives a restart kill -TERM "$pid" code=0 for _ in $(seq 50); do kill -0 "$pid" 2>/dev/null || break; sleep 0.1; done if kill -0 "$pid" 2>/dev/null; then bad "still running after SIGTERM"; else wait "$pid"; code=$?; check "SIGTERM exit status" 0 "$code"; fi pid="" # --- the unix-socket contract, with CARLOS_HOST sock="$work/app.sock" CARLOS_HOST=todo.example.test "$bin" --socket "$sock" --db "$work/app.db" >>"$work/app.log" 2>&1 & pid=$! for _ in $(seq 50); do [ -S "$sock" ] && break; sleep 0.1; done u=(curl -s --unix-socket "$sock") check "socket healthz" 200 "$("${u[@]}" -o /dev/null -w '%{http_code}' http://x/healthz)" contains "data survived restart" "" "$("${u[@]}" -H "Cookie: tid=$token" http://x/)" contains "Secure cookie on Carloku" "Secure" "$("${u[@]}" -D - -o /dev/null http://x/)" check "Carloku origin accepted" 303 "$("${u[@]}" -o /dev/null -w '%{http_code}' -H "Cookie: tid=$token" -H 'Origin: https://todo.example.test' -d title=x http://x/todos)" check "local origin refused on Carloku" 403 "$("${u[@]}" -o /dev/null -w '%{http_code}' -H "Cookie: tid=$token" -H "Origin: $base" -d title=x http://x/todos)" kill -TERM "$pid" for _ in $(seq 50); do kill -0 "$pid" 2>/dev/null || break; sleep 0.1; done if kill -0 "$pid" 2>/dev/null; then bad "socket server still running after SIGTERM"; else wait "$pid"; check "socket SIGTERM exit status" 0 "$?"; fi pid="" echo "$pass passed, $fail failed" [ "$fail" -eq 0 ]