diff --git a/mole b/mole index cb10c49..f1ed1fc 100755 --- a/mole +++ b/mole @@ -22,7 +22,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/lib/common.sh" # Version info -VERSION="1.10.16" +VERSION="1.10.17" MOLE_TAGLINE="can dig deep to clean your Mac." # Check if Touch ID is already configured diff --git a/tests/safe_functions.bats b/tests/safe_functions.bats new file mode 100644 index 0000000..bf00400 --- /dev/null +++ b/tests/safe_functions.bats @@ -0,0 +1,170 @@ +#!/usr/bin/env bats +# Tests for safe_* functions in lib/common.sh + +setup_file() { + PROJECT_ROOT="$(cd "${BATS_TEST_DIRNAME}/.." && pwd)" + export PROJECT_ROOT + + ORIGINAL_HOME="${HOME:-}" + export ORIGINAL_HOME + + HOME="$(mktemp -d "${BATS_TEST_DIRNAME}/tmp-safe-functions.XXXXXX")" + export HOME + + mkdir -p "$HOME" +} + +teardown_file() { + rm -rf "$HOME" + if [[ -n "${ORIGINAL_HOME:-}" ]]; then + export HOME="$ORIGINAL_HOME" + fi +} + +setup() { + source "$PROJECT_ROOT/lib/common.sh" + TEST_DIR="$HOME/test_safe_functions" + mkdir -p "$TEST_DIR" +} + +teardown() { + rm -rf "$TEST_DIR" +} + +# Test validate_path_for_deletion +@test "validate_path_for_deletion rejects empty path" { + run bash -c "source '$PROJECT_ROOT/lib/common.sh'; validate_path_for_deletion ''" + [ "$status" -eq 1 ] +} + +@test "validate_path_for_deletion rejects relative path" { + run bash -c "source '$PROJECT_ROOT/lib/common.sh'; validate_path_for_deletion 'relative/path'" + [ "$status" -eq 1 ] +} + +@test "validate_path_for_deletion rejects path traversal" { + run bash -c "source '$PROJECT_ROOT/lib/common.sh'; validate_path_for_deletion '/tmp/../etc'" + [ "$status" -eq 1 ] +} + +@test "validate_path_for_deletion rejects system directories" { + run bash -c "source '$PROJECT_ROOT/lib/common.sh'; validate_path_for_deletion '/System'" + [ "$status" -eq 1 ] + + run bash -c "source '$PROJECT_ROOT/lib/common.sh'; validate_path_for_deletion '/usr/bin'" + [ "$status" -eq 1 ] + + run bash -c "source '$PROJECT_ROOT/lib/common.sh'; validate_path_for_deletion '/etc'" + [ "$status" -eq 1 ] +} + +@test "validate_path_for_deletion accepts valid path" { + run bash -c "source '$PROJECT_ROOT/lib/common.sh'; validate_path_for_deletion '$TEST_DIR/valid'" + [ "$status" -eq 0 ] +} + +# Test safe_remove +@test "safe_remove validates path before deletion" { + run bash -c "source '$PROJECT_ROOT/lib/common.sh'; safe_remove '/System/test' 2>&1" + [ "$status" -eq 1 ] +} + +@test "safe_remove successfully removes file" { + local test_file="$TEST_DIR/test_file.txt" + echo "test" > "$test_file" + + run bash -c "source '$PROJECT_ROOT/lib/common.sh'; safe_remove '$test_file' true" + [ "$status" -eq 0 ] + [ ! -f "$test_file" ] +} + +@test "safe_remove successfully removes directory" { + local test_subdir="$TEST_DIR/test_subdir" + mkdir -p "$test_subdir" + touch "$test_subdir/file.txt" + + run bash -c "source '$PROJECT_ROOT/lib/common.sh'; safe_remove '$test_subdir' true" + [ "$status" -eq 0 ] + [ ! -d "$test_subdir" ] +} + +@test "safe_remove handles non-existent path gracefully" { + run bash -c "source '$PROJECT_ROOT/lib/common.sh'; safe_remove '$TEST_DIR/nonexistent' true" + [ "$status" -eq 0 ] +} + +@test "safe_remove in silent mode suppresses error output" { + # Try to remove system directory in silent mode + run bash -c "source '$PROJECT_ROOT/lib/common.sh'; safe_remove '/System/test' true 2>&1" + [ "$status" -eq 1 ] + # Should not output error in silent mode +} + +# Test safe_sudo_remove +@test "safe_sudo_remove rejects symlinks" { + local test_file="$TEST_DIR/real_file" + local test_link="$TEST_DIR/symlink" + touch "$test_file" + ln -s "$test_file" "$test_link" + + run bash -c "source '$PROJECT_ROOT/lib/common.sh'; safe_sudo_remove '$test_link' 2>&1" + [ "$status" -eq 1 ] + [[ "$output" == *"symlink"* ]] + + rm -f "$test_link" "$test_file" +} + +# Test safe_find_delete +@test "safe_find_delete validates base directory" { + run bash -c "source '$PROJECT_ROOT/lib/common.sh'; safe_find_delete '/nonexistent' '*.tmp' 7 'f' 2>&1" + [ "$status" -eq 1 ] +} + +@test "safe_find_delete rejects symlinked directory" { + local real_dir="$TEST_DIR/real" + local link_dir="$TEST_DIR/link" + mkdir -p "$real_dir" + ln -s "$real_dir" "$link_dir" + + run bash -c "source '$PROJECT_ROOT/lib/common.sh'; safe_find_delete '$link_dir' '*.tmp' 7 'f' 2>&1" + [ "$status" -eq 1 ] + [[ "$output" == *"symlink"* ]] + + rm -rf "$link_dir" "$real_dir" +} + +@test "safe_find_delete validates type filter" { + run bash -c "source '$PROJECT_ROOT/lib/common.sh'; safe_find_delete '$TEST_DIR' '*.tmp' 7 'x' 2>&1" + [ "$status" -eq 1 ] + [[ "$output" == *"Invalid type filter"* ]] +} + +@test "safe_find_delete deletes old files" { + # Create test files with different ages + local old_file="$TEST_DIR/old.tmp" + local new_file="$TEST_DIR/new.tmp" + + touch "$old_file" + touch "$new_file" + + # Make old_file 8 days old (requires touch -t) + touch -t "$(date -v-8d '+%Y%m%d%H%M.%S' 2>/dev/null || date -d '8 days ago' '+%Y%m%d%H%M.%S')" "$old_file" 2>/dev/null || true + + run bash -c "source '$PROJECT_ROOT/lib/common.sh'; safe_find_delete '$TEST_DIR' '*.tmp' 7 'f'" + [ "$status" -eq 0 ] +} + +# Test MOLE constants are defined +@test "MOLE_* constants are defined" { + run bash -c "source '$PROJECT_ROOT/lib/common.sh'; echo \$MOLE_TEMP_FILE_AGE_DAYS" + [ "$status" -eq 0 ] + [ "$output" = "7" ] + + run bash -c "source '$PROJECT_ROOT/lib/common.sh'; echo \$MOLE_MAX_PARALLEL_JOBS" + [ "$status" -eq 0 ] + [ "$output" = "15" ] + + run bash -c "source '$PROJECT_ROOT/lib/common.sh'; echo \$MOLE_TM_BACKUP_SAFE_HOURS" + [ "$status" -eq 0 ] + [ "$output" = "48" ] +}