1
0
mirror of https://github.com/tw93/Mole.git synced 2026-02-04 15:39:42 +00:00

Complete automated testing

This commit is contained in:
Tw93
2025-10-12 14:17:40 +08:00
parent 41047d6ee8
commit f9a93f6052
10 changed files with 738 additions and 0 deletions

22
.github/workflows/tests.yml vendored Normal file
View File

@@ -0,0 +1,22 @@
name: Mole Tests
on:
push:
branches: [main]
pull_request:
jobs:
test:
runs-on: macos-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install bats-core
run: |
brew update
brew install bats-core
- name: Run test suite
run: tests/run.sh

71
tests/analyze.bats Normal file
View File

@@ -0,0 +1,71 @@
#!/usr/bin/env bats
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-analyze-home.XXXXXX")"
export HOME
}
teardown_file() {
rm -rf "$HOME"
if [[ -n "${ORIGINAL_HOME:-}" ]]; then
export HOME="$ORIGINAL_HOME"
fi
}
setup() {
export TERM="dumb"
rm -rf "${HOME:?}"/*
mkdir -p "$HOME"
}
@test "scan_directories lists largest folders first" {
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc <<'EOF'
set -euo pipefail
source "$PROJECT_ROOT/bin/analyze.sh"
root="$HOME/analyze-root"
mkdir -p "$root/Small" "$root/Large"
printf 'tiny' > "$root/Small/file.txt"
dd if=/dev/zero of="$root/Large/big.dat" bs=1024 count=200 >/dev/null 2>&1
output_file="$HOME/directories.txt"
scan_directories "$root" "$output_file" 1
head -n1 "$output_file"
EOF
[ "$status" -eq 0 ]
[[ "$output" == *"Large"* ]]
}
@test "aggregate_by_directory sums child sizes per parent" {
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc <<'EOF'
set -euo pipefail
source "$PROJECT_ROOT/bin/analyze.sh"
root="$HOME/group"
mkdir -p "$root/a" "$root/b"
input_file="$HOME/files.txt"
cat > "$input_file" <<LIST
1024|$root/a/file1
2048|$root/a/file2
512|$root/b/data.bin
LIST
output_file="$HOME/aggregated.txt"
aggregate_by_directory "$input_file" "$output_file"
cat "$output_file"
EOF
[ "$status" -eq 0 ]
[[ "$output" == *"3072|$HOME/group/a/"* ]]
[[ "$output" == *"512|$HOME/group/b/"* ]]
}

60
tests/clean.bats Normal file
View File

@@ -0,0 +1,60 @@
#!/usr/bin/env bats
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-clean-home.XXXXXX")"
export HOME
mkdir -p "$HOME"
}
teardown_file() {
rm -rf "$HOME"
if [[ -n "${ORIGINAL_HOME:-}" ]]; then
export HOME="$ORIGINAL_HOME"
fi
}
setup() {
export TERM="xterm-256color"
rm -rf "${HOME:?}"/*
rm -rf "$HOME/Library" "$HOME/.config"
mkdir -p "$HOME/Library/Caches" "$HOME/.config/mole"
}
@test "mo clean --dry-run skips system cleanup in non-interactive mode" {
run env HOME="$HOME" "$PROJECT_ROOT/mole" clean --dry-run
[ "$status" -eq 0 ]
[[ "$output" == *"Dry Run Mode"* ]]
[[ "$output" != *"Deep system-level cleanup"* ]]
}
@test "mo clean --dry-run reports user cache without deleting it" {
mkdir -p "$HOME/Library/Caches/TestApp"
echo "cache data" > "$HOME/Library/Caches/TestApp/cache.tmp"
run env HOME="$HOME" "$PROJECT_ROOT/mole" clean --dry-run
[ "$status" -eq 0 ]
[[ "$output" == *"User app cache"* ]]
[[ "$output" == *"Potential space"* ]]
[ -f "$HOME/Library/Caches/TestApp/cache.tmp" ]
}
@test "mo clean honors whitelist entries" {
mkdir -p "$HOME/Library/Caches/WhitelistedApp"
echo "keep me" > "$HOME/Library/Caches/WhitelistedApp/data.tmp"
cat > "$HOME/.config/mole/whitelist" <<EOF
$HOME/Library/Caches/WhitelistedApp*
EOF
run env HOME="$HOME" "$PROJECT_ROOT/mole" clean --dry-run
[ "$status" -eq 0 ]
[[ "$output" == *"Protected: 1"* ]]
[ -f "$HOME/Library/Caches/WhitelistedApp/data.tmp" ]
}

80
tests/cli.bats Normal file
View File

@@ -0,0 +1,80 @@
#!/usr/bin/env bats
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-cli-home.XXXXXX")"
export HOME
mkdir -p "$HOME"
}
teardown_file() {
rm -rf "$HOME"
if [[ -n "${ORIGINAL_HOME:-}" ]]; then
export HOME="$ORIGINAL_HOME"
fi
}
setup() {
rm -rf "$HOME/.config"
mkdir -p "$HOME"
}
@test "mole --help prints command overview" {
run env HOME="$HOME" "$PROJECT_ROOT/mole" --help
[ "$status" -eq 0 ]
[[ "$output" == *"mo clean"* ]]
[[ "$output" == *"mo analyze"* ]]
}
@test "mole --version reports script version" {
expected_version="$(grep '^VERSION=' "$PROJECT_ROOT/mole" | head -1 | sed 's/VERSION=\"\(.*\)\"/\1/')"
run env HOME="$HOME" "$PROJECT_ROOT/mole" --version
[ "$status" -eq 0 ]
[[ "$output" == *"$expected_version"* ]]
}
@test "mole unknown command returns error" {
run env HOME="$HOME" "$PROJECT_ROOT/mole" unknown-command
[ "$status" -ne 0 ]
[[ "$output" == *"Unknown command: unknown-command"* ]]
}
@test "clean.sh --help shows usage details" {
run env HOME="$HOME" "$PROJECT_ROOT/bin/clean.sh" --help
[ "$status" -eq 0 ]
[[ "$output" == *"Mole - Deeper system cleanup"* ]]
[[ "$output" == *"--dry-run"* ]]
}
@test "uninstall.sh --help highlights controls" {
run env HOME="$HOME" "$PROJECT_ROOT/bin/uninstall.sh" --help
[ "$status" -eq 0 ]
[[ "$output" == *"Usage: mole uninstall"* ]]
[[ "$output" == *"Keyboard Controls"* ]]
}
@test "analyze.sh --help outlines explorer features" {
run env HOME="$HOME" "$PROJECT_ROOT/bin/analyze.sh" --help
[ "$status" -eq 0 ]
[[ "$output" == *"Interactive disk space explorer"* ]]
[[ "$output" == *"mole analyze"* ]]
}
@test "touchid --help describes configuration options" {
run env HOME="$HOME" "$PROJECT_ROOT/mole" touchid --help
[ "$status" -eq 0 ]
[[ "$output" == *"Touch ID"* ]]
[[ "$output" == *"mo touchid enable"* ]]
}
@test "touchid status reports current configuration" {
run env HOME="$HOME" "$PROJECT_ROOT/mole" touchid status
[ "$status" -eq 0 ]
[[ "$output" == *"Touch ID"* ]]
}

140
tests/common.bats Normal file
View File

@@ -0,0 +1,140 @@
#!/usr/bin/env bats
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-home.XXXXXX")"
export HOME
mkdir -p "$HOME"
}
teardown_file() {
rm -rf "$HOME"
if [[ -n "${ORIGINAL_HOME:-}" ]]; then
export HOME="$ORIGINAL_HOME"
fi
}
setup() {
rm -rf "$HOME/.config"
mkdir -p "$HOME"
}
teardown() {
unset MO_SPINNER_CHARS || true
}
@test "mo_spinner_chars returns default sequence when unset" {
result="$(HOME="$HOME" bash --noprofile --norc -c "source '$PROJECT_ROOT/lib/common.sh'; mo_spinner_chars")"
[ "$result" = "|/-\\" ]
}
@test "mo_spinner_chars respects MO_SPINNER_CHARS override" {
export MO_SPINNER_CHARS="abcd"
result="$(HOME="$HOME" MO_SPINNER_CHARS="$MO_SPINNER_CHARS" bash --noprofile --norc -c "source '$PROJECT_ROOT/lib/common.sh'; mo_spinner_chars")"
[ "$result" = "abcd" ]
}
@test "detect_architecture maps current CPU to friendly label" {
expected="Intel"
if [[ "$(uname -m)" == "arm64" ]]; then
expected="Apple Silicon"
fi
result="$(HOME="$HOME" bash --noprofile --norc -c "source '$PROJECT_ROOT/lib/common.sh'; detect_architecture")"
[ "$result" = "$expected" ]
}
@test "get_free_space returns a non-empty value" {
result="$(HOME="$HOME" bash --noprofile --norc -c "source '$PROJECT_ROOT/lib/common.sh'; get_free_space")"
[[ -n "$result" ]]
}
@test "log_info prints message and appends to log file" {
local message="Informational message from test"
local stdout_output
stdout_output="$(HOME="$HOME" bash --noprofile --norc -c "source '$PROJECT_ROOT/lib/common.sh'; log_info '$message'")"
[[ "$stdout_output" == *"$message"* ]]
local log_file="$HOME/.config/mole/mole.log"
[[ -f "$log_file" ]]
grep -q "INFO: $message" "$log_file"
}
@test "log_error writes to stderr and log file" {
local message="Something went wrong"
local stderr_file="$HOME/log_error_stderr.txt"
HOME="$HOME" bash --noprofile --norc -c "source '$PROJECT_ROOT/lib/common.sh'; log_error '$message' 1>/dev/null 2>'$stderr_file'"
[[ -s "$stderr_file" ]]
grep -q "$message" "$stderr_file"
local log_file="$HOME/.config/mole/mole.log"
[[ -f "$log_file" ]]
grep -q "ERROR: $message" "$log_file"
}
@test "bytes_to_human converts byte counts into readable units" {
output="$(HOME="$HOME" bash --noprofile --norc <<'EOF'
source "$PROJECT_ROOT/lib/common.sh"
bytes_to_human 512
bytes_to_human 2048
bytes_to_human $((5 * 1024 * 1024))
bytes_to_human $((3 * 1024 * 1024 * 1024))
EOF
)"
bytes_lines=()
while IFS= read -r line; do
bytes_lines+=("$line")
done <<< "$output"
[ "${bytes_lines[0]}" = "512B" ]
[ "${bytes_lines[1]}" = "2KB" ]
[ "${bytes_lines[2]}" = "5.0MB" ]
[ "${bytes_lines[3]}" = "3.00GB" ]
}
@test "create_temp_file and create_temp_dir are tracked and cleaned" {
HOME="$HOME" bash --noprofile --norc <<'EOF'
source "$PROJECT_ROOT/lib/common.sh"
create_temp_file > "$HOME/temp_file_path.txt"
create_temp_dir > "$HOME/temp_dir_path.txt"
cleanup_temp_files
EOF
file_path="$(cat "$HOME/temp_file_path.txt")"
dir_path="$(cat "$HOME/temp_dir_path.txt")"
[ ! -e "$file_path" ]
[ ! -e "$dir_path" ]
rm -f "$HOME/temp_file_path.txt" "$HOME/temp_dir_path.txt"
}
@test "parallel_execute runs worker across all items" {
output_file="$HOME/parallel_output.txt"
HOME="$HOME" bash --noprofile --norc <<'EOF'
source "$PROJECT_ROOT/lib/common.sh"
worker() {
echo "$1" >> "$HOME/parallel_output.txt"
}
parallel_execute 2 worker "first" "second" "third"
EOF
sort "$output_file" > "$output_file.sorted"
results=()
while IFS= read -r line; do
results+=("$line")
done < "$output_file.sorted"
[ "${#results[@]}" -eq 3 ]
joined=" ${results[*]} "
[[ "$joined" == *" first "* ]]
[[ "$joined" == *" second "* ]]
[[ "$joined" == *" third "* ]]
rm -f "$output_file" "$output_file.sorted"
}

View File

@@ -0,0 +1,22 @@
#!/usr/bin/env bash
# shellcheck disable=SC2329
# Helper stub definitions for uninstall tests
setup_uninstall_stubs() {
request_sudo_access() { return 0; }
start_inline_spinner() { :; }
stop_inline_spinner() { :; }
enter_alt_screen() { :; }
leave_alt_screen() { :; }
hide_cursor() { :; }
show_cursor() { :; }
remove_apps_from_dock() { :; }
pgrep() { return 1; }
pkill() { return 0; }
sudo() { return 0; }
export -f request_sudo_access start_inline_spinner stop_inline_spinner \
enter_alt_screen leave_alt_screen hide_cursor show_cursor \
remove_apps_from_dock pgrep pkill sudo || true
}

29
tests/run.sh Executable file
View File

@@ -0,0 +1,29 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
if ! command -v bats >/dev/null 2>&1; then
cat <<'EOF' >&2
bats is required to run Mole's test suite.
Install via Homebrew with 'brew install bats-core' or via npm with 'npm install -g bats'.
EOF
exit 1
fi
cd "$PROJECT_ROOT"
if [[ -z "${TERM:-}" ]]; then
export TERM="xterm-256color"
fi
if [[ $# -eq 0 ]]; then
set -- tests
fi
if [[ -t 1 ]]; then
bats -p "$@"
else
TERM="${TERM:-xterm-256color}" bats --tap "$@"
fi

104
tests/uninstall.bats Normal file
View File

@@ -0,0 +1,104 @@
#!/usr/bin/env bats
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-uninstall-home.XXXXXX")"
export HOME
}
teardown_file() {
rm -rf "$HOME"
if [[ -n "${ORIGINAL_HOME:-}" ]]; then
export HOME="$ORIGINAL_HOME"
fi
}
setup() {
export TERM="dumb"
rm -rf "${HOME:?}"/*
mkdir -p "$HOME"
}
create_app_artifacts() {
mkdir -p "$HOME/Applications/TestApp.app"
mkdir -p "$HOME/Library/Application Support/TestApp"
mkdir -p "$HOME/Library/Caches/TestApp"
mkdir -p "$HOME/Library/Containers/com.example.TestApp"
mkdir -p "$HOME/Library/Preferences"
touch "$HOME/Library/Preferences/com.example.TestApp.plist"
mkdir -p "$HOME/Library/Preferences/ByHost"
touch "$HOME/Library/Preferences/ByHost/com.example.TestApp.ABC123.plist"
mkdir -p "$HOME/Library/Saved Application State/com.example.TestApp.savedState"
}
@test "find_app_files discovers user-level leftovers" {
create_app_artifacts
result="$(HOME="$HOME" bash --noprofile --norc <<'EOF'
set -euo pipefail
source "$PROJECT_ROOT/lib/common.sh"
find_app_files "com.example.TestApp" "TestApp"
EOF
)"
[[ "$result" == *"Application Support/TestApp"* ]]
[[ "$result" == *"Caches/TestApp"* ]]
[[ "$result" == *"Preferences/com.example.TestApp.plist"* ]]
[[ "$result" == *"Saved Application State/com.example.TestApp.savedState"* ]]
[[ "$result" == *"Containers/com.example.TestApp"* ]]
}
@test "calculate_total_size returns aggregate kilobytes" {
mkdir -p "$HOME/sized"
dd if=/dev/zero of="$HOME/sized/file1" bs=1024 count=1 >/dev/null 2>&1
dd if=/dev/zero of="$HOME/sized/file2" bs=1024 count=2 >/dev/null 2>&1
result="$(HOME="$HOME" bash --noprofile --norc <<'EOF'
set -euo pipefail
source "$PROJECT_ROOT/lib/common.sh"
files="$(printf '%s\n%s\n' "$HOME/sized/file1" "$HOME/sized/file2")"
calculate_total_size "$files"
EOF
)"
# Result should be >=3 KB (some filesystems allocate slightly more)
[ "$result" -ge 3 ]
}
@test "batch_uninstall_applications removes selected app data" {
create_app_artifacts
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc <<'EOF'
set -euo pipefail
source "$PROJECT_ROOT/lib/common.sh"
source "$PROJECT_ROOT/lib/batch_uninstall.sh"
source "$PROJECT_ROOT/tests/helpers/uninstall_stubs.sh"
setup_uninstall_stubs
app_bundle="$HOME/Applications/TestApp.app"
mkdir -p "$app_bundle"
related="$(find_app_files "com.example.TestApp" "TestApp")"
encoded_related=$(printf '%s' "$related" | base64 | tr -d '\n')
selected_apps=()
selected_apps+=("0|$app_bundle|TestApp|com.example.TestApp|0|Never")
files_cleaned=0
total_items=0
total_size_cleaned=0
printf '\n' | batch_uninstall_applications >/dev/null
[[ ! -d "$app_bundle" ]] || exit 1
[[ ! -d "$HOME/Library/Application Support/TestApp" ]] || exit 1
[[ ! -d "$HOME/Library/Caches/TestApp" ]] || exit 1
[[ ! -f "$HOME/Library/Preferences/com.example.TestApp.plist" ]] || exit 1
EOF
[ "$status" -eq 0 ]
}

111
tests/update_remove.bats Normal file
View File

@@ -0,0 +1,111 @@
#!/usr/bin/env bats
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-update-home.XXXXXX")"
export HOME
}
teardown_file() {
rm -rf "$HOME"
if [[ -n "${ORIGINAL_HOME:-}" ]]; then
export HOME="$ORIGINAL_HOME"
fi
}
setup() {
export TERM="dumb"
rm -rf "${HOME:?}"/*
mkdir -p "$HOME"
}
@test "update_via_homebrew reports already on latest version" {
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc <<'EOF'
set -euo pipefail
MOLE_TEST_BREW_UPDATE_OUTPUT="Updated 0 formulae"
MOLE_TEST_BREW_UPGRADE_OUTPUT="Warning: mole 1.7.9 already installed"
MOLE_TEST_BREW_LIST_OUTPUT="mole 1.7.9"
start_inline_spinner() { :; }
stop_inline_spinner() { :; }
brew() {
case "$1" in
update) echo "$MOLE_TEST_BREW_UPDATE_OUTPUT";;
upgrade) echo "$MOLE_TEST_BREW_UPGRADE_OUTPUT";;
list) if [[ "$2" == "--versions" ]]; then echo "$MOLE_TEST_BREW_LIST_OUTPUT"; fi ;;
esac
}
export -f brew start_inline_spinner stop_inline_spinner
source "$PROJECT_ROOT/lib/common.sh"
update_via_homebrew "1.7.9"
EOF
[ "$status" -eq 0 ]
[[ "$output" == *"Already on latest version"* ]]
}
@test "update_mole skips download when already latest" {
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" PATH="$HOME/fake-bin:/usr/bin:/bin" TERM="dumb" bash --noprofile --norc <<'EOF'
set -euo pipefail
mkdir -p "$HOME/fake-bin"
cat > "$HOME/fake-bin/curl" <<'SCRIPT'
#!/usr/bin/env bash
out=""
while [[ $# -gt 0 ]]; do
case "$1" in
-o)
out="$2"
shift 2
;;
*)
shift
;;
esac
done
if [[ -n "$out" ]]; then
cat <<'INSTALLER' > "$out"
#!/usr/bin/env bash
echo "Installer executed"
INSTALLER
else
echo 'VERSION="1.7.9"'
fi
SCRIPT
chmod +x "$HOME/fake-bin/curl"
cat > "$HOME/fake-bin/brew" <<'SCRIPT'
#!/usr/bin/env bash
exit 1
SCRIPT
chmod +x "$HOME/fake-bin/brew"
"$PROJECT_ROOT/mole" update
EOF
[ "$status" -eq 0 ]
[[ "$output" == *"Already on latest version"* ]]
}
@test "remove_mole deletes manual binaries and caches" {
mkdir -p "$HOME/.local/bin"
touch "$HOME/.local/bin/mole"
touch "$HOME/.local/bin/mo"
mkdir -p "$HOME/.config/mole" "$HOME/.cache/mole"
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" PATH="/usr/bin:/bin" bash --noprofile --norc <<'EOF'
set -euo pipefail
start_inline_spinner() { :; }
stop_inline_spinner() { :; }
export -f start_inline_spinner stop_inline_spinner
printf '\n' | "$PROJECT_ROOT/mole" remove
EOF
[ "$status" -eq 0 ]
[ ! -f "$HOME/.local/bin/mole" ]
[ ! -f "$HOME/.local/bin/mo" ]
[ ! -d "$HOME/.config/mole" ]
[ ! -d "$HOME/.cache/mole" ]
}

99
tests/whitelist.bats Normal file
View File

@@ -0,0 +1,99 @@
#!/usr/bin/env bats
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-whitelist-home.XXXXXX")"
export HOME
mkdir -p "$HOME"
}
teardown_file() {
rm -rf "$HOME"
if [[ -n "${ORIGINAL_HOME:-}" ]]; then
export HOME="$ORIGINAL_HOME"
fi
}
setup() {
rm -rf "$HOME/.config"
mkdir -p "$HOME"
WHITELIST_PATH="$HOME/.config/mole/whitelist"
}
@test "patterns_equivalent treats paths with tilde expansion as equal" {
local status
if HOME="$HOME" bash --noprofile --norc -c "source '$PROJECT_ROOT/lib/whitelist_manager.sh'; patterns_equivalent '~/.cache/test' \"\$HOME/.cache/test\""; then
status=0
else
status=$?
fi
[ "$status" -eq 0 ]
}
@test "patterns_equivalent distinguishes different paths" {
local status
if HOME="$HOME" bash --noprofile --norc -c "source '$PROJECT_ROOT/lib/whitelist_manager.sh'; patterns_equivalent '~/.cache/test' \"\$HOME/.cache/other\""; then
status=0
else
status=$?
fi
[ "$status" -ne 0 ]
}
@test "save_whitelist_patterns keeps unique entries and preserves header" {
HOME="$HOME" bash --noprofile --norc -c "source '$PROJECT_ROOT/lib/whitelist_manager.sh'; save_whitelist_patterns \"\$HOME/.cache/foo\" \"\$HOME/.cache/foo\" \"\$HOME/.cache/bar\""
[[ -f "$WHITELIST_PATH" ]]
lines=()
while IFS= read -r line; do
lines+=("$line")
done < "$WHITELIST_PATH"
# Header is at least two lines (comments), plus two unique patterns
[ "${#lines[@]}" -ge 4 ]
# Ensure duplicate was not written twice
occurrences=$(grep -c "$HOME/.cache/foo" "$WHITELIST_PATH")
[ "$occurrences" -eq 1 ]
}
@test "load_whitelist falls back to defaults when config missing" {
rm -f "$WHITELIST_PATH"
HOME="$HOME" bash --noprofile --norc -c "source '$PROJECT_ROOT/lib/whitelist_manager.sh'; rm -f \"\$HOME/.config/mole/whitelist\"; load_whitelist; printf '%s\n' \"\${CURRENT_WHITELIST_PATTERNS[@]}\"" > "$HOME/current_whitelist.txt"
HOME="$HOME" bash --noprofile --norc -c "source '$PROJECT_ROOT/lib/whitelist_manager.sh'; printf '%s\n' \"\${DEFAULT_WHITELIST_PATTERNS[@]}\"" > "$HOME/default_whitelist.txt"
current=()
while IFS= read -r line; do
current+=("$line")
done < "$HOME/current_whitelist.txt"
defaults=()
while IFS= read -r line; do
defaults+=("$line")
done < "$HOME/default_whitelist.txt"
[ "${#current[@]}" -eq "${#defaults[@]}" ]
[ "${current[0]}" = "${defaults[0]/\$HOME/$HOME}" ]
}
@test "is_whitelisted matches saved patterns exactly" {
local status
if HOME="$HOME" bash --noprofile --norc -c "source '$PROJECT_ROOT/lib/whitelist_manager.sh'; save_whitelist_patterns \"\$HOME/.cache/unique-pattern\"; load_whitelist; is_whitelisted \"\$HOME/.cache/unique-pattern\""; then
status=0
else
status=$?
fi
[ "$status" -eq 0 ]
if HOME="$HOME" bash --noprofile --norc -c "source '$PROJECT_ROOT/lib/whitelist_manager.sh'; save_whitelist_patterns \"\$HOME/.cache/unique-pattern\"; load_whitelist; is_whitelisted \"\$HOME/.cache/other-pattern\""; then
status=0
else
status=$?
fi
[ "$status" -ne 0 ]
}