Files
TSYSDevStack-SupportStack-L…/demo/scripts/demo-test.sh
T
mrcharles f0101e482b feat(demo): add deep HTTP and browser validation layers
The existing smoke test only checked TCP port connectivity, which cannot
detect a service that accepts connections but returns errors or serves
the wrong content (exactly how the broken Kiwix slipped through).

Add two new validation layers:

1. validate-http.sh — curls every service's health/UI endpoint and
   asserts both the HTTP status code AND meaningful body content (JSON
   health fields like "database":"ok", XML markers, page strings). Also
   verifies Kiwix has a ZIM actually loaded via its OPDS catalog feed.

2. run-browser-tests.sh — drives a real headless Chromium via the
   official Playwright Docker image (no host Node install needed) to
   confirm pages render correctly after JavaScript execution and SPA
   hydration. Updated playwright-services.spec.ts with correct content
   tokens and a waitForFunction step that reliably handles SPA timing.

Both are wired into demo-test.sh (new `http` and `browser` subcommands)
and the full test suite. AGENTS.md and demo/AGENTS.md updated with the
new commands and protocols.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-07-30 15:35:34 -05:00

276 lines
8.2 KiB
Bash
Executable File

#!/bin/bash
# TSYS Developer Support Stack - Demo Testing Script
# Version: 2.0
# Purpose: Comprehensive QA and validation
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
DEMO_ENV_FILE="$PROJECT_ROOT/demo.env"
COMPOSE_FILE="$PROJECT_ROOT/docker-compose.yml"
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
TESTS_PASSED=0
TESTS_FAILED=0
TESTS_TOTAL=0
log_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
log_success() { echo -e "${GREEN}[PASS]${NC} $1"; ((TESTS_PASSED++)) || true; }
log_warning() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[FAIL]${NC} $1"; ((TESTS_FAILED++)) || true; }
log_test() { echo -e "${BLUE}[TEST]${NC} $1"; ((TESTS_TOTAL++)) || true; }
test_file_ownership() {
log_test "File ownership (no root-owned files)"
local root_files
root_files=$(find "$PROJECT_ROOT" -type f -user root 2>/dev/null || true)
if [[ -z "$root_files" ]]; then
log_success "No root-owned files"
else
log_error "Root-owned files found: $root_files"
fi
}
test_user_mapping() {
log_test "UID/GID detection"
source "$DEMO_ENV_FILE"
if [[ -z "${DEMO_UID:-}" || -z "${DEMO_GID:-}" ]]; then
log_error "DEMO_UID or DEMO_GID not set"
return
fi
local cur_uid cur_gid
cur_uid=$(id -u)
cur_gid=$(id -g)
if [[ "$DEMO_UID" -eq "$cur_uid" && "$DEMO_GID" -eq "$cur_gid" ]]; then
log_success "UID/GID correct ($DEMO_UID/$DEMO_GID)"
else
log_error "UID/GID mismatch: env=$DEMO_UID/$DEMO_GID actual=$cur_uid/$cur_gid"
fi
}
test_docker_group() {
log_test "Docker group access"
source "$DEMO_ENV_FILE"
if [[ -z "${DEMO_DOCKER_GID:-}" ]]; then
log_error "DEMO_DOCKER_GID not set"
return
fi
local actual_gid
actual_gid=$(getent group docker | cut -d: -f3)
if [[ "$DEMO_DOCKER_GID" -eq "$actual_gid" ]]; then
log_success "Docker GID correct ($DEMO_DOCKER_GID)"
else
log_error "Docker GID mismatch: env=$DEMO_DOCKER_GID actual=$actual_gid"
fi
}
test_service_health() {
log_test "Service health"
local unhealthy=0
while IFS= read -r line; do
local name
name=$(echo "$line" | awk '{print $1}')
[[ "$name" == "NAMES" || -z "$name" ]] && continue
if echo "$line" | grep -q "(healthy)"; then
log_success "$name healthy"
elif echo "$line" | grep -q "Up"; then
log_success "$name running"
else
log_error "$name not running: $line"
((unhealthy++)) || true
fi
done < <(docker ps --filter "name=${COMPOSE_PROJECT_NAME:-kneldevstack}" --format "{{.Names}} {{.Status}}" 2>/dev/null)
if [[ $unhealthy -eq 0 ]]; then
log_success "All services running"
fi
}
test_port_accessibility() {
log_test "Port accessibility"
source "$DEMO_ENV_FILE"
# These are exposed to host
local port_tests=(
"$HOMEPAGE_PORT:Homepage"
"$PIHOLE_PORT:Pi-hole"
"$DOCKHAND_PORT:Dockhand"
"$INFLUXDB_PORT:InfluxDB"
"$GRAFANA_PORT:Grafana"
"$DRAWIO_PORT:Draw.io"
"$KROKI_PORT:Kroki"
"$ATOMIC_TRACKER_PORT:AtomicTracker"
"$ARCHIVEBOX_PORT:ArchiveBox"
"$TUBE_ARCHIVIST_PORT:TubeArchivist"
"$WAKAPI_PORT:Wakapi"
"$MAILHOG_PORT:MailHog"
"$ATUIN_PORT:Atuin"
"$REACTIVE_RESUME_PORT:ReactiveResume"
"$METRICS_PORT:Metrics"
"$KIWIX_PORT:Kiwix"
"$RESUME_MATCHER_PORT:ResumeMatcher"
"$APPLEHEALTH_PORT:AppleHealth"
)
local failed=0
for pt in "${port_tests[@]}"; do
local port="${pt%:*}"
local svc="${pt#*:}"
if timeout 5 bash -c "echo > /dev/tcp/localhost/$port" 2>/dev/null; then
log_success "$svc (:$port)"
else
log_error "$svc (:$port) not accessible"
((failed++)) || true
fi
done
if [[ $failed -eq 0 ]]; then
log_success "All exposed ports accessible"
fi
}
test_network_isolation() {
log_test "Network isolation"
source "$DEMO_ENV_FILE"
if docker network ls --format '{{.Name}}' | grep -q "$COMPOSE_NETWORK_NAME"; then
log_success "Network $COMPOSE_NETWORK_NAME exists"
local driver
driver=$(docker network inspect "$COMPOSE_NETWORK_NAME" --format '{{.Driver}}' 2>/dev/null || echo "")
if [[ "$driver" == "bridge" ]]; then
log_success "Bridge driver confirmed"
else
log_warning "Driver: $driver"
fi
else
log_error "Network $COMPOSE_NETWORK_NAME not found"
fi
}
test_volume_permissions() {
log_test "Docker volumes exist"
source "$DEMO_ENV_FILE"
local vol_count
vol_count=$(docker volume ls --filter "name=${COMPOSE_PROJECT_NAME}" -q 2>/dev/null | wc -l)
if [[ $vol_count -ge 19 ]]; then
log_success "$vol_count volumes created"
else
log_error "Only $vol_count volumes found"
fi
}
test_security_compliance() {
log_test "Security compliance"
source "$DEMO_ENV_FILE"
# Docker socket proxy present
if grep -q "docker-socket-proxy" "$COMPOSE_FILE"; then
log_success "Docker socket proxy configured"
else
log_error "Docker socket proxy not found"
fi
# Count direct socket mounts - only proxy should have one
local socket_mounts
socket_mounts=$(grep -c '/var/run/docker.sock' "$COMPOSE_FILE" || echo "0")
if [[ "$socket_mounts" -le 1 ]]; then
log_success "Socket mount on proxy only ($socket_mounts)"
else
log_error "Unexpected socket mounts: $socket_mounts (expected 1, proxy only)"
fi
# Dockhand uses proxy, not direct socket
if grep -q 'DOCKER_HOST=tcp://docker-socket-proxy' "$COMPOSE_FILE"; then
log_success "Dockhand routes through socket proxy"
else
log_error "Dockhand not using socket proxy"
fi
}
run_full_tests() {
log_info "Running comprehensive test suite..."
test_file_ownership || true
test_user_mapping || true
test_docker_group || true
test_service_health || true
test_port_accessibility || true
test_network_isolation || true
test_volume_permissions || true
test_security_compliance || true
bash "$SCRIPT_DIR/validate-http.sh" || true
display_test_results
}
run_security_tests() {
log_info "Running security tests..."
test_file_ownership || true
test_network_isolation || true
test_security_compliance || true
display_test_results
}
run_permission_tests() {
log_info "Running permission tests..."
test_file_ownership || true
test_user_mapping || true
test_docker_group || true
test_volume_permissions || true
display_test_results
}
run_network_tests() {
log_info "Running network tests..."
test_network_isolation || true
test_port_accessibility || true
display_test_results
}
run_http_tests() {
log_info "Running HTTP response validation..."
bash "$SCRIPT_DIR/validate-http.sh" || log_warning "Some HTTP checks failed"
display_test_results
}
run_browser_tests() {
log_info "Running browser (Playwright) validation..."
bash "$PROJECT_ROOT/tests/e2e/run-browser-tests.sh" || log_warning "Some browser checks failed"
display_test_results
}
display_test_results() {
echo ""
echo "===================================="
echo "TEST RESULTS"
echo "===================================="
echo "Total: $TESTS_TOTAL"
echo -e "Passed: ${GREEN}$TESTS_PASSED${NC}"
echo -e "Failed: ${RED}$TESTS_FAILED${NC}"
if [[ $TESTS_FAILED -eq 0 ]]; then
echo -e "\n${GREEN}ALL TESTS PASSED${NC}"
return 0
else
echo -e "\n${RED}SOME TESTS FAILED${NC}"
return 1
fi
}
main() {
case "${1:-full}" in
full) run_full_tests ;;
security) run_security_tests ;;
permissions) run_permission_tests ;;
network) run_network_tests ;;
http) run_http_tests ;;
browser) run_browser_tests ;;
help|--help|-h)
echo "Usage: $0 {full|security|permissions|network|http|browser|help}"
;;
*) log_error "Unknown: $1"; exit 1 ;;
esac
}
main "$@"