#!/bin/bash # Unit test: Environment and configuration validation # These tests validate the project configuration without requiring Docker. set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(dirname "$(dirname "$SCRIPT_DIR")")" TEMPLATE_FILE="$PROJECT_ROOT/docker-compose.yml.template" ENV_TEMPLATE="$PROJECT_ROOT/demo.env.template" RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' NC='\033[0m' PASS=0 FAIL=0 pass() { echo -e "${GREEN}[PASS]${NC} $1"; ((PASS++)) || true; } fail() { echo -e "${RED}[FAIL]${NC} $1"; ((FAIL++)) || true; } check() { echo -e "${YELLOW}[CHECK]${NC} $1"; } grep_exists() { grep "$@" >/dev/null 2>&1 || true } test_template_exists() { check "docker-compose.yml.template exists" if [[ -f "$TEMPLATE_FILE" ]]; then pass "Template file exists" else fail "Template file not found at $TEMPLATE_FILE" fi } test_template_has_required_sections() { check "Template has required top-level sections" local sections=("networks:" "volumes:" "services:") for section in "${sections[@]}"; do if grep_exists "^$section" "$TEMPLATE_FILE"; then pass "Template contains '$section' section" else fail "Template missing '$section' section" fi done } test_template_has_all_services() { check "Template defines all 16 services" local services=( "docker-socket-proxy:" "homepage:" "pihole:" "dockhand:" "influxdb:" "grafana:" "drawio:" "kroki:" "atomictracker:" "archivebox:" "ta-redis:" "ta-elasticsearch:" "tubearchivist:" "wakapi:" "mailhog:" "atuin:" ) local found=0 for svc in "${services[@]}"; do if grep_exists " ${svc}" "$TEMPLATE_FILE"; then ((found++)) || true else fail "Service not found in template: $svc" fi done if [[ $found -eq ${#services[@]} ]]; then pass "All ${#services[@]} services defined in template" fi } test_all_services_have_healthchecks() { check "All exposed services have healthcheck blocks" local exposed_services=("homepage" "pihole" "dockhand" "influxdb" "grafana" "drawio" "kroki" "atomictracker" "archivebox" "tubearchivist" "wakapi" "mailhog" "atuin") local missing=() for svc in "${exposed_services[@]}"; do local svc_block svc_block=$(sed -n "/^ ${svc}:/,/^[^ ]/p" "$TEMPLATE_FILE" || true) if echo "$svc_block" | grep_exists "healthcheck:"; then : else missing+=("$svc") fi done if [[ ${#missing[@]} -eq 0 ]]; then pass "All exposed services have health checks" else fail "Services missing health checks: ${missing[*]}" fi } test_all_services_have_restart_policy() { check "All services have restart policy" local restart_count restart_count=$(grep -c "restart:" "$TEMPLATE_FILE" || true) if [[ $restart_count -ge 16 ]]; then pass "$restart_count services have restart policies" else fail "Only $restart_count services have restart policies (expected >= 16)" fi } test_all_services_have_labels() { check "All user-facing services have Homepage labels" local label_services=("homepage" "pihole" "dockhand" "influxdb" "grafana" "drawio" "kroki" "atomictracker" "archivebox" "tubearchivist" "wakapi" "mailhog" "atuin") local missing=() for svc in "${label_services[@]}"; do local svc_block svc_block=$(sed -n "/^ ${svc}:/,/^[^ ]/p" "$TEMPLATE_FILE" || true) if echo "$svc_block" | grep_exists "homepage.group:"; then : else missing+=("$svc") fi done if [[ ${#missing[@]} -eq 0 ]]; then pass "All user-facing services have Homepage discovery labels" else fail "Services missing labels: ${missing[*]}" fi } test_dockhand_uses_proxy() { check "Dockhand connects through docker-socket-proxy" local dockhand_block dockhand_block=$(sed -n "/^ dockhand:/,/^[^ ]/p" "$TEMPLATE_FILE" || true) if echo "$dockhand_block" | grep_exists "DOCKER_HOST=tcp://docker-socket-proxy:2375"; then pass "Dockhand routes through socket proxy" else fail "Dockhand not configured to use socket proxy (security issue)" fi } test_no_direct_socket_mounts_except_proxy() { check "No direct Docker socket mounts except on socket-proxy" local socket_lines socket_lines=$(grep -n '/var/run/docker\.sock' "$TEMPLATE_FILE" || true) local bad_mounts=0 while IFS= read -r line; do [[ -z "$line" ]] && continue local line_num line_num=$(echo "$line" | cut -d: -f1) local context context=$(head -n "$line_num" "$TEMPLATE_FILE" | grep "^ [a-z]" | tail -1 || true) if [[ "$context" != *"docker-socket-proxy"* ]]; then ((bad_mounts++)) || true fail "Direct socket mount found outside proxy at line $line_num" fi done <<< "$socket_lines" if [[ $bad_mounts -eq 0 ]]; then pass "Only docker-socket-proxy mounts the Docker socket" fi } test_env_template_completeness() { check "demo.env.template has all required variables" local required_vars=( "COMPOSE_PROJECT_NAME" "COMPOSE_NETWORK_NAME" "DEMO_UID" "DEMO_GID" "DEMO_DOCKER_GID" "HOMEPAGE_PORT" "PIHOLE_PORT" "DOCKHAND_PORT" "INFLUXDB_PORT" "GRAFANA_PORT" "DRAWIO_PORT" "KROKI_PORT" "ATOMIC_TRACKER_PORT" "ARCHIVEBOX_PORT" "TUBE_ARCHIVIST_PORT" "WAKAPI_PORT" "MAILHOG_PORT" "MAILHOG_SMTP_PORT" "ATUIN_PORT" "NETWORK_SUBNET" "NETWORK_GATEWAY" "TA_USERNAME" "TA_PASSWORD" "ELASTIC_PASSWORD" "GF_SECURITY_ADMIN_USER" "GF_SECURITY_ADMIN_PASSWORD" "PIHOLE_WEBPASSWORD" ) for var in "${required_vars[@]}"; do if grep_exists "^${var}=" "$ENV_TEMPLATE"; then pass "Env template has $var" else fail "Env template missing $var" fi done } test_env_template_port_range() { check "All ports in env template are in 4000-4099 range" local ports_out_of_range=() while IFS='=' read -r var val; do if [[ "$var" == *"_PORT" && "$val" =~ ^[0-9]+$ ]]; then if [[ "$val" -lt 4000 || "$val" -gt 4099 ]]; then ports_out_of_range+=("$var=$val") fi fi done < "$ENV_TEMPLATE" if [[ ${#ports_out_of_range[@]} -eq 0 ]]; then pass "All ports within 4000-4099 range" else fail "Ports outside range: ${ports_out_of_range[*]}" fi } test_homepage_configs_exist() { check "Homepage configuration files exist" local configs=("services.yaml" "widgets.yaml" "settings.yaml" "bookmarks.yaml" "docker.yaml") for cfg in "${configs[@]}"; do if [[ -f "$PROJECT_ROOT/config/homepage/$cfg" ]]; then pass "Homepage config exists: $cfg" else fail "Homepage config missing: $cfg" fi done } test_grafana_configs_exist() { check "Grafana configuration files exist" local configs=("datasources.yml" "dashboards.yml" "dashboards/docker-overview.json") for cfg in "${configs[@]}"; do if [[ -f "$PROJECT_ROOT/config/grafana/$cfg" ]]; then pass "Grafana config exists: $cfg" else fail "Grafana config missing: $cfg" fi done } test_scripts_exist() { check "Deployment scripts exist" local scripts=("scripts/demo-stack.sh" "scripts/demo-test.sh" "scripts/validate-all.sh") for script in "${scripts[@]}"; do if [[ -f "$PROJECT_ROOT/$script" ]]; then pass "Script exists: $script" else fail "Script missing: $script" fi done } test_scripts_use_strict_mode() { check "All scripts use strict mode (set -euo pipefail)" local found_scripts found_scripts=("$PROJECT_ROOT/scripts/"*.sh) for script in "${found_scripts[@]}"; do if head -5 "$script" | grep_exists "set -euo pipefail"; then pass "$(basename "$script") uses strict mode" else fail "$(basename "$script") missing strict mode" fi done } echo "======================================" echo "Unit Tests: Configuration Validation" echo "======================================" echo "" test_template_exists test_template_has_required_sections test_template_has_all_services test_all_services_have_healthchecks test_all_services_have_restart_policy test_all_services_have_labels test_dockhand_uses_proxy test_no_direct_socket_mounts_except_proxy test_env_template_completeness test_env_template_port_range test_homepage_configs_exist test_grafana_configs_exist test_scripts_exist test_scripts_use_strict_mode echo "" echo "======================================" echo "RESULTS: $PASS passed, $FAIL failed" echo "======================================" [[ $FAIL -eq 0 ]]