161 lines
7 KiB
Bash
161 lines
7 KiB
Bash
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
repo_root=$(cd -- "$(dirname -- "$0")/.." && pwd -P)
|
|
pull_script="$repo_root/scripts/pull-pilot-backups.sh"
|
|
test_parent=$(cd -- /tmp && pwd -P)
|
|
test_root=$(mktemp -d "$test_parent/krona-backup-pull-test.XXXXXX")
|
|
test_root=$(cd -- "$test_root" && pwd -P)
|
|
is_test_root() {
|
|
[ "$(dirname -- "$1")" = "$test_parent" ] || return 1
|
|
case "$(basename -- "$1")" in krona-backup-pull-test.*) return 0 ;; *) return 1 ;; esac
|
|
}
|
|
if ! is_test_root "$test_root"; then
|
|
printf 'unexpected temporary test directory: %s\n' "$test_root" >&2
|
|
exit 1
|
|
fi
|
|
mock_bin="$test_root/bin"
|
|
remote_store="$test_root/mock-remote"
|
|
mkdir -p "$mock_bin" "$remote_store"
|
|
cleanup() {
|
|
resolved_test_root=$(cd -- "$test_root" && pwd -P) || return 1
|
|
if ! is_test_root "$resolved_test_root"; then
|
|
printf 'refusing to remove unexpected test directory: %s\n' "$resolved_test_root" >&2
|
|
return 1
|
|
fi
|
|
rm -rf -- "$resolved_test_root"
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
cat > "$mock_bin/ssh" <<'MOCK_SSH'
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
[[ "$#" -eq 5 && "$1" == -o && "$2" == BatchMode=yes && "$3" == -- ]] || exit 61
|
|
printf '%s\n' "$4" "$5" >> "$MOCK_SSH_LOG"
|
|
printf '%s' "$MOCK_REMOTE_LISTING"
|
|
MOCK_SSH
|
|
|
|
cat > "$mock_bin/scp" <<'MOCK_SCP'
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
[[ "$#" -eq 4 && "$1" == -B && "$2" == -- ]] || exit 62
|
|
count=0
|
|
[[ ! -f "$MOCK_SCP_STATE" ]] || read -r count < "$MOCK_SCP_STATE"
|
|
count=$((count + 1))
|
|
printf '%s\n' "$count" > "$MOCK_SCP_STATE"
|
|
if [[ "${MOCK_SCP_FAIL_AT:-0}" == "$count" ]]; then exit 63; fi
|
|
source_name=${3##*/}
|
|
cp -- "$MOCK_REMOTE_STORE/$source_name" "$4"
|
|
MOCK_SCP
|
|
chmod +x "$mock_bin/ssh" "$mock_bin/scp"
|
|
export PATH="$mock_bin:$PATH"
|
|
|
|
cert="$test_root/recipient.crt"
|
|
key="$test_root/recipient.key"
|
|
/usr/bin/openssl req -x509 -newkey rsa:2048 -nodes -sha256 -days 1 \
|
|
-keyout "$key" -out "$cert" -subj '/CN=pull-test-recipient' >/dev/null 2>&1
|
|
|
|
remote_directory=/srv/krona-staff/backups/
|
|
ssh_log="$test_root/ssh.log"
|
|
success_state="$test_root/scp-success.count"
|
|
assertion_failures=0
|
|
|
|
run_pull() {
|
|
local remote_dir=$1 local_dir=$2 listing=$3 state=$4
|
|
shift 4
|
|
env PATH="$mock_bin:$PATH" \
|
|
PILOT_SSH_TARGET=pull-test@invalid \
|
|
PILOT_REMOTE_BACKUP_DIRECTORY="$remote_dir" \
|
|
BACKUP_LOCAL_DIRECTORY="$local_dir" \
|
|
BACKUP_MAX_AGE_HOURS=36 \
|
|
MOCK_REMOTE_STORE="$remote_store" \
|
|
MOCK_REMOTE_LISTING="$listing" \
|
|
MOCK_SCP_STATE="$state" \
|
|
MOCK_SSH_LOG="$ssh_log" \
|
|
"$@" bash "$pull_script"
|
|
}
|
|
|
|
record_failure() {
|
|
printf 'FAIL: %s\n' "$*" >&2
|
|
assertion_failures=$((assertion_failures + 1))
|
|
}
|
|
|
|
make_backup() {
|
|
local name=$1
|
|
printf 'synthetic PostgreSQL backup\n' | /usr/bin/openssl cms -encrypt -binary \
|
|
-aes-256-cbc -outform DER -stream -in /dev/stdin \
|
|
-out "$remote_store/$name" "$cert"
|
|
(cd -- "$remote_store" && sha256sum -- "$name" > "$name.sha256")
|
|
}
|
|
|
|
assert_rejected_before_ssh() {
|
|
local label=$1 remote_dir=$2 local_dir=$3 state=$4
|
|
rm -f -- "$ssh_log"
|
|
if run_pull "$remote_dir" "$local_dir" "$fresh_name" "$state" > "$test_root/reject.log" 2>&1; then
|
|
record_failure "$label unexpectedly succeeded"
|
|
elif [[ -e "$ssh_log" ]]; then
|
|
record_failure "$label reached mocked ssh before rejection"
|
|
fi
|
|
}
|
|
|
|
[[ "$(command -v ssh)" == "$mock_bin/ssh" ]] || { echo 'mock ssh is not first in PATH' >&2; exit 1; }
|
|
[[ "$(command -v scp)" == "$mock_bin/scp" ]] || { echo 'mock scp is not first in PATH' >&2; exit 1; }
|
|
bash -n "$pull_script"
|
|
|
|
fresh_stamp=$(date -u '+%Y%m%dT%H%M%SZ')
|
|
fresh_name="krona-staff-$fresh_stamp-101.dump.cms"
|
|
make_backup "$fresh_name"
|
|
local_success="$test_root/local-success"
|
|
first_output=$(run_pull "$remote_directory" "$local_success" "$fresh_name" "$success_state")
|
|
[[ -f "$local_success/$fresh_name" && -f "$local_success/$fresh_name.sha256" ]] || {
|
|
record_failure 'fresh CMS and checksum were not installed locally'
|
|
}
|
|
cmp -s "$remote_store/$fresh_name" "$local_success/$fresh_name" || record_failure 'fresh CMS differs from remote bytes'
|
|
(cd -- "$local_success" && sha256sum -c -- "$fresh_name.sha256") >/dev/null || {
|
|
record_failure 'fresh local checksum does not validate'
|
|
}
|
|
[[ "$first_output" == *'newly_copied=1'* ]] || record_failure 'first pull did not report one new copy'
|
|
[[ "$(cat "$success_state")" == 2 ]] || record_failure 'first pull did not fetch exactly CMS and checksum'
|
|
|
|
second_output=$(run_pull "$remote_directory" "$local_success" "$fresh_name" "$success_state")
|
|
[[ "$second_output" == *'newly_copied=0'* ]] || record_failure 'repeat pull was not idempotent'
|
|
[[ "$(cat "$success_state")" == 2 ]] || record_failure 'repeat pull issued additional scp calls'
|
|
|
|
stale_stamp=$(date -u -d '48 hours ago' '+%Y%m%dT%H%M%SZ')
|
|
stale_name="krona-staff-$stale_stamp-102.dump.cms"
|
|
make_backup "$stale_name"
|
|
if run_pull "$remote_directory" "$test_root/local-stale" "$stale_name" "$test_root/scp-stale.count" > "$test_root/stale.log" 2>&1; then
|
|
record_failure 'stale backup unexpectedly succeeded'
|
|
elif ! grep -q 'Newest encrypted backup is stale or has a future timestamp' "$test_root/stale.log"; then
|
|
record_failure 'stale backup failed for an unexpected reason'
|
|
fi
|
|
|
|
bad_checksum_name="krona-staff-$fresh_stamp-103.dump.cms"
|
|
make_backup "$bad_checksum_name"
|
|
printf '%064d %s\n' 0 "$bad_checksum_name" > "$remote_store/$bad_checksum_name.sha256"
|
|
if run_pull "$remote_directory" "$test_root/local-bad-checksum" "$bad_checksum_name" "$test_root/scp-bad.count" > "$test_root/bad-checksum.log" 2>&1; then
|
|
record_failure 'checksum mismatch unexpectedly succeeded'
|
|
elif ! grep -q 'Transfer checksum mismatch' "$test_root/bad-checksum.log"; then
|
|
record_failure 'checksum mismatch failed for an unexpected reason'
|
|
fi
|
|
[[ ! -e "$test_root/local-bad-checksum/$bad_checksum_name" ]] || record_failure 'checksum mismatch left a completed backup'
|
|
|
|
assert_rejected_before_ssh 'remote root' / "$test_root/local-remote-root" "$test_root/scp-root.count"
|
|
assert_rejected_before_ssh 'remote traversal' /srv/krona-staff/../escape/ "$test_root/local-remote-traversal" "$test_root/scp-traversal.count"
|
|
assert_rejected_before_ssh 'local root' "$remote_directory" / "$test_root/scp-local-root.count"
|
|
local_traversal="$test_root/local-traversal/../local-target"
|
|
assert_rejected_before_ssh 'local traversal' "$remote_directory" "$local_traversal" "$test_root/scp-local-traversal.count"
|
|
|
|
scp_failure_name="krona-staff-$fresh_stamp-104.dump.cms"
|
|
make_backup "$scp_failure_name"
|
|
if run_pull "$remote_directory" "$test_root/local-scp-failure" "$scp_failure_name" "$test_root/scp-failure.count" MOCK_SCP_FAIL_AT=2 > "$test_root/scp-failure.log" 2>&1; then
|
|
record_failure 'scp failure unexpectedly succeeded'
|
|
fi
|
|
[[ ! -e "$test_root/local-scp-failure/$scp_failure_name" ]] || record_failure 'scp failure left a completed local backup'
|
|
[[ ! -e "$test_root/local-scp-failure/$scp_failure_name.sha256" ]] || record_failure 'scp failure left a completed local checksum'
|
|
|
|
if (( assertion_failures > 0 )); then
|
|
printf 'backup pull tests had %s failure(s)\n' "$assertion_failures" >&2
|
|
exit 1
|
|
fi
|
|
printf 'backup pull tests passed: CMS+checksum, idempotence, stale/mismatch rejection, path validation, scp failure\n'
|