Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a05e2e-805b-7042-a750-71f473bceea2
105 lines
3.2 KiB
Python
105 lines
3.2 KiB
Python
import json
|
|
|
|
import pytest
|
|
|
|
from approval_engine.pep import (
|
|
ApprovalHTTPClient,
|
|
ApprovalProtocolError,
|
|
ProtectedActionHarness,
|
|
)
|
|
|
|
|
|
class Client:
|
|
def __init__(self, claim=None, consume=None, failure=None):
|
|
self.claim_result = claim or {"valid_now": True, "consumed": False}
|
|
self.consume_result = consume
|
|
self.failure = failure
|
|
self.calls = []
|
|
|
|
def claim(self, approval_id):
|
|
self.calls.append("claim")
|
|
if self.failure == "claim":
|
|
raise ApprovalProtocolError("down")
|
|
return self.claim_result
|
|
|
|
def consume(self, approval_id, digest, decision_id):
|
|
self.calls.append("consume")
|
|
if self.failure == "consume":
|
|
raise ApprovalProtocolError("conflict")
|
|
return self.consume_result or {"status": "consumed", "request_digest": digest}
|
|
|
|
|
|
DIGEST = "sha256:" + "ab" * 32
|
|
|
|
|
|
class Response:
|
|
def __init__(self, body):
|
|
self.body = json.dumps(body).encode()
|
|
|
|
def getcode(self):
|
|
return 200
|
|
|
|
def read(self, _size):
|
|
return self.body
|
|
|
|
def close(self):
|
|
pass
|
|
|
|
|
|
def test_http_client_rereads_mounted_token(tmp_path):
|
|
token = tmp_path / "token"
|
|
token.write_text("first")
|
|
seen = []
|
|
|
|
def opener(request, timeout):
|
|
seen.append((request.get_header("Authorization"), timeout))
|
|
return Response({"valid_now": True, "consumed": False})
|
|
|
|
client = ApprovalHTTPClient("http://approval-engine:8080", token, opener=opener)
|
|
client.claim("approval-1")
|
|
token.write_text("second")
|
|
client.claim("approval-1")
|
|
assert [item[0] for item in seen] == ["Bearer first", "Bearer second"]
|
|
|
|
|
|
def allow(_claim):
|
|
return {"effect": "ALLOW", "decision_id": "decision:1", "request_digest": DIGEST}
|
|
|
|
|
|
def test_side_effect_occurs_only_after_claim_decision_and_consume():
|
|
client = Client()
|
|
order = client.calls
|
|
result = ProtectedActionHarness(client).execute(
|
|
"approval:1",
|
|
DIGEST,
|
|
lambda claim: (order.append("decision"), allow(claim))[1],
|
|
lambda: (order.append("side-effect"), "dry-run-only")[1],
|
|
)
|
|
assert result == "dry-run-only"
|
|
assert order == ["claim", "decision", "consume", "side-effect"]
|
|
|
|
|
|
@pytest.mark.parametrize("failure", ["claim", "consume"])
|
|
def test_unavailable_or_conflicting_engine_prevents_side_effect(failure):
|
|
client = Client(failure=failure)
|
|
effects = []
|
|
with pytest.raises(ApprovalProtocolError):
|
|
ProtectedActionHarness(client).execute(
|
|
"approval:1", DIGEST, allow, lambda: effects.append("called")
|
|
)
|
|
assert effects == []
|
|
|
|
|
|
def test_deny_or_digest_mismatch_prevents_consume_and_side_effect():
|
|
for decision in (
|
|
{"effect": "DENY", "decision_id": "decision:1", "request_digest": DIGEST},
|
|
{"effect": "ALLOW", "decision_id": "decision:1", "request_digest": "sha256:" + "cd" * 32},
|
|
):
|
|
client = Client()
|
|
effects = []
|
|
with pytest.raises(ApprovalProtocolError):
|
|
ProtectedActionHarness(client).execute(
|
|
"approval:1", DIGEST, lambda _claim, value=decision: value, lambda: effects.append("called")
|
|
)
|
|
assert client.calls == ["claim"]
|
|
assert effects == []
|