import os import shutil import subprocess import tarfile import tempfile import unittest from pathlib import Path ROOT = Path(__file__).resolve().parents[1] class BackupTests(unittest.TestCase): def setUp(self): self.assertTrue((ROOT / "backup.sh").exists(), "Backup script has not been implemented") self.temp = tempfile.TemporaryDirectory() self.addCleanup(self.temp.cleanup) self.home = Path(self.temp.name) self.bin = self.home / "bin" self.bin.mkdir() shutil.copy(ROOT / "backup.sh", self.home / "backup.sh") for name in (".env", "compose.yaml", "setup.yaml", "Caddyfile", "cron.cjs", "source.json"): (self.home / name).write_text(f"synthetic {name}\n") (self.home / ".backup-bucket").write_text("example-private-bucket\n") (self.home / "unrelated-secret").write_text("must not be archived") self.command("docker", """if [[ "$*" == *pg_dump* ]]; then printf 'synthetic dump' exit "${DUMP_EXIT:-0}" fi printf 'synthetic image identity\\n' """) self.command("aws", 'touch "$TEST_ROOT/upload-attempted"\nexit "${UPLOAD_EXIT:-0}"\n') self.env = {**os.environ, "PATH": f"{self.bin}:{os.environ['PATH']}", "TEST_ROOT": str(self.home)} def command(self, name, body): path = self.bin / name path.write_text("#!/bin/bash\nset -euo pipefail\n" + body) path.chmod(0o755) def run_backup(self, *args, **env): return subprocess.run(["bash", str(self.home / "backup.sh"), *args], env={**self.env, **env}, capture_output=True, text=True) def test_dump_failure_never_uploads_or_marks_success(self): result = self.run_backup(DUMP_EXIT="1") self.assertNotEqual(result.returncode, 0) self.assertFalse((self.home / "upload-attempted").exists()) self.assertFalse((self.home / "backup.last-success").exists()) def test_upload_failure_never_marks_success(self): result = self.run_backup(UPLOAD_EXIT="1") self.assertNotEqual(result.returncode, 0) self.assertTrue((self.home / "upload-attempted").exists()) self.assertFalse((self.home / "backup.last-success").exists()) def test_local_backup_is_private_complete_and_does_not_contact_aws(self): destination = self.home / "backups" result = self.run_backup("--local", str(destination)) self.assertEqual(result.returncode, 0, result.stderr) archives = list(destination.glob("*.tar.gz")) self.assertEqual(len(archives), 1) self.assertEqual(archives[0].stat().st_mode & 0o777, 0o600) with tarfile.open(archives[0]) as archive: self.assertEqual(set(archive.getnames()), {"database.dump", ".env", "compose.yaml", "setup.yaml", "Caddyfile", "cron.cjs", "source.json", "images.txt"}) self.assertEqual(archive.extractfile("database.dump").read(), b"synthetic dump") self.assertFalse((self.home / "upload-attempted").exists()) self.assertFalse((self.home / "backup.last-success").exists()) def test_only_successful_upload_marks_offsite_backup_success(self): result = self.run_backup() self.assertEqual(result.returncode, 0, result.stderr) self.assertTrue((self.home / "backup.last-success").is_file()) def test_public_backup_preserves_overlay_and_served_files(self): (self.home / "public.yaml").write_text("services: {}\n") source = self.home / "public" / "source" source.mkdir(parents=True) (self.home / "public" / "index.html").write_text("Host setup") (source / "source.tar.gz").write_bytes(b"synthetic source archive") result = self.run_backup("--local", str(self.home / "backups")) self.assertEqual(result.returncode, 0, result.stderr) with tarfile.open(next((self.home / "backups").glob("*.tar.gz"))) as archive: self.assertEqual(archive.extractfile("public.yaml").read(), b"services: {}\n") self.assertEqual(archive.extractfile("public/index.html").read(), b"Host setup") self.assertEqual(archive.extractfile("public/source/source.tar.gz").read(), b"synthetic source archive") self.assertNotIn("unrelated-secret", archive.getnames()) def test_public_overlay_without_served_directory_refuses_incomplete_backup(self): (self.home / "public.yaml").write_text("services: {}\n") result = self.run_backup() self.assertNotEqual(result.returncode, 0) self.assertFalse((self.home / "upload-attempted").exists()) self.assertFalse((self.home / "backup.last-success").exists()) if __name__ == "__main__": unittest.main()