from __future__ import annotations import hashlib import json from decimal import Decimal from pathlib import Path from types import SimpleNamespace import pytest from psycopg.conninfo import conninfo_to_dict, make_conninfo from muse.共享.调用身份 import 用途 from muse.基础设施 import 备份恢复 from muse.基础设施.备份恢复 import 创建备份, 备份恢复错误, 恢复备份, 核对备份 from muse.基础设施.数据库.维护锁 import 迁移维护锁 from muse.基础设施.数据库.连接 import 数据库工厂 from muse.资源加载 import 加载清单 from muse.配置 import 应用配置, 数据库引用 pytestmark = pytest.mark.数据库 def _资源身份() -> str: return 加载清单()["构建身份"] 敏感标记 = "w29-password-must-never-leak" def _断言目标尚无迁移表(工厂: 数据库工厂) -> None: with 工厂.连接() as 连: assert 连.execute("SELECT to_regclass('public.muse_migration')").fetchone() == (None,) def _记录客户端动作(monkeypatch: pytest.MonkeyPatch) -> list[str]: 动作: list[str] = [] 原始 = 备份恢复._运行客户端 def 记录(命令, 参数, 引用, 动作名): 动作.append(动作名) 原始(命令, 参数, 引用, 动作名) monkeypatch.setattr(备份恢复, "_运行客户端", 记录) return 动作 def _记录客户端命令(monkeypatch: pytest.MonkeyPatch) -> list[list[str]]: 命令行 = [] 原始 = 备份恢复.subprocess.run def 记录(args, **kwargs): 命令行.append(list(args)) return 原始(args, **kwargs) monkeypatch.setattr(备份恢复.subprocess, "run", 记录) return 命令行 def _应用配置( 工厂: 数据库工厂, raw: Path, drafts: Path, *, 引用: 数据库引用 | None = None, ) -> 应用配置: return 应用配置( 引用 or 工厂.引用, _资源身份(), 运行用途=用途.维护, 原文暂存=str(raw), 探索草稿=str(drafts), ) def _带敏感值引用(工厂: 数据库工厂, tmp_path: Path, 名称: str) -> 数据库引用: 原串 = Path(工厂.引用.位置).read_text(encoding="utf-8") 参数 = conninfo_to_dict(原串) 参数["password"] = 敏感标记 路径 = tmp_path / 名称 路径.write_text(make_conninfo(**参数), encoding="utf-8") 路径.chmod(0o600) return 数据库引用("受控存储", str(路径)) 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", "审计理由保留"), ) @pytest.mark.case_id( "NC-w29-29b001", environment="隔离PG", given="显式隔离配置、合成数据与已知发布构建", when="经真实维护、业务或浏览器入口执行并读取持久结果", then=["完整PG文件权限关系与动态内容实际往返"], contract="docs/系统架构/新版设计/迁移与验收.md#备份与恢复入口", ) def test_完整PG文件权限关系与动态内容实际往返__29b001( 应用测试库: dict, 新空目标库: SimpleNamespace, tmp_path: Path ) -> None: 来源工厂 = 应用测试库[用途.维护] _建立数据库探针(来源工厂) raw, drafts = _准备文件(tmp_path, "source") 敏感引用 = _带敏感值引用(来源工厂, tmp_path, "source-secret.txt") 来源配置 = _应用配置(来源工厂, raw, drafts, 引用=敏感引用) 备份目录 = tmp_path / "backup" 清单 = 创建备份(来源配置, 备份目录) 目标raw = tmp_path / "restored-raw" 目标drafts = tmp_path / "restored-drafts" 目标配置 = _应用配置(新空目标库.工厂, 目标raw, 目标drafts) 结果 = 恢复备份(备份目录, 目标配置, 清单["backup_id"]) assert 结果["status"] == "verified" assert 结果["services_started"] is False assert 结果["external_model_calls_restored"] is False assert 核对备份(备份目录) == 清单 assert (目标raw / "供应方原文.bin").read_bytes() == b"\x00\xffraw-evidence\x80" assert (目标drafts / "第一章 草稿.txt").read_text(encoding="utf-8") == "风从旧城来。\n" with 新空目标库.工厂.连接() as 连: 行 = 连.execute( "SELECT dynamic_payload, raw_bytes FROM public.w29_parent WHERE id = 'parent-1'" ).fetchone() assert 行 == ( {"扩展字段": {"层级": [1, "二", {"保留": True}]}}, b"\x00\xff\x10" + "原文-bytea".encode(), ) assert 连.execute( "SELECT audit_note FROM public.w29_child WHERE parent_id = 'parent-1'" ).fetchone() == ("审计理由保留",) assert 连.execute( "SELECT has_table_privilege('muse_app', 'public.w29_parent', 'SELECT'), " "has_table_privilege('muse_eval', 'public.w29_parent', 'SELECT')" ).fetchone() == (True, True) assert 连.execute( "SELECT count(*) FROM pg_constraint WHERE conrelid = 'public.w29_child'::regclass " "AND contype = 'f'" ).fetchone() == (1,) 清单正文 = (备份目录 / "manifest.json").read_text(encoding="utf-8") assert 敏感标记 not in 清单正文 assert str(raw) not in 清单正文 assert str(drafts) not in 清单正文 assert 来源工厂.引用.位置 not in 清单正文 @pytest.mark.case_id( "NC-w29-29b002", environment="隔离PG", given="显式隔离配置、合成数据与已知发布构建", when="经真实维护、业务或浏览器入口执行并读取持久结果", then=["并发业务写锁存在时备份立即失败且无半成品"], contract="docs/系统架构/新版设计/迁移与验收.md#备份与恢复入口", ) def test_并发业务写锁存在时备份立即失败且无半成品__29b002(应用测试库: dict, tmp_path: Path) -> None: 维护工厂 = 应用测试库[用途.维护] _建立数据库探针(维护工厂) raw, drafts = _准备文件(tmp_path, "locked") 配置 = _应用配置(维护工厂, raw, drafts) 输出 = tmp_path / "locked-backup" with 应用测试库[用途.生产].连接() as 写连, 写连.transaction(): 写连.execute( "INSERT INTO public.w29_parent VALUES (%s, %s, %s)", ("uncommitted", json.dumps({"状态": "writing"}), b"pending"), ) with pytest.raises(备份恢复错误, match="备份锁"): 创建备份(配置, 输出, pg_dump命令="should-not-run") assert not 输出.exists() @pytest.mark.case_id( "NC-w29-29b003", environment="隔离PG", given="显式隔离配置、合成数据与已知发布构建", when="经真实维护、业务或浏览器入口执行并读取持久结果", then=["数据库归档损坏与清单字段篡改均在连接目标前拒绝"], contract="docs/系统架构/新版设计/迁移与验收.md#备份与恢复入口", ) def test_数据库归档损坏与清单字段篡改均在连接目标前拒绝__29b003( 应用测试库: dict, 新空目标库: SimpleNamespace, tmp_path: Path ) -> None: 来源工厂 = 应用测试库[用途.维护] _建立数据库探针(来源工厂) raw, drafts = _准备文件(tmp_path, "damaged") 备份目录 = tmp_path / "damaged-backup" 清单 = 创建备份(_应用配置(来源工厂, raw, drafts), 备份目录) 目标raw, 目标drafts = tmp_path / "target-raw", tmp_path / "target-drafts" 目标配置 = _应用配置(新空目标库.工厂, 目标raw, 目标drafts) 数据库归档 = 备份目录 / "database.dump" 原字节 = 数据库归档.read_bytes() 数据库归档.write_bytes(原字节[:-1] + bytes([原字节[-1] ^ 0xFF])) 数据库归档.chmod(0o600) with pytest.raises(备份恢复错误, match="哈希"): 恢复备份(备份目录, 目标配置, 清单["backup_id"]) assert not 目标raw.exists() and not 目标drafts.exists() _断言目标尚无迁移表(新空目标库.工厂) 数据库归档.write_bytes(原字节) 数据库归档.chmod(0o600) 清单路径 = 备份目录 / "manifest.json" 篡改 = json.loads(清单路径.read_text(encoding="utf-8")) 篡改["resource_release"] = "tampered-release" 清单路径.write_text(json.dumps(篡改), encoding="utf-8") 清单路径.chmod(0o600) with pytest.raises(备份恢复错误, match="备份ID"): 核对备份(备份目录) @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() @pytest.mark.case_id( "NC-w29-29b005", environment="隔离PG", given="显式隔离配置、合成数据与已知发布构建", when="经真实维护、业务或浏览器入口执行并读取持久结果", then=["pg_restore中途失败时单事务回滚并清理文件目标"], contract="docs/系统架构/新版设计/迁移与验收.md#备份与恢复入口", ) def test_pg_restore中途失败时单事务回滚并清理文件目标__29b005( 应用测试库: dict, 新空目标库: SimpleNamespace, tmp_path: Path, monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture, ) -> None: 来源工厂 = 应用测试库[用途.维护] _建立数据库探针(来源工厂) raw, drafts = _准备文件(tmp_path, "rollback") 备份目录 = tmp_path / "rollback-backup" 清单 = 创建备份(_应用配置(来源工厂, raw, drafts), 备份目录) 目标raw = tmp_path / "rollback-target-raw" 目标drafts = tmp_path / "rollback-target-drafts" 敏感引用 = _带敏感值引用(新空目标库.工厂, tmp_path, "restore-secret.txt") 目标配置 = _应用配置(新空目标库.工厂, 目标raw, 目标drafts, 引用=敏感引用) 命令行 = _记录客户端命令(monkeypatch) 原运行 = 备份恢复._运行客户端 def 注入恢复竞争(命令: str, 参数: tuple[str, ...], 引用: 数据库引用, 动作: str) -> None: if 动作 == "数据库恢复": with 数据库工厂(引用, 用途.维护).连接() as 连, 连.transaction(): 连.execute( "CREATE TABLE public.muse_migration " "(version integer PRIMARY KEY, marker text NOT NULL)" ) 连.execute("INSERT INTO public.muse_migration VALUES (9999, 'keep-race-marker')") 原运行(命令, 参数, 引用, 动作) monkeypatch.setattr(备份恢复, "_运行客户端", 注入恢复竞争) with pytest.raises(备份恢复错误, match="^数据库恢复客户端失败$") as 捕获: 恢复备份(备份目录, 目标配置, 清单["backup_id"]) assert 命令行 assert 敏感标记 not in str(捕获.value.呈现()) assert 敏感标记 not in caplog.text assert 敏感标记 not in json.dumps(命令行) assert not 目标raw.exists() and not 目标drafts.exists() with 新空目标库.工厂.连接() as 连: assert 连.execute("SELECT marker FROM public.muse_migration").fetchone() == ( "keep-race-marker", ) assert 连.execute("SELECT to_regnamespace('metadata')").fetchone() == (None,) assert 连.execute("SELECT to_regtype('public.muse_purpose')").fetchone() == (None,) @pytest.mark.case_id( "NC-w29-29b006", environment="隔离PG", given="显式隔离配置、合成数据与已知发布构建", when="经真实维护、业务或浏览器入口执行并读取持久结果", then=["错误客户端输出不回显DSN凭据或绝对来源"], contract="docs/系统架构/新版设计/迁移与验收.md#备份与恢复入口", ) def test_错误客户端输出不回显DSN凭据或绝对来源__29b006( 应用测试库: dict, tmp_path: Path, monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture, ) -> None: 来源工厂 = 应用测试库[用途.维护] _建立数据库探针(来源工厂) raw, drafts = _准备文件(tmp_path, "secret") 引用 = _带敏感值引用(来源工厂, tmp_path, "failed-secret.txt") 配置 = _应用配置(来源工厂, raw, drafts, 引用=引用) 假客户端 = tmp_path / "failing-pg-dump" 假客户端.write_text('#!/bin/sh\nprintf "%s" "$PGPASSWORD" >&2\nexit 1\n') 假客户端.chmod(0o700) 命令行 = _记录客户端命令(monkeypatch) with pytest.raises(备份恢复错误) as 捕获: 创建备份(配置, tmp_path / "failed-backup", pg_dump命令=str(假客户端)) 呈现 = json.dumps(捕获.value.呈现(), ensure_ascii=False) assert 命令行 assert 捕获.value.说明 == "数据库备份客户端失败" assert 捕获.value.上下文["stderr_sha256"] == hashlib.sha256(敏感标记.encode()).hexdigest() assert 敏感标记 not in caplog.text assert 敏感标记 not in json.dumps(命令行) assert 敏感标记 not in 呈现 assert str(raw) not in 呈现 assert str(drafts) not in 呈现 assert 来源工厂.引用.位置 not in 呈现 assert not (tmp_path / "failed-backup").exists() @pytest.mark.case_id( "NC-w29-29b007", environment="隔离PG", given="显式隔离配置、合成数据与已知发布构建", when="经真实维护、业务或浏览器入口执行并读取持久结果", then=["默认ACL等价归一仍拒绝恢复后额外生产权限"], contract="docs/系统架构/新版设计/迁移与验收.md#备份与恢复入口", ) def test_默认ACL等价归一仍拒绝恢复后额外生产权限__29b007( 应用测试库, 新空目标库, tmp_path, monkeypatch ): raw, drafts = _准备文件(tmp_path, "acl-source") source = _应用配置(应用测试库[用途.维护], raw, drafts) archive = tmp_path / "acl-backup" record = 创建备份(source, archive) target = _应用配置(新空目标库.工厂, tmp_path / "acl-raw", tmp_path / "acl-drafts") original = 备份恢复._运行客户端 def changed(command, args, reference, action): original(command, args, reference, action) if action == "数据库恢复": with 数据库工厂(reference, 用途.维护).连接() as conn, conn.transaction(): conn.execute("GRANT SELECT ON public.muse_enablement_receipt TO muse_app") monkeypatch.setattr(备份恢复, "_运行客户端", changed) with pytest.raises(备份恢复错误, match="权限或关系摘要不一致"): 恢复备份(archive, target, record["backup_id"]) with 应用测试库[用途.维护].连接() as conn: assert conn.execute( "SELECT has_table_privilege('muse_app','public.muse_enablement_receipt','SELECT')" ).fetchone() == (False,) 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})), ) 连.execute("INSERT INTO public.w29_seq_probe(note) VALUES ('甲'),('乙'),('丙')") @pytest.mark.case_id( "TC-e95a51d27a2b", environment="隔离 PostgreSQL 与临时目录", when="备份、校验、恢复或模拟切换失败。", contract="docs/系统架构/新版设计/迁移与验收.md", ) def test_相同主键集合的确定性清单与特殊数据库类型往返__e95a51( 应用测试库: dict, 新空目标库: SimpleNamespace, tmp_path: Path ) -> None: """相同内容两次备份得到相同清单与指纹;decimal/timestamptz/jsonb 往返保持值。""" 来源工厂 = 应用测试库[用途.维护] _建立类型探针(来源工厂) raw, drafts = _准备文件(tmp_path, "typed") 配置 = _应用配置(来源工厂, raw, drafts) 一 = 创建备份(配置, tmp_path / "typed-backup-1") 二 = 创建备份(配置, tmp_path / "typed-backup-2") # 表内容、模式、约束与序列摘要按确定性顺序哈希,不因查询返回顺序改变; # pg_dump 归档字节本身可能带非确定性头,不强求 backup_id 相同。 assert 一["database"]["summary"] == 二["database"]["summary"] assert 一["database"]["source_fingerprint"] == 二["database"]["source_fingerprint"] 目标raw, 目标drafts = tmp_path / "typed-target-raw", tmp_path / "typed-target-drafts" 目标配置 = _应用配置(新空目标库.工厂, 目标raw, 目标drafts) 恢复备份(tmp_path / "typed-backup-1", 目标配置, 一["backup_id"]) with 新空目标库.工厂.连接() as 连: 连.execute("SELECT set_config('TimeZone', 'UTC', true)") assert 连.execute( "SELECT id, to_char(at AT TIME ZONE 'UTC', 'YYYY-MM-DD HH24:MI:SS'), " "score::text, payload::text FROM public.w29_typed ORDER BY id" ).fetchall() == [ (2, "2026-07-20 00:00:00", "0.1000", '{"序": 2}'), ( 10, "2026-07-21 08:00:00", "1.2300", '{"扩展": {"层级": [1, "二"]}}', ), ] @pytest.mark.case_id( "TC-f14761e9f048", environment="隔离 PostgreSQL 与临时目录", when="备份、校验、恢复或模拟切换失败。", contract="docs/系统架构/新版设计/迁移与验收.md", ) def test_备份输出只允许全新目录且不与来源重叠__f14761(应用测试库: dict, tmp_path: Path) -> None: 来源工厂 = 应用测试库[用途.维护] raw, drafts = _准备文件(tmp_path, "path") 配置 = _应用配置(来源工厂, raw, drafts) 已存在 = tmp_path / "occupied" 已存在.mkdir() (已存在 / "保留文件").write_text("keep", encoding="utf-8") with pytest.raises(备份恢复错误, match="全新"): 创建备份(配置, 已存在) assert (已存在 / "保留文件").read_text(encoding="utf-8") == "keep" with pytest.raises(备份恢复错误, match="重叠"): 创建备份(配置, raw / "nested-backup") @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( "TC-9a68f7f96bf6", environment="隔离 PostgreSQL 与临时目录", when="备份、校验、恢复或模拟切换失败。", contract="docs/系统架构/新版设计/迁移与验收.md", ) def test_文件归档意外损坏后离线核对失败__9a68f7(应用测试库: dict, tmp_path: Path) -> None: 来源工厂 = 应用测试库[用途.维护] raw, drafts = _准备文件(tmp_path, "corrupt") 备份目录 = tmp_path / "corrupt-backup" 创建备份(_应用配置(来源工厂, raw, drafts), 备份目录) 原文 = 备份目录 / "files" / "data" / "raw" / "供应方原文.bin" 原文.write_bytes(原文.read_bytes() + b"\x00tampered") with pytest.raises(备份恢复错误): 核对备份(备份目录) @pytest.mark.case_id( "TC-545fb1541c3a", environment="隔离 PostgreSQL 与临时目录", when="备份、校验、恢复或模拟切换失败。", contract="docs/系统架构/新版设计/迁移与验收.md", ) def test_备份清单拒绝重复键和未知字段__545fb1(应用测试库: dict, tmp_path: Path) -> None: 来源工厂 = 应用测试库[用途.维护] raw, drafts = _准备文件(tmp_path, "contract") 备份目录 = tmp_path / "contract-backup" 创建备份(_应用配置(来源工厂, raw, drafts), 备份目录) 清单路径 = 备份目录 / "manifest.json" 原文 = 清单路径.read_text(encoding="utf-8") 重复 = 原文.replace('"backup_id":"', '"backup_id":"", "backup_id":"', 1) 清单路径.write_text(重复, encoding="utf-8") 清单路径.chmod(0o600) with pytest.raises(备份恢复错误, match="重复字段"): 核对备份(备份目录) 清单路径.write_text(原文, encoding="utf-8") 清单路径.chmod(0o600) 未知 = 原文.rstrip()[:-1] + ',"extra_field":1}' 清单路径.write_text(未知, encoding="utf-8") 清单路径.chmod(0o600) with pytest.raises(备份恢复错误): 核对备份(备份目录) @pytest.mark.case_id( "TC-2d7f171ffd33", environment="隔离 PostgreSQL 与临时目录", when="备份、校验、恢复或模拟切换失败。", contract="docs/系统架构/新版设计/迁移与验收.md", ) def test_资源构建身份漂移在连接前拒绝恢复__2d7f17( 应用测试库: dict, 新空目标库: SimpleNamespace, tmp_path: Path, monkeypatch ) -> None: 来源工厂 = 应用测试库[用途.维护] raw, drafts = _准备文件(tmp_path, "identity") 备份目录 = tmp_path / "identity-backup" 清单 = 创建备份(_应用配置(来源工厂, raw, drafts), 备份目录) 目标raw, 目标drafts = tmp_path / "identity-target-raw", tmp_path / "identity-target-drafts" 目标配置 = _应用配置(新空目标库.工厂, 目标raw, 目标drafts) monkeypatch.setattr( 备份恢复, "_核对安装", lambda 配置: {"发布身份": "wrong-release", "构建身份": "0" * 64}, ) with pytest.raises(备份恢复错误, match="构建身份不属于此备份"): 恢复备份(备份目录, 目标配置, 清单["backup_id"], pg_restore命令="never") assert not 目标raw.exists() and not 目标drafts.exists() _断言目标尚无迁移表(新空目标库.工厂) @pytest.mark.case_id( "TC-7c7fadfd3680", environment="隔离 PostgreSQL 与临时目录", when="备份、校验、恢复或模拟切换失败。", contract="docs/系统架构/新版设计/迁移与验收.md", ) def test_占用的恢复文件目标在破坏性写入前拒绝__7c7fad( 应用测试库: dict, 新空目标库: SimpleNamespace, tmp_path: Path, monkeypatch ) -> None: 来源工厂 = 应用测试库[用途.维护] raw, drafts = _准备文件(tmp_path, "occupied-target") 备份目录 = tmp_path / "occupied-target-backup" 清单 = 创建备份(_应用配置(来源工厂, raw, drafts), 备份目录) 占用根 = tmp_path / "occupied-target-root" 占用根.mkdir() 目标配置 = _应用配置(新空目标库.工厂, 占用根, tmp_path / "occupied-target-drafts") 动作 = _记录客户端动作(monkeypatch) with pytest.raises(备份恢复错误, match="恢复文件目标必须全新"): 恢复备份(备份目录, 目标配置, 清单["backup_id"], pg_restore命令="should-not-run") assert "数据库恢复" not in 动作 _断言目标尚无迁移表(新空目标库.工厂) @pytest.mark.case_id( "TC-2b32b67a3482", environment="隔离 PostgreSQL 与临时目录", when="备份、校验、恢复或模拟切换失败。", contract="docs/系统架构/新版设计/迁移与验收.md", ) def test_恢复要求资源发布身份与安装一致并在连接前拒绝__2b32b6( 应用测试库: dict, 新空目标库: SimpleNamespace, tmp_path: Path, monkeypatch ) -> None: 来源工厂 = 应用测试库[用途.维护] raw, drafts = _准备文件(tmp_path, "code-identity") 备份目录 = tmp_path / "code-identity-backup" 清单 = 创建备份(_应用配置(来源工厂, raw, drafts), 备份目录) 目标raw, 目标drafts = tmp_path / "code-id-raw", tmp_path / "code-id-drafts" 错误配置 = 应用配置( 新空目标库.工厂.引用, "wrong-build-identity", 运行用途=用途.维护, 原文暂存=str(目标raw), 探索草稿=str(目标drafts), ) 动作 = _记录客户端动作(monkeypatch) with pytest.raises(备份恢复错误, match="当前程序、资源或配置不一致"): 恢复备份(备份目录, 错误配置, 清单["backup_id"], pg_restore命令="never") assert 动作 == [] assert not 目标raw.exists() and not 目标drafts.exists() _断言目标尚无迁移表(新空目标库.工厂) @pytest.mark.case_id( "TC-863e5ced39df", environment="隔离 PostgreSQL 与临时目录", when="备份、校验、恢复或模拟切换失败。", contract="docs/系统架构/新版设计/迁移与验收.md", ) def test_备份确认不匹配不创建业务连接__863e5c( 应用测试库: dict, 新空目标库: SimpleNamespace, tmp_path: Path, monkeypatch ) -> None: 来源工厂 = 应用测试库[用途.维护] raw, drafts = _准备文件(tmp_path, "confirm") 备份目录 = tmp_path / "confirm-backup" 清单 = 创建备份(_应用配置(来源工厂, raw, drafts), 备份目录) 目标raw, 目标drafts = tmp_path / "confirm-target-raw", tmp_path / "confirm-target-drafts" 目标配置 = _应用配置(新空目标库.工厂, 目标raw, 目标drafts) 动作 = _记录客户端动作(monkeypatch) with pytest.raises(备份恢复错误, match="预期备份ID"): 恢复备份(备份目录, 目标配置, "bkp-" + "0" * 40, pg_restore命令="never") assert 动作 == [] assert not 目标raw.exists() and not 目标drafts.exists() _断言目标尚无迁移表(新空目标库.工厂) assert 清单["backup_id"].startswith("bkp-") @pytest.mark.case_id( "TC-c2a43647c057", environment="隔离 PostgreSQL 与临时目录", when="备份、校验、恢复或模拟切换失败。", contract="docs/系统架构/新版设计/迁移与验收.md", ) def test_备份迁移与安装清单漂移在恢复任何客户端前拒绝__c2a436( 应用测试库: dict, 新空目标库: SimpleNamespace, tmp_path: Path, monkeypatch ) -> None: """正文/字段/数据库身份类漂移由迁移版本与校验和清单承接:不一致在连接前拒绝。""" 来源工厂 = 应用测试库[用途.维护] raw, drafts = _准备文件(tmp_path, "migration-drift") 备份目录 = tmp_path / "migration-drift-backup" 清单 = 创建备份(_应用配置(来源工厂, raw, drafts), 备份目录) 目标raw, 目标drafts = ( tmp_path / "migration-drift-target-raw", tmp_path / "migration-drift-target-drafts", ) 目标配置 = _应用配置(新空目标库.工厂, 目标raw, 目标drafts) monkeypatch.setattr( 备份恢复, "_安装迁移清单", lambda: [{"version": 999, "name": "V999__fake.sql", "checksum": "0" * 64}], ) 动作 = _记录客户端动作(monkeypatch) with pytest.raises(备份恢复错误, match="迁移版本或校验和"): 恢复备份(备份目录, 目标配置, 清单["backup_id"], pg_restore命令="never") assert 动作 == [] assert not 目标raw.exists() and not 目标drafts.exists() _断言目标尚无迁移表(新空目标库.工厂) @pytest.mark.case_id( "TC-a0e1e2234d2d", environment="隔离 PostgreSQL 与临时目录", when="备份、校验、恢复或模拟切换失败。", contract="docs/系统架构/新版设计/迁移与验收.md", ) def test_缺少可用备份在取锁连接前失败__a0e1e2( 应用测试库: dict, 新空目标库: SimpleNamespace, tmp_path: Path, monkeypatch ) -> None: raw, drafts = _准备文件(tmp_path, "missing-archive") 目标raw, 目标drafts = tmp_path / "missing-raw", tmp_path / "missing-drafts" 目标配置 = _应用配置(新空目标库.工厂, 目标raw, 目标drafts) 动作 = _记录客户端动作(monkeypatch) with pytest.raises(备份恢复错误, match="无法核对"): 恢复备份(tmp_path / "不存在备份", 目标配置, "bkp-" + "0" * 40, pg_restore命令="never") assert 动作 == [] assert not 目标raw.exists() and not 目标drafts.exists() @pytest.mark.case_id( "TC-c8063c15e318", environment="隔离 PostgreSQL 与临时目录", when="备份、校验、恢复或模拟切换失败。", contract="docs/系统架构/新版设计/迁移与验收.md", ) def test_恢复在并发维护锁下不执行数据库客户端__c8063c( 应用测试库: dict, 新空目标库: SimpleNamespace, tmp_path: Path, monkeypatch ) -> None: 来源工厂 = 应用测试库[用途.维护] _建立类型探针(来源工厂) raw, drafts = _准备文件(tmp_path, "lock") 备份目录 = tmp_path / "lock-backup" 清单 = 创建备份(_应用配置(来源工厂, raw, drafts), 备份目录) 目标raw, 目标drafts = tmp_path / "lock-target-raw", tmp_path / "lock-target-drafts" 目标配置 = _应用配置(新空目标库.工厂, 目标raw, 目标drafts) 动作 = _记录客户端动作(monkeypatch) with 新空目标库.工厂.连接() as 连, 连.transaction(): 持锁 = 连.execute("SELECT pg_try_advisory_lock(%s)", (迁移维护锁,)).fetchone()[0] assert 持锁 with pytest.raises(备份恢复错误, match="维护锁"): 恢复备份(备份目录, 目标配置, 清单["backup_id"], pg_restore命令="never") assert "数据库恢复" not in 动作 assert not 目标raw.exists() @pytest.mark.case_id( "TC-74a6ec3a8017", environment="隔离 PostgreSQL 与临时目录", when="备份、校验、恢复或模拟切换失败。", contract="docs/系统架构/新版设计/迁移与验收.md", ) def test_非维护用途在创建备份前拒绝__74a6ec(应用测试库: dict, tmp_path: Path) -> None: raw, drafts = _准备文件(tmp_path, "purpose") 生产配置 = 应用配置( 应用测试库[用途.生产].引用, _资源身份(), 运行用途=用途.生产, 原文暂存=str(raw), 探索草稿=str(drafts), ) with pytest.raises(备份恢复错误, match="maintenance"): 创建备份(生产配置, tmp_path / "purpose-backup", pg_dump命令="never") assert not (tmp_path / "purpose-backup").exists() @pytest.mark.case_id( "TC-b40e21e62183", environment="隔离 PostgreSQL 与临时目录", when="备份、校验、恢复或模拟切换失败。", contract="docs/系统架构/新版设计/迁移与验收.md", ) def test_磁盘归档摘要漂移在恢复任何写入前失败__b40e21( 应用测试库: dict, 新空目标库: SimpleNamespace, tmp_path: Path ) -> None: 来源工厂 = 应用测试库[用途.维护] raw, drafts = _准备文件(tmp_path, "drift") 备份目录 = tmp_path / "drift-backup" 清单 = 创建备份(_应用配置(来源工厂, raw, drafts), 备份目录) 目标raw, 目标drafts = tmp_path / "drift-target-raw", tmp_path / "drift-target-drafts" 目标配置 = _应用配置(新空目标库.工厂, 目标raw, 目标drafts) 原文 = 备份目录 / "files" / "data" / "raw" / "供应方原文.bin" 原文.write_bytes(原文.read_bytes() + b"changed") with pytest.raises(备份恢复错误): 恢复备份(备份目录, 目标配置, 清单["backup_id"], pg_restore命令="never") assert not 目标raw.exists() and not 目标drafts.exists() _断言目标尚无迁移表(新空目标库.工厂) @pytest.mark.case_id( "TC-6740be8d17a3", environment="隔离 PostgreSQL 与临时目录", when="备份、校验、恢复或模拟切换失败。", contract="docs/系统架构/新版设计/迁移与验收.md", ) def test_序列恢复只向前且保持确认__6740be( 应用测试库: dict, 新空目标库: SimpleNamespace, tmp_path: Path ) -> None: 来源工厂 = 应用测试库[用途.维护] _建立类型探针(来源工厂) raw, drafts = _准备文件(tmp_path, "sequence") 备份目录 = tmp_path / "sequence-backup" 清单 = 创建备份(_应用配置(来源工厂, raw, drafts), 备份目录) 目标raw, 目标drafts = tmp_path / "sequence-target-raw", tmp_path / "sequence-target-drafts" 目标配置 = _应用配置(新空目标库.工厂, 目标raw, 目标drafts) 恢复备份(备份目录, 目标配置, 清单["backup_id"]) with 新空目标库.工厂.连接() as 连, 连.transaction(): 语句 = 连.execute( "SELECT last_value, is_called FROM public.w29_seq_probe_id_seq" ).fetchone() assert 语句 == (3, True) 连.execute("INSERT INTO public.w29_seq_probe(note) VALUES ('丁')") assert 连.execute("SELECT max(id) FROM public.w29_seq_probe").fetchone() == (4,) @pytest.mark.case_id( "TC-d07d1f9ee590", environment="隔离 PostgreSQL 与临时目录", when="备份、校验、恢复或模拟切换失败。", contract="docs/系统架构/新版设计/迁移与验收.md", ) def test_恢复后复核失败清理文件根并报错__d07d1f( 应用测试库: dict, 新空目标库: SimpleNamespace, tmp_path: Path, monkeypatch ) -> None: 来源工厂 = 应用测试库[用途.维护] _建立类型探针(来源工厂) raw, drafts = _准备文件(tmp_path, "postcheck") 备份目录 = tmp_path / "postcheck-backup" 清单 = 创建备份(_应用配置(来源工厂, raw, drafts), 备份目录) 目标raw, 目标drafts = tmp_path / "postcheck-target-raw", tmp_path / "postcheck-target-drafts" 目标配置 = _应用配置(新空目标库.工厂, 目标raw, 目标drafts) 原摘要 = 备份恢复._数据库摘要 def 漂移(连接, 关系): 库 = 原摘要(连接, 关系) if "table_data" in 库: 库 = json.loads(json.dumps(库)) 条目 = dict(库["table_data"][0]) 条目["sha256"] = "0" * 64 库["table_data"] = [条目] return 库 monkeypatch.setattr(备份恢复, "_数据库摘要", 漂移) with pytest.raises(备份恢复错误, match="摘要不一致"): 恢复备份(备份目录, 目标配置, 清单["backup_id"]) assert not 目标raw.exists() and not 目标drafts.exists()