diff --git a/.woodpecker/edge-subpath.yml b/.woodpecker/edge-subpath.yml new file mode 100644 index 0000000..2c11980 --- /dev/null +++ b/.woodpecker/edge-subpath.yml @@ -0,0 +1,317 @@ +# ============================================================================= +# .woodpecker/edge-subpath.yml — Edge subpath routing static checks +# +# Static validation for edge subpath routing configuration. This pipeline does +# NOT run live service curls — it validates the configuration that would be +# used by a deployed edge proxy. +# +# Checks: +# 1. shellcheck — syntax check on tests/smoke-edge-subpath.sh +# 2. caddy validate — validate the Caddyfile template syntax +# 3. caddyfile-routing-test — verify Caddyfile routing block shape +# 4. test-caddyfile-routing — run standalone unit test for Caddyfile structure +# +# Triggers: +# - Pull requests that modify edge-related files +# +# Environment variables (inherited from WOODPECKER_ENVIRONMENT): +# EDGE_BASE_URL — Edge proxy URL for reference (default: http://localhost) +# EDGE_TIMEOUT — Request timeout in seconds (default: 30) +# EDGE_MAX_RETRIES — Max retries per request (default: 3) +# ============================================================================= + +when: + event: pull_request + +steps: + # ── 1. ShellCheck on smoke script ──────────────────────────────────────── + # `shellcheck` validates bash syntax, style, and common pitfalls. + # Exit codes: + # 0 — all checks passed + # 1 — one or more issues found + - name: shellcheck-smoke + image: koalaman/shellcheck-alpine:stable + commands: + - shellcheck --severity=warning tests/smoke-edge-subpath.sh tests/test-caddyfile-routing.sh + + # ── 2. Caddyfile template rendering ─────────────────────────────────────── + # Render a mock Caddyfile for validation. The template uses Nomad's + # templating syntax ({{ range ... }}) which must be processed before Caddy + # can validate it. We render a mock version with Nomad templates expanded + # to static values for validation purposes. + - name: render-caddyfile + image: alpine:3.19 + commands: + - apk add --no-cache coreutils + - | + set -e + mkdir -p edge-render + # Render mock Caddyfile with Nomad templates expanded + { + echo '# Caddyfile — edge proxy configuration (Nomad-rendered)' + echo '# Staging upstream discovered via Nomad service registration.' + echo '' + echo ':80 {' + echo ' # Redirect root to Forgejo' + echo ' handle / {' + echo ' redir /forge/ 302' + echo ' }' + echo '' + echo ' # Reverse proxy to Forgejo' + echo ' handle /forge/* {' + echo ' reverse_proxy 127.0.0.1:3000' + echo ' }' + echo '' + echo ' # Reverse proxy to Woodpecker CI' + echo ' handle /ci/* {' + echo ' reverse_proxy 127.0.0.1:8000' + echo ' }' + echo '' + echo ' # Reverse proxy to staging — dynamic port via Nomad service discovery' + echo ' handle /staging/* {' + echo ' reverse_proxy 127.0.0.1:8081' + echo ' }' + echo '' + echo ' # Chat service — reverse proxy to disinto-chat backend (#705)' + echo ' # OAuth routes bypass forward_auth — unauthenticated users need these (#709)' + echo ' handle /chat/login {' + echo ' reverse_proxy 127.0.0.1:8080' + echo ' }' + echo ' handle /chat/oauth/callback {' + echo ' reverse_proxy 127.0.0.1:8080' + echo ' }' + echo ' # Defense-in-depth: forward_auth stamps X-Forwarded-User from session (#709)' + echo ' handle /chat/* {' + echo ' forward_auth 127.0.0.1:8080 {' + echo ' uri /chat/auth/verify' + echo ' copy_headers X-Forwarded-User' + echo ' header_up X-Forward-Auth-Secret {$FORWARD_AUTH_SECRET}' + echo ' }' + echo ' reverse_proxy 127.0.0.1:8080' + echo ' }' + echo '}' + } > edge-render/Caddyfile + cp edge-render/Caddyfile edge-render/Caddyfile.rendered + echo "Caddyfile rendered successfully" + + # ── 3. Caddy config validation ─────────────────────────────────────────── + # `caddy validate` checks Caddyfile syntax and configuration. + # This validates the rendered Caddyfile against Caddy's parser. + # Exit codes: + # 0 — configuration is valid + # 1 — configuration has errors + - name: caddy-validate + image: alpine:3.19 + commands: + - apk add --no-cache ca-certificates curl + - curl -sS -o /tmp/caddy "https://caddyserver.com/api/download?os=linux&arch=amd64" + - chmod +x /tmp/caddy + - /tmp/caddy version + - /tmp/caddy validate --config edge-render/Caddyfile.rendered --adapter caddyfile + + # ── 4. Caddyfile routing block shape test ───────────────────────────────── + # Verify that the Caddyfile contains all required routing blocks: + # - /forge/ — Forgejo subpath + # - /ci/ — Woodpecker subpath + # - /staging/ — Staging subpath + # - /chat/ — Chat subpath with forward_auth + # + # This is a unit test that validates the expected structure without + # requiring a running Caddy instance. + - name: caddyfile-routing-test + image: alpine:3.19 + commands: + - apk add --no-cache grep coreutils + - | + set -e + + CADDYFILE="edge-render/Caddyfile.rendered" + + echo "=== Validating Caddyfile routing blocks ===" + + # Check that all required subpath handlers exist + # POSIX-safe loop (alpine /bin/sh has no arrays) + FAILED=0 + for handler in "handle /forge/\*" "handle /ci/\*" "handle /staging/\*" "handle /chat/login" "handle /chat/oauth/callback" "handle /chat/\*"; do + if grep -q "$handler" "$CADDYFILE"; then + echo "[PASS] Found handler: $handler" + else + echo "[FAIL] Missing handler: $handler" + FAILED=1 + fi + done + + # Check forward_auth block exists for /chat/* + if grep -A5 "handle /chat/\*" "$CADDYFILE" | grep -q "forward_auth"; then + echo "[PASS] forward_auth block found for /chat/*" + else + echo "[FAIL] forward_auth block missing for /chat/*" + FAILED=1 + fi + + # Check reverse_proxy to Forgejo (port 3000) + if grep -q "reverse_proxy 127.0.0.1:3000" "$CADDYFILE"; then + echo "[PASS] Forgejo reverse_proxy configured (port 3000)" + else + echo "[FAIL] Forgejo reverse_proxy not configured" + FAILED=1 + fi + + # Check reverse_proxy to Woodpecker (port 8000) + if grep -q "reverse_proxy 127.0.0.1:8000" "$CADDYFILE"; then + echo "[PASS] Woodpecker reverse_proxy configured (port 8000)" + else + echo "[FAIL] Woodpecker reverse_proxy not configured" + FAILED=1 + fi + + # Check reverse_proxy to Chat (port 8080) + if grep -q "reverse_proxy 127.0.0.1:8080" "$CADDYFILE"; then + echo "[PASS] Chat reverse_proxy configured (port 8080)" + else + echo "[FAIL] Chat reverse_proxy not configured" + FAILED=1 + fi + + # Check root redirect to /forge/ + if grep -q "redir /forge/ 302" "$CADDYFILE"; then + echo "[PASS] Root redirect to /forge/ configured" + else + echo "[FAIL] Root redirect to /forge/ not configured" + FAILED=1 + fi + + echo "" + if [ $FAILED -eq 0 ]; then + echo "=== All routing blocks validated ===" + exit 0 + else + echo "=== Routing block validation failed ===" >&2 + exit 1 + fi + + # ── 5. Standalone Caddyfile routing test ───────────────────────────────── + # Run the standalone unit test for Caddyfile routing block validation. + # This test extracts the Caddyfile template from edge.hcl and validates + # its structure without requiring a running Caddy instance. + - name: test-caddyfile-routing + image: alpine:3.19 + commands: + - apk add --no-cache grep coreutils + - | + set -e + EDGE_TEMPLATE="nomad/jobs/edge.hcl" + + echo "=== Extracting Caddyfile template from $EDGE_TEMPLATE ===" + + # Extract the Caddyfile template (content between <&2 + exit 1 + fi + + echo "Caddyfile template extracted successfully" + echo "" + + FAILED=0 + + # Check Forgejo subpath + if echo "$CADDYFILE" | grep -q "handle /forge/\*"; then + echo "[PASS] Forgejo handle block" + else + echo "[FAIL] Forgejo handle block" + FAILED=1 + fi + + if echo "$CADDYFILE" | grep -q "reverse_proxy 127.0.0.1:3000"; then + echo "[PASS] Forgejo reverse_proxy (port 3000)" + else + echo "[FAIL] Forgejo reverse_proxy (port 3000)" + FAILED=1 + fi + + # Check Woodpecker subpath + if echo "$CADDYFILE" | grep -q "handle /ci/\*"; then + echo "[PASS] Woodpecker handle block" + else + echo "[FAIL] Woodpecker handle block" + FAILED=1 + fi + + if echo "$CADDYFILE" | grep -q "reverse_proxy 127.0.0.1:8000"; then + echo "[PASS] Woodpecker reverse_proxy (port 8000)" + else + echo "[FAIL] Woodpecker reverse_proxy (port 8000)" + FAILED=1 + fi + + # Check Staging subpath + if echo "$CADDYFILE" | grep -q "handle /staging/\*"; then + echo "[PASS] Staging handle block" + else + echo "[FAIL] Staging handle block" + FAILED=1 + fi + + if echo "$CADDYFILE" | grep -q "nomadService"; then + echo "[PASS] Staging Nomad service discovery" + else + echo "[FAIL] Staging Nomad service discovery" + FAILED=1 + fi + + # Check Chat subpath + if echo "$CADDYFILE" | grep -q "handle /chat/login"; then + echo "[PASS] Chat login handle block" + else + echo "[FAIL] Chat login handle block" + FAILED=1 + fi + + if echo "$CADDYFILE" | grep -q "handle /chat/oauth/callback"; then + echo "[PASS] Chat OAuth callback handle block" + else + echo "[FAIL] Chat OAuth callback handle block" + FAILED=1 + fi + + if echo "$CADDYFILE" | grep -q "handle /chat/\*"; then + echo "[PASS] Chat catch-all handle block" + else + echo "[FAIL] Chat catch-all handle block" + FAILED=1 + fi + + if echo "$CADDYFILE" | grep -q "reverse_proxy 127.0.0.1:8080"; then + echo "[PASS] Chat reverse_proxy (port 8080)" + else + echo "[FAIL] Chat reverse_proxy (port 8080)" + FAILED=1 + fi + + # Check forward_auth for chat + if echo "$CADDYFILE" | grep -A10 "handle /chat/\*" | grep -q "forward_auth"; then + echo "[PASS] forward_auth block for /chat/*" + else + echo "[FAIL] forward_auth block for /chat/*" + FAILED=1 + fi + + # Check root redirect + if echo "$CADDYFILE" | grep -q "redir /forge/ 302"; then + echo "[PASS] Root redirect to /forge/" + else + echo "[FAIL] Root redirect to /forge/" + FAILED=1 + fi + + echo "" + if [ $FAILED -eq 0 ]; then + echo "=== All routing blocks validated ===" + exit 0 + else + echo "=== Routing block validation failed ===" >&2 + exit 1 + fi diff --git a/tests/smoke-edge-subpath.sh b/tests/smoke-edge-subpath.sh new file mode 100755 index 0000000..6a1f383 --- /dev/null +++ b/tests/smoke-edge-subpath.sh @@ -0,0 +1,310 @@ +#!/usr/bin/env bash +# ============================================================================= +# smoke-edge-subpath.sh — End-to-end subpath routing smoke test +# +# Verifies Forgejo, Woodpecker, and chat function correctly under subpaths: +# - Forgejo at /forge/ +# - Woodpecker at /ci/ +# - Chat at /chat/ +# - Staging at /staging/ +# +# Usage: +# smoke-edge-subpath.sh [--base-url BASE_URL] +# +# Environment variables: +# BASE_URL — Edge proxy URL (default: http://localhost) +# EDGE_TIMEOUT — Request timeout in seconds (default: 30) +# EDGE_MAX_RETRIES — Max retries per request (default: 3) +# +# Exit codes: +# 0 — All checks passed +# 1 — One or more checks failed +# ============================================================================= +set -euo pipefail + +# Script directory for relative paths +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Source common helpers if available +source "${SCRIPT_DIR}/../lib/env.sh" 2>/dev/null || true + +# ───────────────────────────────────────────────────────────────────────────── +# Configuration +# ───────────────────────────────────────────────────────────────────────────── + +BASE_URL="${BASE_URL:-http://localhost}" +EDGE_TIMEOUT="${EDGE_TIMEOUT:-30}" +EDGE_MAX_RETRIES="${EDGE_MAX_RETRIES:-3}" + +# Subpaths to test +FORGE_PATH="/forge/" +CI_PATH="/ci/" +CHAT_PATH="/chat/" +STAGING_PATH="/staging/" + +# Track overall test status +FAILED=0 +PASSED=0 +SKIPPED=0 + +# ───────────────────────────────────────────────────────────────────────────── +# Logging helpers +# ───────────────────────────────────────────────────────────────────────────── + +log_info() { + echo "[INFO] $*" +} + +log_pass() { + echo "[PASS] $*" + ((PASSED++)) || true +} + +log_fail() { + echo "[FAIL] $*" + ((FAILED++)) || true +} + +log_skip() { + echo "[SKIP] $*" + ((SKIPPED++)) || true +} + +log_section() { + echo "" + echo "=== $* ===" + echo "" +} + +# ───────────────────────────────────────────────────────────────────────────── +# HTTP helpers +# ───────────────────────────────────────────────────────────────────────────── + +# Make an HTTP request with retry logic +# Usage: http_request [options...] +# Returns: HTTP status code on stdout +http_request() { + local method="$1" + local url="$2" + shift 2 + + local retries=0 + local response status + + while [ "$retries" -lt "$EDGE_MAX_RETRIES" ]; do + response=$(curl -sS -w '\n%{http_code}' -X "$method" \ + --max-time "$EDGE_TIMEOUT" \ + -o /tmp/edge-response-$$ \ + "$@" 2>&1) || { + retries=$((retries + 1)) + log_info "Retry $retries/$EDGE_MAX_RETRIES for $url" + sleep 1 + continue + } + + status=$(echo "$response" | tail -n1) + + echo "$status" + return 0 + done + + log_fail "Max retries exceeded for $url" + return 1 +} + +# Make a GET request and return status code +http_get() { + local url="$1" + shift || true + http_request "GET" "$url" "$@" +} + +# Make a HEAD request (no body) +http_head() { + local url="$1" + shift || true + http_request "HEAD" "$url" "$@" +} + +# Make a GET request and return the response body +http_get_body() { + local url="$1" + shift || true + curl -sS --max-time "$EDGE_TIMEOUT" "$@" "$url" +} + +# ───────────────────────────────────────────────────────────────────────────── +# Test functions +# ───────────────────────────────────────────────────────────────────────────── + +test_root_redirect() { + log_section "Test 1: Root redirect to /forge/" + + local status + status=$(http_head "$BASE_URL/") + + if [ "$status" = "302" ]; then + log_pass "Root / redirects with 302" + else + log_fail "Expected 302 redirect from /, got status $status" + fi +} + +test_forgejo_subpath() { + log_section "Test 2: Forgejo at /forge/" + + local status + status=$(http_head "$BASE_URL${FORGE_PATH}") + + if [ "$status" -ge 200 ] && [ "$status" -lt 400 ]; then + log_pass "Forgejo at ${BASE_URL}${FORGE_PATH} returns status $status" + else + log_fail "Forgejo at ${BASE_URL}${FORGE_PATH} returned unexpected status $status" + fi +} + +test_woodpecker_subpath() { + log_section "Test 3: Woodpecker at /ci/" + + local status + status=$(http_head "$BASE_URL${CI_PATH}") + + if [ "$status" -ge 200 ] && [ "$status" -lt 400 ]; then + log_pass "Woodpecker at ${BASE_URL}${CI_PATH} returns status $status" + else + log_fail "Woodpecker at ${BASE_URL}${CI_PATH} returned unexpected status $status" + fi +} + +test_chat_subpath() { + log_section "Test 4: Chat at /chat/" + + # Test chat login endpoint + local status + status=$(http_head "$BASE_URL${CHAT_PATH}login") + + if [ "$status" -ge 200 ] && [ "$status" -lt 400 ]; then + log_pass "Chat login at ${BASE_URL}${CHAT_PATH}login returns status $status" + else + log_fail "Chat login at ${BASE_URL}${CHAT_PATH}login returned unexpected status $status" + fi + + # Test chat OAuth callback endpoint + status=$(http_head "$BASE_URL${CHAT_PATH}oauth/callback") + + if [ "$status" -ge 200 ] && [ "$status" -lt 400 ]; then + log_pass "Chat OAuth callback at ${BASE_URL}${CHAT_PATH}oauth/callback returns status $status" + else + log_fail "Chat OAuth callback at ${BASE_URL}${CHAT_PATH}oauth/callback returned unexpected status $status" + fi +} + +test_staging_subpath() { + log_section "Test 5: Staging at /staging/" + + local status + status=$(http_head "$BASE_URL${STAGING_PATH}") + + if [ "$status" -ge 200 ] && [ "$status" -lt 400 ]; then + log_pass "Staging at ${BASE_URL}${STAGING_PATH} returns status $status" + else + log_fail "Staging at ${BASE_URL}${STAGING_PATH} returned unexpected status $status" + fi +} + +test_forward_auth_rejection() { + log_section "Test 6: Forward auth on /chat/* rejects unauthenticated requests" + + # Request a protected chat endpoint without auth header + # Should return 401 (Unauthorized) due to forward_auth + local status + status=$(http_head "$BASE_URL${CHAT_PATH}auth/verify") + + if [ "$status" = "401" ]; then + log_pass "Unauthenticated /chat/auth/verify returns 401 (forward_auth working)" + elif [ "$status" -ge 200 ] && [ "$status" -lt 400 ]; then + log_skip "Unauthenticated /chat/auth/verify returns $status (forward_auth may be disabled)" + else + log_fail "Expected 401 for unauthenticated /chat/auth/verify, got status $status" + fi +} + +test_forgejo_oauth_callback() { + log_section "Test 7: Forgejo OAuth callback for Woodpecker under subpath" + + # Test that Forgejo OAuth callback path works (Woodpecker OAuth integration) + local status + status=$(http_head "$BASE_URL${FORGE_PATH}login/oauth/callback") + + if [ "$status" -ge 200 ] && [ "$status" -lt 400 ]; then + log_pass "Forgejo OAuth callback at ${BASE_URL}${FORGE_PATH}login/oauth/callback works" + else + log_fail "Forgejo OAuth callback returned unexpected status $status" + fi +} + +# ───────────────────────────────────────────────────────────────────────────── +# Main +# ───────────────────────────────────────────────────────────────────────────── + +main() { + log_info "Starting subpath routing smoke test" + log_info "Base URL: $BASE_URL" + log_info "Timeout: ${EDGE_TIMEOUT}s, Max retries: ${EDGE_MAX_RETRIES}" + + # Run all tests + test_root_redirect + test_forgejo_subpath + test_woodpecker_subpath + test_chat_subpath + test_staging_subpath + test_forward_auth_rejection + test_forgejo_oauth_callback + + # Summary + log_section "Test Summary" + log_info "Passed: $PASSED" + log_info "Failed: $FAILED" + log_info "Skipped: $SKIPPED" + + if [ "$FAILED" -gt 0 ]; then + log_fail "Some tests failed" + exit 1 + fi + + log_pass "All tests passed!" + exit 0 +} + +# Parse arguments +while [[ $# -gt 0 ]]; do + case "$1" in + --base-url) + BASE_URL="$2" + shift 2 + ;; + --base-url=*) + BASE_URL="${1#*=}" + shift + ;; + --help) + echo "Usage: $0 [options]" + echo "" + echo "Options:" + echo " --base-url URL Set base URL (default: http://localhost)" + echo " --help Show this help message" + echo "" + echo "Environment variables:" + echo " BASE_URL Base URL for edge proxy (default: http://localhost)" + echo " EDGE_TIMEOUT Request timeout in seconds (default: 30)" + echo " EDGE_MAX_RETRIES Max retries per request (default: 3)" + exit 0 + ;; + *) + echo "Unknown option: $1" >&2 + exit 1 + ;; + esac +done + +main diff --git a/tests/test-caddyfile-routing.sh b/tests/test-caddyfile-routing.sh new file mode 100755 index 0000000..52a7a3d --- /dev/null +++ b/tests/test-caddyfile-routing.sh @@ -0,0 +1,231 @@ +#!/usr/bin/env bash +# ============================================================================= +# test-caddyfile-routing.sh — Caddyfile routing block unit test +# +# Extracts the Caddyfile template from nomad/jobs/edge.hcl and validates its +# structure without requiring a running Caddy instance. +# +# Checks: +# - Forgejo subpath (/forge/* -> :3000) +# - Woodpecker subpath (/ci/* -> :8000) +# - Staging subpath (/staging/* -> nomadService discovery) +# - Chat subpath (/chat/* with forward_auth and OAuth routes) +# - Root redirect to /forge/ +# +# Usage: +# test-caddyfile-routing.sh +# +# Exit codes: +# 0 — All checks passed +# 1 — One or more checks failed +# ============================================================================= +set -euo pipefail + +# Script directory for relative paths +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" + +EDGE_TEMPLATE="${REPO_ROOT}/nomad/jobs/edge.hcl" + +# Track test status +FAILED=0 +PASSED=0 + +# ───────────────────────────────────────────────────────────────────────────── +# Logging helpers +# ───────────────────────────────────────────────────────────────────────────── + +tr_info() { + echo "[INFO] $*" +} + +tr_pass() { + echo "[PASS] $*" + ((PASSED++)) || true +} + +tr_fail() { + echo "[FAIL] $*" + ((FAILED++)) || true +} + +tr_section() { + echo "" + echo "=== $* ===" + echo "" +} + +# ───────────────────────────────────────────────────────────────────────────── +# Caddyfile extraction +# ───────────────────────────────────────────────────────────────────────────── + +extract_caddyfile() { + local template_file="$1" + + # Extract the Caddyfile template (content between <&2 + return 1 + fi + + echo "$caddyfile" +} + +# ───────────────────────────────────────────────────────────────────────────── +# Validation functions +# ───────────────────────────────────────────────────────────────────────────── + +check_forgejo_routing() { + tr_section "Validating Forgejo routing" + + # Check handle block for /forge/* + if echo "$CADDYFILE" | grep -q "handle /forge/\*"; then + tr_pass "Forgejo handle block (handle /forge/*)" + else + tr_fail "Missing Forgejo handle block (handle /forge/*)" + fi + + # Check reverse_proxy to Forgejo on port 3000 + if echo "$CADDYFILE" | grep -q "reverse_proxy 127.0.0.1:3000"; then + tr_pass "Forgejo reverse_proxy configured (127.0.0.1:3000)" + else + tr_fail "Missing Forgejo reverse_proxy (127.0.0.1:3000)" + fi +} + +check_woodpecker_routing() { + tr_section "Validating Woodpecker routing" + + # Check handle block for /ci/* + if echo "$CADDYFILE" | grep -q "handle /ci/\*"; then + tr_pass "Woodpecker handle block (handle /ci/*)" + else + tr_fail "Missing Woodpecker handle block (handle /ci/*)" + fi + + # Check reverse_proxy to Woodpecker on port 8000 + if echo "$CADDYFILE" | grep -q "reverse_proxy 127.0.0.1:8000"; then + tr_pass "Woodpecker reverse_proxy configured (127.0.0.1:8000)" + else + tr_fail "Missing Woodpecker reverse_proxy (127.0.0.1:8000)" + fi +} + +check_staging_routing() { + tr_section "Validating Staging routing" + + # Check handle block for /staging/* + if echo "$CADDYFILE" | grep -q "handle /staging/\*"; then + tr_pass "Staging handle block (handle /staging/*)" + else + tr_fail "Missing Staging handle block (handle /staging/*)" + fi + + # Check for nomadService discovery (dynamic port) + if echo "$CADDYFILE" | grep -q "nomadService"; then + tr_pass "Staging uses Nomad service discovery" + else + tr_fail "Missing Nomad service discovery for staging" + fi +} + +check_chat_routing() { + tr_section "Validating Chat routing" + + # Check login endpoint + if echo "$CADDYFILE" | grep -q "handle /chat/login"; then + tr_pass "Chat login handle block (handle /chat/login)" + else + tr_fail "Missing Chat login handle block (handle /chat/login)" + fi + + # Check OAuth callback endpoint + if echo "$CADDYFILE" | grep -q "handle /chat/oauth/callback"; then + tr_pass "Chat OAuth callback handle block (handle /chat/oauth/callback)" + else + tr_fail "Missing Chat OAuth callback handle block (handle /chat/oauth/callback)" + fi + + # Check catch-all for /chat/* + if echo "$CADDYFILE" | grep -q "handle /chat/\*"; then + tr_pass "Chat catch-all handle block (handle /chat/*)" + else + tr_fail "Missing Chat catch-all handle block (handle /chat/*)" + fi + + # Check reverse_proxy to Chat on port 8080 + if echo "$CADDYFILE" | grep -q "reverse_proxy 127.0.0.1:8080"; then + tr_pass "Chat reverse_proxy configured (127.0.0.1:8080)" + else + tr_fail "Missing Chat reverse_proxy (127.0.0.1:8080)" + fi + + # Check forward_auth block for /chat/* + if echo "$CADDYFILE" | grep -A10 "handle /chat/\*" | grep -q "forward_auth"; then + tr_pass "forward_auth block configured for /chat/*" + else + tr_fail "Missing forward_auth block for /chat/*" + fi + + # Check forward_auth URI + if echo "$CADDYFILE" | grep -q "uri /chat/auth/verify"; then + tr_pass "forward_auth URI configured (/chat/auth/verify)" + else + tr_fail "Missing forward_auth URI (/chat/auth/verify)" + fi +} + +check_root_redirect() { + tr_section "Validating root redirect" + + # Check root redirect to /forge/ + if echo "$CADDYFILE" | grep -q "redir /forge/ 302"; then + tr_pass "Root redirect to /forge/ configured (302)" + else + tr_fail "Missing root redirect to /forge/" + fi +} + +# ───────────────────────────────────────────────────────────────────────────── +# Main +# ───────────────────────────────────────────────────────────────────────────── + +main() { + tr_info "Extracting Caddyfile template from $EDGE_TEMPLATE" + + # Extract Caddyfile + CADDYFILE=$(extract_caddyfile "$EDGE_TEMPLATE") + + if [ -z "$CADDYFILE" ]; then + tr_fail "Could not extract Caddyfile template" + exit 1 + fi + + tr_pass "Caddyfile template extracted successfully" + + # Run all validation checks + check_forgejo_routing + check_woodpecker_routing + check_staging_routing + check_chat_routing + check_root_redirect + + # Summary + tr_section "Test Summary" + tr_info "Passed: $PASSED" + tr_info "Failed: $FAILED" + + if [ "$FAILED" -gt 0 ]; then + tr_fail "Some checks failed" + exit 1 + fi + + tr_pass "All routing blocks validated!" + exit 0 +} + +main