aps-agent/tests/golden/test_approval_delegate.py

121 lines
5.3 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 审批委托(delegation)黄金测试(plan.md §3.3 / 矩阵 P3)
# ============================================================
# 覆盖:delegate 可审批 P2、非 delegate 拒绝、owner 仍可、P3 双人分离不受影响、
# 双后端(文件/DB)record 持久化 delegateUserId。
from __future__ import annotations
from pathlib import Path
import pytest
from server.agent_core import harness
from server.auth.context import IdentityContext, bind_identity, reset_identity
def _identity(user_id: int, tenant: str = "platform") -> IdentityContext:
return IdentityContext(user_id, f"user-{user_id}", f"User {user_id}", tenant,
roles=("planner", "approver", "admin"))
def _as(identity: IdentityContext, callback):
token = bind_identity(identity)
try:
return callback()
finally:
reset_identity(token)
def _stage(owner: IdentityContext, action: str = "schedule.publish",
delegate_user_id: int | None = None) -> str:
params = {"versionId": 91}
token = bind_identity(owner)
try:
block = harness.stage_confirmation(
"delegate-test", action, params, title="委托测试", summary_lines=["t"],
delegate_user_id=delegate_user_id,
)
finally:
reset_identity(token)
return str(block.props["confirmId"])
@pytest.fixture(autouse=True)
def _clear_pending():
# 文件后端在测试前后清空;DB 后端由 database_backend fixture 管理(避免 reset 后跨库 clear)
if getattr(harness._approval_store, "backend", "file") == "file":
harness._approval_store.clear()
yield
if getattr(harness._approval_store, "backend", "file") == "file":
harness._approval_store.clear()
@pytest.fixture
def database_backend(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
"""共享 database 审批后端(SQLite 测试介质,configure 时自动迁移建表)。"""
from server.db.database import reset_engine
monkeypatch.setenv("APS_APPROVAL_BACKEND", "database")
monkeypatch.delenv("APS_APPROVAL_PATH", raising=False)
db_path = (tmp_path / "approval.db").as_posix()
monkeypatch.setenv("APS_DATABASE_URL", f"sqlite:///{db_path}")
monkeypatch.setenv("APS_APPROVAL_DATABASE_ALLOW_SQLITE", "1")
reset_engine()
store = harness.configure_approval_store() # 内部校验/迁移共享审批表
yield store
store.clear()
reset_engine()
def test_delegate_can_approve_p2(tmp_path: Path):
"""P2 委托:被委托人可审批通过。"""
harness.configure_approval_store(str(tmp_path / "approvals.json"))
owner, delegate = _identity(4001), _identity(4002)
confirm_id = _stage(owner, delegate_user_id=4002)
result = _as(delegate, lambda: harness.take_confirmation(confirm_id, approve=True))
assert result is not None
assert result["needsSecondConfirm"] is False
assert str(result["approvals"][-1]["userId"]) == "4002"
def test_non_delegate_rejected(tmp_path: Path):
"""非 owner 非 delegate 的第三人不可审批。"""
harness.configure_approval_store(str(tmp_path / "approvals.json"))
owner, stranger = _identity(4001), _identity(4003)
confirm_id = _stage(owner, delegate_user_id=4002)
result = _as(stranger, lambda: harness.take_confirmation(confirm_id, approve=True))
assert result is None
def test_owner_still_can_approve_when_delegated(tmp_path: Path):
"""委托后 owner 仍可审批(委托是扩展,不是转移)。"""
harness.configure_approval_store(str(tmp_path / "approvals.json"))
owner = _identity(4001)
confirm_id = _stage(owner, delegate_user_id=4002)
result = _as(owner, lambda: harness.take_confirmation(confirm_id, approve=True))
assert result is not None and result["needsSecondConfirm"] is False
def test_p3_separation_of_duties_unchanged_by_delegate(tmp_path: Path):
"""P3 双人分离不受委托影响:同一用户(含被委托)不能完成两次批准。"""
harness.configure_approval_store(str(tmp_path / "approvals.json"))
owner, delegate = _identity(4001), _identity(4002)
confirm_id = _stage(owner, action="mes.dispatch", delegate_user_id=4002)
# 第一次:owner 批准(P3 首重)
first = _as(owner, lambda: harness.take_confirmation(confirm_id, approve=True))
assert first is not None and first["needsSecondConfirm"] is True
# 第二次仍由 owner(同人)→ 拒绝(SOD)
denied = _as(owner, lambda: harness.take_confirmation(confirm_id, approve=True))
assert denied is not None and denied.get("separationRequired") is True
# 第二次由 delegate(不同人)→ 通过并签发 grant
second = _as(delegate, lambda: harness.take_confirmation(confirm_id, approve=True))
assert second is not None and second["needsSecondConfirm"] is False
assert second.get("executionGrant")
def test_database_backend_persists_delegate(database_backend):
"""共享 database 后端:delegateUserId 随 record 持久化,delegate 可审批。"""
owner, delegate = _identity(7101), _identity(7102)
confirm_id = _as(owner, lambda: _stage(owner, delegate_user_id=7102))
approved = _as(delegate, lambda: harness.take_confirmation(confirm_id, approve=True))
assert approved is not None and approved["needsSecondConfirm"] is False
assert str(approved["approvals"][-1]["userId"]) == "7102"