aboutsummaryrefslogtreecommitdiff
path: root/tests/cases
diff options
context:
space:
mode:
Diffstat (limited to 'tests/cases')
-rwxr-xr-xtests/cases/test_backup.sh73
-rwxr-xr-xtests/cases/test_cron.sh8
-rw-r--r--tests/cases/test_functions.sh146
-rw-r--r--tests/cases/test_remote.sh60
-rw-r--r--tests/cases/test_rsync_flags.sh53
-rwxr-xr-xtests/cases/test_validation.sh56
6 files changed, 371 insertions, 25 deletions
diff --git a/tests/cases/test_backup.sh b/tests/cases/test_backup.sh
index 1ab4fbd..af01221 100755
--- a/tests/cases/test_backup.sh
+++ b/tests/cases/test_backup.sh
@@ -15,9 +15,7 @@ test_creates_backup_structure() {
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=$?
+ sudo INSTALLHOME="$TEST_CONFIG_DIR" RSYNCSHOT_SKIP_MOUNT_CHECK=1 "$SCRIPT_PATH" manual 1 >/dev/null 2>&1
# Check directory structure
assert_dir_exists "$TEST_BACKUP_DIR/$HOSTNAME" "should create hostname dir" || {
@@ -46,15 +44,14 @@ test_copies_files() {
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)
+ sudo INSTALLHOME="$TEST_CONFIG_DIR" RSYNCSHOT_SKIP_MOUNT_CHECK=1 "$SCRIPT_PATH" manual 1 >/dev/null 2>&1
# Check files were copied
- assert_file_exists "$TEST_BACKUP_DIR/$HOSTNAME/latest/home/testuser/file1.txt" "should copy file1.txt" || {
+ assert_file_exists "$TEST_BACKUP_DIR/$HOSTNAME/latest$TEST_SOURCE_DIR/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" || {
+ assert_file_exists "$TEST_BACKUP_DIR/$HOSTNAME/latest$TEST_SOURCE_DIR/etc/test.conf" "should copy test.conf" || {
teardown_test_env
return 1
}
@@ -72,8 +69,7 @@ test_snapshot_readonly() {
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)
+ sudo INSTALLHOME="$TEST_CONFIG_DIR" RSYNCSHOT_SKIP_MOUNT_CHECK=1 "$SCRIPT_PATH" manual 1 >/dev/null 2>&1
# Check snapshot directory has no write permission
# Note: We use stat to check actual permissions because -w always returns true for root
@@ -100,8 +96,8 @@ test_rotation() {
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
+ sudo INSTALLHOME="$TEST_CONFIG_DIR" RSYNCSHOT_SKIP_MOUNT_CHECK=1 "$SCRIPT_PATH" manual 3 >/dev/null 2>&1
+ sudo INSTALLHOME="$TEST_CONFIG_DIR" RSYNCSHOT_SKIP_MOUNT_CHECK=1 "$SCRIPT_PATH" manual 3 >/dev/null 2>&1
# Should have MANUAL.0 and MANUAL.1
assert_dir_exists "$TEST_BACKUP_DIR/$HOSTNAME/MANUAL.0" "should have MANUAL.0" || {
@@ -127,8 +123,8 @@ test_retention_limit() {
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
+ for _ in 1 2 3 4; do
+ sudo INSTALLHOME="$TEST_CONFIG_DIR" RSYNCSHOT_SKIP_MOUNT_CHECK=1 "$SCRIPT_PATH" manual 3 >/dev/null 2>&1
done
# Should have MANUAL.0, MANUAL.1, MANUAL.2 but NOT MANUAL.3
@@ -162,9 +158,7 @@ test_backup_command() {
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=$?
+ sudo INSTALLHOME="$TEST_CONFIG_DIR" RSYNCSHOT_SKIP_MOUNT_CHECK=1 "$SCRIPT_PATH" backup >/dev/null 2>&1
# Should create MANUAL.0 (backup is alias for manual 1)
assert_dir_exists "$TEST_BACKUP_DIR/$HOSTNAME/MANUAL.0" "backup should create MANUAL.0" || {
@@ -194,20 +188,56 @@ EOF
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)
+ sudo INSTALLHOME="$TEST_CONFIG_DIR" RSYNCSHOT_SKIP_MOUNT_CHECK=1 "$SCRIPT_PATH" manual 1 >/dev/null 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" || {
+ assert_file_not_exists "$TEST_BACKUP_DIR/$HOSTNAME/latest$TEST_SOURCE_DIR/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" || {
+ assert_file_not_exists "$TEST_BACKUP_DIR/$HOSTNAME/latest$TEST_SOURCE_DIR/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" || {
+ assert_file_exists "$TEST_BACKUP_DIR/$HOSTNAME/latest$TEST_SOURCE_DIR/home/testuser/file1.txt" "should include regular files" || {
+ teardown_test_env
+ return 1
+ }
+
+ teardown_test_env
+}
+
+# ------------------------------------------------------------------------------
+# Test: Same-basename sources don't collide (relative paths)
+# ------------------------------------------------------------------------------
+# Two includes with the same basename ("bin") previously both mapped to
+# latest/bin, and the second sync's --delete wiped the first. With -R each is
+# stored under its full path, so both survive.
+test_no_basename_collision() {
+ setup_test_env
+
+ mkdir -p "$TEST_SOURCE_DIR/opt/bin" "$TEST_SOURCE_DIR/usr/bin"
+ echo "from-opt" > "$TEST_SOURCE_DIR/opt/bin/optfile"
+ echo "from-usr" > "$TEST_SOURCE_DIR/usr/bin/usrfile"
+
+ cat > "$TEST_CONFIG_DIR/config" <<EOF
+REMOTE_HOST=""
+MOUNTDIR="$TEST_BACKUP_DIR"
+EOF
+ cat > "$TEST_CONFIG_DIR/include.txt" <<EOF
+$TEST_SOURCE_DIR/opt/bin
+$TEST_SOURCE_DIR/usr/bin
+EOF
+ create_test_excludes >/dev/null
+
+ sudo INSTALLHOME="$TEST_CONFIG_DIR" RSYNCSHOT_SKIP_MOUNT_CHECK=1 "$SCRIPT_PATH" manual 1 >/dev/null 2>&1
+
+ assert_file_exists "$TEST_BACKUP_DIR/$HOSTNAME/latest$TEST_SOURCE_DIR/opt/bin/optfile" "opt/bin should survive" || {
+ teardown_test_env
+ return 1
+ }
+ assert_file_exists "$TEST_BACKUP_DIR/$HOSTNAME/latest$TEST_SOURCE_DIR/usr/bin/usrfile" "usr/bin should survive (no collision)" || {
teardown_test_env
return 1
}
@@ -230,6 +260,7 @@ run_backup_tests() {
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_test "same-basename sources don't collide" test_no_basename_collision
}
# Run if executed directly
diff --git a/tests/cases/test_cron.sh b/tests/cases/test_cron.sh
index 2fdb6f5..5c9bbfa 100755
--- a/tests/cases/test_cron.sh
+++ b/tests/cases/test_cron.sh
@@ -39,7 +39,7 @@ EOF
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
+ sudo INSTALLHOME="$TEST_CONFIG_DIR" SCRIPTLOC="/tmp/rsyncshot-test" RSYNCSHOT_SKIP_MOUNT_CHECK=1 "$SCRIPT_PATH" setup >/dev/null 2>&1 || true
# Check crontab contains rsyncshot entries
local crontab_content
@@ -72,8 +72,8 @@ EOF
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
+ sudo INSTALLHOME="$TEST_CONFIG_DIR" SCRIPTLOC="/tmp/rsyncshot-test" RSYNCSHOT_SKIP_MOUNT_CHECK=1 "$SCRIPT_PATH" setup >/dev/null 2>&1 || true
+ sudo INSTALLHOME="$TEST_CONFIG_DIR" SCRIPTLOC="/tmp/rsyncshot-test" RSYNCSHOT_SKIP_MOUNT_CHECK=1 "$SCRIPT_PATH" setup >/dev/null 2>&1 || true
# Count rsyncshot entries
local crontab_content hourly_count
@@ -105,7 +105,7 @@ EOF
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
+ sudo INSTALLHOME="$TEST_CONFIG_DIR" SCRIPTLOC="/tmp/rsyncshot-test" RSYNCSHOT_SKIP_MOUNT_CHECK=1 "$SCRIPT_PATH" setup >/dev/null 2>&1 || true
# Check custom job still exists
local crontab_content
diff --git a/tests/cases/test_functions.sh b/tests/cases/test_functions.sh
new file mode 100644
index 0000000..cd607ab
--- /dev/null
+++ b/tests/cases/test_functions.sh
@@ -0,0 +1,146 @@
+#!/usr/bin/env bash
+# ==============================================================================
+# Unit Tests for Internal Functions (is_mounted, derive_paths)
+# ==============================================================================
+# These source the script with RSYNCSHOT_SOURCE_ONLY=1 so the functions are
+# defined without running the main flow, then exercise them directly. Each test
+# runs the source + assertions in a subshell so the derived globals and config
+# variables don't leak into other suites.
+
+source "$(dirname "${BASH_SOURCE[0]}")/../lib/test_helpers.sh"
+
+# ------------------------------------------------------------------------------
+# is_mounted: exact mount point matches
+# ------------------------------------------------------------------------------
+test_is_mounted_exact_match() {
+ setup_test_env
+ local mounts="$TEST_DIR/mounts"
+ cat > "$mounts" <<EOF
+/dev/sda1 /media/backup ext4 rw,relatime 0 0
+/dev/sdb1 /home ext4 rw,relatime 0 0
+EOF
+ (
+ INSTALLHOME="$TEST_CONFIG_DIR" RSYNCSHOT_SOURCE_ONLY=1 source "$SCRIPT_PATH"
+ is_mounted "/media/backup" "$mounts"
+ )
+ local rc=$?
+ teardown_test_env
+ assert_exit_code 0 "$rc" "exact mount point should match" || return 1
+}
+
+# ------------------------------------------------------------------------------
+# is_mounted: a prefix is NOT a match (the substring-grep bug)
+# ------------------------------------------------------------------------------
+test_is_mounted_no_substring_match() {
+ setup_test_env
+ local mounts="$TEST_DIR/mounts"
+ cat > "$mounts" <<EOF
+/dev/sda1 /media/backup2 ext4 rw,relatime 0 0
+EOF
+ (
+ INSTALLHOME="$TEST_CONFIG_DIR" RSYNCSHOT_SOURCE_ONLY=1 source "$SCRIPT_PATH"
+ is_mounted "/media/backup" "$mounts"
+ )
+ local rc=$?
+ teardown_test_env
+ assert_exit_code 1 "$rc" "/media/backup must NOT match /media/backup2" || return 1
+}
+
+# ------------------------------------------------------------------------------
+# is_mounted: mount points with spaces (encoded as \040 in /proc/mounts)
+# ------------------------------------------------------------------------------
+test_is_mounted_handles_spaces() {
+ setup_test_env
+ local mounts="$TEST_DIR/mounts"
+ printf '/dev/sda1 /media/my\\040backup ext4 rw 0 0\n' > "$mounts"
+ (
+ INSTALLHOME="$TEST_CONFIG_DIR" RSYNCSHOT_SOURCE_ONLY=1 source "$SCRIPT_PATH"
+ is_mounted "/media/my backup" "$mounts"
+ )
+ local rc=$?
+ teardown_test_env
+ assert_exit_code 0 "$rc" "mount point with spaces should match" || return 1
+}
+
+# ------------------------------------------------------------------------------
+# is_mounted: target is treated literally, not as a regex
+# ------------------------------------------------------------------------------
+test_is_mounted_target_is_literal() {
+ setup_test_env
+ local mounts="$TEST_DIR/mounts"
+ cat > "$mounts" <<EOF
+/dev/sda1 /media/backup ext4 rw 0 0
+EOF
+ (
+ INSTALLHOME="$TEST_CONFIG_DIR" RSYNCSHOT_SOURCE_ONLY=1 source "$SCRIPT_PATH"
+ is_mounted "/media/b.ckup" "$mounts"
+ )
+ local rc=$?
+ teardown_test_env
+ assert_exit_code 1 "$rc" "regex metacharacters in target must be literal" || return 1
+}
+
+# ------------------------------------------------------------------------------
+# derive_paths: remote mode uses bare cp/mv/rm (resolved by remote PATH)
+# ------------------------------------------------------------------------------
+test_derive_paths_remote() {
+ setup_test_env
+ cat > "$TEST_CONFIG_DIR/config" <<EOF
+REMOTE_HOST="truenas"
+REMOTE_PATH="/mnt/vault/Backups"
+EOF
+ (
+ # The script sources the config and runs derive_paths during load.
+ INSTALLHOME="$TEST_CONFIG_DIR" RSYNCSHOT_SOURCE_ONLY=1 source "$SCRIPT_PATH"
+ assert_equals "remote" "$MODE" "should be remote mode" &&
+ assert_equals "truenas:/mnt/vault/Backups/$HOSTNAME" "$DESTINATION" "remote destination" &&
+ assert_equals "cp" "$CP" "remote cp is bare (remote PATH resolves it)" &&
+ assert_equals "mv" "$MV" "remote mv is bare" &&
+ assert_equals "rm" "$RM" "remote rm is bare"
+ )
+ local rc=$?
+ teardown_test_env
+ return $rc
+}
+
+# ------------------------------------------------------------------------------
+# derive_paths: local mode resolves cp/mv/rm to real absolute binaries
+# ------------------------------------------------------------------------------
+test_derive_paths_local() {
+ setup_test_env
+ cat > "$TEST_CONFIG_DIR/config" <<EOF
+REMOTE_HOST=""
+MOUNTDIR="/media/backup"
+EOF
+ (
+ INSTALLHOME="$TEST_CONFIG_DIR" RSYNCSHOT_SOURCE_ONLY=1 source "$SCRIPT_PATH"
+ assert_equals "local" "$MODE" "should be local mode" &&
+ assert_equals "/media/backup/$HOSTNAME" "$DESTINATION" "local destination" &&
+ assert_contains "$CP" "/" "local cp resolves to an absolute path, not a bare name"
+ )
+ local rc=$?
+ teardown_test_env
+ return $rc
+}
+
+# ------------------------------------------------------------------------------
+# Run tests
+# ------------------------------------------------------------------------------
+run_functions_tests() {
+ echo ""
+ echo "Running function unit tests..."
+ echo "------------------------------------------------------------"
+
+ run_test "is_mounted exact match" test_is_mounted_exact_match
+ run_test "is_mounted rejects prefix (no substring match)" test_is_mounted_no_substring_match
+ run_test "is_mounted handles spaces" test_is_mounted_handles_spaces
+ run_test "is_mounted treats target literally" test_is_mounted_target_is_literal
+ run_test "derive_paths remote mode" test_derive_paths_remote
+ run_test "derive_paths local mode" test_derive_paths_local
+}
+
+# Run if executed directly
+if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
+ run_functions_tests
+ print_summary
+fi
diff --git a/tests/cases/test_remote.sh b/tests/cases/test_remote.sh
new file mode 100644
index 0000000..c2ee75e
--- /dev/null
+++ b/tests/cases/test_remote.sh
@@ -0,0 +1,60 @@
+#!/usr/bin/env bash
+# ==============================================================================
+# Remote (SSH) Mode Tests
+# ==============================================================================
+# First coverage of the remote path — backup + rotation run over SSH. The whole
+# transfer and rotation happen via ssh to localhost, which exercises run_cmd's
+# remote branch and the bare cp/mv/rm rotation commands. Gated: if root can't
+# ssh to localhost with key auth, the suite skips rather than fails, so machines
+# without loopback SSH still pass.
+
+source "$(dirname "${BASH_SOURCE[0]}")/../lib/test_helpers.sh"
+
+remote_available() {
+ ssh -o BatchMode=yes -o ConnectTimeout=5 localhost true 2>/dev/null
+}
+
+# ------------------------------------------------------------------------------
+# Backup + rotation over SSH-to-localhost
+# ------------------------------------------------------------------------------
+test_remote_backup_and_rotation() {
+ setup_test_env
+ cat > "$TEST_CONFIG_DIR/config" <<EOF
+REMOTE_HOST="localhost"
+REMOTE_PATH="$TEST_BACKUP_DIR"
+EOF
+ create_test_includes >/dev/null
+ create_test_excludes >/dev/null
+
+ # Two runs to exercise rotation over SSH (MANUAL.0 rotates to MANUAL.1)
+ INSTALLHOME="$TEST_CONFIG_DIR" "$SCRIPT_PATH" manual 2 >/dev/null 2>&1
+ INSTALLHOME="$TEST_CONFIG_DIR" "$SCRIPT_PATH" manual 2 >/dev/null 2>&1
+
+ assert_dir_exists "$TEST_BACKUP_DIR/$HOSTNAME/MANUAL.0" "remote backup should create MANUAL.0" || { teardown_test_env; return 1; }
+ assert_dir_exists "$TEST_BACKUP_DIR/$HOSTNAME/MANUAL.1" "remote rotation should produce MANUAL.1" || { teardown_test_env; return 1; }
+ assert_file_exists "$TEST_BACKUP_DIR/$HOSTNAME/latest$TEST_SOURCE_DIR/home/testuser/file1.txt" "remote backup should copy files" || { teardown_test_env; return 1; }
+
+ teardown_test_env
+}
+
+# ------------------------------------------------------------------------------
+# Run tests
+# ------------------------------------------------------------------------------
+run_remote_tests() {
+ echo ""
+ echo "Running remote (SSH) tests..."
+ echo "------------------------------------------------------------"
+
+ if ! remote_available; then
+ echo " SKIP: root cannot ssh to localhost with key auth — skipping remote tests"
+ return 0
+ fi
+
+ run_test "remote backup + rotation over SSH" test_remote_backup_and_rotation
+}
+
+# Run if executed directly
+if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
+ run_remote_tests
+ print_summary
+fi
diff --git a/tests/cases/test_rsync_flags.sh b/tests/cases/test_rsync_flags.sh
new file mode 100644
index 0000000..768b432
--- /dev/null
+++ b/tests/cases/test_rsync_flags.sh
@@ -0,0 +1,53 @@
+#!/usr/bin/env bash
+# ==============================================================================
+# rsync Invocation Flag Tests
+# ==============================================================================
+# Uses the command-capture shim to intercept rsync and assert on the flags the
+# script passes, without performing a real transfer. Runs a dryrun so only rsync
+# is invoked (no rotation, no cp/mv/rm).
+
+source "$(dirname "${BASH_SOURCE[0]}")/../lib/test_helpers.sh"
+
+# ------------------------------------------------------------------------------
+# rsync is called with --numeric-ids (ownership fidelity) and -R (relative paths)
+# ------------------------------------------------------------------------------
+test_rsync_flags_numeric_ids_and_relative() {
+ setup_test_env
+ create_test_config >/dev/null
+ create_test_includes >/dev/null
+ create_test_excludes >/dev/null
+
+ local shim
+ shim=$(make_command_shim rsync)
+
+ PATH="$shim:$PATH" INSTALLHOME="$TEST_CONFIG_DIR" RSYNCSHOT_SKIP_MOUNT_CHECK=1 \
+ "$SCRIPT_PATH" dryrun manual 1 >/dev/null 2>&1
+
+ local args
+ args=$(cat "$shim/rsync.args" 2>/dev/null)
+
+ assert_file_exists "$shim/rsync.args" "rsync shim should have been invoked" || { rm -rf "$shim"; teardown_test_env; return 1; }
+ assert_contains "$args" "--numeric-ids" "rsync should be called with --numeric-ids" || { rm -rf "$shim"; teardown_test_env; return 1; }
+ assert_contains "$args" "-avhR" "rsync should be called with -R (relative paths, in -avhR)" || { rm -rf "$shim"; teardown_test_env; return 1; }
+ assert_contains "$args" "--dry-run" "dryrun should pass --dry-run" || { rm -rf "$shim"; teardown_test_env; return 1; }
+
+ rm -rf "$shim"
+ teardown_test_env
+}
+
+# ------------------------------------------------------------------------------
+# Run tests
+# ------------------------------------------------------------------------------
+run_rsync_flags_tests() {
+ echo ""
+ echo "Running rsync flag tests..."
+ echo "------------------------------------------------------------"
+
+ run_test "rsync uses --numeric-ids and -R" test_rsync_flags_numeric_ids_and_relative
+}
+
+# Run if executed directly
+if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
+ run_rsync_flags_tests
+ print_summary
+fi
diff --git a/tests/cases/test_validation.sh b/tests/cases/test_validation.sh
index 03da676..9a89787 100755
--- a/tests/cases/test_validation.sh
+++ b/tests/cases/test_validation.sh
@@ -80,6 +80,59 @@ test_accepts_valid_snapshot_types() {
}
# ------------------------------------------------------------------------------
+# Test: Rejects a retention count of zero
+# ------------------------------------------------------------------------------
+test_rejects_zero_count() {
+ local output
+ output=$(sudo "$SCRIPT_PATH" "manual" "0" 2>&1)
+ local exit_code=$?
+
+ assert_exit_code 1 "$exit_code" "should reject count 0" || return 1
+ assert_contains "$output" "at least 1" "should explain minimum count" || return 1
+}
+
+# ------------------------------------------------------------------------------
+# Test: Refuses an empty MOUNTDIR (would resolve to the filesystem root)
+# ------------------------------------------------------------------------------
+test_rejects_empty_mountdir() {
+ setup_test_env
+ cat > "$TEST_CONFIG_DIR/config" <<EOF
+REMOTE_HOST=""
+MOUNTDIR=""
+EOF
+ create_test_includes >/dev/null
+ create_test_excludes >/dev/null
+
+ local output
+ output=$(sudo INSTALLHOME="$TEST_CONFIG_DIR" RSYNCSHOT_SKIP_MOUNT_CHECK=1 "$SCRIPT_PATH" manual 1 2>&1)
+ local exit_code=$?
+
+ teardown_test_env
+
+ assert_exit_code 1 "$exit_code" "empty MOUNTDIR should be rejected" || return 1
+ assert_contains "$output" "MOUNTDIR is empty" "should explain the empty-MOUNTDIR refusal" || return 1
+}
+
+# ------------------------------------------------------------------------------
+# Test: status produces no stderr noise (the grep "0\n0" integer-test bug)
+# ------------------------------------------------------------------------------
+test_status_no_stderr_noise() {
+ setup_test_env
+ create_test_config >/dev/null
+ create_test_includes >/dev/null
+ create_test_excludes >/dev/null
+
+ local stderr_file="$TEST_DIR/status.stderr"
+ sudo INSTALLHOME="$TEST_CONFIG_DIR" RSYNCSHOT_SKIP_MOUNT_CHECK=1 "$SCRIPT_PATH" status >/dev/null 2>"$stderr_file"
+ local err
+ err=$(cat "$stderr_file")
+
+ teardown_test_env
+
+ assert_no_stderr "$err" "status should emit no stderr (no 'integer expression' noise)" || return 1
+}
+
+# ------------------------------------------------------------------------------
# Run tests
# ------------------------------------------------------------------------------
run_validation_tests() {
@@ -95,6 +148,9 @@ run_validation_tests() {
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
+ run_test "rejects retention count of zero" test_rejects_zero_count
+ run_test "refuses empty MOUNTDIR" test_rejects_empty_mountdir
+ run_test "status emits no stderr noise" test_status_no_stderr_noise
teardown_test_env
}