mirror of
https://github.com/lobaro/restic-backup-docker.git
synced 2026-11-14 07:52:44 +00:00
The repository had no tests: CI only built the image, and the old container-structure-test setup was parked as config-broken.yml / docker-compose.test.broken.yml. This adds suites that actually run the image. - test/compat.sh: backup, restore, forget, check, hooks, failure handling and timezone behaviour against the default configuration. These describe what users depend on today, so a change that turns them red is a breaking change. - test/features.sh: optional features, each guarded by a detection check against the built image. Absent features are reported as SKIP, so open pull requests do not turn master red, and their feature tests start running by themselves once the pull request is rebased. Covers #109 (RESTIC_STREAM), #99 (docker cli, bash) and #84 (msmtp). - test/required-features.txt: once a feature is merged its gate is listed here and a missing feature becomes a failure instead of a skip. - test/structure.yml: revived image layout checks, falls back to the official container-structure-test image when the tool is not installed. - test/run.sh --sabotage: breaks the backup call on a throwaway copy of the tree and expects the suite to fail, so the tests cannot be watered down into always passing. CI runs this as its own step. - .github/workflows/test.yml: shellcheck plus all suites on every pull request. Also adds .gitattributes pinning shell scripts, the Dockerfile and YAML to LF. With a Windows checkout (core.autocrlf=true) the scripts were checked out with CRLF, and the resulting image failed to start with "exec /entry.sh: no such file or directory". No runtime file is touched, the image itself is unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Agent-Id: agent-7034f75b-1ddc-4aa1-a4ea-3a54b340e8d9
188 lines
7.1 KiB
Bash
188 lines
7.1 KiB
Bash
#!/usr/bin/env bash
|
|
# Backwards-compatibility suite.
|
|
#
|
|
# Everything in here describes behaviour the image has today and that users
|
|
# depend on. These tests always run, for every pull request. A change that
|
|
# turns one of them red is a breaking change, whether or not that was intended.
|
|
|
|
set -u
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
# shellcheck source=test/lib.sh
|
|
. "${SCRIPT_DIR}/lib.sh"
|
|
|
|
trap stop_container EXIT
|
|
|
|
echo "=== compat: repository bootstrap and default backup ==="
|
|
|
|
start_container default
|
|
wait_for_start || { fail "container starts with default configuration"; summary "compat"; exit 1; }
|
|
pass "container starts with default configuration"
|
|
|
|
startup_log="$(docker logs "$CONTAINER" 2>&1)"
|
|
assert_contains "$startup_log" "Running restic init" \
|
|
"empty repository is initialised on first start"
|
|
assert_contains "$startup_log" "Repo init status 0" \
|
|
"repository initialisation succeeds"
|
|
|
|
crontab="$(in_container cat /var/spool/cron/crontabs/root)"
|
|
assert_contains "$crontab" "/bin/backup" \
|
|
"BACKUP_CRON is written to the crontab"
|
|
assert_contains "$crontab" "flock -n /var/run/backup.lock" \
|
|
"backup cron job is guarded by flock"
|
|
assert_not_contains "$crontab" "/bin/check" \
|
|
"no check cron job is installed while CHECK_CRON is unset"
|
|
|
|
write_file /data/hello.txt "backup me"
|
|
|
|
backup_log="$(in_container /bin/backup 2>&1)"
|
|
assert_contains "$backup_log" "Backup Successful" \
|
|
"backup of /data succeeds"
|
|
assert_contains "$backup_log" "Pre-backup script not found" \
|
|
"missing pre-backup hook is reported, not fatal"
|
|
assert_contains "$backup_log" "Post-backup script not found" \
|
|
"missing post-backup hook is reported, not fatal"
|
|
assert_not_contains "$backup_log" "notification" \
|
|
"no mail or Teams notification is attempted without configuration"
|
|
assert_eq "1" "$(snapshot_count)" \
|
|
"exactly one snapshot exists after one backup"
|
|
|
|
snapshots="$(in_container restic snapshots --json 2>/dev/null)"
|
|
assert_contains "$snapshots" '"testtag"' \
|
|
"snapshot carries the configured RESTIC_TAG"
|
|
|
|
in_container restic restore latest --target /tmp/restore >/dev/null 2>&1
|
|
restored="$(in_container cat /tmp/restore/data/hello.txt 2>&1)"
|
|
assert_eq "backup me" "$restored" \
|
|
"restored file content matches the original"
|
|
|
|
logfile="$(in_container cat /var/log/backup-last.log 2>&1)"
|
|
assert_contains "$logfile" "RESTIC_REPOSITORY: /mnt/restic" \
|
|
"backup-last.log records the run parameters"
|
|
|
|
in_container /bin/backup >/dev/null 2>&1
|
|
assert_eq "2" "$(snapshot_count)" \
|
|
"a second backup adds a second snapshot"
|
|
|
|
echo
|
|
echo "=== compat: data integrity check ==="
|
|
|
|
check_log="$(in_container /bin/check 2>&1)"
|
|
assert_contains "$check_log" "Check Successful" \
|
|
"restic check passes on a healthy repository"
|
|
assert_contains "$check_log" "Pre-check script not found" \
|
|
"missing pre-check hook is reported, not fatal"
|
|
|
|
echo
|
|
echo "=== compat: pre- and post-backup hooks ==="
|
|
|
|
write_file /hooks/pre-backup.sh '#!/bin/sh
|
|
echo "PRE-HOOK-RAN"
|
|
'
|
|
write_file /hooks/post-backup.sh '#!/bin/sh
|
|
echo "POST-HOOK-RAN with rc=$1"
|
|
'
|
|
in_container chmod +x /hooks/pre-backup.sh /hooks/post-backup.sh
|
|
|
|
hook_log="$(in_container /bin/backup 2>&1)"
|
|
assert_contains "$hook_log" "PRE-HOOK-RAN" \
|
|
"pre-backup hook is executed"
|
|
assert_contains "$hook_log" "POST-HOOK-RAN with rc=0" \
|
|
"post-backup hook receives the backup return code"
|
|
|
|
stop_container
|
|
|
|
echo
|
|
echo "=== compat: RESTIC_FORGET_ARGS ==="
|
|
|
|
start_container forget -e "RESTIC_FORGET_ARGS=--keep-last 1"
|
|
wait_for_start || { fail "container starts with RESTIC_FORGET_ARGS"; summary "compat"; exit 1; }
|
|
write_file /data/hello.txt "backup me"
|
|
in_container /bin/backup >/dev/null 2>&1
|
|
forget_log="$(in_container /bin/backup 2>&1)"
|
|
assert_contains "$forget_log" "Forget Successful" \
|
|
"forget runs after a successful backup when configured"
|
|
assert_eq "1" "$(snapshot_count)" \
|
|
"--keep-last 1 leaves exactly one snapshot"
|
|
stop_container
|
|
|
|
echo
|
|
echo "=== compat: BACKUP_SOURCES ==="
|
|
|
|
start_container sources -e "BACKUP_SOURCES=/opt/custom"
|
|
wait_for_start || { fail "container starts with BACKUP_SOURCES"; summary "compat"; exit 1; }
|
|
write_file /opt/custom/custom.txt "custom source"
|
|
write_file /data/ignored.txt "should not be backed up"
|
|
in_container /bin/backup >/dev/null 2>&1
|
|
in_container restic restore latest --target /tmp/restore >/dev/null 2>&1
|
|
assert_eq "custom source" "$(in_container cat /tmp/restore/opt/custom/custom.txt 2>&1)" \
|
|
"BACKUP_SOURCES overrides the default /data source"
|
|
if in_container test -f /tmp/restore/data/ignored.txt; then
|
|
fail "BACKUP_SOURCES excludes /data" "/data was backed up although BACKUP_SOURCES was set"
|
|
else
|
|
pass "BACKUP_SOURCES excludes /data"
|
|
fi
|
|
stop_container
|
|
|
|
echo
|
|
echo "=== compat: failure handling ==="
|
|
|
|
start_container failure
|
|
wait_for_start || { fail "container starts for failure test"; summary "compat"; exit 1; }
|
|
write_file /data/hello.txt "backup me"
|
|
# Point the run at a repository that does not exist. Deleting files inside the
|
|
# repository is not enough, restic happily recreates its directory structure.
|
|
fail_log="$(in_container_env RESTIC_REPOSITORY=/mnt/does-not-exist /bin/backup 2>&1)"
|
|
assert_contains "$fail_log" "Backup Failed with Status" \
|
|
"a failing backup is reported as failed"
|
|
if in_container test -f /var/log/backup-error-last.log; then
|
|
pass "failing backup writes backup-error-last.log"
|
|
else
|
|
fail "failing backup writes backup-error-last.log" "file is missing"
|
|
fi
|
|
assert_eq "0" "$(snapshot_count)" \
|
|
"a failed backup leaves no snapshot behind"
|
|
stop_container
|
|
|
|
echo
|
|
echo "=== compat: timezone ==="
|
|
# The base image already ships tzdata, so TZ works today. Pinned here so that
|
|
# neither a base image change nor a slimming effort silently breaks cron times
|
|
# for users who already rely on it.
|
|
|
|
tz_default="$(docker run --rm --entrypoint date "$IMAGE" +%Z 2>&1 | tr -d '\r')"
|
|
assert_eq "UTC" "$tz_default" \
|
|
"the default timezone is UTC"
|
|
|
|
tz_berlin="$(docker run --rm -e TZ=Europe/Berlin --entrypoint date "$IMAGE" +%Z 2>&1 | tr -d '\r')"
|
|
if [ "$tz_berlin" = "CET" ] || [ "$tz_berlin" = "CEST" ]; then
|
|
pass "TZ=Europe/Berlin changes the container timezone"
|
|
else
|
|
fail "TZ=Europe/Berlin changes the container timezone" "got: ${tz_berlin}"
|
|
fi
|
|
|
|
echo
|
|
echo "=== compat: RESTIC_TAG handling ==="
|
|
|
|
start_container tag
|
|
wait_for_start || { fail "container starts for RESTIC_TAG test"; summary "compat"; exit 1; }
|
|
write_file /data/hello.txt "backup me"
|
|
|
|
# The Dockerfile declares RESTIC_TAG="", so in a normal container the variable
|
|
# is always set and an empty value means "no tag". Documented here because the
|
|
# `${RESTIC_TAG?...}` guard in backup.sh only triggers when it is truly unset.
|
|
emptytag_log="$(in_container_env RESTIC_TAG= /bin/backup 2>&1)"
|
|
assert_contains "$emptytag_log" "Backup Successful" \
|
|
"an empty RESTIC_TAG creates an untagged backup"
|
|
assert_eq "1" "$(snapshot_count)" \
|
|
"an empty RESTIC_TAG still produces a snapshot"
|
|
|
|
notag_log="$(in_container env -u RESTIC_TAG /bin/backup 2>&1)"
|
|
assert_contains "$notag_log" "Missing environment variable RESTIC_TAG" \
|
|
"backup refuses to run when RESTIC_TAG is unset entirely"
|
|
assert_eq "1" "$(snapshot_count)" \
|
|
"no extra snapshot is created when RESTIC_TAG is unset"
|
|
stop_container
|
|
|
|
summary "compat"
|