#!/bin/bash # ============================================================================ # Update Automation Integration Tests # ============================================================================ # Tests script integration with Docker components in isolated environment. # Does NOT touch production infrastructure or AWS services. # # Requirements: # - Docker daemon running # - docker compose plugin installed # # Tests: # 1. Script syntax validation (static) # 2. Docker Compose configuration validity (static) # 3. Backup creates valid archives (integration) # 4. Health checks detect container failures (integration) # 5. Update workflow with rollback (integration) # # Usage: ./test-update.sh # Exit: 0 if all tests pass, 1 if any test fails # ============================================================================ set -e # ============================================================================ # Configuration # ============================================================================ readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" readonly DOCKER_COMPOSE_DIR="$(cd "${SCRIPT_DIR}/../docker" && pwd)" readonly BACKUP_SCRIPT="${SCRIPT_DIR}/backup.sh" readonly HEALTH_CHECK_SCRIPT="${SCRIPT_DIR}/health-check.sh" readonly AUTO_UPDATE_SCRIPT="${SCRIPT_DIR}/auto-update.sh" readonly MANUAL_UPDATE_SCRIPT="${SCRIPT_DIR}/manual-update.sh" readonly COMPOSE_FILE="${DOCKER_COMPOSE_DIR}/docker-compose.yml" readonly TEST_LOG="/tmp/test-update-$$.log" readonly TEST_DIR="/tmp/test-gitea-$$" # Test images and credentials readonly PG_IMAGE="postgres:18.4" readonly PG_USER="testuser" readonly PG_PASS="testpass" readonly PG_DB="testdb" readonly NGINX_IMAGE="nginx:1.27-alpine" readonly ALPINE_OLD="alpine:3.19" readonly ALPINE_NEW="alpine:3.20" # Wait timeouts (seconds) readonly WAIT_TIMEOUT=30 readonly WAIT_INTERVAL=0.5 # Output colors readonly GREEN='\033[0;32m' readonly RED='\033[0;31m' readonly BLUE='\033[0;34m' readonly NC='\033[0m' # No Color # Test counters TESTS_PASSED=0 TESTS_FAILED=0 # Cleanup tracking CONTAINERS_TO_CLEANUP=() # ============================================================================ # Cleanup Functions # ============================================================================ cleanup() { log_info "Cleaning up test environment..." # Stop and remove test containers if [[ ${#CONTAINERS_TO_CLEANUP[@]} -gt 0 ]]; then for container in "${CONTAINERS_TO_CLEANUP[@]}"; do docker rm -f "${container}" &>/dev/null || true done fi # Remove test directory if [[ -d "${TEST_DIR}" ]]; then rm -rf "${TEST_DIR}" fi log_info "Cleanup complete" } trap cleanup EXIT # ============================================================================ # Output Functions # ============================================================================ log_info() { echo -e "${BLUE}[INFO]${NC} $*" | tee -a "${TEST_LOG}" } log_success() { echo -e "${GREEN}[PASS]${NC} $*" | tee -a "${TEST_LOG}" } log_error() { echo -e "${RED}[FAIL]${NC} $*" | tee -a "${TEST_LOG}" } pass_test() { local message="$1" TESTS_PASSED=$((TESTS_PASSED + 1)) log_success "${message}" } fail_test() { local message="$1" TESTS_FAILED=$((TESTS_FAILED + 1)) log_error "${message}" } # ============================================================================ # Helper Functions # ============================================================================ wait_for_postgres() { local container=$1 local attempts=0 local max_attempts=$((WAIT_TIMEOUT * 2)) # Check every 0.5s # First wait for container to be running while ! docker ps --filter "name=${container}" --format "{{.Names}}" | grep -q "^${container}$"; do ((attempts++)) if [[ $attempts -ge $max_attempts ]]; then return 1 fi sleep "${WAIT_INTERVAL}" done # Then wait for postgres to be ready attempts=0 while ! docker exec "${container}" pg_isready -U "${PG_USER}" &>/dev/null; do ((attempts++)) if [[ $attempts -ge $max_attempts ]]; then return 1 fi sleep "${WAIT_INTERVAL}" done # Give it a moment to fully initialize sleep 1 return 0 } wait_for_container() { local container=$1 local attempts=0 local max_attempts=$((WAIT_TIMEOUT * 2)) while ! docker ps --filter "name=${container}" --format "{{.Names}}" | grep -q "^${container}$"; do ((attempts++)) if [[ $attempts -ge $max_attempts ]]; then return 1 fi sleep "${WAIT_INTERVAL}" done return 0 } start_postgres_container() { local name=$1 docker run -d \ --name "${name}" \ -e POSTGRES_USER="${PG_USER}" \ -e POSTGRES_PASSWORD="${PG_PASS}" \ -e POSTGRES_DB="${PG_DB}" \ "${PG_IMAGE}" &>> "${TEST_LOG}" CONTAINERS_TO_CLEANUP+=("${name}") wait_for_postgres "${name}" } start_container() { local name=$1 local image=$2 shift 2 local extra_args=("$@") docker run -d \ --name "${name}" \ "${image}" \ "${extra_args[@]}" &>> "${TEST_LOG}" CONTAINERS_TO_CLEANUP+=("${name}") wait_for_container "${name}" } validate_sql_archive() { local file=$1 local pattern=$2 gunzip -t "${file}" 2>> "${TEST_LOG}" && \ zcat "${file}" | grep -q "${pattern}" } validate_tar_archive() { local file=$1 local pattern=$2 tar -tzf "${file}" &>> "${TEST_LOG}" && \ tar -tzf "${file}" | grep -q "${pattern}" } get_container_image() { local container=$1 docker inspect --format='{{.Config.Image}}' "${container}" } # ============================================================================ # Test Functions # ============================================================================ test_script_syntax() { log_info "Test 1: Script syntax validation..." local scripts=( "${BACKUP_SCRIPT}" "${HEALTH_CHECK_SCRIPT}" "${AUTO_UPDATE_SCRIPT}" "${MANUAL_UPDATE_SCRIPT}" ) for script in "${scripts[@]}"; do if [[ ! -f "${script}" ]]; then fail_test "Script not found: ${script}" continue fi if bash -n "${script}" 2>> "${TEST_LOG}"; then pass_test "Syntax valid: $(basename "${script}")" else fail_test "Syntax error in: $(basename "${script}")" fi done } test_docker_compose_validity() { log_info "Test 2: Docker Compose configuration..." if [[ ! -f "${COMPOSE_FILE}" ]]; then fail_test "docker-compose.yml not found" return fi # Validate compose file syntax if ! docker compose -f "${COMPOSE_FILE}" config &>> "${TEST_LOG}"; then fail_test "docker-compose.yml has syntax errors" return fi pass_test "docker-compose.yml is valid" # Check for latest tags (anti-pattern) if grep -E "image:.*:latest" "${COMPOSE_FILE}" &>> "${TEST_LOG}"; then fail_test "Found 'latest' tags (versions should be pinned)" else pass_test "No 'latest' tags (versions properly pinned)" fi } test_backup_creates_valid_archives() { log_info "Test 3: Backup creates valid archives..." # Create test environment mkdir -p "${TEST_DIR}/backups" mkdir -p "${TEST_DIR}/gitea-data" echo "test data" > "${TEST_DIR}/gitea-data/test-file.txt" # Start test postgres container local db_container="test-postgres-$$" if ! start_postgres_container "${db_container}"; then fail_test "Failed to start postgres container" return fi # Create test table with data docker exec "${db_container}" psql -U "${PG_USER}" -d "${PG_DB}" -c \ "CREATE TABLE test_data (id SERIAL PRIMARY KEY, value TEXT);" &>> "${TEST_LOG}" docker exec "${db_container}" psql -U "${PG_USER}" -d "${PG_DB}" -c \ "INSERT INTO test_data (value) VALUES ('test value');" &>> "${TEST_LOG}" # Test database backup local backup_file="${TEST_DIR}/backups/test-backup.sql.gz" if ! docker exec "${db_container}" pg_dump -U "${PG_USER}" "${PG_DB}" | gzip > "${backup_file}" 2>> "${TEST_LOG}"; then fail_test "Database backup failed" return fi if ! validate_sql_archive "${backup_file}" "test_data"; then fail_test "Database backup archive is invalid" return fi pass_test "Database backup creates valid SQL archive" # Test Gitea data backup local data_backup="${TEST_DIR}/backups/test-data.tar.gz" if ! tar -czf "${data_backup}" -C "${TEST_DIR}" gitea-data 2>> "${TEST_LOG}"; then fail_test "Gitea data backup failed" return fi if ! validate_tar_archive "${data_backup}" "test-file.txt"; then fail_test "Gitea data backup archive is invalid" return fi pass_test "Gitea data backup creates valid tar archive" } test_health_checks_detect_failures() { log_info "Test 4: Health checks detect container failures..." # Start healthy test container local test_container="test-nginx-$$" if ! start_container "${test_container}" "${NGINX_IMAGE}"; then fail_test "Failed to start nginx container" return fi # Test 1: Detect running container if docker ps --filter "name=${test_container}" --format "{{.Names}}" | grep -q "^${test_container}$"; then pass_test "Health check detects running container" else fail_test "Health check failed to detect running container" fi # Test 2: Stop container and verify detection docker stop "${test_container}" &>> "${TEST_LOG}" sleep 1 if ! docker ps --filter "name=${test_container}" --format "{{.Names}}" | grep -q "^${test_container}$"; then pass_test "Health check detects stopped container" else fail_test "Health check failed to detect stopped container" fi # Test 3: Start postgres and verify health check local pg_container="test-pg-health-$$" if ! start_postgres_container "${pg_container}"; then fail_test "Failed to start postgres for health check" return fi # Test pg_isready (how health-check.sh validates postgres) if docker exec "${pg_container}" pg_isready -U "${PG_USER}" &>> "${TEST_LOG}"; then pass_test "Postgres health check (pg_isready) works" else fail_test "Postgres health check failed" fi } test_update_workflow_with_rollback() { log_info "Test 5: Update workflow with rollback simulation..." # Create test container with versioned images local test_container="test-rollback-$$" # Start with old version if ! start_container "${test_container}" "${ALPINE_OLD}" tail -f /dev/null; then fail_test "Failed to start container with initial image" return fi # Verify initial version local initial_image=$(get_container_image "${test_container}") if [[ "${initial_image}" == "${ALPINE_OLD}" ]]; then pass_test "Container starts with correct initial image" else fail_test "Container has wrong initial image: ${initial_image}" fi # Simulate update: save current image info (like auto-update.sh does) local saved_image="${initial_image}" # "Update" to new version docker rm -f "${test_container}" &>> "${TEST_LOG}" if ! start_container "${test_container}" "${ALPINE_NEW}" tail -f /dev/null; then fail_test "Failed to update container" return fi local updated_image=$(get_container_image "${test_container}") if [[ "${updated_image}" == "${ALPINE_NEW}" ]]; then pass_test "Container updates to new image" else fail_test "Container update failed" fi # Simulate rollback (health check failed scenario) docker rm -f "${test_container}" &>> "${TEST_LOG}" if ! start_container "${test_container}" "${saved_image}" tail -f /dev/null; then fail_test "Failed to rollback container" return fi local rolled_back_image=$(get_container_image "${test_container}") if [[ "${rolled_back_image}" == "${saved_image}" ]]; then pass_test "Rollback restores previous image" else fail_test "Rollback failed: got ${rolled_back_image}, expected ${saved_image}" fi } # ============================================================================ # Main Execution # ============================================================================ main() { echo "==========================================" echo "Update Automation Integration Tests" echo "==========================================" echo "" log_info "Starting tests at $(date)" log_info "Test environment: ${TEST_DIR}" echo "" # Check Docker is available if ! command -v docker &> /dev/null; then log_error "Docker is not installed or not in PATH" exit 1 fi if ! docker ps &> /dev/null; then log_error "Docker daemon is not running or not accessible" exit 1 fi # Create log file : > "${TEST_LOG}" # Create test directory mkdir -p "${TEST_DIR}" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "Static Analysis Tests" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" test_script_syntax echo "" test_docker_compose_validity echo "" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "Integration Tests (Docker Required)" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" test_backup_creates_valid_archives echo "" test_health_checks_detect_failures echo "" test_update_workflow_with_rollback echo "" # Summary echo "==========================================" echo "Test Summary" echo "==========================================" echo -e "${GREEN}Passed: ${TESTS_PASSED}${NC}" echo -e "${RED}Failed: ${TESTS_FAILED}${NC}" echo "" if [[ ${TESTS_FAILED} -eq 0 ]]; then echo -e "${GREEN}All integration tests passed!${NC}" echo "" log_info "Full log: ${TEST_LOG}" exit 0 else echo -e "${RED}${TESTS_FAILED} test(s) failed${NC}" echo "" log_error "Full log: ${TEST_LOG}" exit 1 fi } main "$@"