diff --git a/demo/tests/integration/test_service_communication.sh b/demo/tests/integration/test_service_communication.sh index 51a923c..4a17fed 100755 --- a/demo/tests/integration/test_service_communication.sh +++ b/demo/tests/integration/test_service_communication.sh @@ -1,71 +1,117 @@ #!/bin/bash # Integration test: Service-to-service communication +# Requires a running stack. Validates inter-service connectivity. set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(dirname "$(dirname "$SCRIPT_DIR")")" ENV_FILE="$PROJECT_ROOT/demo.env" +if [[ ! -f "$ENV_FILE" ]]; then + echo "ERROR: $ENV_FILE not found. Copy demo.env.template to demo.env and configure." + exit 1 +fi + set -a; source "$ENV_FILE"; set +a +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + PASS=0 FAIL=0 -pass() { echo "PASS: $1"; ((PASS++)); } -fail() { echo "FAIL: $1"; ((FAIL++)); } +pass() { echo -e "${GREEN}[PASS]${NC} $1"; ((PASS++)); } +fail() { echo -e "${RED}[FAIL]${NC} $1"; ((FAIL++)); } +check() { echo -e "${YELLOW}[CHECK]${NC} $1"; } -test_grafana_influxdb_integration() { - if docker exec "${COMPOSE_PROJECT_NAME}-grafana" wget -q --spider http://influxdb:8086/ping 2>/dev/null; then - pass "Grafana-InfluxDB integration" - else - fail "Grafana-InfluxDB integration" +require_stack_running() { + if ! docker ps --filter "name=${COMPOSE_PROJECT_NAME}" --format "{{.Names}}" | grep -q .; then + echo "ERROR: No running containers found for ${COMPOSE_PROJECT_NAME}" + echo "Run ./scripts/demo-stack.sh deploy first" + exit 1 fi } -test_dockhand_docker_integration() { - if docker exec "${COMPOSE_PROJECT_NAME}-dockhand" sh -c 'command -v docker >/dev/null 2>&1 && docker version >/dev/null 2>&1' 2>/dev/null; then - pass "Dockhand-Docker integration" +test_grafana_influxdb_integration() { + check "Grafana can reach InfluxDB on internal network" + if docker exec "${COMPOSE_PROJECT_NAME}-grafana" wget -q --spider http://influxdb:8086/ping 2>/dev/null; then + pass "Grafana reaches InfluxDB via internal DNS" else - pass "Dockhand-Docker integration (socket mount OK - no docker CLI in container)" + fail "Grafana cannot reach InfluxDB" + fi +} + +test_dockhand_proxy_integration() { + check "Dockhand can reach Docker via socket proxy" + local dockhand_env + dockhand_env=$(docker exec "${COMPOSE_PROJECT_NAME}-dockhand" env 2>/dev/null || echo "") + if echo "$dockhand_env" | grep -q "DOCKER_HOST=tcp://docker-socket-proxy:2375"; then + pass "Dockhand configured with DOCKER_HOST pointing to socket proxy" + else + fail "Dockhand DOCKER_HOST not configured for socket proxy" fi } test_homepage_discovery() { - local discovered - discovered=$(curl -sf "http://localhost:${HOMEPAGE_PORT}" 2>/dev/null | grep -ci "service\|href\|homepage" || echo "0") - if [[ "$discovered" -ge 1 ]]; then - pass "Homepage service discovery (found references)" + check "Homepage responds and contains service references" + local http_code + http_code=$(curl -s -o /dev/null -w "%{http_code}" "http://localhost:${HOMEPAGE_PORT}" 2>/dev/null || echo "000") + if [[ "$http_code" -ge 200 && "$http_code" -lt 400 ]]; then + pass "Homepage accessible (HTTP $http_code)" else - fail "Homepage service discovery" + fail "Homepage not accessible (HTTP $http_code)" fi } test_tubearchivist_redis() { - if docker exec "${COMPOSE_PROJECT_NAME}-tubearchivist" curl -sf http://ta-redis:6379 2>/dev/null || \ - docker exec "${COMPOSE_PROJECT_NAME}-ta-redis" redis-cli ping 2>/dev/null | grep -q PONG; then - pass "TubeArchivist-Redis integration" + check "Tube Archivist can reach Redis" + if docker exec "${COMPOSE_PROJECT_NAME}-ta-redis" redis-cli ping 2>/dev/null | grep -q PONG; then + pass "Redis responds to PING" else - fail "TubeArchivist-Redis integration" + fail "Redis not responding" fi } test_tubearchivist_elasticsearch() { - if docker exec "${COMPOSE_PROJECT_NAME}-tubearchivist" curl -sf http://ta-elasticsearch:9200 2>/dev/null; then - pass "TubeArchivist-Elasticsearch integration" + check "Elasticsearch cluster is healthy" + local es_status + es_status=$(docker exec "${COMPOSE_PROJECT_NAME}-ta-elasticsearch" curl -sf http://localhost:9200/_cluster/health 2>/dev/null || echo "") + if echo "$es_status" | grep -q '"status"'; then + pass "Elasticsearch cluster responding" else - fail "TubeArchivist-Elasticsearch integration" + fail "Elasticsearch not responding" fi } -echo "Running integration tests..." -test_grafana_influxdb_integration || true -test_dockhand_docker_integration || true -test_homepage_discovery || true -test_tubearchivist_redis || true -test_tubearchivist_elasticsearch || true +test_network_isolation() { + check "Services are on the correct network" + local net_count + net_count=$(docker network inspect "${COMPOSE_NETWORK_NAME}" --format '{{range .Containers}}{{.Name}} {{end}}' 2>/dev/null | wc -w || echo "0") + if [[ "$net_count" -ge 14 ]]; then + pass "$net_count containers on ${COMPOSE_NETWORK_NAME}" + else + fail "Only $net_count containers on network (expected >= 14)" + fi +} + +require_stack_running + +echo "======================================" +echo "Integration Tests: Service Communication" +echo "======================================" +echo "" + +test_grafana_influxdb_integration +test_dockhand_proxy_integration +test_homepage_discovery +test_tubearchivist_redis +test_tubearchivist_elasticsearch +test_network_isolation echo "" -echo "====================================" -echo "Integration Test Results: $PASS passed, $FAIL failed" -echo "====================================" +echo "======================================" +echo "RESULTS: $PASS passed, $FAIL failed" +echo "======================================" [[ $FAIL -eq 0 ]] diff --git a/demo/tests/unit/test_env_validation.sh b/demo/tests/unit/test_env_validation.sh index 7ce45a5..ae00c53 100755 --- a/demo/tests/unit/test_env_validation.sh +++ b/demo/tests/unit/test_env_validation.sh @@ -1,30 +1,266 @@ #!/bin/bash -# Unit test: User ID detection accuracy +# Unit test: Environment and configuration validation +# These tests validate the project configuration without requiring Docker. set -euo pipefail -test_uid_detection() { - local expected_uid - local expected_gid - local expected_docker_gid - expected_uid=$(id -u) - expected_gid=$(id -g) - expected_docker_gid=$(getent group docker | cut -d: -f3) - - # Simulate script detection - local detected_uid=$expected_uid - local detected_gid=$expected_gid - local detected_docker_gid=$expected_docker_gid - - if [[ "$detected_uid" -eq "$expected_uid" && - "$detected_gid" -eq "$expected_gid" && - "$detected_docker_gid" -eq "$expected_docker_gid" ]]; then - echo "PASS: User detection accurate" - return 0 +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 - echo "FAIL: User detection inaccurate" - return 1 + fail "Template file not found at $TEMPLATE_FILE" fi } -test_uid_detection \ No newline at end of file +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 ]]