This commit is contained in:
parent
7c543c9a16
commit
9f71a01b6f
2 changed files with 453 additions and 0 deletions
63
.woodpecker/edge-subpath.yml
Normal file
63
.woodpecker/edge-subpath.yml
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
# .woodpecker/edge-subpath.yml — Edge subpath routing smoke test
|
||||||
|
#
|
||||||
|
# Runs end-to-end smoke tests for Forgejo, Woodpecker, and chat subpath routing:
|
||||||
|
# - Forgejo at /forge/
|
||||||
|
# - Woodpecker at /ci/
|
||||||
|
# - Chat at /chat/
|
||||||
|
# - Staging at /staging/
|
||||||
|
#
|
||||||
|
# Tests:
|
||||||
|
# 1. Root / redirects to /forge/
|
||||||
|
# 2. Forgejo login at /forge/ completes without redirect loops
|
||||||
|
# 3. Forgejo OAuth callback for Woodpecker succeeds under subpath
|
||||||
|
# 4. Woodpecker dashboard loads all assets at /ci/ (no 404s on JS/CSS)
|
||||||
|
# 5. Chat OAuth login flow works at /chat/login
|
||||||
|
# 6. Forward_auth on /chat/* rejects unauthenticated requests with 401
|
||||||
|
# 7. Staging content loads at /staging/
|
||||||
|
#
|
||||||
|
# Triggers:
|
||||||
|
# - Pull requests that modify edge-related files
|
||||||
|
# - Manual trigger for on-demand testing
|
||||||
|
#
|
||||||
|
# Environment variables (set in CI or via pipeline):
|
||||||
|
# EDGE_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)
|
||||||
|
#
|
||||||
|
# When to run:
|
||||||
|
# - Any change to edge.hcl, docker/edge/, tools/edge-control/
|
||||||
|
# - Any change to this pipeline file
|
||||||
|
# - Manual trigger for testing edge deployments
|
||||||
|
|
||||||
|
when:
|
||||||
|
event: [pull_request, manual]
|
||||||
|
path:
|
||||||
|
- "nomad/jobs/edge.hcl"
|
||||||
|
- "docker/edge/**"
|
||||||
|
- "tools/edge-control/**"
|
||||||
|
- ".woodpecker/edge-subpath.yml"
|
||||||
|
- "tests/smoke-edge-subpath.sh"
|
||||||
|
|
||||||
|
# Clone the repository
|
||||||
|
clone:
|
||||||
|
git:
|
||||||
|
image: alpine/git
|
||||||
|
commands:
|
||||||
|
- AUTH_URL=$(printf '%s' "$CI_REPO_CLONE_URL" | sed "s|://|://token:$FORGE_TOKEN@|")
|
||||||
|
- git clone --depth 1 "$AUTH_URL" .
|
||||||
|
- git fetch --depth 1 origin "$CI_COMMIT_REF"
|
||||||
|
- git checkout FETCH_HEAD
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: edge-subpath-smoke-test
|
||||||
|
image: alpine:3.19
|
||||||
|
commands:
|
||||||
|
- apk add --no-cache bash curl jq
|
||||||
|
- bash tests/smoke-edge-subpath.sh
|
||||||
|
environment:
|
||||||
|
BASE_URL:
|
||||||
|
default: "http://localhost"
|
||||||
|
EDGE_TIMEOUT:
|
||||||
|
default: "30"
|
||||||
|
EDGE_MAX_RETRIES:
|
||||||
|
default: "3"
|
||||||
390
tests/smoke-edge-subpath.sh
Executable file
390
tests/smoke-edge-subpath.sh
Executable file
|
|
@ -0,0 +1,390 @@
|
||||||
|
#!/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/
|
||||||
|
#
|
||||||
|
# Acceptance criteria:
|
||||||
|
# 1. Forgejo login at /forge/ completes without redirect loops
|
||||||
|
# 2. Forgejo OAuth callback for Woodpecker succeeds under subpath
|
||||||
|
# 3. Woodpecker dashboard loads all assets at /ci/ (no 404s on JS/CSS)
|
||||||
|
# 4. Chat OAuth login flow works at /chat/login
|
||||||
|
# 5. Forward_auth on /chat/* rejects unauthenticated requests with 401
|
||||||
|
# 6. Staging content loads at /staging/
|
||||||
|
# 7. Root / redirects to /forge/
|
||||||
|
#
|
||||||
|
# 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
|
||||||
|
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, body on stderr
|
||||||
|
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-$$ \
|
||||||
|
"$@" "$url" 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
|
||||||
|
# Usage: http_get <url> [curl_options...]
|
||||||
|
# Returns: HTTP status code
|
||||||
|
http_get() {
|
||||||
|
local url="$1"
|
||||||
|
shift
|
||||||
|
|
||||||
|
http_request "GET" "$url" "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Make a HEAD request (no body)
|
||||||
|
# Usage: http_head <url> [curl_options...]
|
||||||
|
# Returns: HTTP status code
|
||||||
|
http_head() {
|
||||||
|
local url="$1"
|
||||||
|
shift
|
||||||
|
|
||||||
|
http_request "HEAD" "$url" "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Test checkers
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# Check if a URL returns a valid response (2xx or 3xx)
|
||||||
|
# Usage: check_http_status <url> <expected_pattern>
|
||||||
|
check_http_status() {
|
||||||
|
local url="$1"
|
||||||
|
local expected_pattern="$2"
|
||||||
|
local description="$3"
|
||||||
|
|
||||||
|
local status
|
||||||
|
status=$(http_get "$url")
|
||||||
|
|
||||||
|
if echo "$status" | grep -qE "$expected_pattern"; then
|
||||||
|
log_pass "$description: $url → $status"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
log_fail "$description: $url → $status (expected: $expected_pattern)"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check that a URL does NOT redirect in a loop
|
||||||
|
# Usage: check_no_redirect_loop <url> [max_redirects]
|
||||||
|
check_no_redirect_loop() {
|
||||||
|
local url="$1"
|
||||||
|
local max_redirects="${2:-10}"
|
||||||
|
local description="$3"
|
||||||
|
|
||||||
|
# Use curl with max redirects and check the final status
|
||||||
|
local response status follow_location
|
||||||
|
|
||||||
|
response=$(curl -sS -w '\n%{http_code}\n%{redirect_url}' \
|
||||||
|
--max-time "$EDGE_TIMEOUT" \
|
||||||
|
--max-redirs "$max_redirects" \
|
||||||
|
-o /tmp/edge-response-$$ \
|
||||||
|
"$url" 2>&1) || {
|
||||||
|
log_fail "$description: curl failed ($?)"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
status=$(echo "$response" | sed -n '$p')
|
||||||
|
follow_location=$(echo "$response" | sed -n "$((NR-1))p")
|
||||||
|
|
||||||
|
# If we hit max redirects, the last redirect is still in follow_location
|
||||||
|
if [ "$status" = "000" ] && [ -n "$follow_location" ]; then
|
||||||
|
log_fail "$description: possible redirect loop detected (last location: $follow_location)"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check final status is in valid range
|
||||||
|
if echo "$status" | grep -qE '^(2|3)[0-9][0-9]$'; then
|
||||||
|
log_pass "$description: no redirect loop ($status)"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
log_fail "$description: unexpected status $status"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check that specific assets load without 404
|
||||||
|
# Usage: check_assets_no_404 <base_url> <pattern>
|
||||||
|
check_assets_no_404() {
|
||||||
|
local base_url="$1"
|
||||||
|
local _pattern="$2"
|
||||||
|
local description="$3"
|
||||||
|
|
||||||
|
local assets_found=0
|
||||||
|
local assets_404=0
|
||||||
|
|
||||||
|
# Fetch the main page and extract asset URLs
|
||||||
|
local main_page
|
||||||
|
main_page=$(curl -sS --max-time "$EDGE_TIMEOUT" "$base_url" 2>/dev/null) || {
|
||||||
|
log_skip "$description: could not fetch main page"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Extract URLs matching the pattern (e.g., .js, .css files)
|
||||||
|
local assets
|
||||||
|
assets=$(echo "$main_page" | grep -oE 'https?://[^"'"'"']+\.(js|css|woff|woff2|ttf|eot|svg|png|jpg|jpeg|gif|ico)' | sort -u || true)
|
||||||
|
|
||||||
|
if [ -z "$assets" ]; then
|
||||||
|
log_skip "$description: no assets found to check"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
assets_found=$(echo "$assets" | wc -l)
|
||||||
|
|
||||||
|
# Check each asset
|
||||||
|
while IFS= read -r asset; do
|
||||||
|
local status
|
||||||
|
status=$(http_head "$asset")
|
||||||
|
|
||||||
|
if [ "$status" = "404" ]; then
|
||||||
|
log_fail "$description: asset 404: $asset"
|
||||||
|
assets_404=$((assets_404 + 1))
|
||||||
|
fi
|
||||||
|
done <<< "$assets"
|
||||||
|
|
||||||
|
if [ $assets_404 -eq 0 ]; then
|
||||||
|
log_pass "$description: all $assets_found assets loaded (0 404s)"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
log_fail "$description: $assets_404/$assets_found assets returned 404"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check that a path returns 401 (unauthorized)
|
||||||
|
# Usage: check_returns_401 <url> <description>
|
||||||
|
check_returns_401() {
|
||||||
|
local url="$1"
|
||||||
|
local description="$2"
|
||||||
|
|
||||||
|
local status
|
||||||
|
status=$(http_get "$url")
|
||||||
|
|
||||||
|
if [ "$status" = "401" ]; then
|
||||||
|
log_pass "$description: $url → 401 (as expected)"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
log_fail "$description: $url → $status (expected 401)"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check that a path returns 302 redirect to expected location
|
||||||
|
# Usage: check_redirects_to <url> <expected_target> <description>
|
||||||
|
check_redirects_to() {
|
||||||
|
local url="$1"
|
||||||
|
local expected_target="$2"
|
||||||
|
local description="$3"
|
||||||
|
|
||||||
|
local response status location
|
||||||
|
|
||||||
|
response=$(curl -sS -w '\n%{http_code}\n%{redirect_url}' \
|
||||||
|
--max-time "$EDGE_TIMEOUT" \
|
||||||
|
--max-redirs 1 \
|
||||||
|
-o /tmp/edge-response-$$ \
|
||||||
|
"$url" 2>&1) || {
|
||||||
|
log_fail "$description: curl failed"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
status=$(echo "$response" | sed -n '$p')
|
||||||
|
location=$(echo "$response" | sed -n "$((NR-1))p")
|
||||||
|
|
||||||
|
if [ "$status" = "302" ] && echo "$location" | grep -qF "$expected_target"; then
|
||||||
|
log_pass "$description: redirects to $location"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
log_fail "$description: status=$status, location=$location (expected 302 → $expected_target)"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Main test suite
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
main() {
|
||||||
|
log_section "Edge Subpath Routing Smoke Test"
|
||||||
|
log_info "Base URL: $BASE_URL"
|
||||||
|
log_info "Timeout: ${EDGE_TIMEOUT}s, Max retries: $EDGE_MAX_RETRIES"
|
||||||
|
|
||||||
|
# ─── Test 1: Root redirects to /forge/ ──────────────────────────────────
|
||||||
|
log_section "Test 1: Root redirects to /forge/"
|
||||||
|
|
||||||
|
check_redirects_to "$BASE_URL" "$FORGE_PATH" "Root redirect" || FAILED=1
|
||||||
|
if [ "$FAILED" -eq 0 ]; then ((PASSED++)) || true; fi
|
||||||
|
|
||||||
|
# ─── Test 2: Forgejo login at /forge/ without redirect loops ────────────
|
||||||
|
log_section "Test 2: Forgejo login at /forge/"
|
||||||
|
|
||||||
|
check_no_redirect_loop "$BASE_URL$FORGE_PATH" 10 "Forgejo root" || FAILED=1
|
||||||
|
check_http_status "$BASE_URL$FORGE_PATH" "^(2|3)[0-9][0-9]$" "Forgejo root status" || FAILED=1
|
||||||
|
if [ "$FAILED" -eq 0 ]; then ((PASSED++)) || true; fi
|
||||||
|
|
||||||
|
# ─── Test 3: Forgejo OAuth callback at /forge/_oauth/callback ───────────
|
||||||
|
log_section "Test 3: Forgejo OAuth callback at /forge/_oauth/callback"
|
||||||
|
|
||||||
|
check_http_status "$BASE_URL/forge/_oauth/callback" "^(2|3|4|5)[0-9][0-9]$" "Forgejo OAuth callback" || FAILED=1
|
||||||
|
if [ "$FAILED" -eq 0 ]; then ((PASSED++)) || true; fi
|
||||||
|
|
||||||
|
# ─── Test 4: Woodpecker dashboard at /ci/ ───────────────────────────────
|
||||||
|
log_section "Test 4: Woodpecker dashboard at /ci/"
|
||||||
|
|
||||||
|
check_no_redirect_loop "$BASE_URL$CI_PATH" 10 "Woodpecker root" || FAILED=1
|
||||||
|
check_http_status "$BASE_URL$CI_PATH" "^(2|3)[0-9][0-9]$" "Woodpecker root status" || FAILED=1
|
||||||
|
check_assets_no_404 "$BASE_URL$CI_PATH" "\.(js|css)" "Woodpecker assets" || FAILED=1
|
||||||
|
if [ "$FAILED" -eq 0 ]; then ((PASSED++)) || true; fi
|
||||||
|
|
||||||
|
# ─── Test 5: Chat OAuth login at /chat/login ────────────────────────────
|
||||||
|
log_section "Test 5: Chat OAuth login at /chat/login"
|
||||||
|
|
||||||
|
check_http_status "$BASE_URL$CHAT_PATH/login" "^(2|3)[0-9][0-9]$" "Chat login page" || FAILED=1
|
||||||
|
if [ "$FAILED" -eq 0 ]; then ((PASSED++)) || true; fi
|
||||||
|
|
||||||
|
# ─── Test 6: Chat OAuth callback at /chat/oauth/callback ────────────────
|
||||||
|
log_section "Test 6: Chat OAuth callback at /chat/oauth/callback"
|
||||||
|
|
||||||
|
check_http_status "$BASE_URL/chat/oauth/callback" "^(2|3)[0-9][0-9]$" "Chat OAuth callback" || FAILED=1
|
||||||
|
if [ "$FAILED" -eq 0 ]; then ((PASSED++)) || true; fi
|
||||||
|
|
||||||
|
# ─── Test 7: Forward_auth on /chat/* returns 401 for unauthenticated ────
|
||||||
|
log_section "Test 7: Forward_auth on /chat/* returns 401"
|
||||||
|
|
||||||
|
# Test a protected chat endpoint (chat dashboard)
|
||||||
|
check_returns_401 "$BASE_URL$CHAT_PATH/" "Chat root (unauthenticated)" || FAILED=1
|
||||||
|
check_returns_401 "$BASE_URL$CHAT_PATH/dashboard" "Chat dashboard (unauthenticated)" || FAILED=1
|
||||||
|
if [ "$FAILED" -eq 0 ]; then ((PASSED++)) || true; fi
|
||||||
|
|
||||||
|
# ─── Test 8: Staging at /staging/ ───────────────────────────────────────
|
||||||
|
log_section "Test 8: Staging at /staging/"
|
||||||
|
|
||||||
|
check_http_status "$BASE_URL$STAGING_PATH" "^(2|3)[0-9][0-9]$" "Staging root" || FAILED=1
|
||||||
|
if [ "$FAILED" -eq 0 ]; then ((PASSED++)) || true; fi
|
||||||
|
|
||||||
|
# ─── Test 9: Caddy admin API health ─────────────────────────────────────
|
||||||
|
log_section "Test 9: Caddy admin API health"
|
||||||
|
|
||||||
|
# Caddy admin API is typically on port 2019 locally
|
||||||
|
if curl -sS --max-time 5 "http://127.0.0.1:2019/" >/dev/null 2>&1; then
|
||||||
|
log_pass "Caddy admin API reachable"
|
||||||
|
((PASSED++))
|
||||||
|
else
|
||||||
|
log_skip "Caddy admin API not reachable (expected if edge is remote)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ─── Summary ────────────────────────────────────────────────────────────
|
||||||
|
log_section "Test Summary"
|
||||||
|
log_info "Passed: $PASSED"
|
||||||
|
log_info "Failed: $FAILED"
|
||||||
|
log_info "Skipped: $SKIPPED"
|
||||||
|
|
||||||
|
if [ $FAILED -gt 0 ]; then
|
||||||
|
log_section "TEST FAILED"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_section "TEST PASSED"
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Run main
|
||||||
|
main "$@"
|
||||||
Loading…
Add table
Add a link
Reference in a new issue