aboutsummaryrefslogtreecommitdiff
path: root/tests/cases
diff options
context:
space:
mode:
Diffstat (limited to 'tests/cases')
-rwxr-xr-xtests/cases/test_backup.sh239
-rwxr-xr-xtests/cases/test_cron.sh138
-rwxr-xr-xtests/cases/test_dryrun.sh120
-rwxr-xr-xtests/cases/test_includes.sh161
-rwxr-xr-xtests/cases/test_validation.sh106
5 files changed, 764 insertions, 0 deletions
diff --git a/tests/cases/test_backup.sh b/tests/cases/test_backup.sh
new file mode 100755
index 0000000..1ab4fbd
--- /dev/null
+++ b/tests/cases/test_backup.sh
@@ -0,0 +1,239 @@
+#!/usr/bin/env bash
+# ==============================================================================
+# Backup and Rotation Tests
+# ==============================================================================
+
+source "$(dirname "${BASH_SOURCE[0]}")/../lib/test_helpers.sh"
+
+# ------------------------------------------------------------------------------
+# Test: Creates backup directory structure
+# ------------------------------------------------------------------------------
+test_creates_backup_structure() {
+ setup_test_env
+
+ create_test_config
+ create_test_includes
+ create_test_excludes
+
+ local output
+ output=$(sudo INSTALLHOME="$TEST_CONFIG_DIR" RSYNCSHOT_SKIP_MOUNT_CHECK=1 "$SCRIPT_PATH" manual 1 2>&1)
+ local exit_code=$?
+
+ # Check directory structure
+ assert_dir_exists "$TEST_BACKUP_DIR/$HOSTNAME" "should create hostname dir" || {
+ teardown_test_env
+ return 1
+ }
+ assert_dir_exists "$TEST_BACKUP_DIR/$HOSTNAME/latest" "should create latest dir" || {
+ teardown_test_env
+ return 1
+ }
+ assert_dir_exists "$TEST_BACKUP_DIR/$HOSTNAME/MANUAL.0" "should create MANUAL.0 snapshot" || {
+ teardown_test_env
+ return 1
+ }
+
+ teardown_test_env
+}
+
+# ------------------------------------------------------------------------------
+# Test: Copies files to backup
+# ------------------------------------------------------------------------------
+test_copies_files() {
+ setup_test_env
+
+ create_test_config
+ create_test_includes
+ create_test_excludes
+
+ local output
+ output=$(sudo INSTALLHOME="$TEST_CONFIG_DIR" RSYNCSHOT_SKIP_MOUNT_CHECK=1 "$SCRIPT_PATH" manual 1 2>&1)
+
+ # Check files were copied
+ assert_file_exists "$TEST_BACKUP_DIR/$HOSTNAME/latest/home/testuser/file1.txt" "should copy file1.txt" || {
+ teardown_test_env
+ return 1
+ }
+ assert_file_exists "$TEST_BACKUP_DIR/$HOSTNAME/latest/etc/test.conf" "should copy test.conf" || {
+ teardown_test_env
+ return 1
+ }
+
+ teardown_test_env
+}
+
+# ------------------------------------------------------------------------------
+# Test: Snapshot is read-only
+# ------------------------------------------------------------------------------
+test_snapshot_readonly() {
+ setup_test_env
+
+ create_test_config
+ create_test_includes
+ create_test_excludes
+
+ local output
+ output=$(sudo INSTALLHOME="$TEST_CONFIG_DIR" RSYNCSHOT_SKIP_MOUNT_CHECK=1 "$SCRIPT_PATH" manual 1 2>&1)
+
+ # Check snapshot directory has no write permission
+ # Note: We use stat to check actual permissions because -w always returns true for root
+ local perms
+ perms=$(stat -c '%A' "$TEST_BACKUP_DIR/$HOSTNAME/MANUAL.0" 2>/dev/null)
+ if [[ "$perms" == *w* ]]; then
+ echo "FAIL: Snapshot should be read-only (perms: $perms)"
+ teardown_test_env
+ return 1
+ fi
+
+ teardown_test_env
+ return 0
+}
+
+# ------------------------------------------------------------------------------
+# Test: Rotation works correctly
+# ------------------------------------------------------------------------------
+test_rotation() {
+ setup_test_env
+
+ create_test_config
+ create_test_includes
+ create_test_excludes
+
+ # Run backup twice
+ sudo INSTALLHOME="$TEST_CONFIG_DIR" RSYNCSHOT_SKIP_MOUNT_CHECK=1 "$SCRIPT_PATH" manual 3 2>&1 >/dev/null
+ sudo INSTALLHOME="$TEST_CONFIG_DIR" RSYNCSHOT_SKIP_MOUNT_CHECK=1 "$SCRIPT_PATH" manual 3 2>&1 >/dev/null
+
+ # Should have MANUAL.0 and MANUAL.1
+ assert_dir_exists "$TEST_BACKUP_DIR/$HOSTNAME/MANUAL.0" "should have MANUAL.0" || {
+ teardown_test_env
+ return 1
+ }
+ assert_dir_exists "$TEST_BACKUP_DIR/$HOSTNAME/MANUAL.1" "should have MANUAL.1 after rotation" || {
+ teardown_test_env
+ return 1
+ }
+
+ teardown_test_env
+}
+
+# ------------------------------------------------------------------------------
+# Test: Deletes oldest snapshot beyond retention
+# ------------------------------------------------------------------------------
+test_retention_limit() {
+ setup_test_env
+
+ create_test_config
+ create_test_includes
+ create_test_excludes
+
+ # Run backup 4 times with retention of 3
+ for i in 1 2 3 4; do
+ sudo INSTALLHOME="$TEST_CONFIG_DIR" RSYNCSHOT_SKIP_MOUNT_CHECK=1 "$SCRIPT_PATH" manual 3 2>&1 >/dev/null
+ done
+
+ # Should have MANUAL.0, MANUAL.1, MANUAL.2 but NOT MANUAL.3
+ assert_dir_exists "$TEST_BACKUP_DIR/$HOSTNAME/MANUAL.0" "should have MANUAL.0" || {
+ teardown_test_env
+ return 1
+ }
+ assert_dir_exists "$TEST_BACKUP_DIR/$HOSTNAME/MANUAL.1" "should have MANUAL.1" || {
+ teardown_test_env
+ return 1
+ }
+ assert_dir_exists "$TEST_BACKUP_DIR/$HOSTNAME/MANUAL.2" "should have MANUAL.2" || {
+ teardown_test_env
+ return 1
+ }
+ assert_dir_not_exists "$TEST_BACKUP_DIR/$HOSTNAME/MANUAL.3" "should NOT have MANUAL.3 (beyond retention)" || {
+ teardown_test_env
+ return 1
+ }
+
+ teardown_test_env
+}
+
+# ------------------------------------------------------------------------------
+# Test: Backup command works as alias
+# ------------------------------------------------------------------------------
+test_backup_command() {
+ setup_test_env
+
+ create_test_config
+ create_test_includes
+ create_test_excludes
+
+ local output
+ output=$(sudo INSTALLHOME="$TEST_CONFIG_DIR" RSYNCSHOT_SKIP_MOUNT_CHECK=1 "$SCRIPT_PATH" backup 2>&1)
+ local exit_code=$?
+
+ # Should create MANUAL.0 (backup is alias for manual 1)
+ assert_dir_exists "$TEST_BACKUP_DIR/$HOSTNAME/MANUAL.0" "backup should create MANUAL.0" || {
+ teardown_test_env
+ return 1
+ }
+
+ teardown_test_env
+}
+
+# ------------------------------------------------------------------------------
+# Test: Excludes files matching patterns
+# ------------------------------------------------------------------------------
+test_excludes_patterns() {
+ setup_test_env
+
+ create_test_config
+ create_test_includes
+
+ # Create exclude file
+ cat > "$TEST_CONFIG_DIR/exclude.txt" << 'EOF'
+*.tmp
+*.log
+EOF
+
+ # Create files that should be excluded
+ echo "temp" > "$TEST_SOURCE_DIR/home/testuser/temp.tmp"
+ echo "log" > "$TEST_SOURCE_DIR/home/testuser/app.log"
+
+ local output
+ output=$(sudo INSTALLHOME="$TEST_CONFIG_DIR" RSYNCSHOT_SKIP_MOUNT_CHECK=1 "$SCRIPT_PATH" manual 1 2>&1)
+
+ # Excluded files should not exist in backup
+ assert_file_not_exists "$TEST_BACKUP_DIR/$HOSTNAME/latest/home/testuser/temp.tmp" "should exclude .tmp files" || {
+ teardown_test_env
+ return 1
+ }
+ assert_file_not_exists "$TEST_BACKUP_DIR/$HOSTNAME/latest/home/testuser/app.log" "should exclude .log files" || {
+ teardown_test_env
+ return 1
+ }
+ # Regular files should still exist
+ assert_file_exists "$TEST_BACKUP_DIR/$HOSTNAME/latest/home/testuser/file1.txt" "should include regular files" || {
+ teardown_test_env
+ return 1
+ }
+
+ teardown_test_env
+}
+
+# ------------------------------------------------------------------------------
+# Run tests
+# ------------------------------------------------------------------------------
+run_backup_tests() {
+ echo ""
+ echo "Running backup tests..."
+ echo "------------------------------------------------------------"
+
+ run_test "creates backup directory structure" test_creates_backup_structure
+ run_test "copies files to backup" test_copies_files
+ run_test "snapshot is read-only" test_snapshot_readonly
+ run_test "rotation works correctly" test_rotation
+ run_test "respects retention limit" test_retention_limit
+ run_test "backup command works as alias" test_backup_command
+ run_test "excludes files matching patterns" test_excludes_patterns
+}
+
+# Run if executed directly
+if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
+ run_backup_tests
+ print_summary
+fi
diff --git a/tests/cases/test_cron.sh b/tests/cases/test_cron.sh
new file mode 100755
index 0000000..2fdb6f5
--- /dev/null
+++ b/tests/cases/test_cron.sh
@@ -0,0 +1,138 @@
+#!/usr/bin/env bash
+# ==============================================================================
+# Cron Job Management Tests
+# ==============================================================================
+
+source "$(dirname "${BASH_SOURCE[0]}")/../lib/test_helpers.sh"
+
+# Save original crontab to restore later
+ORIGINAL_CRONTAB=""
+
+save_crontab() {
+ ORIGINAL_CRONTAB=$(crontab -l 2>/dev/null || true)
+}
+
+restore_crontab() {
+ if [ -n "$ORIGINAL_CRONTAB" ]; then
+ echo "$ORIGINAL_CRONTAB" | crontab -
+ else
+ crontab -r 2>/dev/null || true
+ fi
+}
+
+# ------------------------------------------------------------------------------
+# Test: Setup adds cron jobs
+# ------------------------------------------------------------------------------
+test_setup_adds_cron_jobs() {
+ setup_test_env
+ save_crontab
+
+ # Clear existing crontab
+ crontab -r 2>/dev/null || true
+
+ # Create minimal config
+ cat > "$TEST_CONFIG_DIR/config" << EOF
+REMOTE_HOST=""
+MOUNTDIR="$TEST_BACKUP_DIR"
+EOF
+ create_test_includes
+ create_test_excludes
+
+ # Run setup (will fail on some checks but should still add cron)
+ sudo INSTALLHOME="$TEST_CONFIG_DIR" SCRIPTLOC="/tmp/rsyncshot-test" RSYNCSHOT_SKIP_MOUNT_CHECK=1 "$SCRIPT_PATH" setup 2>&1 >/dev/null || true
+
+ # Check crontab contains rsyncshot entries
+ local crontab_content
+ crontab_content=$(crontab -l 2>/dev/null)
+
+ restore_crontab
+ teardown_test_env
+
+ assert_contains "$crontab_content" "rsyncshot" "crontab should contain rsyncshot" || return 1
+ assert_contains "$crontab_content" "hourly" "crontab should contain hourly job" || return 1
+ assert_contains "$crontab_content" "daily" "crontab should contain daily job" || return 1
+ assert_contains "$crontab_content" "weekly" "crontab should contain weekly job" || return 1
+}
+
+# ------------------------------------------------------------------------------
+# Test: Repeated setup doesn't duplicate entries
+# ------------------------------------------------------------------------------
+test_no_duplicate_cron_entries() {
+ setup_test_env
+ save_crontab
+
+ # Clear existing crontab
+ crontab -r 2>/dev/null || true
+
+ cat > "$TEST_CONFIG_DIR/config" << EOF
+REMOTE_HOST=""
+MOUNTDIR="$TEST_BACKUP_DIR"
+EOF
+ create_test_includes
+ create_test_excludes
+
+ # Run setup twice
+ sudo INSTALLHOME="$TEST_CONFIG_DIR" SCRIPTLOC="/tmp/rsyncshot-test" RSYNCSHOT_SKIP_MOUNT_CHECK=1 "$SCRIPT_PATH" setup 2>&1 >/dev/null || true
+ sudo INSTALLHOME="$TEST_CONFIG_DIR" SCRIPTLOC="/tmp/rsyncshot-test" RSYNCSHOT_SKIP_MOUNT_CHECK=1 "$SCRIPT_PATH" setup 2>&1 >/dev/null || true
+
+ # Count rsyncshot entries
+ local crontab_content hourly_count
+ crontab_content=$(crontab -l 2>/dev/null)
+ hourly_count=$(echo "$crontab_content" | grep -c "hourly" || echo 0)
+
+ restore_crontab
+ teardown_test_env
+
+ # Should only have 1 hourly entry, not 2
+ assert_equals "1" "$hourly_count" "should have exactly 1 hourly entry after repeated setup" || return 1
+}
+
+# ------------------------------------------------------------------------------
+# Test: Setup preserves existing cron jobs
+# ------------------------------------------------------------------------------
+test_preserves_existing_cron() {
+ setup_test_env
+ save_crontab
+
+ # Add a custom cron job
+ (crontab -l 2>/dev/null || true; echo "0 5 * * * /custom/job.sh") | crontab -
+
+ cat > "$TEST_CONFIG_DIR/config" << EOF
+REMOTE_HOST=""
+MOUNTDIR="$TEST_BACKUP_DIR"
+EOF
+ create_test_includes
+ create_test_excludes
+
+ # Run setup
+ sudo INSTALLHOME="$TEST_CONFIG_DIR" SCRIPTLOC="/tmp/rsyncshot-test" RSYNCSHOT_SKIP_MOUNT_CHECK=1 "$SCRIPT_PATH" setup 2>&1 >/dev/null || true
+
+ # Check custom job still exists
+ local crontab_content
+ crontab_content=$(crontab -l 2>/dev/null)
+
+ restore_crontab
+ teardown_test_env
+
+ assert_contains "$crontab_content" "/custom/job.sh" "should preserve existing cron jobs" || return 1
+ assert_contains "$crontab_content" "rsyncshot" "should also have rsyncshot jobs" || return 1
+}
+
+# ------------------------------------------------------------------------------
+# Run tests
+# ------------------------------------------------------------------------------
+run_cron_tests() {
+ echo ""
+ echo "Running cron tests..."
+ echo "------------------------------------------------------------"
+
+ run_test "setup adds cron jobs" test_setup_adds_cron_jobs
+ run_test "repeated setup doesn't duplicate entries" test_no_duplicate_cron_entries
+ run_test "setup preserves existing cron jobs" test_preserves_existing_cron
+}
+
+# Run if executed directly
+if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
+ run_cron_tests
+ print_summary
+fi
diff --git a/tests/cases/test_dryrun.sh b/tests/cases/test_dryrun.sh
new file mode 100755
index 0000000..bff45e1
--- /dev/null
+++ b/tests/cases/test_dryrun.sh
@@ -0,0 +1,120 @@
+#!/usr/bin/env bash
+# ==============================================================================
+# Dry-Run Mode Tests
+# ==============================================================================
+
+source "$(dirname "${BASH_SOURCE[0]}")/../lib/test_helpers.sh"
+
+# ------------------------------------------------------------------------------
+# Test: Dry-run doesn't create backup directory
+# ------------------------------------------------------------------------------
+test_dryrun_no_directory_creation() {
+ setup_test_env
+
+ create_test_config
+ create_test_includes
+ create_test_excludes
+
+ # Remove backup dir to verify it's not created
+ rmdir "$TEST_BACKUP_DIR"
+
+ local output
+ output=$(sudo INSTALLHOME="$TEST_CONFIG_DIR" RSYNCSHOT_SKIP_MOUNT_CHECK=1 "$SCRIPT_PATH" dryrun manual 1 2>&1)
+
+ # Backup directory should NOT be created in dry-run mode
+ assert_dir_not_exists "$TEST_BACKUP_DIR/$HOSTNAME" "backup dir should not be created in dryrun" || {
+ teardown_test_env
+ return 1
+ }
+
+ teardown_test_env
+}
+
+# ------------------------------------------------------------------------------
+# Test: Dry-run shows what would be transferred
+# ------------------------------------------------------------------------------
+test_dryrun_shows_transfer_info() {
+ setup_test_env
+
+ create_test_config
+ create_test_includes
+ create_test_excludes
+
+ # Create the backup directory structure for dryrun to work
+ mkdir -p "$TEST_BACKUP_DIR/$HOSTNAME/latest"
+
+ local output
+ output=$(sudo INSTALLHOME="$TEST_CONFIG_DIR" RSYNCSHOT_SKIP_MOUNT_CHECK=1 "$SCRIPT_PATH" dryrun manual 1 2>&1)
+
+ teardown_test_env
+
+ # Should show syncing messages
+ assert_contains "$output" "Syncing" "should show syncing info" || return 1
+ # Should show dry run message
+ assert_contains "$output" "Dry run complete" "should show dryrun complete message" || return 1
+}
+
+# ------------------------------------------------------------------------------
+# Test: Dry-run shows command to run actual backup
+# ------------------------------------------------------------------------------
+test_dryrun_shows_actual_command() {
+ setup_test_env
+
+ create_test_config
+ create_test_includes
+ create_test_excludes
+
+ mkdir -p "$TEST_BACKUP_DIR/$HOSTNAME/latest"
+
+ local output
+ output=$(sudo INSTALLHOME="$TEST_CONFIG_DIR" RSYNCSHOT_SKIP_MOUNT_CHECK=1 "$SCRIPT_PATH" dryrun manual 1 2>&1)
+
+ teardown_test_env
+
+ # Should show how to run actual backup
+ assert_contains "$output" "sudo rsyncshot manual 1" "should show actual command" || return 1
+}
+
+# ------------------------------------------------------------------------------
+# Test: Dry-run doesn't create snapshots
+# ------------------------------------------------------------------------------
+test_dryrun_no_snapshot_creation() {
+ setup_test_env
+
+ create_test_config
+ create_test_includes
+ create_test_excludes
+
+ mkdir -p "$TEST_BACKUP_DIR/$HOSTNAME/latest"
+
+ local output
+ output=$(sudo INSTALLHOME="$TEST_CONFIG_DIR" RSYNCSHOT_SKIP_MOUNT_CHECK=1 "$SCRIPT_PATH" dryrun manual 1 2>&1)
+
+ # Should not create manual.0 snapshot
+ assert_dir_not_exists "$TEST_BACKUP_DIR/$HOSTNAME/manual.0" "snapshot should not be created in dryrun" || {
+ teardown_test_env
+ return 1
+ }
+
+ teardown_test_env
+}
+
+# ------------------------------------------------------------------------------
+# Run tests
+# ------------------------------------------------------------------------------
+run_dryrun_tests() {
+ echo ""
+ echo "Running dry-run tests..."
+ echo "------------------------------------------------------------"
+
+ run_test "dry-run doesn't create backup directory" test_dryrun_no_directory_creation
+ run_test "dry-run shows transfer info" test_dryrun_shows_transfer_info
+ run_test "dry-run shows actual command" test_dryrun_shows_actual_command
+ run_test "dry-run doesn't create snapshots" test_dryrun_no_snapshot_creation
+}
+
+# Run if executed directly
+if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
+ run_dryrun_tests
+ print_summary
+fi
diff --git a/tests/cases/test_includes.sh b/tests/cases/test_includes.sh
new file mode 100755
index 0000000..8f556e3
--- /dev/null
+++ b/tests/cases/test_includes.sh
@@ -0,0 +1,161 @@
+#!/usr/bin/env bash
+# ==============================================================================
+# Include File Parsing Tests
+# ==============================================================================
+
+source "$(dirname "${BASH_SOURCE[0]}")/../lib/test_helpers.sh"
+
+# ------------------------------------------------------------------------------
+# Test: Reads newline-separated paths
+# ------------------------------------------------------------------------------
+test_reads_newline_paths() {
+ setup_test_env
+
+ # Create config and include files
+ create_test_config
+ create_test_excludes
+
+ # Create include file with newline-separated paths
+ cat > "$TEST_CONFIG_DIR/include.txt" << EOF
+$TEST_SOURCE_DIR/home
+$TEST_SOURCE_DIR/etc
+EOF
+
+ # Run dryrun to test parsing
+ local output
+ output=$(sudo INSTALLHOME="$TEST_CONFIG_DIR" RSYNCSHOT_SKIP_MOUNT_CHECK=1 "$SCRIPT_PATH" dryrun manual 1 2>&1)
+ local exit_code=$?
+
+ teardown_test_env
+
+ # Should process both directories
+ assert_contains "$output" "Syncing $TEST_SOURCE_DIR/home" "should sync home" || return 1
+ assert_contains "$output" "Syncing $TEST_SOURCE_DIR/etc" "should sync etc" || return 1
+}
+
+# ------------------------------------------------------------------------------
+# Test: Skips comment lines
+# ------------------------------------------------------------------------------
+test_skips_comments() {
+ setup_test_env
+
+ create_test_config
+ create_test_excludes
+
+ # Create include file with comments
+ cat > "$TEST_CONFIG_DIR/include.txt" << EOF
+# This is a comment
+$TEST_SOURCE_DIR/home
+# Another comment
+$TEST_SOURCE_DIR/etc
+EOF
+
+ local output
+ output=$(sudo INSTALLHOME="$TEST_CONFIG_DIR" RSYNCSHOT_SKIP_MOUNT_CHECK=1 "$SCRIPT_PATH" dryrun manual 1 2>&1)
+
+ teardown_test_env
+
+ # Should not try to sync comment lines
+ assert_not_contains "$output" "Syncing # This" "should skip comments" || return 1
+ assert_contains "$output" "Syncing $TEST_SOURCE_DIR/home" "should sync home" || return 1
+}
+
+# ------------------------------------------------------------------------------
+# Test: Skips empty lines
+# ------------------------------------------------------------------------------
+test_skips_empty_lines() {
+ setup_test_env
+
+ create_test_config
+ create_test_excludes
+
+ # Create include file with empty lines
+ cat > "$TEST_CONFIG_DIR/include.txt" << EOF
+$TEST_SOURCE_DIR/home
+
+$TEST_SOURCE_DIR/etc
+
+EOF
+
+ local output
+ output=$(sudo INSTALLHOME="$TEST_CONFIG_DIR" RSYNCSHOT_SKIP_MOUNT_CHECK=1 "$SCRIPT_PATH" dryrun manual 1 2>&1)
+
+ teardown_test_env
+
+ # Should process both directories without errors
+ assert_contains "$output" "Syncing $TEST_SOURCE_DIR/home" "should sync home" || return 1
+ assert_contains "$output" "Syncing $TEST_SOURCE_DIR/etc" "should sync etc" || return 1
+}
+
+# ------------------------------------------------------------------------------
+# Test: Handles paths with spaces
+# ------------------------------------------------------------------------------
+test_handles_paths_with_spaces() {
+ setup_test_env
+
+ create_test_config
+ create_test_excludes
+
+ # Create a directory with spaces
+ mkdir -p "$TEST_SOURCE_DIR/path with spaces"
+ echo "test" > "$TEST_SOURCE_DIR/path with spaces/file.txt"
+
+ # Create include file with path containing spaces
+ cat > "$TEST_CONFIG_DIR/include.txt" << EOF
+$TEST_SOURCE_DIR/path with spaces
+EOF
+
+ local output
+ output=$(sudo INSTALLHOME="$TEST_CONFIG_DIR" RSYNCSHOT_SKIP_MOUNT_CHECK=1 "$SCRIPT_PATH" dryrun manual 1 2>&1)
+
+ teardown_test_env
+
+ # Should handle the path with spaces
+ assert_contains "$output" "path with spaces" "should handle spaces in path" || return 1
+ assert_not_contains "$output" "not found" "should not report path not found" || return 1
+}
+
+# ------------------------------------------------------------------------------
+# Test: Reports missing directory
+# ------------------------------------------------------------------------------
+test_reports_missing_directory() {
+ setup_test_env
+
+ create_test_config
+ create_test_excludes
+
+ # Create include file with non-existent path
+ cat > "$TEST_CONFIG_DIR/include.txt" << EOF
+/nonexistent/path/that/does/not/exist
+EOF
+
+ local output
+ output=$(sudo INSTALLHOME="$TEST_CONFIG_DIR" RSYNCSHOT_SKIP_MOUNT_CHECK=1 "$SCRIPT_PATH" dryrun manual 1 2>&1)
+ local exit_code=$?
+
+ teardown_test_env
+
+ assert_exit_code 1 "$exit_code" "should fail for missing directory" || return 1
+ assert_contains "$output" "not found" "should report directory not found" || return 1
+}
+
+# ------------------------------------------------------------------------------
+# Run tests
+# ------------------------------------------------------------------------------
+run_includes_tests() {
+ echo ""
+ echo "Running include file tests..."
+ echo "------------------------------------------------------------"
+
+ run_test "reads newline-separated paths" test_reads_newline_paths
+ run_test "skips comment lines" test_skips_comments
+ run_test "skips empty lines" test_skips_empty_lines
+ run_test "handles paths with spaces" test_handles_paths_with_spaces
+ run_test "reports missing directory" test_reports_missing_directory
+}
+
+# Run if executed directly
+if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
+ run_includes_tests
+ print_summary
+fi
diff --git a/tests/cases/test_validation.sh b/tests/cases/test_validation.sh
new file mode 100755
index 0000000..03da676
--- /dev/null
+++ b/tests/cases/test_validation.sh
@@ -0,0 +1,106 @@
+#!/usr/bin/env bash
+# ==============================================================================
+# Input Validation Tests
+# ==============================================================================
+
+source "$(dirname "${BASH_SOURCE[0]}")/../lib/test_helpers.sh"
+
+# ------------------------------------------------------------------------------
+# Test: Help works without root
+# ------------------------------------------------------------------------------
+test_help_without_root() {
+ local output
+ output=$("$SCRIPT_PATH" help 2>&1)
+ local exit_code=$?
+
+ assert_exit_code 0 "$exit_code" "help should exit with 0" || return 1
+ assert_contains "$output" "rsyncshot" "help should mention rsyncshot" || return 1
+ assert_contains "$output" "Usage" "help should show usage" || return 1
+}
+
+# ------------------------------------------------------------------------------
+# Test: Rejects non-alphabetic snapshot type
+# ------------------------------------------------------------------------------
+test_rejects_numeric_snapshot_type() {
+ local output
+ output=$(sudo "$SCRIPT_PATH" "123" "5" 2>&1)
+ local exit_code=$?
+
+ assert_exit_code 1 "$exit_code" "should reject numeric snapshot type" || return 1
+ assert_contains "$output" "must be alphabetic" "should show alphabetic error" || return 1
+}
+
+# ------------------------------------------------------------------------------
+# Test: Rejects mixed alphanumeric snapshot type
+# ------------------------------------------------------------------------------
+test_rejects_mixed_snapshot_type() {
+ local output
+ output=$(sudo "$SCRIPT_PATH" "hourly123" "5" 2>&1)
+ local exit_code=$?
+
+ assert_exit_code 1 "$exit_code" "should reject mixed snapshot type" || return 1
+ assert_contains "$output" "must be alphabetic" "should show alphabetic error" || return 1
+}
+
+# ------------------------------------------------------------------------------
+# Test: Rejects non-numeric retention count
+# ------------------------------------------------------------------------------
+test_rejects_alpha_retention_count() {
+ local output
+ output=$(sudo "$SCRIPT_PATH" "manual" "abc" 2>&1)
+ local exit_code=$?
+
+ assert_exit_code 1 "$exit_code" "should reject alphabetic count" || return 1
+ assert_contains "$output" "must be a number" "should show number error" || return 1
+}
+
+# ------------------------------------------------------------------------------
+# Test: Rejects mixed alphanumeric retention count
+# ------------------------------------------------------------------------------
+test_rejects_mixed_retention_count() {
+ local output
+ output=$(sudo "$SCRIPT_PATH" "manual" "5abc" 2>&1)
+ local exit_code=$?
+
+ assert_exit_code 1 "$exit_code" "should reject mixed count" || return 1
+ assert_contains "$output" "must be a number" "should show number error" || return 1
+}
+
+# ------------------------------------------------------------------------------
+# Test: Accepts valid alphabetic snapshot types
+# ------------------------------------------------------------------------------
+test_accepts_valid_snapshot_types() {
+ # We use dryrun to avoid actual backup, and expect it to fail on missing config
+ # but it should get past the validation stage
+ local output
+ output=$(sudo "$SCRIPT_PATH" dryrun "hourly" "24" 2>&1)
+
+ # Should not contain the validation error (might fail for other reasons like missing config)
+ assert_not_contains "$output" "must be alphabetic" "should accept valid type" || return 1
+}
+
+# ------------------------------------------------------------------------------
+# Run tests
+# ------------------------------------------------------------------------------
+run_validation_tests() {
+ echo ""
+ echo "Running validation tests..."
+ echo "------------------------------------------------------------"
+
+ setup_test_env
+
+ run_test "help works without root" test_help_without_root
+ run_test "rejects numeric snapshot type" test_rejects_numeric_snapshot_type
+ run_test "rejects mixed alphanumeric snapshot type" test_rejects_mixed_snapshot_type
+ run_test "rejects alphabetic retention count" test_rejects_alpha_retention_count
+ run_test "rejects mixed retention count" test_rejects_mixed_retention_count
+ run_test "accepts valid snapshot types" test_accepts_valid_snapshot_types
+
+ teardown_test_env
+}
+
+# Run if executed directly
+if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
+ run_validation_tests
+ print_summary
+fi