audit-core's completeness contract landed, and our single per-source beat is the shape it rules inadequate: it is discharged by whichever class is busy, so a revocation stream that has gone silent looks identical to a quiet one — and revocation is the only silence here that matters. Emit one nothing-to-report assertion per declared class, all four in one transaction so a partial emission cannot report some classes healthy and others stalled. Carry type audit-core.heartbeat with class and assertion on data. Pin that the first beat goes out at startup rather than an interval later, since a declared-but-never-sent class is their no_heartbeat_since_registration finding and not a skip. Declare heartbeat_classes and the reconciliation surface in the source registration, including the residual neither control covers: a compromised emitter suppresses the event and its own heartbeat together. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HyybaE7DUXrWYrhbnESCTe Assistant: claude-code Assistant-Model: opus Assistant-Process: 1275879@bnt-lap001 Assistant-Session: eb464208-f821-41b2-bc5a-a6c33d92a8ad
166 lines
5.6 KiB
Python
166 lines
5.6 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"
|
|
assert body["tenant"] == "tenant:platform"
|
|
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)
|
|
from approval_engine.store import HEARTBEAT_CLASSES
|
|
|
|
first = worker.run_once()
|
|
# The first beat is due immediately, not one interval in: declaring a
|
|
# heartbeat and never sending one is audit-core's
|
|
# `no_heartbeat_since_registration` finding, not a skip.
|
|
assert first["delivered"] == len(HEARTBEAT_CLASSES)
|
|
assert {d["action"] for d in delivered} == {"audit-core.heartbeat"}
|
|
now[0] += timedelta(seconds=301)
|
|
second = worker.run_once()
|
|
assert second["delivered"] == len(HEARTBEAT_CLASSES)
|
|
assert len(delivered) == 2 * len(HEARTBEAT_CLASSES)
|
|
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({})
|
|
|
|
|
|
def test_heartbeat_envelope_carries_class_and_assertion_on_data():
|
|
"""audit-core reads these off `data`, not out of a producer-shaped blob."""
|
|
from approval_engine.audit import audit_envelope
|
|
|
|
envelope = audit_envelope(
|
|
{
|
|
"event_id": "e1",
|
|
"action": "audit-core.heartbeat",
|
|
"source": "approval-engine",
|
|
"resource": "approval-engine:heartbeat",
|
|
"tenant": "tenant:platform",
|
|
"observed_at": "2026-09-10T00:00:00+00:00",
|
|
"schema_version": "audit-core.event.v1alpha1",
|
|
"scope": "netkingdom-approvals",
|
|
"outcome": "success",
|
|
"details": {"class": "revocation", "assertion": "nothing-to-report"},
|
|
}
|
|
)
|
|
assert envelope["type"] == "audit-core.heartbeat"
|
|
assert envelope["data"]["class"] == "revocation"
|
|
assert envelope["data"]["assertion"] == "nothing-to-report"
|
|
|
|
|
|
def test_ordinary_event_envelope_gains_no_heartbeat_fields():
|
|
from approval_engine.audit import audit_envelope
|
|
|
|
envelope = audit_envelope(
|
|
{
|
|
"event_id": "e2",
|
|
"action": "approval.issuance",
|
|
"source": "approval-engine",
|
|
"resource": "approval:a1",
|
|
"tenant": "tenant:platform",
|
|
"observed_at": "2026-09-10T00:00:00+00:00",
|
|
"schema_version": "audit-core.event.v1alpha1",
|
|
"scope": "netkingdom-approvals",
|
|
"outcome": "success",
|
|
"details": {"class": "issuance"},
|
|
}
|
|
)
|
|
assert "assertion" not in envelope["data"]
|