approval-engine/tests/test_audit.py
tegwick 2370f69927 Harden the PEP harness and KeyCape registration request
Close remaining in-repo APPROVAL-WP-0002 gaps: drive GH-DEC-2026-003 against
the real HTTP surface, fail closed on JWT/human-consume/static-token paths,
treat audit 200 duplicates as drained, and ask KeyCape for the production
audience and client grants.

Assistant: grok
Assistant-Session: 01a06253-e557-7971-93d9-4f4c2cfbf455
2026-09-02 15:46:06 +02:00

117 lines
3.7 KiB
Python

import json
from datetime import timedelta
import pytest
from approval_engine.audit import AuditCoreSink, AuditDeliveryError, OutboxWorker
from approval_engine.store import Engine
from tests.conftest import FROZEN, approve
class Response:
def __init__(self, status):
self.status = status
def getcode(self):
return self.status
def close(self):
pass
def test_audit_sender_adapts_envelope_and_rereads_token(tmp_path):
token = tmp_path / "token"
token.write_text("first")
seen = []
def open_request(request, timeout):
seen.append((request, timeout))
return Response(202)
engine = Engine(":memory:", clock=lambda: FROZEN)
approve(engine)
sink = AuditCoreSink("http://audit-core:8080", token, opener=open_request)
sink(engine.undrained()[0]["payload"])
token.write_text("second")
sink(engine.undrained()[0]["payload"])
first, second = (item[0] for item in seen)
assert first.get_header("Authorization") == "Bearer first"
assert second.get_header("Authorization") == "Bearer second"
assert first.get_header("Idempotency-key") == engine.undrained()[0]["event_id"]
body = json.loads(first.data)
assert body["id"] == body["correlation_id"] or body["correlation_id"]
assert body["type"] == "approval.issuance"
assert body["source"] == "approval-engine"
engine.close()
def test_duplicate_audit_status_marks_drained(tmp_path):
token = tmp_path / "token"
token.write_text("token")
engine = Engine(":memory:", clock=lambda: FROZEN)
approve(engine)
sink = AuditCoreSink(
"http://audit-core:8080", token, opener=lambda *_args, **_kwargs: Response(200)
)
result = engine.drain(sink)
assert result == {"delivered": 1, "failed": 0}
assert engine.undrained() == []
engine.close()
def test_httperror_audit_status_remains_pending(tmp_path):
from io import BytesIO
from urllib.error import HTTPError
token = tmp_path / "token"
token.write_text("token")
engine = Engine(":memory:", clock=lambda: FROZEN)
approve(engine)
def opener(request, timeout):
raise HTTPError(
request.full_url, 503, "unavailable", hdrs=None, fp=BytesIO(b"no")
)
sink = AuditCoreSink("http://audit-core:8080", token, opener=opener)
result = engine.drain(sink)
assert result == {"delivered": 0, "failed": 1}
assert engine.undrained()[0]["last_error"] == "AuditDeliveryError"
engine.close()
def test_nonaccepted_audit_status_remains_pending(tmp_path):
token = tmp_path / "token"
token.write_text("token")
engine = Engine(":memory:", clock=lambda: FROZEN)
approve(engine)
sink = AuditCoreSink(
"http://audit-core:8080", token, opener=lambda *_args, **_kwargs: Response(503)
)
result = engine.drain(sink)
assert result == {"delivered": 0, "failed": 1}
assert engine.undrained()[0]["last_error"] == "AuditDeliveryError"
engine.close()
def test_worker_emits_due_heartbeat_and_drains():
now = [FROZEN]
engine = Engine(":memory:", clock=lambda: now[0])
delivered = []
worker = OutboxWorker(engine, delivered.append, heartbeat_interval_seconds=300)
first = worker.run_once()
assert first["delivered"] == 1
assert delivered[0]["action"] == "approval.heartbeat"
now[0] += timedelta(seconds=301)
second = worker.run_once()
assert second["delivered"] == 1
assert len(delivered) == 2
engine.close()
def test_sender_rejects_empty_token(tmp_path):
token = tmp_path / "token"
token.write_text("")
sink = AuditCoreSink("http://audit-core", token)
with pytest.raises(AuditDeliveryError, match="credential"):
sink({})