diff --git a/lib/core/base.sh b/lib/core/base.sh index 3831732..e5a65ae 100644 --- a/lib/core/base.sh +++ b/lib/core/base.sh @@ -168,12 +168,14 @@ get_free_space() { } # Get Darwin kernel major version (e.g., 24 for 24.2.0) +# Returns 999 on failure to adopt conservative behavior (assume modern system) get_darwin_major() { local kernel kernel=$(uname -r 2> /dev/null || true) local major="${kernel%%.*}" if [[ ! "$major" =~ ^[0-9]+$ ]]; then - major=0 + # Return high number to skip potentially dangerous operations on unknown systems + major=999 fi echo "$major" } @@ -321,7 +323,17 @@ ensure_user_dir() { local dir="$target_path" while [[ -n "$dir" && "$dir" != "/" ]]; do + # Early stop: if ownership is already correct, no need to continue up the tree + if [[ -d "$dir" ]]; then + local current_uid + current_uid=$("$STAT_BSD" -f%u "$dir" 2> /dev/null || echo "") + if [[ "$current_uid" == "$owner_uid" ]]; then + break + fi + fi + chown "$owner_uid:$owner_gid" "$dir" 2> /dev/null || true + if [[ "$dir" == "$user_home" ]]; then break fi diff --git a/tests/user_file_handling.bats b/tests/user_file_handling.bats new file mode 100644 index 0000000..f1f2629 --- /dev/null +++ b/tests/user_file_handling.bats @@ -0,0 +1,252 @@ +#!/usr/bin/env bats + +# Tests for user file handling utilities in lib/core/base.sh +# Covers: ensure_user_dir, ensure_user_file, get_invoking_user, etc. + +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-userfile.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" "$HOME/.cache" + mkdir -p "$HOME" +} + +# ============================================================================ +# Darwin Version Detection Tests +# ============================================================================ + +@test "get_darwin_major returns numeric version on macOS" { + result=$(bash -c "source '$PROJECT_ROOT/lib/core/base.sh'; get_darwin_major") + # Should be a number (e.g., 23, 24, etc.) + [[ "$result" =~ ^[0-9]+$ ]] +} + +@test "get_darwin_major returns 999 on failure (mock uname failure)" { + # Mock uname to fail and verify fallback behavior + result=$(bash -c " + uname() { return 1; } + export -f uname + source '$PROJECT_ROOT/lib/core/base.sh' + get_darwin_major + ") + [ "$result" = "999" ] +} + +@test "is_darwin_ge correctly compares versions" { + # Should return true for minimum <= current + bash -c "source '$PROJECT_ROOT/lib/core/base.sh'; is_darwin_ge 1" + [ $? -eq 0 ] + + # Should return false for very high version requirement (unless on futuristic macOS) + # Note: With our 999 fallback, this will actually succeed on error, which is correct behavior + result=$(bash -c "source '$PROJECT_ROOT/lib/core/base.sh'; is_darwin_ge 100 && echo 'yes' || echo 'no'") + # Just verify command runs without error + [[ -n "$result" ]] +} + +# ============================================================================ +# User Context Detection Tests +# ============================================================================ + +@test "is_root_user detects non-root correctly" { + result=$(bash -c "source '$PROJECT_ROOT/lib/core/base.sh'; is_root_user && echo 'root' || echo 'not-root'") + [ "$result" = "not-root" ] +} + +@test "get_invoking_user returns current user when not sudo" { + result=$(bash -c "source '$PROJECT_ROOT/lib/core/base.sh'; get_invoking_user") + [ -n "$result" ] + # Should be current user + [ "$result" = "${USER:-$(whoami)}" ] +} + +@test "get_invoking_uid returns numeric UID" { + result=$(bash -c "source '$PROJECT_ROOT/lib/core/base.sh'; get_invoking_uid") + [[ "$result" =~ ^[0-9]+$ ]] +} + +@test "get_invoking_gid returns numeric GID" { + result=$(bash -c "source '$PROJECT_ROOT/lib/core/base.sh'; get_invoking_gid") + [[ "$result" =~ ^[0-9]+$ ]] +} + +@test "get_invoking_home returns home directory" { + result=$(bash -c "source '$PROJECT_ROOT/lib/core/base.sh'; get_invoking_home") + [ -n "$result" ] + [ -d "$result" ] +} + +@test "get_user_home returns home for valid user" { + current_user="${USER:-$(whoami)}" + result=$(bash -c "source '$PROJECT_ROOT/lib/core/base.sh'; get_user_home '$current_user'") + [ -n "$result" ] + [ -d "$result" ] +} + +@test "get_user_home returns empty for invalid user" { + result=$(bash -c "source '$PROJECT_ROOT/lib/core/base.sh'; get_user_home 'nonexistent_user_12345'") + [ -z "$result" ] || [ "$result" = "~nonexistent_user_12345" ] +} + +# ============================================================================ +# Directory Creation Tests +# ============================================================================ + +@test "ensure_user_dir creates simple directory" { + test_dir="$HOME/.cache/test" + bash -c "source '$PROJECT_ROOT/lib/core/base.sh'; ensure_user_dir '$test_dir'" + [ -d "$test_dir" ] +} + +@test "ensure_user_dir creates nested directory" { + test_dir="$HOME/.config/mole/deep/nested/path" + bash -c "source '$PROJECT_ROOT/lib/core/base.sh'; ensure_user_dir '$test_dir'" + [ -d "$test_dir" ] +} + +@test "ensure_user_dir handles tilde expansion" { + bash -c "source '$PROJECT_ROOT/lib/core/base.sh'; ensure_user_dir '~/.cache/tilde-test'" + [ -d "$HOME/.cache/tilde-test" ] +} + +@test "ensure_user_dir is idempotent" { + test_dir="$HOME/.cache/idempotent" + bash -c "source '$PROJECT_ROOT/lib/core/base.sh'; ensure_user_dir '$test_dir'" + bash -c "source '$PROJECT_ROOT/lib/core/base.sh'; ensure_user_dir '$test_dir'" + [ -d "$test_dir" ] +} + +@test "ensure_user_dir handles empty path gracefully" { + bash -c "source '$PROJECT_ROOT/lib/core/base.sh'; ensure_user_dir ''" + [ $? -eq 0 ] +} + +@test "ensure_user_dir preserves ownership for non-root users" { + test_dir="$HOME/.cache/ownership-test" + bash -c "source '$PROJECT_ROOT/lib/core/base.sh'; ensure_user_dir '$test_dir'" + + current_uid=$(id -u) + dir_uid=$(/usr/bin/stat -f%u "$test_dir") + [ "$dir_uid" = "$current_uid" ] +} + +# ============================================================================ +# File Creation Tests +# ============================================================================ + +@test "ensure_user_file creates file and parent directories" { + test_file="$HOME/.config/mole/test.log" + bash -c "source '$PROJECT_ROOT/lib/core/base.sh'; ensure_user_file '$test_file'" + [ -f "$test_file" ] + [ -d "$(dirname "$test_file")" ] +} + +@test "ensure_user_file handles tilde expansion" { + bash -c "source '$PROJECT_ROOT/lib/core/base.sh'; ensure_user_file '~/.cache/tilde-file.txt'" + [ -f "$HOME/.cache/tilde-file.txt" ] +} + +@test "ensure_user_file is idempotent" { + test_file="$HOME/.cache/idempotent.txt" + bash -c "source '$PROJECT_ROOT/lib/core/base.sh'; ensure_user_file '$test_file'" + echo "content" > "$test_file" + bash -c "source '$PROJECT_ROOT/lib/core/base.sh'; ensure_user_file '$test_file'" + # Should preserve existing content + [ -f "$test_file" ] + [ "$(cat "$test_file")" = "content" ] +} + +@test "ensure_user_file handles empty path gracefully" { + bash -c "source '$PROJECT_ROOT/lib/core/base.sh'; ensure_user_file ''" + [ $? -eq 0 ] +} + +@test "ensure_user_file creates deeply nested files" { + test_file="$HOME/.config/deep/very/nested/structure/file.log" + bash -c "source '$PROJECT_ROOT/lib/core/base.sh'; ensure_user_file '$test_file'" + [ -f "$test_file" ] +} + +@test "ensure_user_file preserves ownership for non-root users" { + test_file="$HOME/.cache/file-ownership-test.txt" + bash -c "source '$PROJECT_ROOT/lib/core/base.sh'; ensure_user_file '$test_file'" + + current_uid=$(id -u) + file_uid=$(/usr/bin/stat -f%u "$test_file") + [ "$file_uid" = "$current_uid" ] +} + +# ============================================================================ +# Performance Tests (Early Stop Optimization) +# ============================================================================ + +@test "ensure_user_dir early stop optimization works" { + # Create a nested structure + test_dir="$HOME/.cache/perf/test/nested" + bash -c "source '$PROJECT_ROOT/lib/core/base.sh'; ensure_user_dir '$test_dir'" + + # Call again - should detect correct ownership and stop early + # This is a behavioral test; we verify it doesn't fail + bash -c "source '$PROJECT_ROOT/lib/core/base.sh'; ensure_user_dir '$test_dir'" + [ -d "$test_dir" ] + + # Verify ownership is still correct + current_uid=$(id -u) + dir_uid=$(/usr/bin/stat -f%u "$test_dir") + [ "$dir_uid" = "$current_uid" ] +} + +# ============================================================================ +# Integration Tests +# ============================================================================ + +@test "ensure_user_dir and ensure_user_file work together" { + cache_dir="$HOME/.cache/mole" + cache_file="$cache_dir/integration_test.log" + + bash -c "source '$PROJECT_ROOT/lib/core/base.sh'; ensure_user_dir '$cache_dir'" + bash -c "source '$PROJECT_ROOT/lib/core/base.sh'; ensure_user_file '$cache_file'" + + [ -d "$cache_dir" ] + [ -f "$cache_file" ] +} + +@test "multiple ensure_user_file calls in same directory" { + bash -c "source '$PROJECT_ROOT/lib/core/base.sh' + ensure_user_file '$HOME/.config/mole/file1.txt' + ensure_user_file '$HOME/.config/mole/file2.txt' + ensure_user_file '$HOME/.config/mole/file3.txt' + " + + [ -f "$HOME/.config/mole/file1.txt" ] + [ -f "$HOME/.config/mole/file2.txt" ] + [ -f "$HOME/.config/mole/file3.txt" ] +} + +@test "ensure functions handle concurrent calls safely" { + # Simulate concurrent directory creation + bash -c "source '$PROJECT_ROOT/lib/core/base.sh' + ensure_user_dir '$HOME/.cache/concurrent' & + ensure_user_dir '$HOME/.cache/concurrent' & + wait + " + + [ -d "$HOME/.cache/concurrent" ] +}