实现侧: - 上下文:任务范围拆分为 范围校验/范围授权;索引按可发现口径重建、索引新鲜度改对称差;依赖校验统一快照漂移说明。 - 知识方法:方法与材料读取口径统一;超限方法材料按可选省略,核对路径不再二次计费;删除无合同的读时重算。 - 任务运行:新增 context.usage/tool.denied 事件类型;连接池常驻并在装配生命周期内开关;调用结算与核对分列。 - 效果评测/审校修订/交付连载/作者经验/作品规划:凭据冻结、标定消费、导出补证、事实引文核对等收尾修复。 - 资源加载:能力正文不再夹带索引用的导航注记(该注记此前进入角色与技能的模型提示)。 - 元数据:受保护骨架与代码保护属性对齐;字段校验与内置结构口径同步。 - 基础设施:环境预检进入装配生命周期;数据库连接运行期字段不参与相等比较;索引指纹归一化 jsonb 浮点。 - 删除被替代实现:7 份旧提示词模板与空壳 资料来源 读取器。 用例侧: - 用例身份与导航元信息迁移;夹具补生命周期、同库暴露与模板封存; - 本轮定向修复:方法材料省略、事实引文、迁移回执、额度与暂停用例、慢用例超时预算等。
587 lines
23 KiB
Python
587 lines
23 KiB
Python
"""端点与身份合同的离线验证;PG协议替身不代表infra实测或真实迁移通过。"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
from pathlib import Path
|
||
from uuid import UUID
|
||
|
||
import psycopg
|
||
import pytest
|
||
from psycopg.conninfo import conninfo_to_dict, make_conninfo
|
||
|
||
from muse.共享.调用身份 import 用途
|
||
from muse.基础设施.数据库.连接 import 数据库工厂
|
||
from muse.配置 import 数据库引用
|
||
from 入口 import main
|
||
from 导入新库 import _允许配置, _连接配置, 初始化目标, 核对隔离目标, 检查实例
|
||
from 建立映射 import 迁移错误
|
||
from 用例身份 import 原样参数ID
|
||
|
||
# 文档保留地址与合成指纹仅供离线合同测试,不是infra允许记录。
|
||
TCP端点 = {"endpoint_type": "tcp", "host": "192.0.2.8", "port": 5433}
|
||
合成指纹 = {"system_identifier": "123456789", "data_directory": "/synthetic/pgdata"}
|
||
|
||
|
||
@pytest.fixture(autouse=True)
|
||
def 无连接环境覆盖(monkeypatch):
|
||
for 名 in ("PGHOSTADDR", "PGSERVICE", "PGSERVICEFILE", "PGOPTIONS", "PGPORT"):
|
||
monkeypatch.delenv(名, raising=False)
|
||
|
||
|
||
def _JSON(tmp_path: Path, 名: str, 值: dict) -> Path:
|
||
文件 = tmp_path / 名
|
||
文件.write_text(json.dumps(值), encoding="utf-8")
|
||
return 文件
|
||
|
||
|
||
def _引用(tmp_path: Path, 串: str, 名: str = "连接.txt") -> 数据库引用:
|
||
文件 = tmp_path / 名
|
||
文件.write_text(串, encoding="utf-8")
|
||
文件.chmod(0o600)
|
||
return 数据库引用("受控存储", str(文件))
|
||
|
||
|
||
def _连接串(**参数) -> str:
|
||
return make_conninfo(
|
||
**{"host": TCP端点["host"], "port": "5433", "dbname": "muse-example", "user": "admin"}
|
||
| 参数
|
||
)
|
||
|
||
|
||
@pytest.mark.case_id(
|
||
"NC-mig-7e034f",
|
||
environment="离线迁移测试;协议替身与临时目录,不连真实数据库",
|
||
given="明确endpoint_type配置与旧本地socket夹具",
|
||
when="解析端点配置",
|
||
then=["合法配置被接受,合同边界不放宽"],
|
||
contract="docs/系统架构/新版设计/数据模型/旧知识记录分流.md",
|
||
)
|
||
@pytest.mark.parametrize(
|
||
"端点",
|
||
[
|
||
TCP端点,
|
||
{"endpoint_type": "unix", "host": "/synthetic/socket", "port": 5433},
|
||
{"socket": "/synthetic/socket", "port": 5433},
|
||
],
|
||
ids=["explicit-tcp", "explicit-unix", "legacy-unix"],
|
||
)
|
||
def test_明确端点与旧本地夹具合法配置合同__3945a2(tmp_path, 端点):
|
||
允许 = _允许配置(_JSON(tmp_path, "允许.json", 端点 | 合成指纹))
|
||
主机 = 端点.get("host", 端点.get("socket"))
|
||
引用 = _引用(tmp_path, _连接串(host=主机, user="muse_app", sslmode="require"))
|
||
参数 = conninfo_to_dict(_连接配置(引用, 允许, 角色="muse_app"))
|
||
assert 参数["host"] == 主机 and 参数["port"] == "5433"
|
||
assert 允许["endpoint_type"] == ("unix" if str(主机).startswith("/") else "tcp")
|
||
|
||
|
||
@pytest.mark.case_id(
|
||
"NC-mig-072793",
|
||
environment="离线迁移测试;协议替身与临时目录,不连真实数据库",
|
||
given="未明确批准或歧义的实例记录",
|
||
when="尝试作为目标",
|
||
then=["拒绝而非猜测核准"],
|
||
contract="docs/系统架构/新版设计/数据模型/旧知识记录分流.md",
|
||
)
|
||
@pytest.mark.parametrize(
|
||
"修改",
|
||
[
|
||
{"endpoint_type": None},
|
||
{"endpoint_type": []},
|
||
{"endpoint_type": "socket"},
|
||
{"host": "infra.example"},
|
||
{"host": "192.0.2.8,192.0.2.9"},
|
||
{"host": "0.0.0.0"},
|
||
{"host": "::"},
|
||
{"host": "192.0.2.8 "},
|
||
{"host": "/synthetic/socket"},
|
||
{"port": "5433"},
|
||
{"port": True},
|
||
{"port": 0},
|
||
{"port": 65536},
|
||
{"port": "5433,5434"},
|
||
{"socket": "/synthetic/socket"},
|
||
{"system_identifier": None},
|
||
{"system_identifier": 123456789},
|
||
{"data_directory": "relative"},
|
||
],
|
||
ids=[
|
||
原样参数ID("修改0"),
|
||
原样参数ID("修改1"),
|
||
原样参数ID("修改2"),
|
||
原样参数ID("修改3"),
|
||
原样参数ID("修改4"),
|
||
原样参数ID("修改5"),
|
||
原样参数ID("修改6"),
|
||
原样参数ID("修改7"),
|
||
原样参数ID("修改8"),
|
||
原样参数ID("修改9"),
|
||
原样参数ID("修改10"),
|
||
原样参数ID("修改11"),
|
||
原样参数ID("修改12"),
|
||
原样参数ID("修改13"),
|
||
原样参数ID("修改14"),
|
||
原样参数ID("修改15"),
|
||
原样参数ID("修改16"),
|
||
原样参数ID("修改17"),
|
||
],
|
||
)
|
||
def test_未明确批准或含歧义的实例记录拒绝__01cb75(tmp_path, 修改):
|
||
文件 = _JSON(tmp_path, "允许.json", TCP端点 | 合成指纹 | 修改)
|
||
with pytest.raises(迁移错误, match="target_denied"):
|
||
_允许配置(文件)
|
||
|
||
|
||
@pytest.mark.case_id(
|
||
"NC-mig-075a2a",
|
||
environment="离线迁移测试;协议替身与临时目录,不连真实数据库",
|
||
given="仅含旧socket字段的记录",
|
||
when="混入TCP端点值",
|
||
then=["不能暗中升级为TCP授权"],
|
||
contract="docs/系统架构/新版设计/数据模型/旧知识记录分流.md",
|
||
)
|
||
def test_旧socket字段不能暗中变成TCP授权__1102ac(tmp_path):
|
||
文件 = _JSON(tmp_path, "允许.json", 合成指纹 | {"socket": "192.0.2.8", "port": 5433})
|
||
with pytest.raises(迁移错误, match="target_denied"):
|
||
_允许配置(文件)
|
||
|
||
|
||
@pytest.mark.case_id(
|
||
"NC-mig-3436bd",
|
||
environment="离线迁移测试;协议替身与临时目录,不连真实数据库",
|
||
given="带hostaddr/service/options等覆盖或错误角色的连接串",
|
||
when="建立连接",
|
||
then=["连接前即拒绝,不触达网络"],
|
||
contract="docs/系统架构/新版设计/数据模型/旧知识记录分流.md",
|
||
)
|
||
@pytest.mark.parametrize(
|
||
"参数",
|
||
[
|
||
{"host": "192.0.2.9"},
|
||
{"host": "192.0.2.8,192.0.2.9"},
|
||
{"port": "5432"},
|
||
{"port": "5433,5433"},
|
||
{"hostaddr": "192.0.2.9"},
|
||
{"hostaddr": ""},
|
||
{"service": "other-instance"},
|
||
{"service": ""},
|
||
{"options": "-c role=admin"},
|
||
{"load_balance_hosts": "random"},
|
||
{"dbname": "host=192.0.2.9 dbname=muse-example"},
|
||
{"dbname": "postgresql://192.0.2.9/muse-example"},
|
||
{"dbname": ""},
|
||
{"user": "muse_maint"},
|
||
],
|
||
ids=[
|
||
原样参数ID("参数0"),
|
||
原样参数ID("参数1"),
|
||
原样参数ID("参数2"),
|
||
原样参数ID("参数3"),
|
||
原样参数ID("参数4"),
|
||
原样参数ID("参数5"),
|
||
原样参数ID("参数6"),
|
||
原样参数ID("参数7"),
|
||
原样参数ID("参数8"),
|
||
原样参数ID("参数9"),
|
||
原样参数ID("参数10"),
|
||
原样参数ID("参数11"),
|
||
原样参数ID("参数12"),
|
||
原样参数ID("参数13"),
|
||
],
|
||
)
|
||
def test_端点覆盖和错误角色在连接前拒绝__ef2ba0(tmp_path, monkeypatch, 参数):
|
||
调用 = []
|
||
monkeypatch.setattr(psycopg, "connect", lambda *a, **k: 调用.append((a, k)))
|
||
引用 = _引用(tmp_path, _连接串(**({"user": "muse_app"} | 参数)))
|
||
with pytest.raises(迁移错误, match="target_denied"):
|
||
_连接配置(引用, TCP端点, 角色="muse_app")
|
||
assert 调用 == []
|
||
|
||
|
||
@pytest.mark.case_id(
|
||
"NC-mig-c87591",
|
||
environment="离线迁移测试;协议替身与临时目录,不连真实数据库",
|
||
given="进程环境含PG默认变量",
|
||
when="建立连接",
|
||
then=["不继承环境默认端点、库或角色"],
|
||
contract="docs/系统架构/新版设计/数据模型/旧知识记录分流.md",
|
||
)
|
||
@pytest.mark.parametrize(
|
||
"缺失", ["host", "port", "dbname", "user"], ids=["host", "port", "dbname", "user"]
|
||
)
|
||
def test_连接不能继承环境默认端点库或角色__35995e(tmp_path, 缺失):
|
||
参数 = conninfo_to_dict(_连接串())
|
||
参数.pop(缺失)
|
||
引用 = _引用(tmp_path, make_conninfo(**参数))
|
||
with pytest.raises(迁移错误, match="target_denied"):
|
||
_连接配置(引用, TCP端点)
|
||
|
||
|
||
@pytest.mark.case_id(
|
||
"NC-mig-7418b0",
|
||
environment="离线迁移测试;协议替身与临时目录,不连真实数据库",
|
||
given="libpq环境覆盖变量",
|
||
when="建立连接",
|
||
then=[
|
||
"PGHOSTADDR、PGSERVICE、PGSERVICEFILE、PGOPTIONS及PGPORT环境覆盖均在连接前拒绝且不回显覆盖值"
|
||
],
|
||
contract="docs/系统架构/新版设计/数据模型/旧知识记录分流.md",
|
||
)
|
||
@pytest.mark.parametrize(
|
||
"环境",
|
||
["PGHOSTADDR", "PGSERVICE", "PGSERVICEFILE", "PGOPTIONS", "PGPORT"],
|
||
ids=["PGHOSTADDR", "PGSERVICE", "PGSERVICEFILE", "PGOPTIONS", "PGPORT"],
|
||
)
|
||
def test_libpq环境覆盖也在连接前拒绝__dfe18e(tmp_path, monkeypatch, 环境):
|
||
monkeypatch.setenv(环境, "不得回显的覆盖值")
|
||
引用 = _引用(tmp_path, _连接串())
|
||
with pytest.raises(迁移错误, match="target_denied") as 错:
|
||
_连接配置(引用, TCP端点)
|
||
assert "不得回显" not in str(错.value)
|
||
|
||
|
||
@pytest.mark.case_id(
|
||
"NC-mig-07747d",
|
||
environment="离线迁移测试;协议替身与临时目录,不连真实数据库",
|
||
given="URI形式连接串",
|
||
when="解析并连接",
|
||
then=["与key=value同等核对端点与覆盖参数"],
|
||
contract="docs/系统架构/新版设计/数据模型/旧知识记录分流.md",
|
||
)
|
||
def test_URI连接同样核对解析后的端点与覆盖参数__456216(tmp_path):
|
||
URI = "postgresql://muse_app:synthetic-secret@192.0.2.8:5433/muse-example"
|
||
引用 = _引用(tmp_path, URI)
|
||
assert conninfo_to_dict(_连接配置(引用, TCP端点, 角色="muse_app"))["host"] == TCP端点["host"]
|
||
引用 = _引用(tmp_path, URI + "?hostaddr=192.0.2.9")
|
||
with pytest.raises(迁移错误, match="target_denied") as 错:
|
||
_连接配置(引用, TCP端点)
|
||
assert "synthetic-secret" not in str(错.value)
|
||
|
||
|
||
class 目录协议替身:
|
||
"""只允许固定目录查询,断言执行前已设置只读;不提供业务表或写语句。"""
|
||
|
||
def __init__(self):
|
||
self.read_only = False
|
||
self.语句 = []
|
||
self.行 = []
|
||
|
||
def __enter__(self):
|
||
return self
|
||
|
||
def __exit__(self, *args):
|
||
return False
|
||
|
||
def execute(self, 语句):
|
||
assert self.read_only
|
||
self.语句.append(语句)
|
||
if 语句 == "SHOW data_directory":
|
||
self.行 = [(合成指纹["data_directory"],)]
|
||
elif 语句 == "SELECT system_identifier FROM pg_catalog.pg_control_system()":
|
||
self.行 = [(合成指纹["system_identifier"],)]
|
||
elif 语句 == "SHOW server_version":
|
||
self.行 = [("17.synthetic",)]
|
||
elif 语句 == "SELECT current_database(), current_user":
|
||
self.行 = [("postgres", "admin")]
|
||
elif 语句 == (
|
||
"SELECT datname, oid, pg_catalog.pg_get_userbyid(datdba), datistemplate "
|
||
"FROM pg_catalog.pg_database ORDER BY datname"
|
||
):
|
||
self.行 = [
|
||
("muse-example", 123, "legacy_owner", False),
|
||
("postgres", 5, "admin", False),
|
||
]
|
||
else:
|
||
pytest.fail("只读入口执行了合同外查询")
|
||
return self
|
||
|
||
def fetchone(self):
|
||
return self.行[0]
|
||
|
||
def fetchall(self):
|
||
return self.行
|
||
|
||
|
||
@pytest.mark.case_id(
|
||
"NC-mig-016cd8",
|
||
environment="离线迁移测试;协议替身与临时目录,不连真实数据库",
|
||
given="目录探测协议替身",
|
||
when="执行只读检查",
|
||
then=["返回身份信息但不自动核准为目标"],
|
||
contract="docs/系统架构/新版设计/数据模型/旧知识记录分流.md",
|
||
)
|
||
def test_只读目录协议替身返回身份但不自动核准__cc8a7f(tmp_path, monkeypatch):
|
||
连 = 目录协议替身()
|
||
monkeypatch.setattr(psycopg, "connect", lambda *a, **k: 连)
|
||
结果 = 检查实例(
|
||
端点记录=_JSON(tmp_path, "端点.json", TCP端点),
|
||
管理引用=_引用(tmp_path, _连接串(password="synthetic-secret")),
|
||
)
|
||
assert 结果["instance"] == TCP端点 | 合成指纹
|
||
assert 结果["databases"][0] == {
|
||
"database": "muse-example",
|
||
"database_oid": 123,
|
||
"owner": "legacy_owner",
|
||
"is_template": False,
|
||
}
|
||
assert 结果["current_database"] == "postgres"
|
||
assert 结果["migration_authorized"] is False
|
||
assert len(连.语句) == 5
|
||
assert "synthetic-secret" not in json.dumps(结果)
|
||
|
||
|
||
@pytest.mark.case_id(
|
||
"NC-mig-c8a454",
|
||
environment="离线迁移测试;协议替身与临时目录,不连真实数据库",
|
||
given="只读检查CLI",
|
||
when="输出检查结果",
|
||
then=["私有信息0600落盘,不回显秘密"],
|
||
contract="docs/系统架构/新版设计/数据模型/旧知识记录分流.md",
|
||
)
|
||
def test_只读检查CLI协议替身私有落盘且不回显秘密__7d5dae(tmp_path, monkeypatch, capsys):
|
||
monkeypatch.setattr(psycopg, "connect", lambda *a, **k: 目录协议替身())
|
||
端点 = _JSON(tmp_path, "端点.json", TCP端点)
|
||
引用 = _引用(tmp_path, _连接串(password="synthetic-secret"))
|
||
输出 = tmp_path / "检查.json"
|
||
参数 = ["检查实例", "--端点记录", str(端点), "--管理引用", 引用.位置, "--输出", str(输出)]
|
||
assert main(参数) == 0
|
||
assert 输出.stat().st_mode & 0o777 == 0o600
|
||
assert json.loads(输出.read_text())["migration_authorized"] is False
|
||
assert "synthetic-secret" not in capsys.readouterr().out
|
||
原凭据 = Path(引用.位置).read_bytes()
|
||
assert main(参数[:-1] + [引用.位置]) == 1
|
||
assert Path(引用.位置).read_bytes() == 原凭据
|
||
assert "output_is_input" in capsys.readouterr().err
|
||
|
||
|
||
@pytest.mark.case_id(
|
||
"NC-mig-866106",
|
||
environment="离线迁移测试;协议替身与临时目录,不连真实数据库",
|
||
given="连接失败的驱动报错",
|
||
when="捕获并呈现",
|
||
then=["不泄漏驱动错误原文中的连接细节"],
|
||
contract="docs/系统架构/新版设计/数据模型/旧知识记录分流.md",
|
||
)
|
||
def test_只读检查连接失败不泄漏驱动原文__6ddc5f(tmp_path, monkeypatch, capsys):
|
||
def 失败(*args, **kwargs):
|
||
raise psycopg.OperationalError("dsn=secret password=synthetic-secret")
|
||
|
||
monkeypatch.setattr(psycopg, "connect", 失败)
|
||
端点 = _JSON(tmp_path, "端点.json", TCP端点)
|
||
引用 = _引用(tmp_path, _连接串())
|
||
输出 = tmp_path / "检查.json"
|
||
assert (
|
||
main(["检查实例", "--端点记录", str(端点), "--管理引用", 引用.位置, "--输出", str(输出)])
|
||
== 1
|
||
)
|
||
错误 = capsys.readouterr().err
|
||
assert "target_inspect_failed" in 错误 and "synthetic-secret" not in 错误
|
||
assert not 输出.exists()
|
||
|
||
|
||
def _合成回执() -> dict:
|
||
身份 = UUID("01234567-89ab-4def-8123-456789abcdef")
|
||
return {
|
||
"version": 1,
|
||
"target_id": str(身份),
|
||
**合成指纹,
|
||
"host": TCP端点["host"],
|
||
"port": TCP端点["port"],
|
||
"database": "muse_migration_" + 身份.hex,
|
||
"database_oid": 321,
|
||
"role": "muse_app",
|
||
"run_purpose": "production",
|
||
"created_at": "2026-09-01T00:00:00+00:00",
|
||
}
|
||
|
||
|
||
@pytest.mark.case_id(
|
||
"NC-mig-c8aee1",
|
||
environment="离线迁移测试;协议替身与临时目录,不连真实数据库",
|
||
given="源库或已有业务库的回执",
|
||
when="尝试初始化为目标",
|
||
then=["拒绝作为迁移目标"],
|
||
contract="docs/系统架构/新版设计/数据模型/旧知识记录分流.md",
|
||
)
|
||
@pytest.mark.parametrize(
|
||
"库名",
|
||
["muse-example", "postgres", "existing-business"],
|
||
ids=["muse-example", "postgres", "existing-business"],
|
||
)
|
||
def test_源库和已有业务库不能凭回执作目标__f7801f(tmp_path, monkeypatch, 库名):
|
||
调用 = []
|
||
monkeypatch.setattr(psycopg, "connect", lambda *a, **k: 调用.append((a, k)))
|
||
允许 = _JSON(tmp_path, "允许.json", TCP端点 | 合成指纹)
|
||
回执 = _JSON(tmp_path, "回执.json", _合成回执() | {"database": 库名})
|
||
引用 = _引用(tmp_path, _连接串(user="muse_app"))
|
||
with pytest.raises(迁移错误, match="target_denied"):
|
||
核对隔离目标(数据库工厂(引用, 用途.生产), 允许实例=允许, 管理引用=引用, 目标回执=回执)
|
||
assert 调用 == []
|
||
|
||
|
||
class 初始化管理协议替身:
|
||
"""模拟CREATE成功/失败与目录OID漂移,仅验证本次清理边界,不创建数据库。"""
|
||
|
||
def __init__(self, *, 创建失败=False, 清理OID=321, 系统号=None):
|
||
self.创建失败 = 创建失败
|
||
self.清理OID = 清理OID
|
||
self.系统号 = 系统号 or 合成指纹["system_identifier"]
|
||
self.语句 = []
|
||
self.库名 = None
|
||
self.OID读取次数 = 0
|
||
self.行 = None
|
||
|
||
def __enter__(self):
|
||
return self
|
||
|
||
def __exit__(self, *args):
|
||
return False
|
||
|
||
def execute(self, 语句, 参数=None):
|
||
文本 = 语句 if isinstance(语句, str) else 语句.as_string()
|
||
self.语句.append(文本)
|
||
if 文本 == "SHOW data_directory":
|
||
self.行 = (合成指纹["data_directory"],)
|
||
elif 文本 == "SELECT system_identifier FROM pg_catalog.pg_control_system()":
|
||
self.行 = (self.系统号,)
|
||
elif 文本.startswith("CREATE DATABASE"):
|
||
if self.创建失败:
|
||
raise psycopg.errors.DuplicateDatabase("已存在,不能当作本次创建")
|
||
self.库名 = 文本.split('"')[1]
|
||
elif 文本 == "SELECT oid FROM pg_catalog.pg_database WHERE datname=%s":
|
||
assert 参数 == (self.库名,)
|
||
self.OID读取次数 += 1
|
||
self.行 = (321 if self.OID读取次数 == 1 else self.清理OID,)
|
||
elif 文本.startswith("DROP DATABASE"):
|
||
assert self.库名 and self.库名 in 文本
|
||
else:
|
||
pytest.fail("初始化执行了未登记的目录语句")
|
||
return self
|
||
|
||
def fetchone(self):
|
||
return self.行
|
||
|
||
|
||
@pytest.mark.case_id(
|
||
"NC-mig-fb5c10",
|
||
environment="离线迁移测试;协议替身与临时目录,不连真实数据库",
|
||
given="初始化中途失败的新库",
|
||
when="执行清理",
|
||
then=["仅清理本次创建且身份未变的库,其他库不动"],
|
||
contract="docs/系统架构/新版设计/数据模型/旧知识记录分流.md",
|
||
)
|
||
@pytest.mark.parametrize(
|
||
"创建失败,清理OID,系统号,应删除,应保留定位",
|
||
[
|
||
(False, 321, None, True, False),
|
||
(False, 999, None, False, True),
|
||
(True, 321, None, False, False),
|
||
(False, 321, "987654321", False, False),
|
||
],
|
||
ids=["own-new-db", "oid-changed", "existing-db", "wrong-instance"],
|
||
)
|
||
def test_初始化失败协议替身只清理本次创建且身份未变的新库__f56478(
|
||
tmp_path, monkeypatch, 创建失败, 清理OID, 系统号, 应删除, 应保留定位
|
||
):
|
||
管理 = 初始化管理协议替身(创建失败=创建失败, 清理OID=清理OID, 系统号=系统号)
|
||
monkeypatch.setattr(psycopg, "connect", lambda *a, **k: 管理)
|
||
维护连接 = []
|
||
|
||
def 迁移失败(self, **kwargs):
|
||
参数 = conninfo_to_dict(Path(self.引用.位置).read_text())
|
||
维护连接.append(参数)
|
||
raise 迁移错误("synthetic_migration_failed", "协议替身中止初始化")
|
||
|
||
monkeypatch.setattr(数据库工厂, "连接", 迁移失败)
|
||
允许 = _JSON(tmp_path, "允许.json", TCP端点 | 合成指纹)
|
||
输出 = tmp_path / "新目标"
|
||
with pytest.raises(迁移错误):
|
||
初始化目标(
|
||
允许实例=允许,
|
||
管理引用=_引用(tmp_path, _连接串(), "管理.txt"),
|
||
维护模板=_引用(tmp_path, _连接串(user="muse_maint"), "维护.txt"),
|
||
应用模板=_引用(tmp_path, _连接串(user="muse_app"), "应用.txt"),
|
||
输出目录=输出,
|
||
)
|
||
assert any(s.startswith("DROP DATABASE") for s in 管理.语句) == 应删除
|
||
assert 输出.exists() == 应保留定位
|
||
if 管理.库名:
|
||
身份 = UUID(管理.库名.removeprefix("muse_migration_"))
|
||
assert 管理.库名 == "muse_migration_" + 身份.hex
|
||
assert 管理.库名 != "muse-example"
|
||
assert 维护连接[0]["dbname"] == 管理.库名
|
||
assert 维护连接[0]["host"] == TCP端点["host"]
|
||
if 应保留定位:
|
||
assert (输出 / "维护连接.txt").is_file()
|
||
assert not (输出 / "目标回执.json").exists()
|
||
|
||
|
||
class 目标标记协议替身:
|
||
"""只提供真实入口要求的目录与库内标记形状,不是数据库实测。"""
|
||
|
||
def __init__(self, 标记, OID, 角色):
|
||
self.标记, self.OID, self.角色 = 标记, OID, 角色
|
||
self.行 = None
|
||
|
||
def __enter__(self):
|
||
return self
|
||
|
||
def __exit__(self, *args):
|
||
return False
|
||
|
||
def execute(self, 语句):
|
||
if 语句.startswith("SELECT oid,current_user FROM pg_catalog.pg_database"):
|
||
self.行 = (self.OID, self.角色)
|
||
elif 语句.startswith("SELECT jsonb_build_object("):
|
||
self.行 = (dict(self.标记),)
|
||
else:
|
||
pytest.fail("目标核对出现合同外查询")
|
||
return self
|
||
|
||
def fetchone(self):
|
||
return self.行
|
||
|
||
|
||
@pytest.mark.case_id(
|
||
"NC-mig-11355f",
|
||
environment="离线迁移测试;协议替身与临时目录,不连真实数据库",
|
||
given="TCP目标协议替身",
|
||
when="核对目标",
|
||
then=["绑定外部回执、库内标记、OID和角色四重身份"],
|
||
contract="docs/系统架构/新版设计/数据模型/旧知识记录分流.md",
|
||
)
|
||
@pytest.mark.parametrize(
|
||
"标记修改,OID,角色,通过",
|
||
[
|
||
({}, 321, "muse_app", True),
|
||
({}, 999, "muse_app", False),
|
||
({}, 321, "muse_maint", False),
|
||
({"target_id": "ffffffff-ffff-4fff-8fff-ffffffffffff"}, 321, "muse_app", False),
|
||
({"host": "192.0.2.9"}, 321, "muse_app", False),
|
||
({"system_identifier": "999"}, 321, "muse_app", False),
|
||
],
|
||
ids=["bound-tcp", "wrong-oid", "wrong-role", "wrong-id", "wrong-host", "wrong-system"],
|
||
)
|
||
def test_TCP目标核对协议替身仍绑定外部回执库内标记OID和角色__763bbe(
|
||
tmp_path, monkeypatch, 标记修改, OID, 角色, 通过
|
||
):
|
||
回执 = _合成回执()
|
||
monkeypatch.setattr(psycopg, "connect", lambda *a, **k: 目录协议替身())
|
||
|
||
def 应用连接(self, *, 只读=False):
|
||
assert 只读
|
||
return 目标标记协议替身(回执 | 标记修改, OID, 角色)
|
||
|
||
monkeypatch.setattr(数据库工厂, "连接", 应用连接)
|
||
应用 = _引用(tmp_path, _连接串(user="muse_app", dbname=回执["database"]), "应用.txt")
|
||
参数 = {
|
||
"允许实例": _JSON(tmp_path, "允许.json", TCP端点 | 合成指纹),
|
||
"管理引用": _引用(tmp_path, _连接串(), "管理.txt"),
|
||
"目标回执": _JSON(tmp_path, "回执.json", 回执),
|
||
}
|
||
if 通过:
|
||
assert 核对隔离目标(数据库工厂(应用, 用途.生产), **参数) == 回执
|
||
else:
|
||
with pytest.raises(迁移错误, match="target_denied"):
|
||
核对隔离目标(数据库工厂(应用, 用途.生产), **参数)
|