fix: vision(#623): end-to-end subpath routing smoke test for Forgejo + Woodpecker + chat (#1025)

This commit is contained in:
dev-qwen2 2026-04-19 20:22:37 +00:00
parent 449611e6df
commit 5b46acb0b9
3 changed files with 873 additions and 0 deletions

310
tests/smoke-edge-subpath.sh Executable file
View file

@ -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 <method> <url> [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
http_request "GET" "$url" "$@"
}
# Make a HEAD request (no body)
http_head() {
local url="$1"
shift
http_request "HEAD" "$url" "$@"
}
# Make a GET request and return the response body
http_get_body() {
local url="$1"
shift
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

231
tests/test-caddyfile-routing.sh Executable file
View file

@ -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
# ─────────────────────────────────────────────────────────────────────────────
log_info() {
echo "[INFO] $*"
}
log_pass() {
echo "[PASS] $*"
((PASSED++)) || true
}
log_fail() {
echo "[FAIL] $*"
((FAILED++)) || true
}
log_section() {
echo ""
echo "=== $* ==="
echo ""
}
# ─────────────────────────────────────────────────────────────────────────────
# Caddyfile extraction
# ─────────────────────────────────────────────────────────────────────────────
extract_caddyfile() {
local template_file="$1"
# Extract the Caddyfile template (content between <<EOT and EOT markers
# within the template stanza)
local caddyfile
caddyfile=$(sed -n '/data[[:space:]]*=[[:space:]]*<<[Ee][Oo][Tt]/,/^EOT$/p' "$template_file" | sed '1s/.*/# Caddyfile extracted from Nomad template/; $d')
if [ -z "$caddyfile" ]; then
echo "ERROR: Could not extract Caddyfile template from $template_file" >&2
return 1
fi
echo "$caddyfile"
}
# ─────────────────────────────────────────────────────────────────────────────
# Validation functions
# ─────────────────────────────────────────────────────────────────────────────
check_forgejo_routing() {
log_section "Validating Forgejo routing"
# Check handle block for /forge/*
if echo "$CADDYFILE" | grep -q "handle /forge/\*"; then
log_pass "Forgejo handle block (handle /forge/*)"
else
log_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
log_pass "Forgejo reverse_proxy configured (127.0.0.1:3000)"
else
log_fail "Missing Forgejo reverse_proxy (127.0.0.1:3000)"
fi
}
check_woodpecker_routing() {
log_section "Validating Woodpecker routing"
# Check handle block for /ci/*
if echo "$CADDYFILE" | grep -q "handle /ci/\*"; then
log_pass "Woodpecker handle block (handle /ci/*)"
else
log_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
log_pass "Woodpecker reverse_proxy configured (127.0.0.1:8000)"
else
log_fail "Missing Woodpecker reverse_proxy (127.0.0.1:8000)"
fi
}
check_staging_routing() {
log_section "Validating Staging routing"
# Check handle block for /staging/*
if echo "$CADDYFILE" | grep -q "handle /staging/\*"; then
log_pass "Staging handle block (handle /staging/*)"
else
log_fail "Missing Staging handle block (handle /staging/*)"
fi
# Check for nomadService discovery (dynamic port)
if echo "$CADDYFILE" | grep -q "nomadService"; then
log_pass "Staging uses Nomad service discovery"
else
log_fail "Missing Nomad service discovery for staging"
fi
}
check_chat_routing() {
log_section "Validating Chat routing"
# Check login endpoint
if echo "$CADDYFILE" | grep -q "handle /chat/login"; then
log_pass "Chat login handle block (handle /chat/login)"
else
log_fail "Missing Chat login handle block (handle /chat/login)"
fi
# Check OAuth callback endpoint
if echo "$CADDYFILE" | grep -q "handle /chat/oauth/callback"; then
log_pass "Chat OAuth callback handle block (handle /chat/oauth/callback)"
else
log_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
log_pass "Chat catch-all handle block (handle /chat/*)"
else
log_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
log_pass "Chat reverse_proxy configured (127.0.0.1:8080)"
else
log_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
log_pass "forward_auth block configured for /chat/*"
else
log_fail "Missing forward_auth block for /chat/*"
fi
# Check forward_auth URI
if echo "$CADDYFILE" | grep -q "uri /chat/auth/verify"; then
log_pass "forward_auth URI configured (/chat/auth/verify)"
else
log_fail "Missing forward_auth URI (/chat/auth/verify)"
fi
}
check_root_redirect() {
log_section "Validating root redirect"
# Check root redirect to /forge/
if echo "$CADDYFILE" | grep -q "redir /forge/ 302"; then
log_pass "Root redirect to /forge/ configured (302)"
else
log_fail "Missing root redirect to /forge/"
fi
}
# ─────────────────────────────────────────────────────────────────────────────
# Main
# ─────────────────────────────────────────────────────────────────────────────
main() {
log_info "Extracting Caddyfile template from $EDGE_TEMPLATE"
# Extract Caddyfile
CADDYFILE=$(extract_caddyfile "$EDGE_TEMPLATE")
if [ -z "$CADDYFILE" ]; then
log_fail "Could not extract Caddyfile template"
exit 1
fi
log_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
log_section "Test Summary"
log_info "Passed: $PASSED"
log_info "Failed: $FAILED"
if [ "$FAILED" -gt 0 ]; then
log_fail "Some checks failed"
exit 1
fi
log_pass "All routing blocks validated!"
exit 0
}
main