Files
gaokao-volunteer-system/tests/test_backup_restore_service_level.py
Hermes Agent 9ddf8bbef4 test(backup): 异机备份恢复 smoke 测试增强
- backup_restore_smoke.py 扩展端到端覆盖
- test_backup_restore_service_level.py 增加服务级断言
- test_backup_workflow.py 工作流覆盖同步

为 L-B 备份恢复异机演练做准备 (脚本本地已就绪, 待目标主机执行)
2026-06-25 09:26:46 +08:00

213 lines
6.8 KiB
Python

"""P1-8: 备份恢复必须升级到服务级验证。
历史:
- ``backup_verify.sh`` 之前只复制文件,然后验证 SQLite 文件能打开。
- 2026-06-14 严格复审判定这只是“文件级验证”,不构成“系统可恢复”。
- 修复后:
1. ``backup_verify.sh`` 优先调用 venv python
2. ``backup_restore_smoke.py`` 真的把 admin/orders DB 喂回 FastAPI TestClient
3. 真实执行 /api/public/orders 等关键端点,确认恢复后系统可用
- 本测试锁定该行为,使未来回归(例如换成 ``python3``)立刻失败。
"""
from __future__ import annotations
import os
import shutil
import sqlite3
import subprocess
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
SCRIPTS_DIR = REPO_ROOT / "scripts"
def _prepare_backup_dir(staging: Path) -> None:
staging.mkdir(parents=True, exist_ok=True)
db_dir = staging / "db"
files_dir = staging / "files"
config_dir = staging / "config"
secrets_dir = staging / "secrets"
db_dir.mkdir(parents=True, exist_ok=True)
files_dir.mkdir(parents=True, exist_ok=True)
config_dir.mkdir(parents=True, exist_ok=True)
secrets_dir.mkdir(parents=True, exist_ok=True)
src_orders = REPO_ROOT / "data" / "orders.db"
src_share = REPO_ROOT / "data" / "share" / "short_links.db"
src_admin = REPO_ROOT / "data" / "orders" / "admin.db"
if src_orders.exists():
shutil.copy(src_orders, db_dir / "orders.db")
if src_share.exists():
shutil.copy(src_share, db_dir / "short_links.db")
if src_admin.exists():
shutil.copy(src_admin, db_dir / "admin.db")
(config_dir / ".env").write_text(
"\n".join(
[
"GAOKAO_ENV=dev",
"GAOKAO_PAYMENT_PROVIDER=mock",
"GAOKAO_PAYMENT_BASE_URL=http://testserver",
"GAOKAO_ADMIN_USER=admin",
]
)
+ "\n",
encoding="utf-8",
)
(secrets_dir / "jwt_secret").write_text(
"backup-restore-jwt-secret-0123456789abcdef", encoding="utf-8"
)
(secrets_dir / "orders_fernet_key").write_text(
"backup-restore-fernet-secret", encoding="utf-8"
)
(secrets_dir / "admin_pass").write_text(
"backup-restore-pass-123", encoding="utf-8"
)
src_reports = REPO_ROOT / "data" / "share" / "reports"
if src_reports.exists():
shutil.copytree(src_reports, files_dir / "reports")
examples_dir = files_dir / "examples"
examples_dir.mkdir(parents=True, exist_ok=True)
sample_html = REPO_ROOT / "data" / "examples" / "gaokao_report_李明_20260611_122443.html"
sample_pdf = REPO_ROOT / "data" / "examples" / "gaokao_report_李明_20260611_122443.pdf"
if sample_html.exists():
shutil.copy(sample_html, examples_dir / sample_html.name)
if sample_pdf.exists():
shutil.copy(sample_pdf, examples_dir / sample_pdf.name)
def test_backup_verify_uses_venv_python_for_service_level_restore(tmp_path):
staging = tmp_path / "backup"
_prepare_backup_dir(staging)
env = os.environ.copy()
env.pop("GAOKAO_PYTHON_BIN", None)
env.pop("PYTHON_BIN", None)
proc = subprocess.run(
["bash", str(SCRIPTS_DIR / "backup_verify.sh"), "--from-backup", str(staging)],
cwd=REPO_ROOT,
env=env,
text=True,
capture_output=True,
check=False,
)
assert proc.returncode == 0, (
f"backup_verify.sh failed\nstdout:\n{proc.stdout}\nstderr:\n{proc.stderr}"
)
assert '"health_status": 200' in proc.stdout
assert '"public_order_create": 201' in proc.stdout
assert '"smoke_order_id"' in proc.stdout
assert "ModuleNotFoundError" not in proc.stderr
assert "ModuleNotFoundError" not in proc.stdout
def test_backup_verify_live_mode_uses_sqlite_backup_for_db_files(tmp_path):
admin_db = tmp_path / "admin.db"
verify_dir = tmp_path / "verify"
live_conn = sqlite3.connect(admin_db)
try:
journal_mode = live_conn.execute("PRAGMA journal_mode=WAL").fetchone()
assert journal_mode is not None
assert str(journal_mode[0]).lower() == "wal"
live_conn.execute("PRAGMA wal_autocheckpoint = 0")
live_conn.execute(
"CREATE TABLE restore_checks (id INTEGER PRIMARY KEY, value TEXT NOT NULL)"
)
live_conn.execute(
"INSERT INTO restore_checks(value) VALUES (?)", ("present",)
)
live_conn.commit()
proc = subprocess.run(
["bash", str(SCRIPTS_DIR / "backup_verify.sh"), str(verify_dir), "--skip-smoke"],
cwd=REPO_ROOT,
env={
**os.environ,
"GAOKAO_DB_PATH": str(admin_db),
},
text=True,
capture_output=True,
check=False,
)
finally:
live_conn.close()
assert proc.returncode == 0, proc.stderr
staged_admin_db = verify_dir / "db" / "admin.db"
conn = sqlite3.connect(staged_admin_db)
try:
tables = {
row[0]
for row in conn.execute(
"SELECT name FROM sqlite_master WHERE type='table'"
).fetchall()
}
assert "restore_checks" in tables
row = conn.execute("SELECT COUNT(*) FROM restore_checks").fetchone()
assert row == (1,)
finally:
conn.close()
def test_backup_restore_smoke_fails_fast_when_admin_db_missing(tmp_path):
staging = tmp_path / "backup"
_prepare_backup_dir(staging)
admin_db = staging / "db" / "admin.db"
if admin_db.exists():
admin_db.unlink()
proc = subprocess.run(
[
str(REPO_ROOT / ".venv" / "bin" / "python"),
str(SCRIPTS_DIR / "backup_restore_smoke.py"),
"--backup-dir",
str(staging),
],
cwd=REPO_ROOT,
text=True,
capture_output=True,
check=False,
timeout=10,
)
assert proc.returncode != 0
assert "restore precondition failed" in proc.stderr
assert "db/admin.db" in proc.stderr
def test_backup_restore_smoke_fails_fast_when_config_env_missing(tmp_path):
staging = tmp_path / "backup"
_prepare_backup_dir(staging)
shutil.rmtree(staging / "config")
proc = subprocess.run(
[
str(REPO_ROOT / ".venv" / "bin" / "python"),
str(SCRIPTS_DIR / "backup_restore_smoke.py"),
"--backup-dir",
str(staging),
],
cwd=REPO_ROOT,
text=True,
capture_output=True,
check=False,
timeout=10,
)
assert proc.returncode != 0
assert "restore precondition failed" in proc.stderr
assert "config/" in proc.stderr
def test_backup_restore_smoke_uses_real_testclient_contract_marker():
script = (SCRIPTS_DIR / "backup_restore_smoke.py").read_text(encoding="utf-8")
assert "TestClient" in script
assert "create_app" in script
assert "/api/public/orders" in script