"""自动备份/恢复演练单元测试(临时目录,不碰真实数据)。""" import os import sqlite3 import zipfile from unittest.mock import patch import pytest from app import backup as bk @pytest.fixture() def isolated_backup(monkeypatch, tmp_path): """隔离 DATA_DIR/KB_DIR/BACKUP_DIR 到临时目录,并造一个含业务表的库。""" data_dir = tmp_path / "data" data_dir.mkdir() db_path = data_dir / "workspace.db" with sqlite3.connect(str(db_path)) as conn: conn.executescript(""" CREATE TABLE campaign (id INTEGER PRIMARY KEY, name TEXT); CREATE TABLE agent (id INTEGER PRIMARY KEY); CREATE TABLE appuser (id INTEGER PRIMARY KEY); CREATE TABLE other (x INTEGER); INSERT INTO campaign (name) VALUES ('任务一'), ('任务二'); """) kb_dir = data_dir / "kb" kb_dir.mkdir() (kb_dir / "note.txt").write_text("知识内容", encoding="utf-8") monkeypatch.setattr(bk, "DATA_DIR", data_dir) monkeypatch.setattr(bk, "KB_DIR", kb_dir) monkeypatch.setattr(bk, "BACKUP_DIR", data_dir / "backups") return data_dir def test_create_backup_returns_zip(isolated_backup): result = bk.create_backup("test") assert result is not None and result.exists() assert result.name.startswith("backup-test-") with zipfile.ZipFile(result) as zf: assert "workspace.db" in zf.namelist() assert "kb/note.txt" in zf.namelist() def test_backup_skipped_when_no_db(tmp_path): empty = tmp_path / "empty" empty.mkdir() (empty / "kb").mkdir() with patch.object(bk, "DATA_DIR", empty), patch.object(bk, "KB_DIR", empty / "kb"), \ patch.object(bk, "BACKUP_DIR", empty / "backups"): assert bk.create_backup("x") is None def test_verify_backup_checks_tables(isolated_backup): result = bk.create_backup("test") ok, detail = bk.verify_backup(result) assert ok, detail assert "campaign 2 行" in detail def test_verify_backup_rejects_corrupt(isolated_backup): bk.BACKUP_DIR.mkdir(parents=True, exist_ok=True) bad = bk.BACKUP_DIR / "backup-bad.zip" bad.write_bytes(b"not a zip") ok, _detail = bk.verify_backup(bad) assert not ok def test_prune_keeps_recent(isolated_backup, monkeypatch): monkeypatch.setattr(bk, "KEEP_COUNT", 3) bk.BACKUP_DIR.mkdir(parents=True, exist_ok=True) for i in range(5): target = bk.BACKUP_DIR / f"backup-auto-2026010{i}-000000.zip" target.write_bytes(b"placeholder") os.utime(target, (1700000000 + i * 100, 1700000000 + i * 100)) removed = bk._prune_old_backups() assert removed == 2 remaining = list(bk.BACKUP_DIR.glob("backup-*.zip")) assert len(remaining) == 3 def test_restore_drill_latest(isolated_backup): bk.create_backup("drill") report = bk.verify_restore_latest() assert report["ok"], report["detail"] assert "解压演练通过" in report["detail"] def test_backup_status_summary(isolated_backup): bk.create_backup("status") status = bk.backup_status() assert status["count"] == 1 assert status["latest"].startswith("backup-status-") assert status["latest_size_kb"] > 0