Adds container_running/container_health/wait_for_container/start_db
helpers and cmd_full(), plus tests. Fixed a delimiter bug in the two
new tests' sed scripts (the literal `${TARGET#/}` replacement text
collided with `#` as the sed delimiter) and relaxed the dry-run
no-external-calls assertion to allow the legitimate borg-list call
from resolve_archive(), mirroring the existing db dry-run test.
207 lines
7.1 KiB
Bash
Executable File
207 lines
7.1 KiB
Bash
Executable File
#!/bin/bash
|
|
set -uo pipefail
|
|
|
|
HERE="$(cd "$(dirname "$0")" && pwd)"
|
|
RESTORE="$HERE/../restore.sh"
|
|
FAILURES=0
|
|
|
|
assert_eq() {
|
|
local expected="$1" actual="$2" msg="$3"
|
|
if [[ "$expected" != "$actual" ]]; then
|
|
echo "FAIL: $msg (expected '$expected', got '$actual')"
|
|
FAILURES=$((FAILURES + 1))
|
|
else
|
|
echo "PASS: $msg"
|
|
fi
|
|
}
|
|
|
|
assert_contains() {
|
|
local haystack="$1" needle="$2" msg="$3"
|
|
if [[ "$haystack" != *"$needle"* ]]; then
|
|
echo "FAIL: $msg (expected to contain '$needle', got '$haystack')"
|
|
FAILURES=$((FAILURES + 1))
|
|
else
|
|
echo "PASS: $msg"
|
|
fi
|
|
}
|
|
|
|
test_help_exits_zero() {
|
|
local out rc
|
|
out="$(bash "$RESTORE" -h)"
|
|
rc=$?
|
|
assert_eq "0" "$rc" "help exits 0"
|
|
assert_contains "$out" "Usage:" "help prints usage"
|
|
}
|
|
|
|
test_no_args_exits_one() {
|
|
local rc
|
|
bash "$RESTORE" >/dev/null 2>&1
|
|
rc=$?
|
|
assert_eq "1" "$rc" "no args exits 1"
|
|
}
|
|
|
|
test_unknown_command_exits_one() {
|
|
local rc
|
|
bash "$RESTORE" bogus >/dev/null 2>&1
|
|
rc=$?
|
|
assert_eq "1" "$rc" "unknown command exits 1"
|
|
}
|
|
|
|
source "$HERE/lib/setup_mocks.sh"
|
|
|
|
test_list_archives() {
|
|
local mockdir out
|
|
mockdir="$(mktemp -d)"
|
|
setup_mock_bin "$mockdir"
|
|
out="$(PATH="$mockdir:$PATH" bash "$RESTORE" --list-archives)"
|
|
assert_contains "$out" "host-2026-01-01T00-00-00" "list-archives shows first archive"
|
|
assert_contains "$out" "host-2026-06-01T00-00-00" "list-archives shows latest archive"
|
|
rm -rf "$mockdir"
|
|
}
|
|
|
|
test_file_restore_extracts_to_dest() {
|
|
local mockdir dest out final
|
|
mockdir="$(mktemp -d)"
|
|
dest="$(mktemp -d)"
|
|
setup_mock_bin "$mockdir"
|
|
out="$(PATH="$mockdir:$PATH" bash "$RESTORE" file photos/img.jpg --dest "$dest")"
|
|
final="$dest/home/srv/files/content/photos/img.jpg"
|
|
assert_contains "$out" "Restored file available at: $final" "file mode reports final path"
|
|
if [[ -f "$final" ]]; then
|
|
echo "PASS: extracted file exists on disk"
|
|
else
|
|
echo "FAIL: extracted file missing at $final"
|
|
FAILURES=$((FAILURES + 1))
|
|
fi
|
|
rm -rf "$mockdir" "$dest"
|
|
}
|
|
|
|
test_file_restore_dry_run_makes_no_borg_call() {
|
|
local mockdir dest
|
|
mockdir="$(mktemp -d)"
|
|
dest="$(mktemp -d)"
|
|
setup_mock_bin "$mockdir"
|
|
PATH="$mockdir:$PATH" bash "$RESTORE" file photos/img.jpg --dest "$dest" --dry-run >/dev/null
|
|
if [[ -s "$mockdir/mock.log" ]] && grep -q "^borg extract" "$mockdir/mock.log"; then
|
|
echo "FAIL: dry-run invoked borg extract"
|
|
FAILURES=$((FAILURES + 1))
|
|
else
|
|
echo "PASS: dry-run made no borg extract call"
|
|
fi
|
|
rm -rf "$mockdir" "$dest"
|
|
}
|
|
|
|
test_db_restore_with_yes_runs_full_sequence() {
|
|
local mockdir out bashenv
|
|
mockdir="$(mktemp -d)"
|
|
setup_mock_bin "$mockdir"
|
|
echo "rootpass" > "$mockdir/rootpw"
|
|
# This host may have a real `docker` CLI in one of the system dirs that
|
|
# restore.sh's hardened PATH prepends (e.g. /usr/local/bin), which would
|
|
# shadow the mock and make a real (unreachable) docker daemon get called
|
|
# instead. A BASH_ENV-sourced function takes precedence over PATH lookup
|
|
# regardless of PATH ordering, so force `docker` to the mock that way.
|
|
bashenv="$mockdir/bash_env.sh"
|
|
cat > "$bashenv" <<EOF
|
|
docker() { "$mockdir/docker" "\$@"; }
|
|
EOF
|
|
out="$(PATH="$mockdir:$PATH" ROOT_PASSWORD_FILE="$mockdir/rootpw" BASH_ENV="$bashenv" bash "$RESTORE" db shopdb --yes </dev/null)"
|
|
assert_contains "$out" "Database 'shopdb' restored from" "db mode reports success"
|
|
if grep -q "CREATE DATABASE IF NOT EXISTS" "$mockdir/mock.log"; then
|
|
echo "PASS: db mode issued CREATE DATABASE"
|
|
else
|
|
echo "FAIL: db mode did not issue CREATE DATABASE"
|
|
FAILURES=$((FAILURES + 1))
|
|
fi
|
|
rm -rf "$mockdir"
|
|
}
|
|
|
|
test_db_restore_dry_run_skips_confirmation_and_calls() {
|
|
local mockdir out
|
|
mockdir="$(mktemp -d)"
|
|
setup_mock_bin "$mockdir"
|
|
out="$(PATH="$mockdir:$PATH" bash "$RESTORE" db shopdb --dry-run </dev/null)"
|
|
assert_contains "$out" "DRY-RUN" "db dry-run prints DRY-RUN plan"
|
|
# cmd_db calls resolve_archive() (a borg list call) before checking DRY_RUN,
|
|
# same as cmd_file does, so mock.log legitimately gets a "borg list" entry.
|
|
# What must NOT happen in dry-run is any docker/mysql/mariadb interaction.
|
|
if grep -qE "^(docker|mysql|mariadb)" "$mockdir/mock.log"; then
|
|
echo "FAIL: dry-run invoked a docker/db-client mock binary"
|
|
FAILURES=$((FAILURES + 1))
|
|
else
|
|
echo "PASS: dry-run made no docker/db-client calls"
|
|
fi
|
|
rm -rf "$mockdir"
|
|
}
|
|
|
|
test_db_restore_aborts_on_wrong_confirmation() {
|
|
local mockdir rc
|
|
mockdir="$(mktemp -d)"
|
|
setup_mock_bin "$mockdir"
|
|
set +e
|
|
echo "wrongname" | PATH="$mockdir:$PATH" bash "$RESTORE" db shopdb >/dev/null 2>&1
|
|
rc=$?
|
|
set -e
|
|
assert_eq "1" "$rc" "db mode aborts on mismatched confirmation"
|
|
rm -rf "$mockdir"
|
|
}
|
|
|
|
test_full_restore_refuses_nonempty_target_without_force() {
|
|
local mockdir target rc
|
|
mockdir="$(mktemp -d)"
|
|
target="$(mktemp -d)"
|
|
touch "$target/existing-file"
|
|
setup_mock_bin "$mockdir"
|
|
set +e
|
|
PATH="$mockdir:$PATH" TARGET_OVERRIDE=1 bash -c '
|
|
sed "s#^TARGET=\"/home/srv/files/content\"#TARGET=\"'"$target"'\"#; s@^ARCHIVE_TARGET_PATH=.*@ARCHIVE_TARGET_PATH=\"\${TARGET#/}\"@" "'"$RESTORE"'" > "'"$mockdir"'/restore_patched.sh"
|
|
bash "'"$mockdir"'/restore_patched.sh" full
|
|
' >/dev/null 2>&1
|
|
rc=$?
|
|
set -e
|
|
assert_eq "1" "$rc" "full mode refuses non-empty target without --force"
|
|
rm -rf "$mockdir" "$target"
|
|
}
|
|
|
|
test_full_restore_dry_run_makes_no_calls() {
|
|
local mockdir target out
|
|
mockdir="$(mktemp -d)"
|
|
target="$(mktemp -d)"
|
|
setup_mock_bin "$mockdir"
|
|
out="$(PATH="$mockdir:$PATH" bash -c '
|
|
sed "s#^TARGET=\"/home/srv/files/content\"#TARGET=\"'"$target"'\"#; s@^ARCHIVE_TARGET_PATH=.*@ARCHIVE_TARGET_PATH=\"\${TARGET#/}\"@" "'"$RESTORE"'" > "'"$mockdir"'/restore_patched.sh"
|
|
bash "'"$mockdir"'/restore_patched.sh" full --dry-run
|
|
')"
|
|
assert_contains "$out" "DRY-RUN" "full dry-run prints DRY-RUN plan"
|
|
# cmd_full calls resolve_archive() (a borg list call) before checking
|
|
# DRY_RUN, same as cmd_db/cmd_file, so mock.log legitimately gets a
|
|
# "borg list" entry. What must NOT happen in dry-run is a borg extract,
|
|
# or any docker/db-client interaction.
|
|
if grep -qE "^(borg extract|docker|mysql|mariadb)" "$mockdir/mock.log" 2>/dev/null; then
|
|
echo "FAIL: full dry-run invoked borg extract or a docker/db-client mock binary"
|
|
FAILURES=$((FAILURES + 1))
|
|
else
|
|
echo "PASS: full dry-run made no borg extract or docker/db-client calls"
|
|
fi
|
|
rm -rf "$mockdir" "$target"
|
|
}
|
|
|
|
test_help_exits_zero
|
|
test_no_args_exits_one
|
|
test_unknown_command_exits_one
|
|
test_list_archives
|
|
test_file_restore_extracts_to_dest
|
|
test_file_restore_dry_run_makes_no_borg_call
|
|
test_db_restore_with_yes_runs_full_sequence
|
|
test_db_restore_dry_run_skips_confirmation_and_calls
|
|
test_db_restore_aborts_on_wrong_confirmation
|
|
test_full_restore_refuses_nonempty_target_without_force
|
|
test_full_restore_dry_run_makes_no_calls
|
|
|
|
echo "-----"
|
|
if [[ "$FAILURES" -gt 0 ]]; then
|
|
echo "$FAILURES failure(s)"
|
|
exit 1
|
|
fi
|
|
echo "All tests passed"
|