1) 客服 Agent 四份交付文档 + 构建脚手架:品牌由包装占位 XX科技 / 旧名 南方财富 统一为南方基金(热线 400-889-8899 / 官网 nffund.com),系统名改为「智能服务系统」; 同步追加 §0.4 修订记录行,工程记录行保留原占位字面以支撑硬编码扫描验收。 2) 开发文档:清理 28 份已作废/残留文档(14 份移出归档 + 14 份仓库副本), 新增《文档规整方案与开发前待决事项-2026-09-17》。 3) 客服agent 四份交付文档首次纳入本分支。
116 lines
4.4 KiB
Python
116 lines
4.4 KiB
Python
"""`AdminService` 权限映射契约测试(不连数据库)。
|
||
|
||
管理面的权限判定是纯映射逻辑,但它决定"谁能改配置、谁能激活发布"。测试手法:把
|
||
`AuthorizationService.require` 换成记录器并**立刻抛错**,流程就在闸门处停下——
|
||
既能断言传进去的权限码与 `admin` 标志,又不需要构造 session 与 repository。
|
||
|
||
覆盖的分支:
|
||
- `query`:审计资源用 `audit:read`,其余用 `config:read`,且一律要求 admin 身份;
|
||
- `mutate`:模型端点用 `model-endpoint:manage`,其余用 `config:write`;
|
||
`reviews` 提升为 `config:review`;`activations`/`rollbacks` **仅当资源是
|
||
`config-releases` 时**才提升为 `config:activate`;
|
||
- 边界:`model-endpoints` 下的 `activations` 不应被提升为 `config:activate`
|
||
(否则模型端点的激活会绕过配置发布的激活语义)。
|
||
"""
|
||
|
||
from typing import Any
|
||
|
||
import pytest
|
||
|
||
from app.core.contracts import RequestContext
|
||
from app.core.errors import ForbiddenAgentError
|
||
from app.service.admin_service import AdminService
|
||
|
||
CONTEXT = RequestContext(user_id="9003", trace_id="trace-1", roles=("admin",))
|
||
KEY = "k" * 16
|
||
|
||
|
||
def recording_auth(captured: list[tuple[str, bool]]) -> type:
|
||
class RecordingAuth:
|
||
@staticmethod
|
||
async def require(
|
||
context: RequestContext, permission: str, *, admin: bool = False
|
||
) -> None:
|
||
del context
|
||
captured.append((permission, admin))
|
||
raise ForbiddenAgentError("stop at gate")
|
||
|
||
return RecordingAuth
|
||
|
||
|
||
def service() -> AdminService:
|
||
"""绕过 __init__:本测试只验证权限映射,不涉及依赖装配。"""
|
||
return AdminService.__new__(AdminService)
|
||
|
||
|
||
async def record_query(monkeypatch: pytest.MonkeyPatch, resource: str) -> list[tuple[str, bool]]:
|
||
captured: list[tuple[str, bool]] = []
|
||
monkeypatch.setattr("app.service.admin_service.AuthorizationService", recording_auth(captured))
|
||
with pytest.raises(ForbiddenAgentError):
|
||
await service().query(resource, CONTEXT)
|
||
return captured
|
||
|
||
|
||
async def record_mutate(
|
||
monkeypatch: pytest.MonkeyPatch, resource: str, action: str
|
||
) -> list[tuple[str, bool]]:
|
||
captured: list[tuple[str, bool]] = []
|
||
monkeypatch.setattr("app.service.admin_service.AuthorizationService", recording_auth(captured))
|
||
with pytest.raises(ForbiddenAgentError):
|
||
await service().mutate(resource, CONTEXT, {}, KEY, None, action=action)
|
||
return captured
|
||
|
||
|
||
@pytest.mark.parametrize(
|
||
("resource", "expected"),
|
||
[
|
||
("audit-records", "audit:read"),
|
||
("config-releases", "config:read"),
|
||
("negative-word-rules", "config:read"),
|
||
],
|
||
)
|
||
async def test_query_permission_mapping(
|
||
monkeypatch: pytest.MonkeyPatch, resource: str, expected: str
|
||
) -> None:
|
||
captured = await record_query(monkeypatch, resource)
|
||
|
||
assert captured == [(expected, True)]
|
||
|
||
|
||
@pytest.mark.parametrize(
|
||
("resource", "action", "expected"),
|
||
[
|
||
("config-releases", "write", "config:write"),
|
||
("model-endpoints", "write", "model-endpoint:manage"),
|
||
("config-releases", "reviews", "config:review"),
|
||
("model-endpoints", "reviews", "config:review"),
|
||
("config-releases", "activations", "config:activate"),
|
||
("config-releases", "rollbacks", "config:activate"),
|
||
# 边界:模型端点的激活不提升为 config:activate,仍走端点管理权限。
|
||
("model-endpoints", "activations", "model-endpoint:manage"),
|
||
("negative-word-rules", "activations", "config:write"),
|
||
],
|
||
)
|
||
async def test_mutate_permission_mapping(
|
||
monkeypatch: pytest.MonkeyPatch, resource: str, action: str, expected: str
|
||
) -> None:
|
||
captured = await record_mutate(monkeypatch, resource, action)
|
||
|
||
assert captured == [(expected, True)]
|
||
|
||
|
||
async def test_gate_is_called_before_any_database_work(monkeypatch: pytest.MonkeyPatch) -> None:
|
||
"""闸门必须早于任何数据库访问:这里用"创建会话即报错"的替身证明。"""
|
||
captured: list[tuple[str, bool]] = []
|
||
monkeypatch.setattr("app.service.admin_service.AuthorizationService", recording_auth(captured))
|
||
|
||
def exploding_factory() -> Any:
|
||
raise AssertionError("权限校验未通过时不应创建数据库会话")
|
||
|
||
monkeypatch.setattr("app.service.admin_service.SessionFactory", exploding_factory)
|
||
|
||
with pytest.raises(ForbiddenAgentError):
|
||
await service().query("config-releases", CONTEXT)
|
||
|
||
assert captured == [("config:read", True)]
|