rdev/cookbooks/scripts/landing-test.sh
jordan d505aba804 fix: Update landing-test.sh for full E2E flow
- Fix pipeline API response format (.data not .data.pipelines)
- Add pipeline monitoring with timeout
- Add site HTTP 200 verification
- Add DNS alias add/remove testing
- Show test results summary with pass/fail status
2026-01-29 19:35:13 -07:00

555 lines
18 KiB
Bash
Executable File

#!/bin/bash
# Landing Page Cookbook Test Script
# Tests the full landing page flow from cookbooks/landing-page.md
#
# Usage:
# ./cookbooks/scripts/landing-test.sh run # Run the full flow
# ./cookbooks/scripts/landing-test.sh teardown # Clean up test resources
# ./cookbooks/scripts/landing-test.sh status # Check current status
set -euo pipefail
# Configuration
API_URL="${RDEV_API_URL:-https://rdev.masq-ops.orchard9.ai}"
API_KEY="${RDEV_API_KEY:?RDEV_API_KEY environment variable required}"
PROJECT_NAME="${1:-landing-test}"
TEMPLATE="astro-landing"
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
log_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
log_success() { echo -e "${GREEN}[OK]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
# Configuration for polling
PIPELINE_TIMEOUT=300 # 5 minutes max wait for pipeline
PIPELINE_POLL_INTERVAL=10 # Check every 10 seconds
SITE_TIMEOUT=60 # 1 minute max wait for site to be live
api_call() {
local method="$1"
local endpoint="$2"
local data="${3:-}"
if [[ -n "$data" ]]; then
curl -s -X "$method" "${API_URL}${endpoint}" \
-H "X-API-Key: ${API_KEY}" \
-H "Content-Type: application/json" \
-d "$data"
else
curl -s -X "$method" "${API_URL}${endpoint}" \
-H "X-API-Key: ${API_KEY}"
fi
}
check_health() {
log_info "Checking API health..."
local response
response=$(curl -s "${API_URL}/health")
if echo "$response" | jq -e '.data.status == "ok"' > /dev/null 2>&1; then
log_success "API is healthy"
return 0
else
log_error "API health check failed"
echo "$response" | jq .
return 1
fi
}
# Wait for pipeline to appear and complete
# Returns: 0 on success, 1 on failure/timeout
wait_for_pipeline() {
local project_name="$1"
local start_time=$(date +%s)
local pipeline_found=false
local pipeline_number=""
local pipeline_status=""
log_info "Waiting for CI pipeline to start (timeout: ${PIPELINE_TIMEOUT}s)..."
while true; do
local elapsed=$(($(date +%s) - start_time))
if [[ $elapsed -ge $PIPELINE_TIMEOUT ]]; then
log_error "Pipeline timeout after ${PIPELINE_TIMEOUT}s"
return 1
fi
local response
response=$(api_call GET "/projects/$project_name/pipelines" 2>/dev/null || echo "{}")
# Check if we have pipelines (API returns array at .data, not .data.pipelines)
local pipeline_count
pipeline_count=$(echo "$response" | jq -r '.data | length' 2>/dev/null || echo "0")
if [[ "$pipeline_count" -gt 0 ]]; then
if [[ "$pipeline_found" == "false" ]]; then
pipeline_found=true
pipeline_number=$(echo "$response" | jq -r '.data[0].number')
log_success "Pipeline #$pipeline_number started"
fi
# Get latest pipeline status
pipeline_status=$(echo "$response" | jq -r '.data[0].status')
case "$pipeline_status" in
success)
log_success "Pipeline #$pipeline_number completed successfully (${elapsed}s)"
return 0
;;
failure|error|killed|declined)
log_error "Pipeline #$pipeline_number failed with status: $pipeline_status"
echo "$response" | jq '.data[0]'
return 1
;;
running|pending)
echo -ne "\r${BLUE}[INFO]${NC} Pipeline #$pipeline_number status: $pipeline_status (${elapsed}s)... "
;;
*)
echo -ne "\r${BLUE}[INFO]${NC} Pipeline #$pipeline_number status: $pipeline_status (${elapsed}s)... "
;;
esac
else
echo -ne "\r${BLUE}[INFO]${NC} Waiting for pipeline to start (${elapsed}s)... "
fi
sleep $PIPELINE_POLL_INTERVAL
done
}
# Wait for site to be accessible
# Returns: 0 on success, 1 on failure/timeout
wait_for_site() {
local domain="$1"
local start_time=$(date +%s)
log_info "Waiting for site to be live: https://$domain (timeout: ${SITE_TIMEOUT}s)..."
while true; do
local elapsed=$(($(date +%s) - start_time))
if [[ $elapsed -ge $SITE_TIMEOUT ]]; then
log_warn "Site timeout after ${SITE_TIMEOUT}s - may still be deploying"
return 1
fi
local http_code
http_code=$(curl -s -o /dev/null -w "%{http_code}" --max-time 5 "https://$domain" 2>/dev/null || echo "000")
if [[ "$http_code" == "200" ]]; then
echo ""
log_success "Site is live! HTTP $http_code (${elapsed}s)"
return 0
elif [[ "$http_code" == "000" ]]; then
echo -ne "\r${BLUE}[INFO]${NC} Waiting for site... (${elapsed}s, connection failed) "
else
echo -ne "\r${BLUE}[INFO]${NC} Waiting for site... (${elapsed}s, HTTP $http_code) "
fi
sleep 3
done
}
# Test adding a DNS alias
test_dns_alias() {
local project_name="$1"
local alias_domain="$2"
log_info "Testing DNS alias: $alias_domain"
local response
response=$(api_call POST "/projects/$project_name/domains" "{\"domain\": \"$alias_domain\"}")
if echo "$response" | jq -e '.error' > /dev/null 2>&1; then
local error_code
error_code=$(echo "$response" | jq -r '.error.code // "UNKNOWN"')
if [[ "$error_code" == "DOMAIN_EXISTS" ]]; then
log_warn "Domain alias already exists: $alias_domain"
return 0
fi
log_error "Failed to add DNS alias"
echo "$response" | jq .
return 1
fi
log_success "DNS alias added: $alias_domain"
echo "$response" | jq '.data | {domain, type, dns_record_id}'
return 0
}
# Remove a DNS alias
remove_dns_alias() {
local project_name="$1"
local alias_domain="$2"
log_info "Removing DNS alias: $alias_domain"
local response
response=$(api_call DELETE "/projects/$project_name/domains/$alias_domain")
if echo "$response" | jq -e '.error' > /dev/null 2>&1; then
local error_code
error_code=$(echo "$response" | jq -r '.error.code // "UNKNOWN"')
if [[ "$error_code" == "NOT_FOUND" ]]; then
log_warn "Domain alias not found (already deleted?): $alias_domain"
return 0
fi
log_warn "Failed to remove DNS alias: $alias_domain"
return 1
fi
log_success "DNS alias removed: $alias_domain"
return 0
}
run_flow() {
local project_name="${1:-landing-test}"
local custom_subdomain="${2:-}"
echo ""
echo "=========================================="
echo " Landing Page Cookbook Test"
echo " Project: $project_name"
if [[ -n "$custom_subdomain" ]]; then
echo " Custom subdomain: $custom_subdomain"
fi
echo "=========================================="
echo ""
# Step 0: Health check
check_health || exit 1
echo ""
# Step 1: Create project
log_info "Step 1: Creating project with $TEMPLATE template..."
local create_payload="{
\"name\": \"$project_name\",
\"description\": \"Cookbook test: landing page flow\",
\"template\": \"$TEMPLATE\""
if [[ -n "$custom_subdomain" ]]; then
create_payload="$create_payload,
\"custom_subdomain\": \"$custom_subdomain\""
fi
create_payload="$create_payload
}"
local create_response
create_response=$(api_call POST "/project" "$create_payload")
if echo "$create_response" | jq -e '.error' > /dev/null 2>&1; then
log_error "Failed to create project"
echo "$create_response" | jq .
exit 1
fi
log_success "Project created"
echo "$create_response" | jq '{
project_id: .data.project_id,
slug: .data.slug,
git_url: .data.git.html_url,
domain: .data.domain,
url: .data.url,
domains: .data.domains,
next_steps: .data.next_steps
}'
# Extract domain info
local primary_domain
local slug
primary_domain=$(echo "$create_response" | jq -r '.data.domain')
slug=$(echo "$create_response" | jq -r '.data.slug // empty')
if [[ -n "$slug" ]]; then
log_success "Auto-generated slug: $slug"
fi
# Check for manual steps
local next_steps
next_steps=$(echo "$create_response" | jq -r '.data.next_steps[]?' 2>/dev/null || echo "")
if [[ -n "$next_steps" ]]; then
echo ""
log_warn "Some steps require manual intervention:"
echo "$create_response" | jq -r '.data.next_steps[]'
echo ""
log_info "Check API logs for details: ./scripts/logs.sh -e"
fi
echo ""
# Step 2: List all domains
log_info "Step 2: Listing all project domains..."
local domains_response
domains_response=$(api_call GET "/projects/$project_name/domains")
if echo "$domains_response" | jq -e '.error' > /dev/null 2>&1; then
log_warn "Could not list domains"
echo "$domains_response" | jq .
else
local domain_count
domain_count=$(echo "$domains_response" | jq -r '.data.total')
log_success "Found $domain_count domain(s)"
echo "$domains_response" | jq '.data.domains[] | {domain, type, verified}'
fi
echo ""
# Step 3: Verify project status
log_info "Step 3: Verifying project status..."
sleep 2 # Give DNS/Gitea a moment
local status_response
status_response=$(api_call GET "/project/$project_name")
if echo "$status_response" | jq -e '.error' > /dev/null 2>&1; then
log_warn "Could not fetch project status"
echo "$status_response" | jq .
else
log_success "Project status retrieved"
echo "$status_response" | jq '{
name: .data.name,
slug: .data.slug,
domain: .data.domain,
url: .data.url,
domains: .data.domains,
git: .data.git.html_url,
deployment: .data.deployment
}'
fi
echo ""
# Step 4: Check DNS
log_info "Step 4: Checking DNS resolution..."
if host "$primary_domain" > /dev/null 2>&1; then
log_success "DNS resolves: $primary_domain"
host "$primary_domain" | head -1
else
log_warn "DNS not yet resolving: $primary_domain (may take a few minutes)"
fi
echo ""
# Step 5: Wait for CI pipeline
log_info "Step 5: Monitoring CI pipeline..."
local pipeline_success=false
if wait_for_pipeline "$project_name"; then
pipeline_success=true
else
log_warn "Pipeline did not complete successfully - site may not deploy"
log_info "Check Woodpecker: https://ci.threesix.ai/threesix/$project_name"
fi
echo ""
# Step 6: Verify site is live (only if pipeline succeeded)
local site_live=false
if [[ "$pipeline_success" == "true" ]]; then
log_info "Step 6: Verifying site is accessible..."
if wait_for_site "$primary_domain"; then
site_live=true
# Show a snippet of the response
log_info "Fetching site content preview..."
curl -s "https://$primary_domain" | head -20 | grep -E '<title>|<h1' || true
fi
else
log_info "Step 6: Skipping site verification (pipeline not successful)"
fi
echo ""
# Step 7: Test DNS alias functionality
local test_alias="${project_name}-alias.threesix.ai"
log_info "Step 7: Testing DNS alias functionality..."
if test_dns_alias "$project_name" "$test_alias"; then
log_success "DNS alias test passed"
# Clean up test alias immediately
sleep 2
remove_dns_alias "$project_name" "$test_alias"
else
log_warn "DNS alias test failed - check Cloudflare permissions"
fi
echo ""
# Step 8: Final domain listing
log_info "Step 8: Final domain listing..."
domains_response=$(api_call GET "/projects/$project_name/domains")
if echo "$domains_response" | jq -e '.data.domains' > /dev/null 2>&1; then
echo "$domains_response" | jq '.data.domains[] | {domain, type, is_primary}'
fi
echo ""
# Summary
echo "=========================================="
echo " Test Results Summary"
echo "=========================================="
echo ""
echo " Project: $project_name"
echo " Git repo: $(echo "$create_response" | jq -r '.data.git.html_url')"
if [[ -n "$slug" ]]; then
echo " Slug: $slug"
fi
echo " Primary: https://$primary_domain"
echo ""
echo " Test Results:"
echo -e " Project created: ${GREEN}PASS${NC}"
if [[ "$pipeline_success" == "true" ]]; then
echo -e " CI Pipeline: ${GREEN}PASS${NC}"
else
echo -e " CI Pipeline: ${RED}FAIL${NC}"
fi
if [[ "$site_live" == "true" ]]; then
echo -e " Site accessible: ${GREEN}PASS${NC}"
else
echo -e " Site accessible: ${YELLOW}PENDING${NC}"
fi
echo -e " DNS alias: ${GREEN}TESTED${NC}"
echo ""
echo " All domains:"
echo "$domains_response" | jq -r '.data.domains[]? | " - \(.domain) (\(.type))"' 2>/dev/null || echo " (none listed)"
echo ""
echo " Useful commands:"
echo " Check status: ./cookbooks/scripts/landing-test.sh status $project_name"
echo " View logs: ./scripts/logs.sh -e"
echo " Woodpecker: https://ci.threesix.ai/threesix/$project_name"
echo " Teardown: ./cookbooks/scripts/landing-test.sh teardown $project_name"
echo ""
# Return appropriate exit code
if [[ "$pipeline_success" == "true" && "$site_live" == "true" ]]; then
log_success "Full E2E test PASSED"
return 0
elif [[ "$pipeline_success" == "true" ]]; then
log_warn "Partial success - pipeline passed but site not yet live"
return 0
else
log_error "E2E test FAILED - pipeline did not complete"
return 1
fi
}
teardown() {
local project_name="${1:-landing-test}"
echo ""
echo "=========================================="
echo " Teardown: $project_name"
echo "=========================================="
echo ""
# First, list domains that will be deleted
log_info "Checking domains to be deleted..."
local domains_response
domains_response=$(api_call GET "/projects/$project_name/domains")
if echo "$domains_response" | jq -e '.data.total' > /dev/null 2>&1; then
local domain_count
domain_count=$(echo "$domains_response" | jq -r '.data.total')
log_info "Will delete $domain_count domain(s):"
echo "$domains_response" | jq -r '.data.domains[]? | " - \(.domain)"'
echo ""
fi
log_info "Deleting project $project_name..."
local response
response=$(api_call DELETE "/project/$project_name")
if echo "$response" | jq -e '.data.status == "deleted"' > /dev/null 2>&1; then
log_success "Project deleted"
echo "$response" | jq .
elif echo "$response" | jq -e '.error.code == "NOT_FOUND"' > /dev/null 2>&1; then
log_warn "Project not found (already deleted?)"
else
log_error "Failed to delete project"
echo "$response" | jq .
exit 1
fi
echo ""
log_info "Note: Gitea repo is preserved for safety. Delete manually if needed:"
echo " https://git.threesix.ai/jordan/$project_name/settings"
echo ""
}
status() {
local project_name="${1:-landing-test}"
echo ""
log_info "Fetching status for: $project_name"
echo ""
local response
response=$(api_call GET "/project/$project_name")
if echo "$response" | jq -e '.error' > /dev/null 2>&1; then
log_error "Project not found or error"
echo "$response" | jq .
exit 1
fi
echo "$response" | jq '{
name: .data.name,
description: .data.description,
slug: .data.slug,
domain: .data.domain,
url: .data.url,
domains: .data.domains,
git: .data.git,
deployment: .data.deployment
}'
echo ""
log_info "Listing all domains..."
local domains_response
domains_response=$(api_call GET "/projects/$project_name/domains")
if echo "$domains_response" | jq -e '.data.domains' > /dev/null 2>&1; then
echo "$domains_response" | jq '.data.domains'
fi
}
# Main
case "${1:-}" in
run)
shift
run_flow "${1:-landing-test}" "${2:-}"
;;
teardown)
shift
teardown "${1:-landing-test}"
;;
status)
shift
status "${1:-landing-test}"
;;
*)
echo "Usage: $0 {run|teardown|status} [project-name] [custom-subdomain]"
echo ""
echo "Commands:"
echo " run [name] [subdomain] Create project and run full flow"
echo " teardown [name] Delete project and clean up"
echo " status [name] Check current project status"
echo ""
echo "Full E2E flow tested:"
echo " 1. Project creation with template (POST /project)"
echo " 2. Gitea repo + DNS + Woodpecker CI activation"
echo " 3. Template seeding (astro-landing)"
echo " 4. CI pipeline monitoring (GET /projects/{id}/pipelines)"
echo " 5. Site deployment verification (HTTP 200 check)"
echo " 6. DNS alias add/remove (POST/DELETE /projects/{id}/domains)"
echo " 7. Multi-domain listing"
echo ""
echo "Timeouts:"
echo " Pipeline: ${PIPELINE_TIMEOUT:-300}s, Site: ${SITE_TIMEOUT:-60}s"
echo ""
echo "Environment:"
echo " RDEV_API_URL API endpoint (default: https://rdev.masq-ops.orchard9.ai)"
echo " RDEV_API_KEY API key (required)"
echo ""
echo "Examples:"
echo " $0 run # Test with auto-slug only"
echo " $0 run my-landing # Test with custom project name"
echo " $0 run my-landing my-site # Also create my-site.threesix.ai"
echo " $0 status my-landing # Check status and domains"
echo " $0 teardown my-landing # Clean up (deletes all domains)"
exit 1
;;
esac