188 lines
6.3 KiB
Bash
Executable File
188 lines
6.3 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Spike verification for the worktree-test-stack-isolation change.
|
|
#
|
|
# Bootstraps a stack with a forced non-default port slot, brings up Keycloak +
|
|
# its seed, and asserts that the seeded redirectUris/webOrigins on the
|
|
# member-console and temporal-ui clients reflect the templated MC_BASE_URL /
|
|
# TEMPORAL_UI_URL — not the hardcoded localhost:8081 / :8233 defaults.
|
|
#
|
|
# This is the canary for tasks 2.6 and 2.7 in tasks.md: it proves Keycloak's
|
|
# admin API actually accepts our partial PUT bodies on /clients/{id} and
|
|
# stores the values, rather than silently ignoring them.
|
|
#
|
|
# Usage:
|
|
# ./test/verify-stack-isolation.sh # uses slot 137 by default
|
|
# FORCE_SLOT=42 ./test/verify-stack-isolation.sh
|
|
#
|
|
# Side effects: brings the stack up, then tears it down on exit. Requires
|
|
# docker, jq, curl. Will refuse to run if test/.env already exists (run
|
|
# teardown-stack.sh first).
|
|
|
|
set -euo pipefail
|
|
|
|
REPO_ROOT="$(git rev-parse --show-toplevel)"
|
|
TEST_DIR="${REPO_ROOT}/test"
|
|
ENV_FILE="${TEST_DIR}/.env"
|
|
|
|
FORCE_SLOT="${FORCE_SLOT:-137}"
|
|
PORT_STEP=50
|
|
|
|
cleanup() {
|
|
local rc=$?
|
|
echo ""
|
|
echo "--- Cleaning up ---"
|
|
"${TEST_DIR}/teardown-stack.sh" >/dev/null 2>&1 || true
|
|
exit "$rc"
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
if [[ -f "$ENV_FILE" ]]; then
|
|
echo "ERROR: ${ENV_FILE} already exists. Run ./test/teardown-stack.sh first." >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Compute expected ports for FORCE_SLOT.
|
|
KEYCLOAK_PORT=$((8080 + FORCE_SLOT * PORT_STEP))
|
|
MC_PORT=$((8081 + FORCE_SLOT * PORT_STEP))
|
|
TEMPORAL_UI_PORT=$((8233 + FORCE_SLOT * PORT_STEP))
|
|
EXPECTED_MC_BASE_URL="http://localhost:${MC_PORT}"
|
|
EXPECTED_TEMPORAL_UI_URL="http://localhost:${TEMPORAL_UI_PORT}"
|
|
EXPECTED_KC_HOSTNAME="keycloak-slot${FORCE_SLOT}.localhost"
|
|
|
|
echo "=== Spike: forcing slot ${FORCE_SLOT} ==="
|
|
echo " Keycloak port: ${KEYCLOAK_PORT}"
|
|
echo " MC port: ${MC_PORT}"
|
|
echo " Temporal UI port: ${TEMPORAL_UI_PORT}"
|
|
|
|
# Hand-write a .env that pins the slot, bypassing bootstrap's hash so this is
|
|
# deterministic regardless of working directory name.
|
|
cat > "$ENV_FILE" <<EOF
|
|
COMPOSE_PROJECT_NAME=mc-spike-slot${FORCE_SLOT}
|
|
SLOT=${FORCE_SLOT}
|
|
|
|
POSTGRES_PORT=$((5432 + FORCE_SLOT * PORT_STEP))
|
|
VALKEY_PORT=$((6379 + FORCE_SLOT * PORT_STEP))
|
|
KEYCLOAK_PORT=${KEYCLOAK_PORT}
|
|
TEMPORAL_PORT=$((7233 + FORCE_SLOT * PORT_STEP))
|
|
TEMPORAL_UI_PORT=${TEMPORAL_UI_PORT}
|
|
FEDWIKI_PORT=$((8090 + FORCE_SLOT * PORT_STEP))
|
|
|
|
KC_HOSTNAME=${EXPECTED_KC_HOSTNAME}
|
|
MC_BASE_URL=${EXPECTED_MC_BASE_URL}
|
|
TEMPORAL_UI_URL=${EXPECTED_TEMPORAL_UI_URL}
|
|
|
|
MC_PORT=${MC_PORT}
|
|
EOF
|
|
|
|
# Ensure secrets/ exists so compose doesn't bail. Copy from main worktree if
|
|
# missing (the bootstrap script does this normally).
|
|
if [[ ! -d "${TEST_DIR}/secrets" ]]; then
|
|
MAIN_WT=$(git worktree list --porcelain | awk '/^worktree / { print $2; exit }')
|
|
if [[ -d "${MAIN_WT}/test/secrets" ]]; then
|
|
cp -r "${MAIN_WT}/test/secrets" "${TEST_DIR}/secrets"
|
|
else
|
|
echo "ERROR: no test/secrets/ in this or main worktree" >&2
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
echo ""
|
|
echo "--- Bringing up keycloak + keycloak-seed ---"
|
|
docker compose -f "${TEST_DIR}/compose.yaml" --env-file "$ENV_FILE" \
|
|
up -d keycloak keycloak-seed
|
|
|
|
echo "Waiting for keycloak-seed to complete..."
|
|
SEED_CONTAINER=$(docker compose -f "${TEST_DIR}/compose.yaml" --env-file "$ENV_FILE" \
|
|
ps -q keycloak-seed)
|
|
for i in $(seq 1 60); do
|
|
STATE=$(docker inspect -f '{{.State.Status}}' "$SEED_CONTAINER" 2>/dev/null || echo "missing")
|
|
EXIT_CODE=$(docker inspect -f '{{.State.ExitCode}}' "$SEED_CONTAINER" 2>/dev/null || echo "")
|
|
if [[ "$STATE" == "exited" ]]; then
|
|
if [[ "$EXIT_CODE" != "0" ]]; then
|
|
echo "ERROR: keycloak-seed exited non-zero (${EXIT_CODE}). Logs:" >&2
|
|
docker logs "$SEED_CONTAINER" >&2
|
|
exit 1
|
|
fi
|
|
echo "Seed completed."
|
|
break
|
|
fi
|
|
if [[ "$i" -eq 60 ]]; then
|
|
echo "ERROR: keycloak-seed did not finish within 120s" >&2
|
|
docker logs "$SEED_CONTAINER" >&2 || true
|
|
exit 1
|
|
fi
|
|
sleep 2
|
|
done
|
|
|
|
echo ""
|
|
echo "--- Querying Keycloak admin API for stored client config ---"
|
|
|
|
KC_URL="http://localhost:${KEYCLOAK_PORT}"
|
|
TOKEN=$(curl -sf "${KC_URL}/realms/master/protocol/openid-connect/token" \
|
|
-d "grant_type=password" \
|
|
-d "client_id=admin-cli" \
|
|
-d "username=admin" \
|
|
-d "password=admin" | jq -r '.access_token')
|
|
|
|
if [[ -z "$TOKEN" || "$TOKEN" == "null" ]]; then
|
|
echo "ERROR: failed to obtain admin token from ${KC_URL}" >&2
|
|
exit 1
|
|
fi
|
|
|
|
kc_get() {
|
|
curl -sf -H "Authorization: Bearer ${TOKEN}" \
|
|
"${KC_URL}/admin/realms/master$1"
|
|
}
|
|
|
|
assert_eq() {
|
|
local label="$1" expected="$2" actual="$3"
|
|
if [[ "$expected" == "$actual" ]]; then
|
|
echo " PASS ${label}: ${actual}"
|
|
else
|
|
echo " FAIL ${label}"
|
|
echo " expected: ${expected}"
|
|
echo " actual: ${actual}"
|
|
FAILED=1
|
|
fi
|
|
}
|
|
|
|
FAILED=0
|
|
|
|
MC_CLIENT=$(kc_get "/clients?clientId=member-console" | jq '.[0]')
|
|
MC_REDIRECT=$(echo "$MC_CLIENT" | jq -r '.redirectUris[0]')
|
|
MC_WEBORIGIN=$(echo "$MC_CLIENT" | jq -r '.webOrigins[0]')
|
|
MC_LOGOUT=$(echo "$MC_CLIENT" | jq -r '.attributes."post.logout.redirect.uris"')
|
|
|
|
assert_eq "member-console.redirectUris[0]" "${EXPECTED_MC_BASE_URL}/*" "$MC_REDIRECT"
|
|
assert_eq "member-console.webOrigins[0]" "${EXPECTED_MC_BASE_URL}" "$MC_WEBORIGIN"
|
|
assert_eq "member-console.post.logout.redirect" "${EXPECTED_MC_BASE_URL}/*" "$MC_LOGOUT"
|
|
|
|
TUI_CLIENT=$(kc_get "/clients?clientId=temporal-ui" | jq '.[0]')
|
|
TUI_REDIRECT=$(echo "$TUI_CLIENT" | jq -r '.redirectUris[0]')
|
|
TUI_WEBORIGIN=$(echo "$TUI_CLIENT" | jq -r '.webOrigins[0]')
|
|
|
|
assert_eq "temporal-ui.redirectUris[0]" "${EXPECTED_TEMPORAL_UI_URL}/*" "$TUI_REDIRECT"
|
|
assert_eq "temporal-ui.webOrigins[0]" "${EXPECTED_TEMPORAL_UI_URL}" "$TUI_WEBORIGIN"
|
|
|
|
echo ""
|
|
echo "--- Re-running seed to verify idempotent update (task 2.6) ---"
|
|
docker compose -f "${TEST_DIR}/compose.yaml" --env-file "$ENV_FILE" \
|
|
run --rm keycloak-seed >/dev/null
|
|
|
|
# Re-fetch and re-assert (values must still match after a second seed run).
|
|
TOKEN=$(curl -sf "${KC_URL}/realms/master/protocol/openid-connect/token" \
|
|
-d "grant_type=password" -d "client_id=admin-cli" \
|
|
-d "username=admin" -d "password=admin" | jq -r '.access_token')
|
|
MC_REDIRECT2=$(kc_get "/clients?clientId=member-console" | jq -r '.[0].redirectUris[0]')
|
|
assert_eq "member-console.redirectUris[0] (after re-seed)" \
|
|
"${EXPECTED_MC_BASE_URL}/*" "$MC_REDIRECT2"
|
|
|
|
echo ""
|
|
if [[ "$FAILED" -eq 0 ]]; then
|
|
echo "=== ALL ASSERTIONS PASSED ==="
|
|
exit 0
|
|
else
|
|
echo "=== ASSERTIONS FAILED ===" >&2
|
|
exit 1
|
|
fi
|