#!/usr/bin/env bash # ============================================================================= # Changemaker Lite — Deployment Test Suite # # Verifies that all core services are running and responding correctly. # Run after install or upgrade to validate the deployment. # # Usage: # bash scripts/test-deployment.sh [OPTIONS] # # Options: # --wait SECS Wait for services before testing (default: 0) # --domain DOM Test tunnel connectivity for this domain # --quiet Only show failures and summary # --help Show this help # ============================================================================= set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" WAIT_SECS=0 TEST_DOMAIN="" QUIET=false # Colors if [[ -t 1 ]] && [[ -z "${NO_COLOR:-}" ]]; then RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[0;33m' BOLD='\033[1m' NC='\033[0m' else RED='' GREEN='' YELLOW='' BOLD='' NC='' fi while [[ $# -gt 0 ]]; do case "$1" in --wait) WAIT_SECS="$2"; shift 2 ;; --domain) TEST_DOMAIN="$2"; shift 2 ;; --quiet) QUIET=true; shift ;; --help|-h) sed -n '2,16p' "$0" | grep '^#' | sed 's/^# \?//' exit 0 ;; *) shift ;; esac done PASS=0 FAIL=0 WARN=0 SKIP=0 pass() { PASS=$((PASS+1)); [[ "$QUIET" == "false" ]] && echo -e " ${GREEN}PASS${NC} $1"; } fail() { FAIL=$((FAIL+1)); echo -e " ${RED}FAIL${NC} $1"; } warn() { WARN=$((WARN+1)); [[ "$QUIET" == "false" ]] && echo -e " ${YELLOW}WARN${NC} $1"; } skip() { SKIP=$((SKIP+1)); [[ "$QUIET" == "false" ]] && echo -e " ${YELLOW}SKIP${NC} $1"; } # Load .env if [[ -f "$SCRIPT_DIR/.env" ]]; then source <(grep -E '^(API_PORT|ADMIN_PORT|MEDIA_API_PORT|DOMAIN|ENABLE_|INITIAL_ADMIN_EMAIL)=' "$SCRIPT_DIR/.env" 2>/dev/null || true) fi API_PORT="${API_PORT:-4000}" ADMIN_PORT="${ADMIN_PORT:-3000}" MEDIA_API_PORT="${MEDIA_API_PORT:-4100}" DOMAIN="${DOMAIN:-cmlite.org}" [[ -n "$TEST_DOMAIN" ]] && DOMAIN="$TEST_DOMAIN" cd "$SCRIPT_DIR" echo -e "${BOLD}Changemaker Lite — Deployment Test${NC}" echo "" # Wait if requested if [[ "$WAIT_SECS" -gt 0 ]]; then echo "Waiting ${WAIT_SECS}s for services to start..." sleep "$WAIT_SECS" fi # ─── Core Health ───────────────────────────────────────────────────────── echo -e "${BOLD}Core Health${NC}" HEALTH=$(curl -sf "http://localhost:${API_PORT}/api/health" 2>/dev/null || echo "") if echo "$HEALTH" | grep -q '"healthy"'; then pass "API health" else fail "API health (${HEALTH:-no response})" fi MEDIA=$(curl -sf "http://localhost:${MEDIA_API_PORT}/health" 2>/dev/null || echo "") if echo "$MEDIA" | grep -q '"ok"'; then pass "Media API health" else fail "Media API health" fi ADMIN_CODE=$(curl -sf -o /dev/null -w "%{http_code}" "http://localhost:${ADMIN_PORT}/" 2>/dev/null || echo "0") if [[ "$ADMIN_CODE" == "200" ]]; then pass "Admin GUI (HTTP $ADMIN_CODE)" else fail "Admin GUI (HTTP $ADMIN_CODE)" fi # ─── Authentication ────────────────────────────────────────────────────── echo "" echo -e "${BOLD}Authentication${NC}" # Get admin email from .env ADMIN_EMAIL="${INITIAL_ADMIN_EMAIL:-admin@${DOMAIN}}" # Try to login — write payload to file to avoid ! escaping LOGIN_FILE=$(mktemp) # Read password from .env directly (avoid bash expansion issues with !) ADMIN_PW=$(grep "^INITIAL_ADMIN_PASSWORD=" "$SCRIPT_DIR/.env" 2>/dev/null | head -1 | cut -d= -f2-) printf '{"email":"%s","password":"%s"}' "$ADMIN_EMAIL" "$ADMIN_PW" > "$LOGIN_FILE" TOKEN=$(curl -sf -X POST "http://localhost:${API_PORT}/api/auth/login" \ -H "Content-Type: application/json" -d "@$LOGIN_FILE" 2>/dev/null \ | python3 -c "import sys,json; print(json.load(sys.stdin).get('accessToken',''))" 2>/dev/null || echo "") rm -f "$LOGIN_FILE" if [[ -n "$TOKEN" ]]; then pass "Admin login" else fail "Admin login ($ADMIN_EMAIL)" fi # ─── Authenticated Endpoints ───────────────────────────────────────────── echo "" echo -e "${BOLD}Authenticated API${NC}" if [[ -n "$TOKEN" ]]; then AUTH="Authorization: Bearer $TOKEN" for ep in "/api/settings" "/api/users" "/api/dashboard/summary" "/api/services/status"; do CODE=$(curl -sf -o /dev/null -w "%{http_code}" "http://localhost:${API_PORT}${ep}" -H "$AUTH" 2>/dev/null || echo "0") if [[ "$CODE" == "200" ]]; then pass "GET $ep" else fail "GET $ep (HTTP $CODE)" fi done else skip "Authenticated endpoints (no token)" fi # ─── Public Endpoints ──────────────────────────────────────────────────── echo "" echo -e "${BOLD}Public API${NC}" for ep in "/api/campaigns/public" "/api/map/shifts/public" "/api/pages/listed" \ "/api/donation-pages" "/api/homepage" "/api/qr?text=test" \ "/api/petitions/public" "/api/payments/plans" "/api/payments/products" \ "/api/map/cuts/public"; do CODE=$(curl -sf -o /dev/null -w "%{http_code}" "http://localhost:${API_PORT}${ep}" 2>/dev/null || echo "0") if [[ "$CODE" == "200" ]]; then pass "GET $ep" else fail "GET $ep (HTTP $CODE)" fi done # ─── Supporting Services ───────────────────────────────────────────────── echo "" echo -e "${BOLD}Supporting Services${NC}" declare -A SERVICES=( [gitea]=3030 [nocodb]=8091 [listmonk]=9001 [n8n]=5678 [homepage]=3010 [excalidraw]=8090 [vaultwarden]=8445 [mailhog]=8025 [mkdocs-dev]=4003 [mkdocs-static]=4004 [mini-qr]=8089 ) for svc in "${!SERVICES[@]}"; do port=${SERVICES[$svc]} CODE=$(curl -sf -o /dev/null -w "%{http_code}" --max-time 5 "http://localhost:${port}/" 2>/dev/null || echo "0") if [[ "$CODE" == "200" || "$CODE" == "302" ]]; then pass "$svc (:$port)" else fail "$svc (:$port) (HTTP $CODE)" fi done # ─── Nginx Proxy ───────────────────────────────────────────────────────── echo "" echo -e "${BOLD}Nginx Proxy${NC}" NGINX_HEALTH=$(docker compose ps nginx --format '{{.Health}}' 2>/dev/null || echo "unknown") if [[ "$NGINX_HEALTH" == "healthy" ]]; then pass "Nginx container healthy" API_PROXY=$(curl -sf -H "Host: api.${DOMAIN}" "http://localhost:80/api/health" 2>/dev/null || echo "") if echo "$API_PROXY" | grep -q '"healthy"'; then pass "Nginx -> API proxy" else fail "Nginx -> API proxy" fi ADMIN_PROXY=$(curl -sf -o /dev/null -w "%{http_code}" -H "Host: app.${DOMAIN}" "http://localhost:80/" 2>/dev/null || echo "0") if [[ "$ADMIN_PROXY" == "200" ]]; then pass "Nginx -> Admin proxy" else fail "Nginx -> Admin proxy (HTTP $ADMIN_PROXY)" fi else fail "Nginx container ($NGINX_HEALTH)" fi # ─── Tunnel (optional) ────────────────────────────────────────────────── echo "" echo -e "${BOLD}Tunnel${NC}" NEWT_STATUS=$(docker compose ps newt --format '{{.Status}}' 2>/dev/null || echo "") if echo "$NEWT_STATUS" | grep -q "Up"; then pass "Newt container running" TUNNEL_API=$(curl -sf -o /dev/null -w "%{http_code}" --max-time 10 "https://api.${DOMAIN}/api/health" 2>/dev/null || echo "0") if [[ "$TUNNEL_API" == "200" ]]; then pass "Tunnel -> api.${DOMAIN}" else warn "Tunnel -> api.${DOMAIN} (HTTP $TUNNEL_API)" fi TUNNEL_APP=$(curl -sf -o /dev/null -w "%{http_code}" --max-time 10 "https://app.${DOMAIN}/" 2>/dev/null || echo "0") if [[ "$TUNNEL_APP" == "200" ]]; then pass "Tunnel -> app.${DOMAIN}" else warn "Tunnel -> app.${DOMAIN} (HTTP $TUNNEL_APP — check Pangolin resource auth)" fi else skip "Tunnel (Newt not running)" fi # ─── Database ──────────────────────────────────────────────────────────── echo "" echo -e "${BOLD}Database${NC}" USER_COUNT=$(docker compose exec -T v2-postgres psql -U changemaker -d changemaker_v2 -t -c "SELECT COUNT(*) FROM users" 2>/dev/null | tr -d ' ' || echo "0") if [[ "$USER_COUNT" -gt "0" ]] 2>/dev/null; then pass "Users in DB: $USER_COUNT" else fail "No users found in database" fi TABLE_COUNT=$(docker compose exec -T v2-postgres psql -U changemaker -d changemaker_v2 -t -c "SELECT COUNT(*) FROM pg_tables WHERE schemaname='public'" 2>/dev/null | tr -d ' ' || echo "0") if [[ "$TABLE_COUNT" -gt "100" ]] 2>/dev/null; then pass "Tables in DB: $TABLE_COUNT" else warn "Table count: $TABLE_COUNT (expected 180+)" fi # ─── Summary ───────────────────────────────────────────────────────────── TOTAL=$((PASS + FAIL + WARN + SKIP)) echo "" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" if [[ "$FAIL" -eq 0 ]]; then echo -e " ${GREEN}${BOLD}ALL TESTS PASSED${NC} ($PASS passed, $WARN warnings, $SKIP skipped)" else echo -e " ${RED}${BOLD}$FAIL FAILURES${NC} ($PASS passed, $FAIL failed, $WARN warnings, $SKIP skipped)" fi echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" exit "$FAIL"