- 51 个增量迁移文件压扁为 V0001__基线.sql 完整快照(结构+种子+授权), 等价门禁:旧链全量执行库与基线库 pg_dump 逐字节一致 - 剔除 pg_dump 固化的 public schema 超级用户归属断言(muse_maint 无权执行) - 测试删减至保留集:备份往返 2 + 预算 2 + 租约 2 + 基线建库 1 - 租约/预算夹具改共享库,消除按例克隆建库 - 修复共享库三类既有污染:账本注入残留(系统管理)、失败触发器残留(评测)、 建表残留(正式变更事务),发布包迁移文件名硬编码改动态核对 - 全量数据库验收 722 passed / 0 failed / 0 errors(main 基线为 24F+291E) - 运行手册登记基线模式改表流程与账本校验和同步
193 lines
7.2 KiB
Python
193 lines
7.2 KiB
Python
"""备份恢复的核心保护:七域真实往返与拒绝覆盖非空目标。
|
|
|
|
个人工具的最小安全网——全部作品资产在一个 PG 库里,备份必须能真恢复,
|
|
恢复不得覆盖任何已有内容。其余切换/篡改/权限变体用例已按简化决策删除。
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from decimal import Decimal
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
|
|
import pytest
|
|
|
|
from muse.共享.调用身份 import 用途
|
|
from muse.基础设施.备份恢复 import 创建备份, 备份恢复错误, 恢复备份, 核对备份
|
|
from muse.基础设施.数据库.连接 import 数据库工厂
|
|
from muse.资源加载 import 加载清单
|
|
from muse.配置 import 应用配置
|
|
|
|
pytestmark = pytest.mark.数据库
|
|
|
|
|
|
def _资源身份() -> str:
|
|
return 加载清单()["构建身份"]
|
|
|
|
|
|
def _应用配置(
|
|
工厂: 数据库工厂,
|
|
raw: Path,
|
|
drafts: Path,
|
|
) -> 应用配置:
|
|
return 应用配置(
|
|
工厂.引用,
|
|
_资源身份(),
|
|
运行用途=用途.维护,
|
|
原文暂存=str(raw),
|
|
探索草稿=str(drafts),
|
|
)
|
|
|
|
|
|
def _准备文件(tmp_path: Path, 前缀: str) -> tuple[Path, Path]:
|
|
raw = tmp_path / f"{前缀}-raw"
|
|
drafts = tmp_path / f"{前缀}-drafts"
|
|
raw.mkdir()
|
|
drafts.mkdir()
|
|
(raw / "供应方原文.bin").write_bytes(b"\x00\xffraw-evidence\x80")
|
|
(drafts / "第一章 草稿.txt").write_text("风从旧城来。\n", encoding="utf-8")
|
|
return raw, drafts
|
|
|
|
|
|
def _建立数据库探针(工厂: 数据库工厂) -> None:
|
|
with 工厂.连接() as 连, 连.transaction():
|
|
连.execute("DROP TABLE IF EXISTS public.w29_child, public.w29_parent CASCADE")
|
|
连.execute(
|
|
"""
|
|
CREATE TABLE public.w29_parent (
|
|
id text PRIMARY KEY,
|
|
dynamic_payload jsonb NOT NULL,
|
|
raw_bytes bytea NOT NULL
|
|
);
|
|
CREATE TABLE public.w29_child (
|
|
id text PRIMARY KEY,
|
|
parent_id text NOT NULL REFERENCES public.w29_parent(id),
|
|
audit_note text NOT NULL
|
|
);
|
|
GRANT SELECT, INSERT, UPDATE, DELETE ON public.w29_parent, public.w29_child
|
|
TO muse_app;
|
|
GRANT SELECT ON public.w29_parent, public.w29_child TO muse_eval;
|
|
"""
|
|
)
|
|
连.execute(
|
|
"INSERT INTO public.w29_parent VALUES (%s, %s, %s)",
|
|
(
|
|
"parent-1",
|
|
json.dumps({"扩展字段": {"层级": [1, "二", {"保留": True}]}}),
|
|
b"\x00\xff\x10" + "原文-bytea".encode(),
|
|
),
|
|
)
|
|
连.execute(
|
|
"INSERT INTO public.w29_child VALUES (%s, %s, %s)",
|
|
("child-1", "parent-1", "审计理由保留"),
|
|
)
|
|
|
|
|
|
def _建立类型探针(维护工厂: 数据库工厂) -> None:
|
|
"""decimal 精度、timestamptz、嵌套 jsonb 与序列推进,供确定性往返用。"""
|
|
with 维护工厂.连接() as 连, 连.transaction():
|
|
连.execute("DROP TABLE IF EXISTS public.w29_seq_probe, public.w29_typed CASCADE")
|
|
连.execute(
|
|
"""
|
|
CREATE TABLE public.w29_typed (
|
|
id integer PRIMARY KEY,
|
|
at timestamptz NOT NULL,
|
|
score numeric(10,4) NOT NULL,
|
|
payload jsonb NOT NULL
|
|
);
|
|
CREATE TABLE public.w29_seq_probe (
|
|
id serial PRIMARY KEY,
|
|
note text NOT NULL
|
|
);
|
|
"""
|
|
)
|
|
连.execute(
|
|
"INSERT INTO public.w29_typed VALUES (%s,%s,%s,%s)",
|
|
(
|
|
10,
|
|
"2026-07-21T08:00:00+00",
|
|
Decimal("1.2300"),
|
|
json.dumps({"扩展": {"层级": [1, "二"]}}),
|
|
),
|
|
)
|
|
连.execute(
|
|
"INSERT INTO public.w29_typed VALUES (%s,%s,%s,%s)",
|
|
(2, "2026-07-20T00:00:00+00", Decimal("0.10"), json.dumps({"序": 2})),
|
|
)
|
|
|
|
|
|
@pytest.mark.case_id(
|
|
"TC-0f7c45927fbe",
|
|
environment="隔离 PostgreSQL 与临时目录",
|
|
when="备份、校验、恢复。",
|
|
contract="docs/系统架构/新版设计/迁移与验收.md",
|
|
)
|
|
def test_备份单快照七域往返且同内容重放一致__0f7c45(
|
|
应用测试库: dict, 新空目标库: SimpleNamespace, tmp_path: Path
|
|
) -> None:
|
|
"""数据库与文件进入同一份校验清单;关闭连接后从磁盘复验一致。"""
|
|
来源工厂 = 应用测试库[用途.维护]
|
|
_建立类型探针(来源工厂)
|
|
raw, drafts = _准备文件(tmp_path, "domains")
|
|
备份目录 = tmp_path / "domains-backup"
|
|
清单 = 创建备份(_应用配置(来源工厂, raw, drafts), 备份目录)
|
|
复验 = 核对备份(备份目录)
|
|
assert 复验 == 清单
|
|
assert set(清单["database"]) == {
|
|
"archive",
|
|
"size",
|
|
"sha256",
|
|
"source_fingerprint",
|
|
"fingerprint_scope",
|
|
"server_version_num",
|
|
"migrations",
|
|
"summary",
|
|
}
|
|
assert 清单["database"]["migrations"] and 清单["file_archive"]["file_count"] == 2
|
|
assert 清单["file_archive"]["roots"] == ["drafts", "raw"]
|
|
|
|
目标raw, 目标drafts = tmp_path / "domains-target-raw", tmp_path / "domains-target-drafts"
|
|
恢复备份(备份目录, _应用配置(新空目标库.工厂, 目标raw, 目标drafts), 清单["backup_id"])
|
|
with 新空目标库.工厂.连接() as 连:
|
|
assert 连.execute("SELECT count(*) FROM public.w29_typed").fetchone() == (2,)
|
|
|
|
|
|
@pytest.mark.case_id(
|
|
"NC-w29-29b004",
|
|
environment="隔离PG",
|
|
given="显式隔离配置与合成数据",
|
|
when="向来源库或已有内容目标发起恢复",
|
|
then=["拒绝来源库与已有内容目标且不覆盖文件"],
|
|
contract="docs/系统架构/新版设计/迁移与验收.md#备份与恢复入口",
|
|
)
|
|
def test_拒绝来源库与已有内容目标且不覆盖文件__29b004(
|
|
应用测试库: dict, 新空目标库: SimpleNamespace, tmp_path: Path
|
|
) -> None:
|
|
来源工厂 = 应用测试库[用途.维护]
|
|
_建立数据库探针(来源工厂)
|
|
raw, drafts = _准备文件(tmp_path, "guard")
|
|
备份目录 = tmp_path / "guard-backup"
|
|
清单 = 创建备份(_应用配置(来源工厂, raw, drafts), 备份目录)
|
|
|
|
来源目标配置 = _应用配置(
|
|
来源工厂, tmp_path / "source-target-raw", tmp_path / "source-target-drafts"
|
|
)
|
|
with pytest.raises(备份恢复错误, match="来源数据库"):
|
|
恢复备份(备份目录, 来源目标配置, 清单["backup_id"])
|
|
|
|
with 新空目标库.工厂.连接() as 连, 连.transaction():
|
|
连.execute("CREATE TABLE public.keep_me (value text NOT NULL)")
|
|
连.execute("INSERT INTO public.keep_me VALUES ('保留')")
|
|
目标raw = tmp_path / "occupied-target-raw"
|
|
目标drafts = tmp_path / "occupied-target-drafts"
|
|
with pytest.raises(备份恢复错误, match="全新空库"):
|
|
恢复备份(
|
|
备份目录,
|
|
_应用配置(新空目标库.工厂, 目标raw, 目标drafts),
|
|
清单["backup_id"],
|
|
)
|
|
with 新空目标库.工厂.连接() as 连:
|
|
assert 连.execute("SELECT value FROM public.keep_me").fetchone() == ("保留",)
|
|
assert not 目标raw.exists() and not 目标drafts.exists()
|