from pathlib import Path import pytest from qonto_assistant.contracts import ActorClaims, CapabilityRequest from qonto_assistant.policy import PolicyEngine POLICY_FILE = Path(__file__).resolve().parents[1] / "src" / "qonto_assistant" / "policy" / "qonto-v1.yaml" def _policy(*, enforce_scope: bool = False) -> PolicyEngine: return PolicyEngine.from_file( POLICY_FILE, required_scope="finance.qonto.read", enforce_scope=enforce_scope, ) def _request(capability_id: str, **request_args: object) -> CapabilityRequest: claims = ActorClaims( actor_id="agent-1", tenant_id="binky", lane="green", scopes=frozenset({"finance.qonto.read"}), ) return CapabilityRequest( capability_id=capability_id, tenant_id="binky", actor_claims=claims, resource_scope="test", request_args=dict(request_args), protocol="rest", ) def test_policy_allows_known_read_capability() -> None: decision = _policy().decide(_request("org_summary")) assert decision.allowed is True assert decision.reason == "allow" def test_policy_denies_cross_tenant_requests() -> None: claims = ActorClaims(actor_id="agent-1", tenant_id="other", lane="green") request = CapabilityRequest( capability_id="org_summary", tenant_id="binky", actor_claims=claims, resource_scope="accounts", request_args={}, protocol="rest", ) decision = _policy().decide(request) assert decision.allowed is False assert decision.reason == "tenant_scope" def test_policy_denies_excessive_page_size() -> None: decision = _policy().decide(_request("list_transactions", page=1, page_size=101, window_days=31)) assert decision.allowed is False assert decision.reason == "arg_constraint" def test_policy_denies_volume_cost_shaped_requests() -> None: decision = _policy().decide( _request("list_transactions", page=1, page_size=50, window_days=31, operation_type="card_operation") ) assert decision.allowed is False assert decision.reason == "volume_cost" def test_policy_denies_credential_exfiltration_flags() -> None: decision = _policy().decide(_request("list_transactions", page=1, page_size=50, window_days=31, include_full_iban=True)) assert decision.allowed is False assert decision.reason == "credential_exfil" def test_policy_enforces_scope_when_enabled() -> None: claims = ActorClaims(actor_id="agent-1", tenant_id="binky", lane="green", scopes=frozenset()) request = CapabilityRequest( capability_id="org_summary", tenant_id="binky", actor_claims=claims, resource_scope="accounts", request_args={}, protocol="rest", ) decision = _policy(enforce_scope=True).decide(request) assert decision.allowed is False assert decision.reason == "authz_denied" @pytest.mark.parametrize("protocol", ["rest", "mcp"]) def test_policy_decision_identical_across_protocols(protocol: str) -> None: """QONTO-WP-0003-T01: REST and MCP must hit the identical policy path. The decision for a known-deny case (a spend-shaped capability id) must not depend on which transport originated the request. """ claims = ActorClaims( actor_id="agent-1", tenant_id="binky", lane="green", scopes=frozenset({"finance.qonto.read"}), ) request = CapabilityRequest( capability_id="list_transactions", tenant_id="binky", actor_claims=claims, resource_scope="test", request_args={ "page": 1, "page_size": 50, "window_days": 31, "requested_action": "transfer_funds", }, protocol=protocol, ) decision = _policy().decide(request) assert decision.allowed is False assert decision.reason == "spend"