diff --git a/bin/lib/machine-detection.bash b/bin/lib/machine-detection.bash new file mode 100644 index 00000000..6dd034cd --- /dev/null +++ b/bin/lib/machine-detection.bash @@ -0,0 +1,77 @@ +#!/usr/bin/env bash + +# Machine detection utilities for dotfiles setup +# Dynamically detects machine type based on hostname patterns + +set -euo pipefail + +# Detect machine type based on hostname +detect_machine_type() { + # Check for environment variable override first + if [[ -n "${MACHINE:-}" ]]; then + echo "$MACHINE" + return 0 + fi + + # Get hostname for detection + local hostname + if command -v hostname >/dev/null 2>&1; then + hostname=$(hostname) + else + # Fallback if hostname command is not available + hostname="${HOST:-unknown}" + fi + + # Pattern matching for machine types + case "$hostname" in + *Air*) + echo "air" + ;; + *Mini*) + echo "mini" + ;; + *) + # Default to work for unknown hostnames + echo "work" + ;; + esac +} + +# Set machine-specific environment variables based on detected type +set_machine_variables() { + local machine_type="$1" + + # Reset all machine variables + export IS_AIR=false + export IS_MINI=false + export IS_WORK=false + + # Set the appropriate variable based on machine type + case "$machine_type" in + "air") + export IS_AIR=true + export MACHINE="air" + ;; + "mini") + export IS_MINI=true + export MACHINE="mini" + ;; + *) + # Default to work for unknown types (including explicit "work") + export IS_WORK=true + export MACHINE="work" + ;; + esac +} + +# Initialize machine detection and set variables +init_machine_detection() { + local detected_type + detected_type=$(detect_machine_type) + set_machine_variables "$detected_type" + + # Optional: print detection result for debugging + if [[ "${DEBUG_MACHINE_DETECTION:-false}" == "true" ]]; then + echo "Detected machine type: $MACHINE" >&2 + fi +} \ No newline at end of file diff --git a/bin/lib/prerequisite-validation.bash b/bin/lib/prerequisite-validation.bash new file mode 100644 index 00000000..c1d7caee --- /dev/null +++ b/bin/lib/prerequisite-validation.bash @@ -0,0 +1,185 @@ +#!/usr/bin/env bash + +# Prerequisite validation utilities for dotfiles setup +# Validates system requirements before installation begins + +set -euo pipefail + +# Minimum supported macOS version (major.minor) +readonly MIN_MACOS_VERSION="12.0" + +# Validate Command Line Tools are installed +validate_command_line_tools() { + local xcode_path + + # Check if xcode-select can find the developer directory + if xcode_path=$(xcode-select -p 2>/dev/null); then + # Verify the path exists and contains expected tools + if [[ -d "$xcode_path" && -f "$xcode_path/usr/bin/git" ]]; then + return 0 + else + echo "Command Line Tools directory exists but appears incomplete" >&2 + return 1 + fi + else + echo "Command Line Tools not found. Install with: xcode-select --install" >&2 + return 1 + fi +} + +# Validate network connectivity to essential services +validate_network_connectivity() { + local hosts=("github.com" "raw.githubusercontent.com") + local failed_hosts=() + + for host in "${hosts[@]}"; do + # Use ping with timeout to test connectivity + if ! ping -c 1 -W 3000 "$host" >/dev/null 2>&1; then + failed_hosts+=("$host") + fi + done + + if [[ ${#failed_hosts[@]} -gt 0 ]]; then + echo "Network connectivity failed for: ${failed_hosts[*]}" >&2 + echo "Please check your internet connection and try again" >&2 + return 1 + fi + + return 0 +} + +# Validate directory exists and has proper permissions +validate_directory_permissions() { + local directory="$1" + + if [[ -z "$directory" ]]; then + echo "Directory path is required" >&2 + return 1 + fi + + # Check if directory exists + if [[ ! -d "$directory" ]]; then + echo "Directory does not exist: $directory" >&2 + return 1 + fi + + return 0 +} + +# Validate write permissions to a directory +validate_write_permissions() { + local directory="$1" + + if [[ -z "$directory" ]]; then + echo "Directory path is required" >&2 + return 1 + fi + + # Check if directory exists first + if [[ ! -d "$directory" ]]; then + echo "Directory does not exist: $directory" >&2 + return 1 + fi + + # Test write permissions by creating a temporary file + local test_file="$directory/.dotfiles_write_test_$$" + if touch "$test_file" 2>/dev/null; then + rm -f "$test_file" + return 0 + else + echo "No write permission for directory: $directory" >&2 + return 1 + fi +} + +# Validate macOS version compatibility +validate_macos_version() { + local current_version + local major minor + local min_major min_minor + + # Get current macOS version + current_version=$(sw_vers -productVersion 2>/dev/null) + if [[ -z "$current_version" ]]; then + echo "Unable to determine macOS version" >&2 + return 1 + fi + + # Parse current version + major=$(echo "$current_version" | cut -d. -f1) + minor=$(echo "$current_version" | cut -d. -f2) + + # Parse minimum version + min_major=$(echo "$MIN_MACOS_VERSION" | cut -d. -f1) + min_minor=$(echo "$MIN_MACOS_VERSION" | cut -d. -f2) + + # Compare versions + if [[ "$major" -gt "$min_major" ]] || + [[ "$major" -eq "$min_major" && "$minor" -ge "$min_minor" ]]; then + return 0 + else + echo "Unsupported macOS version: $current_version (minimum: $MIN_MACOS_VERSION)" >&2 + return 1 + fi +} + +# Validate essential directories for dotfiles installation +validate_essential_directories() { + local directories=( + "$HOME" + "/usr/local" + "/opt" + ) + + local failed_dirs=() + + for dir in "${directories[@]}"; do + if ! validate_directory_permissions "$dir"; then + failed_dirs+=("$dir") + fi + done + + if [[ ${#failed_dirs[@]} -gt 0 ]]; then + echo "Essential directory validation failed for: ${failed_dirs[*]}" >&2 + return 1 + fi + + return 0 +} + +# Run comprehensive prerequisite validation +run_prerequisite_validation() { + local validation_functions=( + "validate_command_line_tools" + "validate_network_connectivity" + "validate_macos_version" + "validate_essential_directories" + ) + + local failed_validations=() + local exit_code=0 + + echo "Running prerequisite validation checks..." + + for validation_func in "${validation_functions[@]}"; do + echo " Checking: ${validation_func#validate_}" + + if ! "$validation_func"; then + failed_validations+=("$validation_func") + exit_code=1 + else + echo " ✓ Passed" + fi + done + + if [[ $exit_code -eq 0 ]]; then + echo "✅ All prerequisite validation checks passed" + else + echo "❌ Prerequisite validation failed:" + for failed_func in "${failed_validations[@]}"; do + echo " - ${failed_func#validate_}" + done + fi + + return $exit_code +} \ No newline at end of file diff --git a/test/setup/test-machine-detection-bash.bats b/test/setup/test-machine-detection-bash.bats new file mode 100644 index 00000000..11dcf5cd --- /dev/null +++ b/test/setup/test-machine-detection-bash.bats @@ -0,0 +1,206 @@ +#!/usr/bin/env bats + +# Machine detection utility tests (bash version) +# Tests the bash version of machine detection functionality + +# Load the machine detection utilities +load "../../bin/lib/machine-detection.bash" + +setup() { + # Create temporary directory for each test + export TEST_TEMP_DIR + TEST_TEMP_DIR="$(mktemp -d)" + + # Save original environment + export ORIGINAL_HOME="$HOME" + export ORIGINAL_PATH="$PATH" + export ORIGINAL_MACHINE="${MACHINE:-}" + export ORIGINAL_HOST="${HOST:-}" + + # Clear machine-related environment variables for clean testing + unset MACHINE IS_AIR IS_MINI IS_WORK +} + +teardown() { + # Restore original environment + export HOME="$ORIGINAL_HOME" + export PATH="$ORIGINAL_PATH" + if [[ -n "$ORIGINAL_MACHINE" ]]; then + export MACHINE="$ORIGINAL_MACHINE" + else + unset MACHINE + fi + if [[ -n "$ORIGINAL_HOST" ]]; then + export HOST="$ORIGINAL_HOST" + else + unset HOST + fi + + # Clean up temporary directory + if [[ -n "${TEST_TEMP_DIR:-}" && -d "$TEST_TEMP_DIR" ]]; then + rm -rf "$TEST_TEMP_DIR" + fi + + # Clean up any machine variables set during tests + unset IS_AIR IS_MINI IS_WORK +} + +# Unit Tests for detect_machine_type function + +@test "detect_machine_type returns air for hostname containing Air" { + # Create fake hostname command + echo '#!/bin/bash +echo "MacBook-Air-2023"' > "$TEST_TEMP_DIR/hostname" + chmod +x "$TEST_TEMP_DIR/hostname" + export PATH="$TEST_TEMP_DIR:$PATH" + + run detect_machine_type + [ "$status" -eq 0 ] + [ "$output" = "air" ] +} + +@test "detect_machine_type returns mini for hostname containing Mini" { + # Create fake hostname command + echo '#!/bin/bash +echo "Mac-Mini-Server"' > "$TEST_TEMP_DIR/hostname" + chmod +x "$TEST_TEMP_DIR/hostname" + export PATH="$TEST_TEMP_DIR:$PATH" + + run detect_machine_type + [ "$status" -eq 0 ] + [ "$output" = "mini" ] +} + +@test "detect_machine_type returns work for other hostnames" { + # Create fake hostname command + echo '#!/bin/bash +echo "MacBook-Pro-Work"' > "$TEST_TEMP_DIR/hostname" + chmod +x "$TEST_TEMP_DIR/hostname" + export PATH="$TEST_TEMP_DIR:$PATH" + + run detect_machine_type + [ "$status" -eq 0 ] + [ "$output" = "work" ] +} + +@test "detect_machine_type uses MACHINE environment variable when set" { + export MACHINE="air" + + run detect_machine_type + [ "$status" -eq 0 ] + [ "$output" = "air" ] +} + +@test "detect_machine_type falls back to HOST when hostname unavailable" { + # Create an empty directory and use only that in PATH + mkdir -p "$TEST_TEMP_DIR/empty" + export PATH="$TEST_TEMP_DIR/empty" + export HOST="Mini-Test-Host" + + run detect_machine_type + [ "$status" -eq 0 ] + [ "$output" = "mini" ] +} + +@test "detect_machine_type defaults to work for unknown hostname" { + # Create fake hostname command with unknown hostname + echo '#!/bin/bash +echo "unknown-machine"' > "$TEST_TEMP_DIR/hostname" + chmod +x "$TEST_TEMP_DIR/hostname" + export PATH="$TEST_TEMP_DIR:$PATH" + + run detect_machine_type + [ "$status" -eq 0 ] + [ "$output" = "work" ] +} + +# Unit Tests for set_machine_variables function + +@test "set_machine_variables sets IS_AIR=true for air machine type" { + # Function sets variables in current shell, don't use run + set_machine_variables "air" + + # Check that variables are set correctly + [ "$IS_AIR" = "true" ] + [ "$IS_MINI" = "false" ] + [ "$IS_WORK" = "false" ] + [ "$MACHINE" = "air" ] +} + +@test "set_machine_variables sets IS_MINI=true for mini machine type" { + # Function sets variables in current shell, don't use run + set_machine_variables "mini" + + # Check that variables are set correctly + [ "$IS_AIR" = "false" ] + [ "$IS_MINI" = "true" ] + [ "$IS_WORK" = "false" ] + [ "$MACHINE" = "mini" ] +} + +@test "set_machine_variables sets IS_WORK=true for work machine type" { + # Function sets variables in current shell, don't use run + set_machine_variables "work" + + # Check that variables are set correctly + [ "$IS_AIR" = "false" ] + [ "$IS_MINI" = "false" ] + [ "$IS_WORK" = "true" ] + [ "$MACHINE" = "work" ] +} + +@test "set_machine_variables defaults to work for unknown machine type" { + # Function sets variables in current shell, don't use run + set_machine_variables "unknown" + + # Check that variables are set correctly + [ "$IS_AIR" = "false" ] + [ "$IS_MINI" = "false" ] + [ "$IS_WORK" = "true" ] + [ "$MACHINE" = "work" ] +} + +# Unit Tests for init_machine_detection function + +@test "init_machine_detection detects and sets variables correctly" { + # Create fake hostname command + echo '#!/bin/bash +echo "MacBook-Air-Personal"' > "$TEST_TEMP_DIR/hostname" + chmod +x "$TEST_TEMP_DIR/hostname" + export PATH="$TEST_TEMP_DIR:$PATH" + + # Function sets variables in current shell, don't use run + init_machine_detection + + # Check that detection worked and variables are set + [ "$MACHINE" = "air" ] + [ "$IS_AIR" = "true" ] + [ "$IS_MINI" = "false" ] + [ "$IS_WORK" = "false" ] +} + +@test "init_machine_detection with debug mode prints detection result" { + # Create fake hostname command + echo '#!/bin/bash +echo "Mini-Server"' > "$TEST_TEMP_DIR/hostname" + chmod +x "$TEST_TEMP_DIR/hostname" + export PATH="$TEST_TEMP_DIR:$PATH" + export DEBUG_MACHINE_DETECTION="true" + + run init_machine_detection + [ "$status" -eq 0 ] + [[ "$output" == *"Detected machine type: mini"* ]] +} + +@test "init_machine_detection without debug mode produces no output" { + # Create fake hostname command + echo '#!/bin/bash +echo "MacBook-Pro-Work"' > "$TEST_TEMP_DIR/hostname" + chmod +x "$TEST_TEMP_DIR/hostname" + export PATH="$TEST_TEMP_DIR:$PATH" + export DEBUG_MACHINE_DETECTION="false" + + run init_machine_detection + [ "$status" -eq 0 ] + [ -z "$output" ] +} \ No newline at end of file diff --git a/test/setup/test-prerequisite-validation-bash.bats b/test/setup/test-prerequisite-validation-bash.bats new file mode 100644 index 00000000..f6461bcd --- /dev/null +++ b/test/setup/test-prerequisite-validation-bash.bats @@ -0,0 +1,281 @@ +#!/usr/bin/env bats + +# Prerequisite validation utility tests (bash version) +# Tests the bash version of prerequisite validation functionality + +# Load the prerequisite validation utilities +load "../../bin/lib/prerequisite-validation.bash" + +setup() { + # Create temporary directory for each test + export TEST_TEMP_DIR + TEST_TEMP_DIR="$(mktemp -d)" + + # Save original environment + export ORIGINAL_HOME="$HOME" + export ORIGINAL_PATH="$PATH" +} + +teardown() { + # Restore original environment + export HOME="$ORIGINAL_HOME" + export PATH="$ORIGINAL_PATH" + + # Clean up temporary directory + if [[ -n "${TEST_TEMP_DIR:-}" && -d "$TEST_TEMP_DIR" ]]; then + rm -rf "$TEST_TEMP_DIR" + fi +} + +# Unit Tests for validate_command_line_tools function + +@test "validate_command_line_tools passes when xcode-select works and tools exist" { + # Create fake xcode-select that returns a valid path + echo '#!/bin/bash +if [[ "$1" == "-p" ]]; then + echo "'"$TEST_TEMP_DIR"'/Developer" +fi' > "$TEST_TEMP_DIR/xcode-select" + chmod +x "$TEST_TEMP_DIR/xcode-select" + + # Create fake developer directory structure + mkdir -p "$TEST_TEMP_DIR/Developer/usr/bin" + touch "$TEST_TEMP_DIR/Developer/usr/bin/git" + + export PATH="$TEST_TEMP_DIR:$PATH" + + run validate_command_line_tools + [ "$status" -eq 0 ] +} + +@test "validate_command_line_tools fails when xcode-select fails" { + # Create fake xcode-select that fails + echo '#!/bin/bash +exit 1' > "$TEST_TEMP_DIR/xcode-select" + chmod +x "$TEST_TEMP_DIR/xcode-select" + + export PATH="$TEST_TEMP_DIR:$PATH" + + run validate_command_line_tools + [ "$status" -eq 1 ] + [[ "$output" == *"Command Line Tools not found"* ]] +} + +@test "validate_command_line_tools fails when directory exists but git missing" { + # Create fake xcode-select that returns a path + echo '#!/bin/bash +if [[ "$1" == "-p" ]]; then + echo "'"$TEST_TEMP_DIR"'/Developer" +fi' > "$TEST_TEMP_DIR/xcode-select" + chmod +x "$TEST_TEMP_DIR/xcode-select" + + # Create directory but don't include git + mkdir -p "$TEST_TEMP_DIR/Developer/usr/bin" + + export PATH="$TEST_TEMP_DIR:$PATH" + + run validate_command_line_tools + [ "$status" -eq 1 ] + [[ "$output" == *"appears incomplete"* ]] +} + +# Unit Tests for validate_network_connectivity function + +@test "validate_network_connectivity passes when all hosts are reachable" { + # Create fake ping that always succeeds + echo '#!/bin/bash +exit 0' > "$TEST_TEMP_DIR/ping" + chmod +x "$TEST_TEMP_DIR/ping" + + export PATH="$TEST_TEMP_DIR:$PATH" + + run validate_network_connectivity + [ "$status" -eq 0 ] +} + +@test "validate_network_connectivity fails when github.com unreachable" { + # Create fake ping that fails for github.com + echo '#!/bin/bash +if [[ "$*" == *"github.com"* ]]; then + exit 1 +else + exit 0 +fi' > "$TEST_TEMP_DIR/ping" + chmod +x "$TEST_TEMP_DIR/ping" + + export PATH="$TEST_TEMP_DIR:$PATH" + + run validate_network_connectivity + [ "$status" -eq 1 ] + [[ "$output" == *"Network connectivity failed"* ]] + [[ "$output" == *"github.com"* ]] +} + +@test "validate_network_connectivity fails when all hosts unreachable" { + # Create fake ping that always fails + echo '#!/bin/bash +exit 1' > "$TEST_TEMP_DIR/ping" + chmod +x "$TEST_TEMP_DIR/ping" + + export PATH="$TEST_TEMP_DIR:$PATH" + + run validate_network_connectivity + [ "$status" -eq 1 ] + [[ "$output" == *"github.com"* ]] + [[ "$output" == *"raw.githubusercontent.com"* ]] +} + +# Unit Tests for validate_directory_permissions function + +@test "validate_directory_permissions passes for existing directory" { + mkdir -p "$TEST_TEMP_DIR/test_dir" + + run validate_directory_permissions "$TEST_TEMP_DIR/test_dir" + [ "$status" -eq 0 ] +} + +@test "validate_directory_permissions fails for non-existent directory" { + run validate_directory_permissions "$TEST_TEMP_DIR/nonexistent" + [ "$status" -eq 1 ] + [[ "$output" == *"Directory does not exist"* ]] +} + +@test "validate_directory_permissions fails when no directory provided" { + run validate_directory_permissions "" + [ "$status" -eq 1 ] + [[ "$output" == *"Directory path is required"* ]] +} + +# Unit Tests for validate_write_permissions function + +@test "validate_write_permissions passes for writable directory" { + mkdir -p "$TEST_TEMP_DIR/writable" + + run validate_write_permissions "$TEST_TEMP_DIR/writable" + [ "$status" -eq 0 ] +} + +@test "validate_write_permissions fails for non-existent directory" { + run validate_write_permissions "$TEST_TEMP_DIR/nonexistent" + [ "$status" -eq 1 ] + [[ "$output" == *"Directory does not exist"* ]] +} + +@test "validate_write_permissions fails when no directory provided" { + run validate_write_permissions "" + [ "$status" -eq 1 ] + [[ "$output" == *"Directory path is required"* ]] +} + +# Unit Tests for validate_macos_version function + +@test "validate_macos_version passes for supported version" { + # Create fake sw_vers that returns a supported version + echo '#!/bin/bash +if [[ "$1" == "-productVersion" ]]; then + echo "14.0" +fi' > "$TEST_TEMP_DIR/sw_vers" + chmod +x "$TEST_TEMP_DIR/sw_vers" + + export PATH="$TEST_TEMP_DIR:$PATH" + + run validate_macos_version + [ "$status" -eq 0 ] +} + +@test "validate_macos_version fails for unsupported version" { + # Create fake sw_vers that returns an old version + echo '#!/bin/bash +if [[ "$1" == "-productVersion" ]]; then + echo "10.15" +fi' > "$TEST_TEMP_DIR/sw_vers" + chmod +x "$TEST_TEMP_DIR/sw_vers" + + export PATH="$TEST_TEMP_DIR:$PATH" + + run validate_macos_version + [ "$status" -eq 1 ] + [[ "$output" == *"Unsupported macOS version"* ]] + [[ "$output" == *"10.15"* ]] +} + +@test "validate_macos_version fails when sw_vers unavailable" { + # Create empty directory first with full PATH + mkdir -p "$TEST_TEMP_DIR/empty" + # Then use empty PATH to make sw_vers unavailable + export PATH="$TEST_TEMP_DIR/empty" + + run validate_macos_version + [ "$status" -eq 1 ] + [[ "$output" == *"Unable to determine macOS version"* ]] +} + +# Unit Tests for validate_essential_directories function + +@test "validate_essential_directories passes when all directories exist" { + # Create fake HOME directory + mkdir -p "$TEST_TEMP_DIR/home" + export HOME="$TEST_TEMP_DIR/home" + + run validate_essential_directories + [ "$status" -eq 0 ] +} + +@test "validate_essential_directories fails when HOME does not exist" { + # Set HOME to non-existent directory + export HOME="$TEST_TEMP_DIR/nonexistent_home" + + run validate_essential_directories + [ "$status" -eq 1 ] + [[ "$output" == *"Essential directory validation failed"* ]] +} + +# Unit Tests for run_prerequisite_validation function + +@test "run_prerequisite_validation passes when all checks pass" { + # Set up environment for successful validation + mkdir -p "$TEST_TEMP_DIR/home" + export HOME="$TEST_TEMP_DIR/home" + + # Create fake commands that succeed + echo '#!/bin/bash +if [[ "$1" == "-p" ]]; then + echo "'"$TEST_TEMP_DIR"'/Developer" +fi' > "$TEST_TEMP_DIR/xcode-select" + + echo '#!/bin/bash +exit 0' > "$TEST_TEMP_DIR/ping" + + echo '#!/bin/bash +if [[ "$1" == "-productVersion" ]]; then + echo "14.0" +fi' > "$TEST_TEMP_DIR/sw_vers" + + chmod +x "$TEST_TEMP_DIR/xcode-select" "$TEST_TEMP_DIR/ping" "$TEST_TEMP_DIR/sw_vers" + + # Create developer directory structure + mkdir -p "$TEST_TEMP_DIR/Developer/usr/bin" + touch "$TEST_TEMP_DIR/Developer/usr/bin/git" + + export PATH="$TEST_TEMP_DIR:$PATH" + + run run_prerequisite_validation + [ "$status" -eq 0 ] + [[ "$output" == *"All prerequisite validation checks passed"* ]] +} + +@test "run_prerequisite_validation fails when any check fails" { + # Set up environment for failed validation (missing tools) + mkdir -p "$TEST_TEMP_DIR/home" + export HOME="$TEST_TEMP_DIR/home" + + # Create fake xcode-select that fails + echo '#!/bin/bash +exit 1' > "$TEST_TEMP_DIR/xcode-select" + chmod +x "$TEST_TEMP_DIR/xcode-select" + + export PATH="$TEST_TEMP_DIR:$PATH" + + run run_prerequisite_validation + [ "$status" -eq 1 ] + [[ "$output" == *"Prerequisite validation failed"* ]] +} \ No newline at end of file