mirror of
https://github.com/tw93/Mole.git
synced 2026-03-22 19:40:07 +00:00
458 lines
12 KiB
Bash
458 lines
12 KiB
Bash
#!/usr/bin/env bats
|
|
|
|
setup_file() {
|
|
PROJECT_ROOT="$(cd "${BATS_TEST_DIRNAME}/.." && pwd)"
|
|
export PROJECT_ROOT
|
|
|
|
ORIGINAL_HOME="${HOME:-}"
|
|
export ORIGINAL_HOME
|
|
|
|
# Capture real GOCACHE before HOME is replaced with a temp dir.
|
|
# Without this, go build would use $HOME/Library/Caches/go-build inside the
|
|
# temp dir (empty), causing a full cold rebuild on every test run (~6s).
|
|
ORIGINAL_GOCACHE="$(go env GOCACHE 2>/dev/null || true)"
|
|
export ORIGINAL_GOCACHE
|
|
|
|
HOME="$(mktemp -d "${BATS_TEST_DIRNAME}/tmp-cli-home.XXXXXX")"
|
|
export HOME
|
|
|
|
mkdir -p "$HOME"
|
|
|
|
# Build Go binaries from current source for JSON tests.
|
|
# Point GOPATH/GOMODCACHE/GOCACHE at the real home so go build can reuse
|
|
# the module and build caches rather than doing a cold rebuild every run.
|
|
if command -v go > /dev/null 2>&1; then
|
|
ANALYZE_BIN="$(mktemp "${TMPDIR:-/tmp}/analyze-go.XXXXXX")"
|
|
STATUS_BIN="$(mktemp "${TMPDIR:-/tmp}/status-go.XXXXXX")"
|
|
GOPATH="${ORIGINAL_HOME}/go" GOMODCACHE="${ORIGINAL_HOME}/go/pkg/mod" \
|
|
GOCACHE="${ORIGINAL_GOCACHE}" \
|
|
go build -o "$ANALYZE_BIN" "$PROJECT_ROOT/cmd/analyze" 2>/dev/null
|
|
GOPATH="${ORIGINAL_HOME}/go" GOMODCACHE="${ORIGINAL_HOME}/go/pkg/mod" \
|
|
GOCACHE="${ORIGINAL_GOCACHE}" \
|
|
go build -o "$STATUS_BIN" "$PROJECT_ROOT/cmd/status" 2>/dev/null
|
|
export ANALYZE_BIN STATUS_BIN
|
|
fi
|
|
}
|
|
|
|
teardown_file() {
|
|
rm -rf "$HOME/.config/mole"
|
|
rm -rf "$HOME"
|
|
if [[ -n "${ORIGINAL_HOME:-}" ]]; then
|
|
export HOME="$ORIGINAL_HOME"
|
|
fi
|
|
rm -f "${ANALYZE_BIN:-}" "${STATUS_BIN:-}"
|
|
}
|
|
|
|
create_fake_utils() {
|
|
local dir="$1"
|
|
mkdir -p "$dir"
|
|
|
|
cat >"$dir/sudo" <<'SCRIPT'
|
|
#!/usr/bin/env bash
|
|
if [[ "$1" == "-n" || "$1" == "-v" ]]; then
|
|
exit 0
|
|
fi
|
|
exec "$@"
|
|
SCRIPT
|
|
chmod +x "$dir/sudo"
|
|
|
|
cat >"$dir/bioutil" <<'SCRIPT'
|
|
#!/usr/bin/env bash
|
|
if [[ "$1" == "-r" ]]; then
|
|
echo "Touch ID: 1"
|
|
exit 0
|
|
fi
|
|
exit 0
|
|
SCRIPT
|
|
chmod +x "$dir/bioutil"
|
|
}
|
|
|
|
setup() {
|
|
rm -rf "$HOME/.config/mole"
|
|
mkdir -p "$HOME/.config/mole"
|
|
}
|
|
|
|
@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 --version shows nightly channel metadata" {
|
|
expected_version="$(grep '^VERSION=' "$PROJECT_ROOT/mole" | head -1 | sed 's/VERSION=\"\(.*\)\"/\1/')"
|
|
mkdir -p "$HOME/.config/mole"
|
|
cat > "$HOME/.config/mole/install_channel" <<'EOF'
|
|
CHANNEL=nightly
|
|
EOF
|
|
|
|
run env HOME="$HOME" "$PROJECT_ROOT/mole" --version
|
|
[ "$status" -eq 0 ]
|
|
[[ "$output" == *"Mole version $expected_version"* ]]
|
|
[[ "$output" == *"Channel: Nightly"* ]]
|
|
}
|
|
|
|
@test "mole unknown command returns error" {
|
|
run env HOME="$HOME" "$PROJECT_ROOT/mole" unknown-command
|
|
[ "$status" -ne 0 ]
|
|
[[ "$output" == *"Unknown command: unknown-command"* ]]
|
|
}
|
|
|
|
@test "mole uninstall --whitelist returns unsupported option error" {
|
|
run env HOME="$HOME" "$PROJECT_ROOT/mole" uninstall --whitelist
|
|
[ "$status" -ne 0 ]
|
|
[[ "$output" == *"Unknown uninstall option: --whitelist"* ]]
|
|
}
|
|
|
|
@test "show_main_menu hides update shortcut when no update notice is available" {
|
|
run bash --noprofile --norc <<'EOF'
|
|
set -euo pipefail
|
|
HOME="$(mktemp -d)"
|
|
export HOME MOLE_TEST_MODE=1 MOLE_SKIP_MAIN=1
|
|
source "$PROJECT_ROOT/mole"
|
|
show_brand_banner() { printf 'banner\n'; }
|
|
show_menu_option() { printf '%s' "$2"; }
|
|
MAIN_MENU_BANNER=""
|
|
MAIN_MENU_UPDATE_MESSAGE=""
|
|
MAIN_MENU_SHOW_UPDATE=false
|
|
show_main_menu 1 true
|
|
EOF
|
|
|
|
[ "$status" -eq 0 ]
|
|
[[ "$output" != *"U Update"* ]]
|
|
}
|
|
|
|
@test "interactive_main_menu ignores U shortcut when update notice is hidden" {
|
|
run bash --noprofile --norc <<'EOF'
|
|
set -euo pipefail
|
|
HOME="$(mktemp -d)"
|
|
export HOME MOLE_TEST_MODE=1 MOLE_SKIP_MAIN=1
|
|
source "$PROJECT_ROOT/mole"
|
|
show_brand_banner() { :; }
|
|
show_main_menu() { :; }
|
|
hide_cursor() { :; }
|
|
show_cursor() { :; }
|
|
clear() { :; }
|
|
update_mole() { echo "UPDATE_CALLED"; }
|
|
state_file="$HOME/read_key_state"
|
|
read_key() {
|
|
if [[ ! -f "$state_file" ]]; then
|
|
: > "$state_file"
|
|
echo "UPDATE"
|
|
else
|
|
echo "QUIT"
|
|
fi
|
|
}
|
|
interactive_main_menu
|
|
EOF
|
|
|
|
[ "$status" -eq 0 ]
|
|
[[ "$output" != *"UPDATE_CALLED"* ]]
|
|
}
|
|
|
|
@test "interactive_main_menu accepts U shortcut when update notice is visible" {
|
|
run bash --noprofile --norc <<'EOF'
|
|
set -euo pipefail
|
|
HOME="$(mktemp -d)"
|
|
export HOME MOLE_TEST_MODE=1 MOLE_SKIP_MAIN=1
|
|
mkdir -p "$HOME/.cache/mole"
|
|
printf 'update available\n' > "$HOME/.cache/mole/update_message"
|
|
source "$PROJECT_ROOT/mole"
|
|
show_brand_banner() { :; }
|
|
show_main_menu() { :; }
|
|
hide_cursor() { :; }
|
|
show_cursor() { :; }
|
|
clear() { :; }
|
|
update_mole() { echo "UPDATE_CALLED"; }
|
|
read_key() { echo "UPDATE"; }
|
|
interactive_main_menu
|
|
EOF
|
|
|
|
[ "$status" -eq 0 ]
|
|
[[ "$output" == *"UPDATE_CALLED"* ]]
|
|
}
|
|
|
|
@test "touchid status reports current configuration" {
|
|
run env HOME="$HOME" "$PROJECT_ROOT/mole" touchid status
|
|
[ "$status" -eq 0 ]
|
|
[[ "$output" == *"Touch ID"* ]]
|
|
}
|
|
|
|
@test "mo optimize command is recognized" {
|
|
run bash -c "grep -q '\"optimize\")' '$PROJECT_ROOT/mole'"
|
|
[ "$status" -eq 0 ]
|
|
}
|
|
|
|
@test "mo analyze binary is valid" {
|
|
if [[ -f "$PROJECT_ROOT/bin/analyze-go" ]]; then
|
|
[ -x "$PROJECT_ROOT/bin/analyze-go" ]
|
|
run file "$PROJECT_ROOT/bin/analyze-go"
|
|
[[ "$output" == *"Mach-O"* ]] || [[ "$output" == *"executable"* ]]
|
|
else
|
|
skip "analyze-go binary not built"
|
|
fi
|
|
}
|
|
|
|
@test "mo clean --debug creates debug log file" {
|
|
mkdir -p "$HOME/.config/mole"
|
|
run env HOME="$HOME" TERM="xterm-256color" MOLE_TEST_MODE=1 MO_DEBUG=1 "$PROJECT_ROOT/mole" clean --dry-run
|
|
[ "$status" -eq 0 ]
|
|
MOLE_OUTPUT="$output"
|
|
|
|
DEBUG_LOG="$HOME/Library/Logs/mole/mole_debug_session.log"
|
|
[ -f "$DEBUG_LOG" ]
|
|
|
|
run grep "Mole Debug Session" "$DEBUG_LOG"
|
|
[ "$status" -eq 0 ]
|
|
|
|
[[ "$MOLE_OUTPUT" =~ "Debug session log saved to" ]]
|
|
}
|
|
|
|
@test "mo clean without debug does not show debug log path" {
|
|
mkdir -p "$HOME/.config/mole"
|
|
run env HOME="$HOME" TERM="xterm-256color" MOLE_TEST_MODE=1 MO_DEBUG=0 "$PROJECT_ROOT/mole" clean --dry-run
|
|
[ "$status" -eq 0 ]
|
|
|
|
[[ "$output" != *"Debug session log saved to"* ]]
|
|
}
|
|
|
|
@test "mo clean --debug logs system info" {
|
|
mkdir -p "$HOME/.config/mole"
|
|
run env HOME="$HOME" TERM="xterm-256color" MOLE_TEST_MODE=1 MO_DEBUG=1 "$PROJECT_ROOT/mole" clean --dry-run
|
|
[ "$status" -eq 0 ]
|
|
|
|
DEBUG_LOG="$HOME/Library/Logs/mole/mole_debug_session.log"
|
|
|
|
run grep "User:" "$DEBUG_LOG"
|
|
[ "$status" -eq 0 ]
|
|
|
|
run grep "Architecture:" "$DEBUG_LOG"
|
|
[ "$status" -eq 0 ]
|
|
}
|
|
|
|
@test "mo clean --help includes external volume option" {
|
|
run env HOME="$HOME" "$PROJECT_ROOT/mole" clean --help
|
|
[ "$status" -eq 0 ]
|
|
[[ "$output" == *"--external PATH"* ]]
|
|
}
|
|
|
|
@test "mo clean --external accepts canonicalized custom root" {
|
|
real_root="$(mktemp -d "$HOME/ext-real.XXXXXX")"
|
|
link_root="$HOME/ext-link"
|
|
ln -s "$real_root" "$link_root"
|
|
mkdir -p "$link_root/USB/.Trashes"
|
|
touch "$link_root/USB/.Trashes/cache.tmp"
|
|
|
|
mock_bin="$HOME/mock-bin"
|
|
mkdir -p "$mock_bin"
|
|
cat > "$mock_bin/diskutil" <<'EOF'
|
|
#!/usr/bin/env bash
|
|
exit 0
|
|
EOF
|
|
chmod +x "$mock_bin/diskutil"
|
|
|
|
run env HOME="$HOME" PATH="$mock_bin:$PATH" MOLE_EXTERNAL_VOLUMES_ROOT="$link_root" \
|
|
MOLE_TEST_NO_AUTH=1 "$PROJECT_ROOT/mole" clean --external "$link_root/USB" --dry-run
|
|
[ "$status" -eq 0 ]
|
|
[[ "$output" == *"Clean External Volume"* ]]
|
|
[[ "$output" == *"External volume cleanup"* ]]
|
|
}
|
|
|
|
@test "touchid status reflects pam file contents" {
|
|
pam_file="$HOME/pam_test"
|
|
cat >"$pam_file" <<'EOF'
|
|
auth sufficient pam_opendirectory.so
|
|
EOF
|
|
|
|
run env MOLE_PAM_SUDO_FILE="$pam_file" "$PROJECT_ROOT/bin/touchid.sh" status
|
|
[ "$status" -eq 0 ]
|
|
[[ "$output" == *"not configured"* ]]
|
|
|
|
cat >"$pam_file" <<'EOF'
|
|
auth sufficient pam_tid.so
|
|
EOF
|
|
|
|
run env MOLE_PAM_SUDO_FILE="$pam_file" "$PROJECT_ROOT/bin/touchid.sh" status
|
|
[ "$status" -eq 0 ]
|
|
[[ "$output" == *"enabled"* ]]
|
|
}
|
|
|
|
@test "enable_touchid inserts pam_tid line in pam file" {
|
|
pam_file="$HOME/pam_enable"
|
|
cat >"$pam_file" <<'EOF'
|
|
auth sufficient pam_opendirectory.so
|
|
EOF
|
|
|
|
fake_bin="$HOME/fake-bin"
|
|
create_fake_utils "$fake_bin"
|
|
|
|
run env PATH="$fake_bin:$PATH" MOLE_PAM_SUDO_FILE="$pam_file" "$PROJECT_ROOT/bin/touchid.sh" enable
|
|
[ "$status" -eq 0 ]
|
|
grep -q "pam_tid.so" "$pam_file"
|
|
[[ -f "${pam_file}.mole-backup" ]]
|
|
}
|
|
|
|
@test "disable_touchid removes pam_tid line" {
|
|
pam_file="$HOME/pam_disable"
|
|
cat >"$pam_file" <<'EOF'
|
|
auth sufficient pam_tid.so
|
|
auth sufficient pam_opendirectory.so
|
|
EOF
|
|
|
|
fake_bin="$HOME/fake-bin-disable"
|
|
create_fake_utils "$fake_bin"
|
|
|
|
run env PATH="$fake_bin:$PATH" MOLE_PAM_SUDO_FILE="$pam_file" "$PROJECT_ROOT/bin/touchid.sh" disable
|
|
[ "$status" -eq 0 ]
|
|
run grep "pam_tid.so" "$pam_file"
|
|
[ "$status" -ne 0 ]
|
|
}
|
|
|
|
@test "touchid enable --dry-run does not modify pam file" {
|
|
pam_file="$HOME/pam_enable_dry_run"
|
|
cat >"$pam_file" <<'EOF'
|
|
auth sufficient pam_opendirectory.so
|
|
EOF
|
|
|
|
run env MOLE_PAM_SUDO_FILE="$pam_file" "$PROJECT_ROOT/bin/touchid.sh" enable --dry-run
|
|
[ "$status" -eq 0 ]
|
|
[[ "$output" == *"DRY RUN MODE"* ]]
|
|
|
|
run grep "pam_tid.so" "$pam_file"
|
|
[ "$status" -ne 0 ]
|
|
}
|
|
|
|
# --- JSON output mode tests ---
|
|
|
|
@test "mo analyze --json outputs valid JSON with expected fields" {
|
|
if [[ ! -x "${ANALYZE_BIN:-}" ]]; then
|
|
skip "analyze binary not available (go not installed?)"
|
|
fi
|
|
|
|
run "$ANALYZE_BIN" --json /tmp
|
|
[ "$status" -eq 0 ]
|
|
|
|
# Validate it is parseable JSON
|
|
echo "$output" | python3 -c "import sys, json; json.load(sys.stdin)"
|
|
|
|
# Check required top-level keys
|
|
echo "$output" | python3 -c "
|
|
import sys, json
|
|
data = json.load(sys.stdin)
|
|
assert 'path' in data, 'missing path'
|
|
assert 'entries' in data, 'missing entries'
|
|
assert 'total_size' in data, 'missing total_size'
|
|
assert 'total_files' in data, 'missing total_files'
|
|
assert isinstance(data['entries'], list), 'entries is not a list'
|
|
"
|
|
}
|
|
|
|
@test "mo analyze --json entries contain required fields" {
|
|
if [[ ! -x "${ANALYZE_BIN:-}" ]]; then
|
|
skip "analyze binary not available (go not installed?)"
|
|
fi
|
|
|
|
run "$ANALYZE_BIN" --json /tmp
|
|
[ "$status" -eq 0 ]
|
|
|
|
echo "$output" | python3 -c "
|
|
import sys, json
|
|
data = json.load(sys.stdin)
|
|
for entry in data['entries']:
|
|
assert 'name' in entry, 'entry missing name'
|
|
assert 'path' in entry, 'entry missing path'
|
|
assert 'size' in entry, 'entry missing size'
|
|
assert 'is_dir' in entry, 'entry missing is_dir'
|
|
"
|
|
}
|
|
|
|
@test "mo analyze --json path reflects target directory" {
|
|
if [[ ! -x "${ANALYZE_BIN:-}" ]]; then
|
|
skip "analyze binary not available (go not installed?)"
|
|
fi
|
|
|
|
run "$ANALYZE_BIN" --json /tmp
|
|
[ "$status" -eq 0 ]
|
|
|
|
echo "$output" | python3 -c "
|
|
import sys, json
|
|
data = json.load(sys.stdin)
|
|
assert data['path'] == '/tmp' or data['path'] == '/private/tmp', \
|
|
f\"unexpected path: {data['path']}\"
|
|
"
|
|
}
|
|
|
|
@test "mo status --json outputs valid JSON with expected fields" {
|
|
if [[ ! -x "${STATUS_BIN:-}" ]]; then
|
|
skip "status binary not available (go not installed?)"
|
|
fi
|
|
|
|
run "$STATUS_BIN" --json
|
|
[ "$status" -eq 0 ]
|
|
|
|
# Validate it is parseable JSON
|
|
echo "$output" | python3 -c "import sys, json; json.load(sys.stdin)"
|
|
|
|
# Check required top-level keys
|
|
echo "$output" | python3 -c "
|
|
import sys, json
|
|
data = json.load(sys.stdin)
|
|
for key in ['cpu', 'memory', 'disks', 'health_score', 'host', 'uptime']:
|
|
assert key in data, f'missing key: {key}'
|
|
"
|
|
}
|
|
|
|
@test "mo status --json cpu section has expected structure" {
|
|
if [[ ! -x "${STATUS_BIN:-}" ]]; then
|
|
skip "status binary not available (go not installed?)"
|
|
fi
|
|
|
|
run "$STATUS_BIN" --json
|
|
[ "$status" -eq 0 ]
|
|
|
|
echo "$output" | python3 -c "
|
|
import sys, json
|
|
data = json.load(sys.stdin)
|
|
cpu = data['cpu']
|
|
assert 'usage' in cpu, 'cpu missing usage'
|
|
assert 'logical_cpu' in cpu, 'cpu missing logical_cpu'
|
|
assert isinstance(cpu['usage'], (int, float)), 'cpu usage is not a number'
|
|
"
|
|
}
|
|
|
|
@test "mo status --json memory section has expected structure" {
|
|
if [[ ! -x "${STATUS_BIN:-}" ]]; then
|
|
skip "status binary not available (go not installed?)"
|
|
fi
|
|
|
|
run "$STATUS_BIN" --json
|
|
[ "$status" -eq 0 ]
|
|
|
|
echo "$output" | python3 -c "
|
|
import sys, json
|
|
data = json.load(sys.stdin)
|
|
mem = data['memory']
|
|
assert 'total' in mem, 'memory missing total'
|
|
assert 'used' in mem, 'memory missing used'
|
|
assert 'used_percent' in mem, 'memory missing used_percent'
|
|
assert mem['total'] > 0, 'memory total should be positive'
|
|
"
|
|
}
|
|
|
|
@test "mo status --json piped to stdout auto-detects JSON mode" {
|
|
if [[ ! -x "${STATUS_BIN:-}" ]]; then
|
|
skip "status binary not available (go not installed?)"
|
|
fi
|
|
|
|
# When piped (not a tty), status should auto-detect and output JSON
|
|
output=$("$STATUS_BIN" 2>/dev/null)
|
|
echo "$output" | python3 -c "import sys, json; json.load(sys.stdin)"
|
|
}
|