diff --git a/bin/linux-toolbox b/bin/linux-toolbox index fd8423b..7016c4b 100755 --- a/bin/linux-toolbox +++ b/bin/linux-toolbox @@ -1,45 +1,28 @@ #!/usr/bin/env bash - -# linux-toolbox – interactive menu front-end for the LinuxScripts utility collection. -# Presents categories and lets the user select a script to run. -# Does not duplicate script logic; invokes utility scripts directly. - +# linux-toolbox – CLI dispatcher and interactive menu for LinuxScripts toolkit. set -euo pipefail show_help() { cat <<'EOF' -Usage: linux-toolbox [options] +Usage: linux-toolbox [options] [command] [args...] + +Commands: + list List all available utility categories and scripts + run [args...] Directly run a utility script (e.g. developer/command-exists bash) + [args...] Shortcut to directly run a utility script Options: - -h, --help Show this help message and exit + -h, --help Show this help message and exit Description: - Provides a keyboard-friendly menu to launch utility scripts located under - the repository's scripts/ directory. Categories: - 1) System - 2) Storage - 3) Network - 4) Processes - 5) Packages - 6) Hardware - 7) SSH - 8) Developer - 9) Maintenance - 10) Exit + When launched without arguments, opens an interactive keyboard menu + to explore and run scripts from the LinuxScripts repository. EOF exit 0 } -for arg in "$@"; do - case "$arg" in - -h|--help) show_help ;; - *) echo "Unknown option: $arg" >&2; exit 1 ;; - esac -done - -# Resolve repository root relative to script location -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)" SCRIPTS_DIR="$REPO_ROOT/scripts" declare -A CATEGORY_MAP=( @@ -54,6 +37,96 @@ declare -A CATEGORY_MAP=( [9]="maintenance" ) +list_all_scripts() { + echo "=== Available LinuxScripts Utilities ===" + echo "" + for idx in $(echo "${!CATEGORY_MAP[@]}" | tr ' ' '\n' | sort -n); do + cat_name="${CATEGORY_MAP[$idx]}" + cat_dir="$SCRIPTS_DIR/$cat_name" + echo "[$cat_name]" + if [[ -d "$cat_dir" ]]; then + for s in "$cat_dir"/*.sh; do + if [[ -f "$s" ]]; then + s_name="$(basename "$s" .sh)" + echo " - $cat_name/$s_name" + fi + done + fi + echo "" + done +} + +find_script() { + local query="$1" + # Strip .sh if user passed it + query="${query%.sh}" + + # 1. Exact match category/script + if [[ -f "$SCRIPTS_DIR/$query.sh" ]]; then + echo "$SCRIPTS_DIR/$query.sh" + return 0 + fi + + # 2. Search by script basename across categories + local matches=() + while IFS= read -r f; do + [[ -n "$f" ]] && matches+=("$f") + done < <(find "$SCRIPTS_DIR" -maxdepth 2 -type f -name "$query.sh" 2>/dev/null || true) + + if [[ ${#matches[@]} -eq 1 ]]; then + echo "${matches[0]}" + return 0 + elif [[ ${#matches[@]} -gt 1 ]]; then + echo "Ambiguous script name '$query'. Please specify category: e.g. '${matches[0]#"$SCRIPTS_DIR/"}'" >&2 + return 1 + fi + + return 1 +} + +run_script_cli() { + local target="$1" + shift + local script_path + if ! script_path="$(find_script "$target")"; then + echo "Error: Utility script '$target' not found." >&2 + echo "Run 'linux-toolbox list' to view available scripts." >&2 + exit 1 + fi + + exec "$script_path" "$@" +} + +# CLI argument handling +if [[ $# -gt 0 ]]; then + case "$1" in + -h|--help) + show_help + ;; + list) + list_all_scripts + exit 0 + ;; + run) + shift + if [[ $# -lt 1 ]]; then + echo "Error: Script name required for 'run' command." >&2 + echo "Usage: linux-toolbox run [args...]" >&2 + exit 1 + fi + run_script_cli "$@" + ;; + -*) + echo "Error: Unknown option: $1" >&2 + show_help + ;; + *) + run_script_cli "$@" + ;; + esac +fi + +# Interactive Menu Mode main_menu() { echo "" echo "==============================" @@ -127,7 +200,6 @@ sub_menu() { echo "" if [[ -n "$extra_args" ]]; then - # Parse into array to avoid eval vulnerabilities read -ra arg_arr <<< "$extra_args" "$selected_script" "${arg_arr[@]}" || echo "Script exited with non-zero status ($?)" else diff --git a/scripts/process/process-info.sh b/scripts/process/process-info.sh index a40da3a..dd16b35 100755 --- a/scripts/process/process-info.sh +++ b/scripts/process/process-info.sh @@ -1,45 +1,109 @@ #!/usr/bin/env bash - set -euo pipefail show_help() { - cat <<'EOF' -Usage: $(basename "$0") [OPTIONS] - -Display information about running processes. + cat <<'EOF' +Usage: process-info.sh [options] [pid|name] Options: - -a Show all processes (default uses ps -e) - -u USER Filter by user - -h Show this help message + -h, --help Show this help message and exit + -u, --user USER Filter processes by user + -a, --all Show all processes with full details + +Description: + Displays detailed process information. + - If a PID is specified, displays status, memory, CPU, and command details for that PID. + - If a process name is specified, lists matching processes. + - If no target is specified, displays top resource-consuming processes. EOF + exit 0 } -ALL=false +TARGET="" USER_FILTER="" -while getopts "au:h" opt; do - case $opt in - a) ALL=true ;; - u) USER_FILTER="$OPTARG" ;; - h) show_help; exit 0 ;; - *) show_help; exit 1 ;; - esac +SHOW_ALL=false + +while (( "$#" )); do + case "$1" in + -h|--help) + show_help + ;; + -u|--user) + if [[ -n "${2-}" && "${2-}" != -* ]]; then + USER_FILTER="$2" + shift + else + echo "Error: Argument for $1 is missing" >&2 + exit 1 + fi + ;; + -a|--all) + SHOW_ALL=true + ;; + -*) + echo "Error: Unknown option: $1" >&2 + exit 1 + ;; + *) + if [[ -z "$TARGET" ]]; then + TARGET="$1" + else + echo "Error: Unexpected additional argument: $1" >&2 + exit 1 + fi + ;; + esac + shift done if ! command -v ps >/dev/null 2>&1; then - echo "Error: 'ps' command not found." >&2 + echo "Error: 'ps' command not found." >&2 + exit 1 +fi + +# Case 1: Target PID provided +if [[ -n "$TARGET" && "$TARGET" =~ ^[0-9]+$ ]]; then + if ! kill -0 "$TARGET" 2>/dev/null; then + echo "Error: Process with PID $TARGET does not exist." >&2 + exit 1 + fi + echo "=== Process Details for PID $TARGET ===" + ps -p "$TARGET" -o pid,user,%cpu,%mem,stat,start,time,comm,args + exit 0 +fi + +# Case 2: Target process name provided +if [[ -n "$TARGET" ]]; then + echo "=== Searching processes matching '$TARGET' ===" + MATCHES="$(pgrep -l -f "$TARGET" 2>/dev/null || true)" + if [[ -z "$MATCHES" ]]; then + echo "Error: No processes matching '$TARGET' found." >&2 exit 1 + fi + echo "$MATCHES" + exit 0 fi -if $ALL; then - CMD="ps -ef" -else - CMD="ps -e" +# Case 3: Show all or filtered by user +if $SHOW_ALL; then + echo "=== All Running Processes ===" + if [[ -n "$USER_FILTER" ]]; then + ps -u "$USER_FILTER" -o pid,user,%cpu,%mem,stat,start,time,comm + else + ps -eo pid,user,%cpu,%mem,stat,start,time,comm + fi + exit 0 fi if [[ -n "$USER_FILTER" ]]; then - CMD="$CMD -u $USER_FILTER" + echo "=== Processes for User '$USER_FILTER' ===" + ps -u "$USER_FILTER" -o pid,user,%cpu,%mem,stat,start,time,comm + exit 0 fi -echo "Process list:" -$CMD +# Case 4: Default overview (top CPU and Memory) +echo "=== Top Processes by CPU Usage ===" +ps -eo pid,user,%cpu,%mem,comm --sort=-%cpu | head -n 10 +echo "" +echo "=== Top Processes by Memory Usage ===" +ps -eo pid,user,%cpu,%mem,comm --sort=-%mem | head -n 10 diff --git a/scripts/storage/directory-size.sh b/scripts/storage/directory-size.sh index 489d61b..ac0218e 100755 --- a/scripts/storage/directory-size.sh +++ b/scripts/storage/directory-size.sh @@ -1,32 +1,67 @@ #!/usr/bin/env bash - set -euo pipefail show_help() { - cat <<'EOF' -Usage: $(basename "$0") [OPTIONS] [DIRECTORY] - -Display the total size of a directory (including subdirectories). -If DIRECTORY is omitted, the current directory is used. + cat <<'EOF' +Usage: directory-size.sh [options] [directory] Options: - -h Show this help message + -h, --help Show this help message and exit + -d, --dir DIR Directory to measure (default: current directory) + +Description: + Displays the total human-readable disk space used by a directory. + If omitted, measures the current working directory. EOF + exit 0 } -if [[ "${1-}" == "-h" ]]; then - show_help - exit 0 -fi +TARGET_DIR="" -DIR="${1:-.}" +while (( "$#" )); do + case "$1" in + -h|--help) + show_help + ;; + -d|--dir) + if [[ -n "${2-}" && "${2-}" != -* ]]; then + TARGET_DIR="$2" + shift + else + echo "Error: Argument for $1 is missing" >&2 + exit 1 + fi + ;; + -*) + echo "Error: Unknown option: $1" >&2 + exit 1 + ;; + *) + if [[ -z "$TARGET_DIR" ]]; then + TARGET_DIR="$1" + else + echo "Error: Unexpected additional argument: $1" >&2 + exit 1 + fi + ;; + esac + shift +done + +TARGET_DIR="${TARGET_DIR:-.}" + +if [[ ! -d "$TARGET_DIR" ]]; then + echo "Error: '$TARGET_DIR' is not a directory." >&2 + exit 1 +fi -if [[ ! -d "$DIR" ]]; then - echo "Error: '$DIR' is not a directory" >&2 - exit 1 +if ! command -v du >/dev/null 2>&1; then + echo "Error: 'du' utility is required but not installed." >&2 + exit 1 fi -# Use du to calculate size, suppress errors for inaccessible files. -size=$(du -sh "${DIR}" 2>/dev/null | cut -f1) +REAL_PATH="$(cd "$TARGET_DIR" && pwd -P)" +SIZE="$(du -sh "$REAL_PATH" 2>/dev/null | cut -f1)" -echo "Directory size of '$DIR': $size" +echo "Directory: $REAL_PATH" +echo "Size: $SIZE" diff --git a/scripts/system/boot-info.sh b/scripts/system/boot-info.sh index 010feab..aad129e 100755 --- a/scripts/system/boot-info.sh +++ b/scripts/system/boot-info.sh @@ -3,37 +3,51 @@ set -euo pipefail show_help() { cat <<'EOF' -Usage: $(basename "$0") [options] +Usage: boot-info.sh [options] Options: -h, --help Show this help message and exit -Displays recent boot performance information using systemd-analyze and recent boot logs. +Description: + Displays recent boot performance information using systemd-analyze + and recent boot logs (journalctl or /var/log/boot.log). + Gracefully handles containers or environments where systemd is not running. EOF exit 0 } -# Parse arguments for arg in "$@"; do case "$arg" in - -h|--help) show_help ;; - *) echo "Unknown option: $arg" >&2; exit 1 ;; + -h|--help) + show_help + ;; + *) + echo "Error: Unknown option: $arg" >&2 + exit 1 + ;; esac done +echo "=== System Boot Information ===" +echo "" + if command -v systemd-analyze >/dev/null 2>&1; then - echo "Boot time breakdown:" && systemd-analyze blame - echo -e "\nCritical chain:" && systemd-analyze critical-chain + echo "Boot time breakdown:" + systemd-analyze blame 2>/dev/null | head -n 15 || echo " (systemd-analyze blame not available; systemd may not be running or boot incomplete)" + echo "" + echo "Critical chain:" + systemd-analyze critical-chain 2>/dev/null || echo " (critical-chain not available)" else echo "systemd-analyze not available on this system." fi +echo "" if command -v journalctl >/dev/null 2>&1; then - echo -e "\nRecent boot log (last 20 lines):" - journalctl -b -n 20 + echo "Recent boot log (last 20 lines):" + journalctl -b -n 20 2>/dev/null || echo " (journalctl boot logs not accessible)" elif [[ -f /var/log/boot.log ]]; then - echo -e "\nRecent boot log (last 20 lines) from /var/log/boot.log:" - tail -n 20 /var/log/boot.log + echo "Recent boot log from /var/log/boot.log:" + tail -n 20 /var/log/boot.log 2>/dev/null || echo " (unable to read /var/log/boot.log)" else echo "No boot log accessible." fi diff --git a/scripts/system/kernel-info.sh b/scripts/system/kernel-info.sh index 0d5779a..0a5a245 100755 --- a/scripts/system/kernel-info.sh +++ b/scripts/system/kernel-info.sh @@ -26,8 +26,8 @@ echo "Kernel version:" && uname -r echo -e "\nLoaded modules:" && lsmod if [[ -r /proc/config.gz ]]; then - echo -e "\nKernel config (compressed):" - zcat /proc/config.gz | head -n 20 + echo -e "\nKernel config (compressed, preview):" + (zcat /proc/config.gz 2>/dev/null || true) | head -n 20 || true else echo -e "\nKernel config not available (no /proc/config.gz)." fi diff --git a/scripts/system/system-health.sh b/scripts/system/system-health.sh index 58fe039..3ff7baa 100755 --- a/scripts/system/system-health.sh +++ b/scripts/system/system-health.sh @@ -1,46 +1,64 @@ #!/usr/bin/env bash - set -euo pipefail show_help() { - cat <<'EOF' -Usage: $(basename "$0") [OPTIONS] - -Display a quick overview of system health indicators. + cat <<'EOF' +Usage: system-health.sh [options] Options: - -h Show this help message + -h, --help Show this help message and exit + +Description: + Displays a quick overview of system health indicators including + load average, memory usage, root disk usage, and top processes. EOF + exit 0 } -if [[ "${1-}" == "-h" ]]; then - show_help - exit 0 -fi +for arg in "$@"; do + case "$arg" in + -h|--help) + show_help + ;; + *) + echo "Error: Unknown option: $arg" >&2 + exit 1 + ;; + esac +done echo "=== System Health Overview ===" +echo "" # Load average if command -v uptime >/dev/null 2>&1; then - echo "Load average: $(uptime | awk -F'load average:' '{print $2}' | sed 's/^ //')" + echo "Load average: $(uptime | awk -F'load average:' '{print $2}' | sed 's/^ //')" fi # Memory usage if command -v free >/dev/null 2>&1; then - echo "Memory usage:" && free -h + echo "" + echo "Memory usage:" + free -h fi # Disk usage (root) if command -v df >/dev/null 2>&1; then - echo "Disk usage (root):" && df -h / + echo "" + echo "Disk usage (root):" + df -h / fi -# Top CPU consuming processes (non-root) +# Top CPU consuming processes if command -v ps >/dev/null 2>&1; then - echo "Top CPU processes:" && ps -eo pid,comm,%cpu --sort=-%cpu | head -n 6 + echo "" + echo "Top CPU processes:" + (ps -eo pid,comm,%cpu --sort=-%cpu 2>/dev/null || true) | head -n 6 || true fi # Top memory consuming processes if command -v ps >/dev/null 2>&1; then - echo "Top memory processes:" && ps -eo pid,comm,%mem --sort=-%mem | head -n 6 + echo "" + echo "Top memory processes:" + (ps -eo pid,comm,%mem --sort=-%mem 2>/dev/null || true) | head -n 6 || true fi diff --git a/tests/functional_test.sh b/tests/functional_test.sh new file mode 100755 index 0000000..f8be4ab --- /dev/null +++ b/tests/functional_test.sh @@ -0,0 +1,322 @@ +#!/usr/bin/env bash +# ============================================================================== +# tests/functional_test.sh +# End-to-end isolated behavioral and security test suite for LinuxScripts. +# Tests actual script execution, edge cases, error conditions, and mocks. +# ============================================================================== +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +TEST_TMP="$(mktemp -d)" +trap 'rm -rf "$TEST_TMP"' EXIT + +TOTAL=0 +PASSED=0 +FAILED=0 + +assert_success() { + local desc="$1" + shift + TOTAL=$((TOTAL + 1)) + if "$@"; then + echo " ✅ PASS: $desc" + PASSED=$((PASSED + 1)) + else + echo " ❌ FAIL: $desc" + FAILED=$((FAILED + 1)) + fi +} + +assert_failure() { + local desc="$1" + shift + TOTAL=$((TOTAL + 1)) + if ! "$@" >/dev/null 2>&1; then + echo " ✅ PASS: $desc" + PASSED=$((PASSED + 1)) + else + echo " ❌ FAIL: $desc (expected command to fail, but succeeded)" + FAILED=$((FAILED + 1)) + fi +} + +echo "==================================================================" +echo " Starting LinuxScripts Behavioral & Functional Test Suite" +echo " Workspace: $TEST_TMP" +echo "==================================================================" +echo "" + +# ---------------------------------------------------------------------- +# 1. Developer Utilities +# ---------------------------------------------------------------------- +echo "--- 1. Developer Utilities ---" +assert_success "command-exists.sh finds existing 'bash'" \ + "$ROOT_DIR/scripts/developer/command-exists.sh" bash + +assert_failure "command-exists.sh fails on nonexistent command" \ + "$ROOT_DIR/scripts/developer/command-exists.sh" nonexistent_command_xyz_12345 + +assert_success "dev-environment-info.sh runs cleanly" \ + "$ROOT_DIR/scripts/developer/dev-environment-info.sh" + +assert_success "git-info.sh runs cleanly" \ + "$ROOT_DIR/scripts/developer/git-info.sh" + +assert_success "python-info.sh runs cleanly" \ + "$ROOT_DIR/scripts/developer/python-info.sh" + +# ---------------------------------------------------------------------- +# 2. System & Storage Info Utilities +# ---------------------------------------------------------------------- +echo "--- 2. System & Storage Utilities ---" +assert_success "system-info.sh runs cleanly" \ + "$ROOT_DIR/scripts/system/system-info.sh" + +assert_success "kernel-info.sh runs cleanly" \ + "$ROOT_DIR/scripts/system/kernel-info.sh" + +assert_success "boot-info.sh runs cleanly" \ + "$ROOT_DIR/scripts/system/boot-info.sh" + +assert_success "failed-services.sh runs cleanly" \ + "$ROOT_DIR/scripts/system/failed-services.sh" + +assert_success "disk-usage.sh runs cleanly" \ + "$ROOT_DIR/scripts/storage/disk-usage.sh" + +assert_success "filesystem-info.sh runs cleanly" \ + "$ROOT_DIR/scripts/storage/filesystem-info.sh" + +assert_success "mount-info.sh runs cleanly" \ + "$ROOT_DIR/scripts/storage/mount-info.sh" + +DIR_SIZE_TMP="$TEST_TMP/dir_size_test" +mkdir -p "$DIR_SIZE_TMP" +echo "hello world" > "$DIR_SIZE_TMP/file.txt" +assert_success "directory-size.sh measures test directory" \ + "$ROOT_DIR/scripts/storage/directory-size.sh" -d "$DIR_SIZE_TMP" + +assert_failure "disk-health.sh rejects invalid non-block device" \ + "$ROOT_DIR/scripts/storage/disk-health.sh" /dev/nonexistent_device_test_xyz + +# ---------------------------------------------------------------------- +# 3. Archive & Extraction Security (extract.sh) +# ---------------------------------------------------------------------- +echo "--- 3. Archive & Extraction Security ---" +EXTRACT_SRC="$TEST_TMP/extract_src" +EXTRACT_DEST="$TEST_TMP/extract_dest" +mkdir -p "$EXTRACT_SRC" "$EXTRACT_DEST" +echo "content" > "$EXTRACT_SRC/sample.txt" +tar -czf "$TEST_TMP/valid.tar.gz" -C "$EXTRACT_SRC" sample.txt + +assert_success "extract.sh lists valid archive (-l)" \ + "$ROOT_DIR/scripts/maintenance/extract.sh" -l "$TEST_TMP/valid.tar.gz" + +assert_success "extract.sh extracts valid archive" \ + "$ROOT_DIR/scripts/maintenance/extract.sh" "$TEST_TMP/valid.tar.gz" "$EXTRACT_DEST" + +# Craft malicious path traversal tar +python3 -c " +import tarfile, io +with tarfile.open('$TEST_TMP/traversal.tar.gz', 'w:gz') as tar: + data = b'attack' + ti = tarfile.TarInfo(name='../evil.txt') + ti.size = len(data) + tar.addfile(ti, io.BytesIO(data)) +" +assert_failure "extract.sh blocks path traversal archive (../evil.txt)" \ + "$ROOT_DIR/scripts/maintenance/extract.sh" "$TEST_TMP/traversal.tar.gz" "$EXTRACT_DEST" + +# Craft malicious absolute path tar +python3 -c " +import tarfile, io +with tarfile.open('$TEST_TMP/absolute.tar.gz', 'w:gz') as tar: + data = b'attack' + ti = tarfile.TarInfo(name='/tmp/evil.txt') + ti.size = len(data) + tar.addfile(ti, io.BytesIO(data)) +" +assert_failure "extract.sh blocks absolute path archive (/tmp/evil.txt)" \ + "$ROOT_DIR/scripts/maintenance/extract.sh" "$TEST_TMP/absolute.tar.gz" "$EXTRACT_DEST" + +# ---------------------------------------------------------------------- +# 4. Backup Safety (backup.sh) +# ---------------------------------------------------------------------- +echo "--- 4. Backup Safety ---" +BACKUP_SRC="$TEST_TMP/backup_src with spaces" +mkdir -p "$BACKUP_SRC/nested" +echo "file 1" > "$BACKUP_SRC/data with spaces.txt" +echo "unicode" > "$BACKUP_SRC/nested/テスト.txt" + +assert_success "backup.sh creates archive with spaces and Unicode" \ + "$ROOT_DIR/scripts/maintenance/backup.sh" -d "$BACKUP_SRC" -o "$BACKUP_SRC/backups" + +BACKUP_ARCHIVE=$(find "$BACKUP_SRC/backups" -name "*.tar.gz" | head -n1) +test -n "$BACKUP_ARCHIVE" && test -f "$BACKUP_ARCHIVE" +assert_success "backup archive file was generated" test -f "$BACKUP_ARCHIVE" + +# Ensure nested backups dir was excluded +EXCLUDED_OK=true +if tar -tf "$BACKUP_ARCHIVE" | grep -q "backups/"; then + EXCLUDED_OK=false +fi +assert_success "backup.sh excludes nested destination directory" $EXCLUDED_OK + +assert_failure "backup.sh rejects nonexistent source directory" \ + "$ROOT_DIR/scripts/maintenance/backup.sh" -d "$TEST_TMP/nonexistent_dir" + +# ---------------------------------------------------------------------- +# 5. File Organizer & Cleanup Safety +# ---------------------------------------------------------------------- +echo "--- 5. File Organizer & Cache Cleanup ---" +ORG_TMP="$TEST_TMP/org_test" +mkdir -p "$ORG_TMP/Documents" +echo "existing document" > "$ORG_TMP/Documents/doc.pdf" +echo "new document" > "$ORG_TMP/doc.pdf" +echo "photo" > "$ORG_TMP/image with spaces.png" +echo "unicode photo" > "$ORG_TMP/写真.jpg" +echo "hidden" > "$ORG_TMP/.hidden.txt" +echo "noext" > "$ORG_TMP/plain_file" +ln -s "$ORG_TMP/plain_file" "$ORG_TMP/symlink_file.png" + +# Test dry-run +DRY_OUT=$("$ROOT_DIR/scripts/maintenance/file-organizer.sh" -d "$ORG_TMP" --dry-run) +assert_success "file-organizer.sh dry-run prints dry run mode" test -n "$DRY_OUT" +assert_success "file-organizer.sh dry-run preserves file locations" test -f "$ORG_TMP/doc.pdf" + +# Run live organizer +"$ROOT_DIR/scripts/maintenance/file-organizer.sh" -d "$ORG_TMP" >/dev/null + +assert_success "file-organizer.sh moves image with spaces" test -f "$ORG_TMP/Images/image with spaces.png" +assert_success "file-organizer.sh moves Unicode filename" test -f "$ORG_TMP/Images/写真.jpg" +assert_success "file-organizer.sh does not overwrite destination collision" grep -q "existing document" "$ORG_TMP/Documents/doc.pdf" +assert_success "file-organizer.sh preserves hidden files" test -f "$ORG_TMP/.hidden.txt" +assert_success "file-organizer.sh preserves files without extensions" test -f "$ORG_TMP/plain_file" +assert_success "file-organizer.sh preserves symlinks without moving" test -L "$ORG_TMP/symlink_file.png" + +# Cache cleanup in mock home +MOCK_HOME="$TEST_TMP/mock_home" +mkdir -p "$MOCK_HOME/.cache/thumbnails" "$MOCK_HOME/.local/share/Trash/files" +touch "$MOCK_HOME/.cache/thumbnails/test.png" "$MOCK_HOME/.local/share/Trash/files/old.txt" + +HOME="$MOCK_HOME" "$ROOT_DIR/scripts/maintenance/cleanup-cache.sh" --dry-run >/dev/null +assert_success "cleanup-cache.sh dry-run preserves thumbnail cache" test -f "$MOCK_HOME/.cache/thumbnails/test.png" + +HOME="$MOCK_HOME" "$ROOT_DIR/scripts/maintenance/cleanup-cache.sh" --yes >/dev/null +assert_success "cleanup-cache.sh cleans thumbnail cache" test ! -f "$MOCK_HOME/.cache/thumbnails/test.png" +assert_success "cleanup-cache.sh empties user trash" test ! -f "$MOCK_HOME/.local/share/Trash/files/old.txt" + +# ---------------------------------------------------------------------- +# 6. Process Management Safety (kill-process.sh, process-info.sh) +# ---------------------------------------------------------------------- +echo "--- 6. Process Management Safety ---" +assert_success "process-info.sh queries current PID ($$)" \ + "$ROOT_DIR/scripts/process/process-info.sh" "$$" + +assert_failure "process-info.sh fails on invalid PID 9999999" \ + "$ROOT_DIR/scripts/process/process-info.sh" 9999999 + +assert_failure "kill-process.sh refuses to kill PID 1 (init)" \ + "$ROOT_DIR/scripts/process/kill-process.sh" 1 + +assert_failure "kill-process.sh refuses to kill self ($$)" \ + "$ROOT_DIR/scripts/process/kill-process.sh" "$$" + +sleep 60 & +SLEEP_PID=$! +assert_success "kill-process.sh safely terminates child process with -f" \ + "$ROOT_DIR/scripts/process/kill-process.sh" -f "$SLEEP_PID" +sleep 0.2 +assert_failure "child process is no longer running" kill -0 "$SLEEP_PID" + +# ---------------------------------------------------------------------- +# 7. SSH Key Setup (ssh-key-setup.sh) +# ---------------------------------------------------------------------- +echo "--- 7. SSH Key Setup ---" +SSH_HOME="$TEST_TMP/ssh_home" +mkdir -p "$SSH_HOME" + +HOME="$SSH_HOME" "$ROOT_DIR/scripts/ssh/ssh-key-setup.sh" -p "" -y >/dev/null +assert_success "ssh-key-setup.sh generates Ed25519 key" test -f "$SSH_HOME/.ssh/id_ed25519" +assert_success "ssh-key-setup.sh enforces 0700 permission on .ssh" test "$(stat -c "%a" "$SSH_HOME/.ssh")" = "700" +assert_success "ssh-key-setup.sh enforces 0600 permission on private key" test "$(stat -c "%a" "$SSH_HOME/.ssh/id_ed25519")" = "600" + +assert_failure "ssh-key-setup.sh refuses overwrite without -f" \ + env HOME="$SSH_HOME" "$ROOT_DIR/scripts/ssh/ssh-key-setup.sh" -p "" + +HOME="$SSH_HOME" "$ROOT_DIR/scripts/ssh/ssh-key-setup.sh" -f -y -p "" -c "updated@host" >/dev/null +BACKUP_KEYS=$(find "$SSH_HOME/.ssh" -name "id_ed25519.pub.bak_*" | wc -l) +assert_success "ssh-key-setup.sh creates backup before overwrite" test "$BACKUP_KEYS" -ge 1 + +# ---------------------------------------------------------------------- +# 8. Package Manager Mock Suite +# ---------------------------------------------------------------------- +echo "--- 8. Package Manager Mock Suite ---" +MOCK_BIN="$TEST_TMP/mock_bin" +MOCK_CORE="$TEST_TMP/mock_core" +mkdir -p "$MOCK_BIN" "$MOCK_CORE" + +for cmd in sh bash cat echo grep sed head dirname basename tr cut test true false; do + p="$(command -v "$cmd" 2>/dev/null || true)" + [[ -n "$p" ]] && ln -s "$p" "$MOCK_CORE/$cmd" +done + +# shellcheck disable=SC2317 +test_mock_pm() { + local target_pm="$1" + local expected_install="$2" + local expected_remove="$3" + + rm -f "$MOCK_BIN"/* + + cat << MOCK > "$MOCK_BIN/$target_pm" +#!/usr/bin/env bash +echo "[$target_pm called: \$*]" +MOCK + chmod +x "$MOCK_BIN/$target_pm" + + PATH="$MOCK_BIN:$MOCK_CORE" "$ROOT_DIR/scripts/packages/package-manager-info.sh" >/dev/null + PATH="$MOCK_BIN:$MOCK_CORE" "$ROOT_DIR/scripts/packages/search-package.sh" dummy_pkg >/dev/null + PATH="$MOCK_BIN:$MOCK_CORE" "$ROOT_DIR/scripts/packages/package-info.sh" dummy_pkg >/dev/null + PATH="$MOCK_BIN:$MOCK_CORE" "$ROOT_DIR/scripts/packages/update-system.sh" --dry-run >/dev/null + PATH="$MOCK_BIN:$MOCK_CORE" "$ROOT_DIR/scripts/packages/install-package.sh" --dry-run dummy_pkg | grep -q "$expected_install" + PATH="$MOCK_BIN:$MOCK_CORE" "$ROOT_DIR/scripts/packages/remove-package.sh" --dry-run dummy_pkg | grep -q "$expected_remove" +} + +assert_success "mock pacman suite execution" test_mock_pm pacman "pacman -S --noconfirm dummy_pkg" "pacman -R --noconfirm dummy_pkg" +assert_success "mock apt suite execution" test_mock_pm apt "apt install -y dummy_pkg" "apt remove -y dummy_pkg" +assert_success "mock dnf suite execution" test_mock_pm dnf "dnf install -y dummy_pkg" "dnf remove -y dummy_pkg" +assert_success "mock zypper suite execution" test_mock_pm zypper "zypper install -y dummy_pkg" "zypper remove -y dummy_pkg" +assert_success "mock apk suite execution" test_mock_pm apk "apk add dummy_pkg" "apk del dummy_pkg" + +# ---------------------------------------------------------------------- +# 9. Linux Toolbox CLI Integration (bin/linux-toolbox) +# ---------------------------------------------------------------------- +echo "--- 9. Linux Toolbox CLI Integration ---" +assert_success "linux-toolbox --help works" \ + "$ROOT_DIR/bin/linux-toolbox" --help + +assert_success "linux-toolbox list works" \ + "$ROOT_DIR/bin/linux-toolbox" list + +assert_success "linux-toolbox executes script subcommand" \ + "$ROOT_DIR/bin/linux-toolbox" developer/command-exists bash + +assert_failure "linux-toolbox fails on non-existent command" \ + "$ROOT_DIR/bin/linux-toolbox" invalid/nonexistent-script-command + +# ---------------------------------------------------------------------- +# Summary +# ---------------------------------------------------------------------- +echo "" +echo "==================================================================" +echo " Functional Test Results: $PASSED / $TOTAL passed" +if [[ $FAILED -gt 0 ]]; then + echo " ❌ $FAILED test(s) FAILED!" + echo "==================================================================" + exit 1 +else + echo " ✅ ALL FUNCTIONAL TESTS PASSED!" + echo "==================================================================" + exit 0 +fi diff --git a/tests/test_all.sh b/tests/test_all.sh index 0f4a1a9..8aa4fea 100755 --- a/tests/test_all.sh +++ b/tests/test_all.sh @@ -69,10 +69,17 @@ while IFS= read -r script; do fi done < <(find "$ROOT_DIR/scripts" -type f -name "*.sh" | sort) -if [[ $FAILED -eq 0 ]]; then - echo "All tests passed." - exit 0 -else - echo "$FAILED test(s) failed." +if [[ $FAILED -ne 0 ]]; then + echo "$FAILED static/help test(s) failed." exit 1 fi + +echo "" +echo "Running functional and behavioral test suite..." +if [[ -f "$ROOT_DIR/tests/functional_test.sh" ]]; then + bash "$ROOT_DIR/tests/functional_test.sh" +fi + +echo "" +echo "All tests passed successfully." +exit 0