#!/usr/bin/env bash # # test_update.sh - Test suite for update.sh # set -euo pipefail # Colors RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' CYAN='\033[0;36m' NC='\033[0m' TESTS_PASSED=0 TESTS_FAILED=0 TEST_DIR="" ORIGINAL_DIR="$PWD" # Helper functions log_test() { echo -e "${BLUE}[TEST]${NC} $1" } log_pass() { echo -e "${GREEN}[PASS]${NC} $1" ((TESTS_PASSED++)) } log_fail() { echo -e "${RED}[FAIL]${NC} $1" ((TESTS_FAILED++)) } log_info() { echo -e "${YELLOW}[INFO]${NC} $1" } # Setup test environment setup_test_env() { local temp_dir temp_dir=$(mktemp -d -t dotfiles-test.XXXXXX) # Create a mock git repo cd "$temp_dir" || exit 1 git init -q git config user.email "test@example.com" git config user.name "Test User" # Create initial commit git commit --allow-empty -m "Initial commit" -q echo "$temp_dir" } # Cleanup test environment cleanup_test_env() { if [[ -n "${TEST_DIR:-}" && -d "$TEST_DIR" ]]; then rm -rf "$TEST_DIR" fi } # Mock functions to avoid side effects mock_git() { if [[ "$1" == "pull" ]]; then echo "Mock: git pull executed" return 0 fi # For other git commands, use real git but in test directory command git "$@" } mock_makepkg() { echo "Mock: makepkg $*" return 0 } # Test 1: Script exists and is executable test_script_exists() { log_test "Checking if update.sh exists and is executable" if [[ ! -f "update.sh" ]]; then log_fail "update.sh not found" return 1 fi if [[ ! -x "update.sh" ]]; then log_fail "update.sh is not executable" return 1 fi log_pass "Script exists and is executable" } # Test 2: Script has no syntax errors test_syntax() { log_test "Checking script syntax" if bash -n update.sh; then log_pass "No syntax errors found" else log_fail "Syntax errors detected" return 1 fi } # Test 3: Help option works test_help_option() { log_test "Testing --help option" if ./update.sh --help 2>&1 | grep -q "Usage:"; then log_pass "Help option works" else log_fail "Help option failed" return 1 fi } # Test 4: Test repository structure detection (dots/ prefix) test_dots_structure() { log_test "Testing dots/ prefix structure detection" local test_repo test_repo=$(setup_test_env) TEST_DIR="$test_repo" cd "$test_repo" || { log_fail "Failed to cd to test directory"; return 1; } # Create dots/ structure mkdir -p dots/.config/test-app mkdir -p dots/.local/bin echo "test config" > dots/.config/test-app/config.conf echo "#!/bin/bash" > dots/.local/bin/test-script # Add and commit git add . git commit -m "Add dots structure" -q # Source the update.sh to test functions source update.sh >/dev/null 2>&1 || true # Test the detection function if result=$(detect_repo_structure 2>/dev/null); then if [[ "$result" == *"dots/.config"* ]] && [[ "$result" == *"dots/.local/bin"* ]]; then log_pass "Dots structure detected correctly" else log_fail "Failed to detect dots structure. Got: $result" fi else log_fail "detect_repo_structure failed" fi cd "$ORIGINAL_DIR" || exit 1 cleanup_test_env } # Test 5: Test flat structure detection test_flat_structure() { log_test "Testing flat structure detection" local test_repo test_repo=$(setup_test_env) TEST_DIR="$test_repo" cd "$test_repo" || { log_fail "Failed to cd to test directory"; return 1; } # Create flat structure mkdir -p .config/test-app mkdir -p .local/bin echo "test config" > .config/test-app/config.conf echo "#!/bin/bash" > .local/bin/test-script # Add and commit git add . git commit -m "Add flat structure" -q # Source the update.sh to test functions source update.sh >/dev/null 2>&1 || true # Test the detection function if result=$(detect_repo_structure 2>/dev/null); then if [[ "$result" == *".config"* ]] && [[ "$result" != *"dots/"* ]]; then log_pass "Flat structure detected correctly" else log_fail "Failed to detect flat structure. Got: $result" fi else log_fail "detect_repo_structure failed" fi cd "$ORIGINAL_DIR" || exit 1 cleanup_test_env } # Test 6: Test dots prefix mapping to home directory test_dots_mapping() { log_test "Testing dots/ prefix home directory mapping" local test_repo test_repo=$(setup_test_env) TEST_DIR="$test_repo" cd "$test_repo" || { log_fail "Failed to cd to test directory"; return 1; } # Create dots/ structure mkdir -p dots/.config/test-app echo "test config" > dots/.config/test-app/config.conf # Source the update.sh source update.sh >/dev/null 2>&1 || true # Test the mapping logic dir_name="dots/.config" if [[ "$dir_name" == dots/* ]]; then home_subdir="${dir_name#dots/}" home_dir_path="${HOME}/${home_subdir}" else home_dir_path="${HOME}/${dir_name}" fi expected_path="${HOME}/.config" if [[ "$home_dir_path" == "$expected_path" ]]; then log_pass "Dots prefix mapping correct: $dir_name → $home_dir_path" else log_fail "Dots prefix mapping failed: $dir_name → $home_dir_path (expected: $expected_path)" fi cd "$ORIGINAL_DIR" || exit 1 cleanup_test_env } # Test 7: Test ignore file patterns test_ignore_patterns() { log_test "Testing ignore file pattern matching" local test_repo test_repo=$(setup_test_env) TEST_DIR="$test_repo" cd "$test_repo" || { log_fail "Failed to cd to test directory"; return 1; } # Create ignore file cat > .updateignore << 'EOF' # Test ignore patterns *.log secrets/ .config/private* *backup* /tmp-file EOF # Create test files mkdir -p .config touch app.log touch secrets/key.txt touch .config/private-config touch .config/backup-file touch normal-config # Source the update.sh source update.sh >/dev/null 2>&1 || true # Test cases local passed=0 local total=0 # Test patterns test_cases=( "$test_repo/app.log:0" "$test_repo/secrets/key.txt:0" "$test_repo/.config/private-config:0" "$test_repo/.config/backup-file:0" "$test_repo/normal-config:1" "$test_repo/.config/normal-file:1" ) for test_case in "${test_cases[@]}"; do IFS=':' read -r file expected <<< "$test_case" touch "$file" 2>/dev/null || true ((total++)) if should_ignore "$file"; then result=0 else result=1 fi if [[ $result -eq $expected ]]; then ((passed++)) else log_fail "Ignore test failed: $file (expected: $expected, got: $result)" fi done if [[ $passed -eq $total ]]; then log_pass "All ignore pattern tests passed ($passed/$total)" else log_fail "Ignore pattern tests failed ($passed/$total passed)" fi cd "$ORIGINAL_DIR" || exit 1 cleanup_test_env } # Test 8: Test safe_read security (no eval injection) test_safe_read_security() { log_test "Testing safe_read security against injection" # Source the update.sh source update.sh >/dev/null 2>&1 || true # Test safe_read with potentially dangerous input dangerous_input="'; echo 'INJECTION'; '" # Use a subshell to capture any injection output=$( { echo "$dangerous_input" | safe_read "Test: " test_var "default" 2>/dev/null || true # Check if injection occurred if declare -p test_var 2>/dev/null | grep -q "INJECTION"; then echo "INJECTION_DETECTED" else echo "SAFE" fi } 2>/dev/null ) if [[ "$output" != *"INJECTION_DETECTED"* ]]; then log_pass "safe_read is secure against injection attacks" else log_fail "safe_read vulnerable to injection attacks" fi } # Test 9: Test dry-run mode test_dry_run() { log_test "Testing dry-run mode" local test_repo test_repo=$(setup_test_env) TEST_DIR="$test_repo" cd "$test_repo" || { log_fail "Failed to cd to test directory"; return 1; } # Create test structure mkdir -p dots/.config/test-app echo "repo config" > dots/.config/test-app/config.conf # Add and commit git add . git commit -m "Add test config" -q # Test dry-run execution output=$(./update.sh -n --skip-notice 2>&1 || true) if [[ "$output" == *"DRY-RUN"* ]] && [[ "$output" == *"would"* || "$output" == *"Would"* ]]; then log_pass "Dry-run mode detected in output" else log_fail "Dry-run mode not properly indicated" fi # Verify no files were actually created in home if [[ ! -f "${HOME}/.config/test-app/config.conf" ]]; then log_pass "No files created in home during dry-run" else log_fail "Files were created in home during dry-run" fi cd "$ORIGINAL_DIR" || exit 1 cleanup_test_env } # Test 10: Test package directory detection test_package_detection() { log_test "Testing package directory detection" local test_repo test_repo=$(setup_test_env) TEST_DIR="$test_repo" cd "$test_repo" || { log_fail "Failed to cd to test directory"; return 1; } # Test different package directory names for dir_name in "dist-arch" "arch-packages" "sdist/arch"; do mkdir -p "$dir_name/test-pkg" echo "pkgbase=test-pkg" > "$dir_name/test-pkg/PKGBUILD" # Source to reset ARCH_PACKAGES_DIR source update.sh >/dev/null 2>&1 || true if [[ -d "$dir_name" ]]; then log_info "Found package directory: $dir_name" # The sourcing should have set ARCH_PACKAGES_DIR correctly if [[ -n "$ARCH_PACKAGES_DIR" ]]; then log_pass "Package directory detection works for $dir_name" else log_fail "Package directory not detected for $dir_name" fi fi rm -rf "$dir_name" done cd "$ORIGINAL_DIR" || exit 1 cleanup_test_env } # Test 11: Test force check mode test_force_check() { log_test "Testing force check mode" local test_repo test_repo=$(setup_test_env) TEST_DIR="$test_repo" cd "$test_repo" || { log_fail "Failed to cd to test directory"; return 1; } # Create test structure mkdir -p .config/test-app echo "config" > .config/test-app/settings.conf # Test with force flag output=$(./update.sh -f --skip-notice --dry-run 2>&1 || true) if [[ "$output" == *"Force check"* ]] || [[ "$output" == *"Force mode"* ]]; then log_pass "Force check mode detected" else log_fail "Force check mode not indicated" fi cd "$ORIGINAL_DIR" || exit 1 cleanup_test_env } # Test 12: Test conflict handling simulation test_conflict_handling() { log_test "Testing file conflict detection" local test_repo test_repo=$(setup_test_env) TEST_DIR="$test_repo" cd "$test_repo" || { log_fail "Failed to cd to test directory"; return 1; } # Create repo file mkdir -p .config/test-app echo "repo version" > .config/test-app/config.conf # Create different home file mkdir -p "${HOME}/.config/test-app" echo "home version" > "${HOME}/.config/test-app/config.conf" # Source the update.sh source update.sh >/dev/null 2>&1 || true # Test the comparison logic repo_file="$test_repo/.config/test-app/config.conf" home_file="${HOME}/.config/test-app/config.conf" if ! cmp -s "$repo_file" "$home_file"; then log_pass "File conflict correctly detected" else log_fail "File conflict not detected" fi # Cleanup home file rm -f "$home_file" rmdir "$(dirname "$home_file")" 2>/dev/null || true cd "$ORIGINAL_DIR" || exit 1 cleanup_test_env } # Test 13: Test all flags are recognized test_flags() { log_test "Testing command-line flags" local flags=("-h" "--help" "-n" "--dry-run" "-f" "--force" "-v" "--verbose") local all_passed=true for flag in "${flags[@]}"; do if ./update.sh "$flag" 2>&1 | grep -q -E "(Usage|dry-run|force|verbose|help)"; then echo " ✓ $flag recognized" else echo " ✗ $flag not recognized" all_passed=false fi done if [[ "$all_passed" == true ]]; then log_pass "All tested flags recognized correctly" else log_fail "Some flags not recognized properly" fi } # Test 14: Test git operations safety test_git_safety() { log_test "Testing git operations safety" local test_repo test_repo=$(setup_test_env) TEST_DIR="$test_repo" cd "$test_repo" || { log_fail "Failed to cd to test directory"; return 1; } # Create uncommitted changes echo "temp" > temp-file.txt # Test that script detects uncommitted changes output=$(./update.sh --dry-run --skip-notice 2>&1 || true) if [[ "$output" == *"uncommitted changes"* ]]; then log_pass "Uncommitted changes detection works" else log_fail "Uncommitted changes not detected" fi cd "$ORIGINAL_DIR" || exit 1 cleanup_test_env } # Test 15: Check for common shellcheck issues test_shellcheck() { log_test "Running shellcheck (if available)" if ! command -v shellcheck &>/dev/null; then log_info "shellcheck not found, skipping static analysis" return 0 fi # Run shellcheck with common exclusions if shellcheck -e SC1090,SC1091,SC2148,SC2034,SC2155,SC2164 update.sh; then log_pass "shellcheck passed" else log_fail "shellcheck found issues" return 1 fi } # Test 16: Test fresh clone scenario (no HEAD@{1}) test_fresh_clone() { log_test "Testing fresh clone scenario" local test_repo test_repo=$(setup_test_env) TEST_DIR="$test_repo" cd "$test_repo" || { log_fail "Failed to cd to test directory"; return 1; } # Create structure mkdir -p .config/test-app echo "config" > .config/test-app/settings.conf # Source the update.sh source update.sh >/dev/null 2>&1 || true # Test has_new_commits in fresh clone (no HEAD@{1}) if has_new_commits; then log_pass "Fresh clone scenario handled correctly" else log_fail "Fresh clone scenario not handled properly" fi cd "$ORIGINAL_DIR" || exit 1 cleanup_test_env } # Test 17: Test verbose mode test_verbose_mode() { log_test "Testing verbose mode" local test_repo test_repo=$(setup_test_env) TEST_DIR="$test_repo" cd "$test_repo" || { log_fail "Failed to cd to test directory"; return 1; } # Test verbose flag output=$(./update.sh -v --dry-run --skip-notice 2>&1 || true) if [[ "$output" == *"Verbose mode"* ]]; then log_pass "Verbose mode detected" else log_fail "Verbose mode not indicated" fi cd "$ORIGINAL_DIR" || exit 1 cleanup_test_env } # Test 18: Test package checking flag test_package_checking() { log_test "Testing package checking flag" local test_repo test_repo=$(setup_test_env) TEST_DIR="$test_repo" cd "$test_repo" || { log_fail "Failed to cd to test directory"; return 1; } # Test package flag output=$(./update.sh -p --dry-run --skip-notice 2>&1 || true) if [[ "$output" == *"Package checking"* ]]; then log_pass "Package checking mode detected" else log_fail "Package checking mode not indicated" fi cd "$ORIGINAL_DIR" || exit 1 cleanup_test_env } # Main test runner main() { echo -e "${BLUE}================================${NC}" echo -e "${BLUE} Update.sh Comprehensive Test Suite${NC}" echo -e "${BLUE}================================${NC}\n" # Check if we're in the right directory if [[ ! -f "update.sh" ]]; then log_error "Please run this test from the directory containing update.sh" exit 1 fi # Make sure update.sh is executable chmod +x update.sh 2>/dev/null || true # Run tests test_script_exists test_syntax test_help_option test_dots_structure test_flat_structure test_dots_mapping test_ignore_patterns test_safe_read_security test_dry_run test_package_detection test_force_check test_conflict_handling test_flags test_git_safety test_shellcheck test_fresh_clone test_verbose_mode test_package_checking # Summary echo -e "\n${BLUE}================================${NC}" echo -e "${BLUE} Test Summary${NC}" echo -e "${BLUE}================================${NC}" echo -e "${GREEN}Passed: $TESTS_PASSED${NC}" echo -e "${RED}Failed: $TESTS_FAILED${NC}" echo -e "${BLUE}Total: $((TESTS_PASSED + TESTS_FAILED))${NC}\n" if [[ $TESTS_FAILED -eq 0 ]]; then echo -e "${GREEN}All tests passed! 🎉${NC}\n" exit 0 else echo -e "${RED}Some tests failed! ❌${NC}\n" exit 1 fi } # Trap cleanup cleanup_on_exit() { if [[ -n "${TEST_DIR:-}" && -d "$TEST_DIR" ]]; then rm -rf "$TEST_DIR" fi # Cleanup any test files created in home rm -rf "${HOME}/.config/test-app" 2>/dev/null || true } trap cleanup_on_exit EXIT INT TERM # Run if executed directly if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then main "$@" fi