"""Local backup/restore. Run from backend/: python tests/test_local_backup.py""" from __future__ import annotations import json import os import sqlite3 import sys import tempfile import zipfile from pathlib import Path ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT)) from local_backup import BackupError, create_backup, restore_backup, verify_archive def expect(ok: bool, message: str) -> None: if not ok: raise SystemExit(f"FAIL: {message}") print(f"OK {message}") def write_db(path: Path, value: str) -> None: conn = sqlite3.connect(path) conn.execute("CREATE TABLE IF NOT EXISTS note (id INTEGER PRIMARY KEY, body TEXT)") conn.execute("DELETE FROM note") conn.execute("INSERT INTO note (body) VALUES (?)", (value,)) conn.commit() conn.close() def read_db(path: Path) -> str: conn = sqlite3.connect(path) row = conn.execute("SELECT body FROM note").fetchone() conn.close() return row[0] if row else "" def main() -> None: with tempfile.TemporaryDirectory(prefix="kansho-backup-test-") as raw: base = Path(raw) db = base / "live" / "kansho.sqlite" media = base / "live" / "media" out = base / "archives" db.parent.mkdir(parents=True) media.mkdir(parents=True) write_db(db, "original-body") sample = media / "profile" / "shot.png" sample.parent.mkdir(parents=True) sample.write_bytes(b"png-bytes") secret = base / "live" / ".env" secret.write_text("KANSHO_PROVIDER_KEY=sk-secret\n", encoding="utf-8") archive = out / "first.zip" created = create_backup(archive, db_path=db, media_root=media) expect(archive.is_file(), "backup file created") expect(created["manifest"]["kind"] == "kansho.local_backup", "manifest kind") expect("persönliche, unverschlüsselte" in created["manifest"]["warning"].lower() or "unverschl" in created["manifest"]["warning"], "personal-data warning") verified = verify_archive(archive) expect(verified["manifest"]["checksums"]["kansho.sqlite"], "sqlite checksum present") with zipfile.ZipFile(archive) as zf: names = zf.namelist() expect("kansho.sqlite" in names, "sqlite in archive") expect("media/profile/shot.png" in names, "media in archive") expect(".env" not in names, ".env excluded") expect(all("sk-secret" not in zf.read(name).decode("latin1", errors="ignore") for name in names if name.endswith(".json") or name.endswith(".txt")), "no secret in manifest/readme") try: restore_backup(archive, db_path=db, media_root=media, confirm=False) expect(False, "restore without confirm must fail") except BackupError as exc: expect(exc.code == "confirm_required", "restore requires explicit confirm") try: restore_backup(archive, db_path=db, media_root=media, confirm=True, allow_overwrite=False) expect(False, "restore must not silently overwrite") except BackupError as exc: expect(exc.code == "would_overwrite", "restore refuses existing data without replace") target_db = base / "restore" / "kansho.sqlite" target_media = base / "restore" / "media" restored = restore_backup( archive, db_path=target_db, media_root=target_media, confirm=True, allow_overwrite=False, safety_dir=out, ) expect(read_db(target_db) == "original-body", "restored sqlite content") expect((target_media / "profile" / "shot.png").read_bytes() == b"png-bytes", "restored media") expect(restored["safety_backup"] is None, "empty target needs no safety backup") write_db(target_db, "changed-live") (target_media / "profile" / "extra.bin").write_bytes(b"extra") replaced = restore_backup( archive, db_path=target_db, media_root=target_media, confirm=True, allow_overwrite=True, safety_dir=out, ) expect(read_db(target_db) == "original-body", "replace restores snapshot") expect(not (target_media / "profile" / "extra.bin").exists(), "extra live media not kept") expect(Path(replaced["safety_backup"]).is_file(), "safety backup written before replace") safety = verify_archive(Path(replaced["safety_backup"])) expect(safety["manifest"]["kind"] == "kansho.local_backup", "safety backup is a real archive") broken = out / "broken.zip" with zipfile.ZipFile(archive) as src, zipfile.ZipFile(broken, "w") as dest: for info in src.infolist(): data = src.read(info.filename) if info.filename == "kansho.sqlite": data = data + b"tamper" dest.writestr(info, data) before = read_db(target_db) try: restore_backup(broken, db_path=target_db, media_root=target_media, confirm=True, allow_overwrite=True) expect(False, "tampered archive must fail") except BackupError as exc: expect(exc.code == "checksum_mismatch", "tamper detected before restore") expect(read_db(target_db) == before, "failed restore leaves current db") extra_zip = out / "extra.zip" with zipfile.ZipFile(archive) as src, zipfile.ZipFile(extra_zip, "w") as dest: for info in src.infolist(): dest.writestr(info, src.read(info.filename)) dest.writestr("notes.txt", "not in manifest") extra_verified = verify_archive(extra_zip) expect("notes.txt" in extra_verified["skipped_extra"], "extra archive files are skipped deterministically") locked_db = base / "locked" / "kansho.sqlite" locked_media = base / "locked" / "media" locked_db.parent.mkdir() locked_media.mkdir() write_db(locked_db, "locked") holder = sqlite3.connect(str(locked_db), timeout=1) holder.execute("BEGIN EXCLUSIVE") try: restore_backup( archive, db_path=locked_db, media_root=locked_media, confirm=True, allow_overwrite=True, safety_dir=out, ) expect(False, "in-use db must abort") except BackupError as exc: expect(exc.code in {"db_in_use", "db_unavailable", "backup_exists", "would_overwrite"} or "in_use" in exc.code or "unavailable" in exc.code, f"busy db fail-closed ({exc.code})") finally: holder.rollback() holder.close() expect(True, "backup tests used only temporary directories") if __name__ == "__main__": main()