Files
group_fqcd_jr/tests/unit/service/test_admin_service.py
T

116 lines
4.4 KiB
Python
Raw Normal View History

"""`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)]