161 lines
5.8 KiB
Bash
161 lines
5.8 KiB
Bash
|
|
#!/usr/bin/env bash
|
||
|
|
# Bridge-integration test for the todo example. Drives FrankenPHP
|
||
|
|
# against an isolated SQLite database, exercises the HTTP + SSE
|
||
|
|
# round-trip, asserts the §4 envelope shape (correlationKey echo,
|
||
|
|
# dual publish), then kills + restarts the backend to verify
|
||
|
|
# crash-and-recover behaviour described in PLAN.md §13 Phase 3.
|
||
|
|
#
|
||
|
|
# Designed to be safe against an existing dev instance: uses port
|
||
|
|
# 8767 (one above dev) and a temporary database under tests/var/.
|
||
|
|
|
||
|
|
set -euo pipefail
|
||
|
|
|
||
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||
|
|
APP_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||
|
|
SYMFONY_DIR="$APP_DIR/symfony"
|
||
|
|
TEST_VAR="$SCRIPT_DIR/var"
|
||
|
|
PORT=8767
|
||
|
|
TOKEN=integration-token
|
||
|
|
JWT="dev_php_qml_bridge_jwt_secret_at_least_256_bits_long_for_lcobucci"
|
||
|
|
|
||
|
|
: "${FRANKENPHP:=frankenphp}"
|
||
|
|
if ! command -v "$FRANKENPHP" >/dev/null 2>&1; then
|
||
|
|
echo "frankenphp not on PATH (set FRANKENPHP env var)" >&2
|
||
|
|
exit 1
|
||
|
|
fi
|
||
|
|
|
||
|
|
mkdir -p "$TEST_VAR"
|
||
|
|
DB_FILE="$TEST_VAR/integration.sqlite"
|
||
|
|
rm -f "$DB_FILE"
|
||
|
|
|
||
|
|
# Generate a per-test Caddyfile bound to a non-conflicting port.
|
||
|
|
CADDYFILE="$TEST_VAR/Caddyfile"
|
||
|
|
cat > "$CADDYFILE" <<EOF
|
||
|
|
{
|
||
|
|
auto_https off
|
||
|
|
admin off
|
||
|
|
frankenphp
|
||
|
|
order php_server before respond
|
||
|
|
order mercure after encode
|
||
|
|
}
|
||
|
|
|
||
|
|
http://127.0.0.1:$PORT {
|
||
|
|
root * $SYMFONY_DIR/public/
|
||
|
|
encode gzip
|
||
|
|
mercure {
|
||
|
|
transport local
|
||
|
|
publisher_jwt $JWT
|
||
|
|
subscriber_jwt $JWT
|
||
|
|
anonymous
|
||
|
|
publish_origins *
|
||
|
|
cors_origins *
|
||
|
|
}
|
||
|
|
php_server
|
||
|
|
log {
|
||
|
|
output stderr
|
||
|
|
format console
|
||
|
|
}
|
||
|
|
}
|
||
|
|
EOF
|
||
|
|
|
||
|
|
start_backend() {
|
||
|
|
DATABASE_URL="sqlite:///$DB_FILE" \
|
||
|
|
BRIDGE_TOKEN="$TOKEN" \
|
||
|
|
MERCURE_URL="http://127.0.0.1:$PORT/.well-known/mercure" \
|
||
|
|
MERCURE_PUBLIC_URL="http://127.0.0.1:$PORT/.well-known/mercure" \
|
||
|
|
MERCURE_JWT_SECRET="$JWT" \
|
||
|
|
MERCURE_PUBLISHER_JWT_KEY="$JWT" \
|
||
|
|
MERCURE_SUBSCRIBER_JWT_KEY="$JWT" \
|
||
|
|
APP_ENV=dev APP_DEBUG=0 APP_SECRET=integration-secret \
|
||
|
|
"$FRANKENPHP" run --config "$CADDYFILE" \
|
||
|
|
> "$TEST_VAR/frankenphp.log" 2>&1 &
|
||
|
|
BACKEND_PID=$!
|
||
|
|
# Wait for /healthz
|
||
|
|
for _ in $(seq 1 50); do
|
||
|
|
curl -fsS -m 1 "http://127.0.0.1:$PORT/healthz" >/dev/null 2>&1 && return 0
|
||
|
|
kill -0 "$BACKEND_PID" 2>/dev/null || { echo "frankenphp died early" >&2; cat "$TEST_VAR/frankenphp.log" >&2; exit 1; }
|
||
|
|
sleep 0.2
|
||
|
|
done
|
||
|
|
echo "frankenphp didn't respond after 10s" >&2
|
||
|
|
cat "$TEST_VAR/frankenphp.log" >&2
|
||
|
|
exit 1
|
||
|
|
}
|
||
|
|
|
||
|
|
stop_backend() {
|
||
|
|
[ -z "${BACKEND_PID:-}" ] && return 0
|
||
|
|
kill -TERM "$BACKEND_PID" 2>/dev/null || true
|
||
|
|
for _ in 1 2 3 4 5 6 7 8 9 10; do
|
||
|
|
kill -0 "$BACKEND_PID" 2>/dev/null || break
|
||
|
|
sleep 0.2
|
||
|
|
done
|
||
|
|
kill -KILL "$BACKEND_PID" 2>/dev/null || true
|
||
|
|
wait "$BACKEND_PID" 2>/dev/null || true
|
||
|
|
BACKEND_PID=""
|
||
|
|
}
|
||
|
|
|
||
|
|
cleanup() {
|
||
|
|
trap - EXIT INT TERM
|
||
|
|
stop_backend
|
||
|
|
}
|
||
|
|
trap cleanup EXIT INT TERM
|
||
|
|
|
||
|
|
step() { echo "→ $*"; }
|
||
|
|
fail() { echo "✗ FAIL: $*" >&2; exit 1; }
|
||
|
|
|
||
|
|
# ── Setup ──────────────────────────────────────────────────────────────
|
||
|
|
step "Migrate schema into $DB_FILE"
|
||
|
|
( cd "$SYMFONY_DIR" && DATABASE_URL="sqlite:///$DB_FILE" APP_ENV=dev APP_DEBUG=0 APP_SECRET=int \
|
||
|
|
bin/console doctrine:migrations:migrate -n >/dev/null )
|
||
|
|
|
||
|
|
step "Boot FrankenPHP on :$PORT"
|
||
|
|
start_backend
|
||
|
|
|
||
|
|
# ── HTTP round-trip ────────────────────────────────────────────────────
|
||
|
|
step "POST /api/todos"
|
||
|
|
RESP="$(curl -fsS -m 3 -X POST "http://127.0.0.1:$PORT/api/todos" \
|
||
|
|
-H "Authorization: Bearer $TOKEN" \
|
||
|
|
-H 'Content-Type: application/json' \
|
||
|
|
-H 'Idempotency-Key: it-key-1' \
|
||
|
|
-d '{"title":"first","done":false}')"
|
||
|
|
echo "$RESP" | grep -q '"title":"first"' || fail "POST response missing title"
|
||
|
|
ID="$(echo "$RESP" | python3 -c 'import json,sys; print(json.load(sys.stdin)["id"])')"
|
||
|
|
[ -n "$ID" ] || fail "POST response missing id"
|
||
|
|
|
||
|
|
step "GET /api/todos returns the row"
|
||
|
|
LIST="$(curl -fsS -m 3 "http://127.0.0.1:$PORT/api/todos" -H "Authorization: Bearer $TOKEN")"
|
||
|
|
echo "$LIST" | grep -q "$ID" || fail "GET didn't include $ID"
|
||
|
|
|
||
|
|
# ── Mercure dual-publish + correlationKey echo ─────────────────────────
|
||
|
|
step "Open SSE, POST during it, verify envelope"
|
||
|
|
SSE_OUT="$TEST_VAR/sse.txt"
|
||
|
|
( curl -sSN -m 4 "http://127.0.0.1:$PORT/.well-known/mercure?topic=app%3A%2F%2Fmodel%2Ftodo" > "$SSE_OUT" 2>/dev/null & SP=$!
|
||
|
|
sleep 0.5
|
||
|
|
curl -fsS -m 3 -X POST "http://127.0.0.1:$PORT/api/todos" \
|
||
|
|
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
|
||
|
|
-H 'Idempotency-Key: it-key-2' \
|
||
|
|
-d '{"title":"second","done":false}' > /dev/null
|
||
|
|
wait $SP 2>/dev/null || true
|
||
|
|
)
|
||
|
|
grep -q '"op":"upsert"' "$SSE_OUT" || fail "SSE missing op:upsert"
|
||
|
|
grep -q '"correlationKey":"it-key-2"' "$SSE_OUT" || fail "SSE missing correlationKey echo"
|
||
|
|
grep -q '"title":"second"' "$SSE_OUT" || fail "SSE missing payload"
|
||
|
|
|
||
|
|
step "Both topics receive the envelope"
|
||
|
|
PUB_COUNT=$(grep -c 'Update published' "$TEST_VAR/frankenphp.log" || true)
|
||
|
|
[ "$PUB_COUNT" -ge 2 ] || fail "expected ≥2 Update published entries (collection + entity), got $PUB_COUNT"
|
||
|
|
|
||
|
|
# ── Crash and recover ──────────────────────────────────────────────────
|
||
|
|
step "Kill the backend"
|
||
|
|
stop_backend
|
||
|
|
sleep 0.3
|
||
|
|
curl -fsS -m 1 "http://127.0.0.1:$PORT/healthz" >/dev/null 2>&1 \
|
||
|
|
&& fail "backend should be down but /healthz still answered"
|
||
|
|
|
||
|
|
step "Restart and verify GET still works"
|
||
|
|
start_backend
|
||
|
|
RESP2="$(curl -fsS -m 3 "http://127.0.0.1:$PORT/api/todos" -H "Authorization: Bearer $TOKEN")"
|
||
|
|
echo "$RESP2" | grep -q "$ID" || fail "after restart GET missing original todo"
|
||
|
|
echo "$RESP2" | grep -q '"second"' || fail "after restart GET missing second todo"
|
||
|
|
|
||
|
|
step "All bridge-integration assertions passed."
|