#!/bin/bash # TSYS Developer Support Stack - Demo Testing Script # Version: 1.0 # Purpose: Comprehensive QA and validation set -euo pipefail # Script Configuration 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" # Color Codes for Output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color # Test Results TESTS_PASSED=0 TESTS_FAILED=0 TESTS_TOTAL=0 # Logging Functions log_info() { echo -e "${BLUE}[INFO]${NC} $1" } log_success() { echo -e "${GREEN}[PASS]${NC} $1" ((TESTS_PASSED++)) } log_warning() { echo -e "${YELLOW}[WARNING]${NC} $1" } log_error() { echo -e "${RED}[FAIL]${NC} $1" ((TESTS_FAILED++)) } log_test() { echo -e "${BLUE}[TEST]${NC} $1" ((TESTS_TOTAL++)) } # Function to test file ownership test_file_ownership() { log_test "Testing file ownership (no root-owned files)..." local project_root_files project_root_files=$(find "$PROJECT_ROOT" -type f -user root 2>/dev/null || true) if [[ -z "$project_root_files" ]]; then log_success "No root-owned files found in project directory" else log_error "Root-owned files found:" echo "$project_root_files" return 1 fi } # Function to test user mapping test_user_mapping() { log_test "Testing UID/GID detection and application..." # Source environment variables # shellcheck disable=SC1090,SC1091 source "$DEMO_ENV_FILE" # Check if UID/GID are set if [[ -z "$DEMO_UID" || -z "$DEMO_GID" ]]; then log_error "DEMO_UID or DEMO_GID not set in demo.env" return 1 fi # Check if values match current user local current_uid local current_gid current_uid=$(id -u) current_gid=$(id -g) if [[ "$DEMO_UID" -eq "$current_uid" && "$DEMO_GID" -eq "$current_gid" ]]; then log_success "UID/GID correctly detected and applied (UID: $DEMO_UID, GID: $DEMO_GID)" else log_error "UID/GID mismatch. Expected: $current_uid/$current_gid, Found: $DEMO_UID/$DEMO_GID" return 1 fi } # Function to test Docker group access test_docker_group() { log_test "Testing Docker group access..." # shellcheck disable=SC1090,SC1091 source "$DEMO_ENV_FILE" if [[ -z "$DEMO_DOCKER_GID" ]]; then log_error "DEMO_DOCKER_GID not set in demo.env" return 1 fi # Check if docker group exists if getent group docker >/dev/null 2>&1; then local docker_gid docker_gid=$(getent group docker | cut -d: -f3) if [[ "$DEMO_DOCKER_GID" -eq "$docker_gid" ]]; then log_success "Docker group ID correctly detected (GID: $DEMO_DOCKER_GID)" else log_error "Docker group ID mismatch. Expected: $docker_gid, Found: $DEMO_DOCKER_GID" return 1 fi else log_error "Docker group not found" return 1 fi } # Function to test service health test_service_health() { log_test "Testing service health..." cd "$PROJECT_ROOT" local unhealthy_services=0 # Get list of services if command -v docker-compose &> /dev/null; then mapfile -t services < <(docker-compose -f "$COMPOSE_FILE" config --services) else mapfile -t services < <(docker compose -f "$COMPOSE_FILE" config --services) fi for service in "${services[@]}"; do local health_status if command -v docker-compose &> /dev/null; then health_status=$(docker-compose -f "$COMPOSE_FILE" ps -q "$service" | xargs docker inspect --format='{{.State.Health.Status}}' 2>/dev/null || echo "none") else health_status=$(docker compose -f "$COMPOSE_FILE" ps -q "$service" | xargs docker inspect --format='{{.State.Health.Status}}' 2>/dev/null || echo "none") fi case "$health_status" in "healthy") log_success "Service $service is healthy" ;; "none") log_warning "Service $service has no health check (assuming healthy)" ;; "unhealthy"|"starting") log_error "Service $service is $health_status" ((unhealthy_services++)) ;; *) log_error "Service $service has unknown status: $health_status" ((unhealthy_services++)) ;; esac done if [[ $unhealthy_services -eq 0 ]]; then log_success "All services are healthy" return 0 else log_error "$unhealthy_services services are not healthy" return 1 fi } # Function to test port accessibility test_port_accessibility() { log_test "Testing port accessibility..." # shellcheck disable=SC1090,SC1091 source "$DEMO_ENV_FILE" local ports=( "$HOMEPAGE_PORT:Homepage" "$DOCKER_SOCKET_PROXY_PORT:Docker Socket Proxy" "$PIHOLE_PORT:Pi-hole" "$DOCKHAND_PORT:Dockhand" "$INFLUXDB_PORT:InfluxDB" "$GRAFANA_PORT:Grafana" "$DRAWIO_PORT:Draw.io" "$KROKI_PORT:Kroki" "$ATOMIC_TRACKER_PORT:Atomic Tracker" "$ARCHIVEBOX_PORT:ArchiveBox" "$TUBE_ARCHIVIST_PORT:Tube Archivist" "$WAKAPI_PORT:Wakapi" "$MAILHOG_PORT:MailHog" "$ATUIN_PORT:Atuin" ) local failed_ports=0 for port_info in "${ports[@]}"; do local port="${port_info%:*}" local service="${port_info#*:}" if [[ -n "$port" && "$port" != " " ]]; then if curl -f -s --max-time 5 "http://localhost:$port" >/dev/null 2>&1; then log_success "Port $port ($service) is accessible" else log_error "Port $port ($service) is not accessible" ((failed_ports++)) fi fi done if [[ $failed_ports -eq 0 ]]; then log_success "All ports are accessible" return 0 else log_error "$failed_ports ports are not accessible" return 1 fi } # Function to test network isolation test_network_isolation() { log_test "Testing network isolation..." # shellcheck disable=SC1090,SC1091 source "$DEMO_ENV_FILE" # Check if the network exists if docker network ls | grep -q "$COMPOSE_NETWORK_NAME"; then log_success "Docker network $COMPOSE_NETWORK_NAME exists" # Check network isolation local network_info network_info=$(docker network inspect "$COMPOSE_NETWORK_NAME" --format='{{.Driver}}' 2>/dev/null || echo "") if [[ "$network_info" == "bridge" ]]; then log_success "Network is properly isolated (bridge driver)" else log_warning "Network driver is $network_info (expected: bridge)" fi return 0 else log_error "Docker network $COMPOSE_NETWORK_NAME not found" return 1 fi } # Function to test volume permissions test_volume_permissions() { log_test "Testing Docker volume permissions..." # shellcheck disable=SC1090,SC1091 source "$DEMO_ENV_FILE" local failed_volumes=0 # Get list of volumes for this project local volumes volumes=$(docker volume ls --filter "name=${COMPOSE_PROJECT_NAME}" --format "{{.Name}}" 2>/dev/null || true) if [[ -z "$volumes" ]]; then log_warning "No project volumes found" return 0 fi for volume in $volumes; do local volume_path local owner volume_path=$(docker volume inspect "$volume" --format '{{ .Mountpoint }}' 2>/dev/null || echo "") if [[ -n "$volume_path" ]]; then owner=$(stat -c "%U:%G" "$volume_path" 2>/dev/null || echo "unknown") if [[ "$owner" == "$(id -u):$(id -g)" || "$owner" == "root:root" ]]; then log_success "Volume $volume has correct permissions ($owner)" else log_error "Volume $volume has incorrect permissions ($owner)" ((failed_volumes++)) fi fi done if [[ $failed_volumes -eq 0 ]]; then log_success "All volumes have correct permissions" return 0 else log_error "$failed_volumes volumes have incorrect permissions" return 1 fi } # Function to test security compliance test_security_compliance() { log_test "Testing security compliance..." # shellcheck disable=SC1090,SC1091 source "$DEMO_ENV_FILE" local security_issues=0 # Check if Docker socket proxy is being used cd "$PROJECT_ROOT" if command -v docker-compose &> /dev/null; then local socket_proxy_services socket_proxy_services=$(docker-compose -f "$COMPOSE_FILE" config | grep -c "docker-socket-proxy" || echo "0") else local socket_proxy_services socket_proxy_services=$(docker compose -f "$COMPOSE_FILE" config | grep -c "docker-socket-proxy" || echo "0") fi if [[ "$socket_proxy_services" -gt 0 ]]; then log_success "Docker socket proxy service found" else log_error "Docker socket proxy service not found" ((security_issues++)) fi # Check for direct Docker socket mounts (excluding docker-socket-proxy service) local total_socket_mounts total_socket_mounts=$(grep -c "/var/run/docker.sock" "$COMPOSE_FILE" || echo "0") local direct_socket_mounts=$((total_socket_mounts - 1)) # Subtract 1 for the proxy service itself if [[ "$direct_socket_mounts" -eq 0 ]]; then log_success "No direct Docker socket mounts found" else log_error "Direct Docker socket mounts found ($direct_socket_mounts)" ((security_issues++)) fi if [[ $security_issues -eq 0 ]]; then log_success "Security compliance checks passed" return 0 else log_error "$security_issues security issues found" return 1 fi } # Function to run full test suite 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 display_test_results } # Function to run security tests only run_security_tests() { log_info "Running security compliance tests..." test_file_ownership || true test_network_isolation || true test_security_compliance || true display_test_results } # Function to run permission tests only run_permission_tests() { log_info "Running permission validation tests..." test_file_ownership || true test_user_mapping || true test_docker_group || true test_volume_permissions || true display_test_results } # Function to run network tests only run_network_tests() { log_info "Running network isolation tests..." test_network_isolation || true test_port_accessibility || true display_test_results } # Function to display test results display_test_results() { echo "" echo "====================================" echo "๐Ÿงช TEST RESULTS SUMMARY" echo "====================================" echo "Total Tests: $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 } # Function to show usage show_usage() { echo "Usage: $0 {full|security|permissions|network|help}" echo "" echo "Test Categories:" echo " full - Run comprehensive test suite" echo " security - Run security compliance tests only" echo " permissions - Run permission validation tests only" echo " network - Run network isolation tests only" echo " help - Show this help message" } # Main script execution main() { case "${1:-full}" in full) run_full_tests ;; security) run_security_tests ;; permissions) run_permission_tests ;; network) run_network_tests ;; help|--help|-h) show_usage ;; *) log_error "Unknown test category: $1" show_usage exit 1 ;; esac } # Execute main function with all arguments main "$@"