diff --git a/CLAUDE.md b/CLAUDE.md index 4855852..baf4893 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -54,6 +54,7 @@ no JS build step: plain forms with small progressive enhancements in `static/js/ # Local (Windows dev box: venv lives at C:\Users\kyled\.venvs\kochbuch due to path-length issues) python -m pytest # tests (SQLite) python -m ruff check . # lint (config in pyproject.toml) +sh tests/shell/test_backup_restore.sh # backup/restore logic with a docker stub # Postgres without Docker (dev box): pip install pgserver; start it, then # POSTGRES_HOST=127.0.0.1 POSTGRES_PORT= POSTGRES_USER=postgres POSTGRES_DB=postgres python -m pytest # (the Windows pgserver build lacks tz data: copy tzdata/zoneinfo to pginstall/share/postgresql/timezone) diff --git a/tests/shell/test_backup_restore.sh b/tests/shell/test_backup_restore.sh new file mode 100755 index 0000000..13600ff --- /dev/null +++ b/tests/shell/test_backup_restore.sh @@ -0,0 +1,50 @@ +#!/bin/sh +# Tests the logic of scripts/backup.sh and scripts/restore.sh without Docker: +# a stub `docker` on PATH logs every call and emulates the compose commands used. +# sh tests/shell/test_backup_restore.sh +set -eu +ROOT=$(cd "$(dirname "$0")/../.." && pwd) +SP=$(mktemp -d) +trap 'rm -rf "$SP"' EXIT +mkdir -p "$SP/proj/scripts" "$SP/bin" "$SP/state/media/recipes" +cp "$ROOT/scripts/backup.sh" "$ROOT/scripts/restore.sh" "$SP/proj/scripts/" +echo "SECRET_KEY=abc" > "$SP/proj/.env" +echo "foto" > "$SP/state/media/recipes/a.webp" + +cat > "$SP/bin/docker" <> "$SP/state/calls.log" +case "\$*" in + *"exec -T db sh -c"*pg_dump*) printf 'PGDUMP-CONTENT' ;; + *"exec -T app tar -C /data -czf - media"*) tar -C "$SP/state" -czf - media ;; + *"exec -T db sh -c"*pg_isready*) exit 0 ;; + *"exec -T db sh -c"*pg_restore*) cat > "$SP/state/restored.dump" ;; + *"run --rm --no-deps -T --entrypoint sh app -c"*) mkdir -p "$SP/state/restored" && tar -C "$SP/state/restored" -xzf - ;; + *) : ;; +esac +STUB +chmod +x "$SP/bin/docker" +PATH="$SP/bin:$PATH" +cd "$SP/proj" + +fail() { echo "FAIL: $*" >&2; exit 1; } + +mkdir -p backups +touch -d "2026-01-01" backups/kochbuch-20260101-000000.tar.gz +touch -d "2026-01-02" backups/kochbuch-20260102-000000.tar.gz +KEEP=2 sh scripts/backup.sh > /dev/null + +[ "$(ls backups | wc -l)" -eq 2 ] || fail "retention should keep 2 archives" +[ ! -e backups/kochbuch-20260101-000000.tar.gz ] || fail "oldest archive should be deleted" +NEW=$(ls -1t backups/kochbuch-*.tar.gz | head -1) +[ "$(tar -tzf "$NEW" | sort | tr '\n' ' ')" = "db.dump env.backup media.tar.gz " ] || fail "archive content" + +rm .env +sh scripts/restore.sh "$NEW" --yes > /dev/null +[ "$(cat "$SP/state/restored.dump")" = "PGDUMP-CONTENT" ] || fail "database not restored" +[ "$(cat "$SP/state/restored/media/recipes/a.webp")" = "foto" ] || fail "photos not restored" +[ "$(cat .env)" = "SECRET_KEY=abc" ] || fail ".env not restored" +grep -q "stop app" "$SP/state/calls.log" || fail "app must be stopped during restore" +[ "$(tail -1 "$SP/state/calls.log")" = "compose up -d app" ] || fail "app must be started last" + +echo "OK: backup/restore scripts"