126 lines
4.4 KiB
Python
126 lines
4.4 KiB
Python
|
|
"""AUDIT-WP-0010-T04. The envelope tenant-engine actually sends.
|
||
|
|
|
||
|
|
`AUDIT-IN-0002` invited a correction if the envelope needed a field the
|
||
|
|
emitter does not send. It does — six of eight required fields, one of them
|
||
|
|
absent entirely rather than merely renamed.
|
||
|
|
|
||
|
|
These tests pin the mismatch rather than describing it, so that the day
|
||
|
|
tenant-engine corrects `envelope_for` the failure here is the signal, and so
|
||
|
|
that nobody quietly relaxes `normalize()` to accept the alternate spellings.
|
||
|
|
"""
|
||
|
|
|
||
|
|
import pytest
|
||
|
|
|
||
|
|
from audit_core.ingestion import normalize
|
||
|
|
from audit_core.senders import SenderIdentity
|
||
|
|
|
||
|
|
|
||
|
|
IDENTITY = SenderIdentity(
|
||
|
|
name="tenant-engine",
|
||
|
|
tokens=("fixture-only",),
|
||
|
|
sources=frozenset({"tenant-engine"}),
|
||
|
|
evidence_kind="attributive",
|
||
|
|
completeness_trade="outbox drains after commit",
|
||
|
|
)
|
||
|
|
|
||
|
|
# Verbatim shape of tenant_engine.audit_core.envelope_for as of 2026-09-10.
|
||
|
|
TENANT_ENGINE_ENVELOPE = {
|
||
|
|
"schema_version": "audit-core.event.v1alpha1",
|
||
|
|
"event_id": "e-1",
|
||
|
|
"observed_at": "2026-09-10T00:00:00+00:00",
|
||
|
|
"tenant": "tenant:acme",
|
||
|
|
"scope": "tenant-engine",
|
||
|
|
"source": "tenant-engine",
|
||
|
|
"actor": "u-1",
|
||
|
|
"action": "role.granted",
|
||
|
|
"resource": "tenant:acme",
|
||
|
|
"outcome": "recorded",
|
||
|
|
"reason": None,
|
||
|
|
"details": {"role": "admin"},
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
def test_the_tenant_engine_envelope_is_rejected_today():
|
||
|
|
"""Not a hypothetical: this is the shape on the wire, and it 400s."""
|
||
|
|
with pytest.raises(ValueError, match="invalid_event"):
|
||
|
|
normalize(TENANT_ENGINE_ENVELOPE, "e-1", IDENTITY)
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.parametrize(
|
||
|
|
"required,sent_as",
|
||
|
|
[
|
||
|
|
("id", "event_id"),
|
||
|
|
("type", "action"),
|
||
|
|
("subject", "resource"),
|
||
|
|
("occurred_at", "observed_at"),
|
||
|
|
("data", "details"),
|
||
|
|
],
|
||
|
|
)
|
||
|
|
def test_each_renamed_field_is_absent_under_the_name_the_receiver_reads(required, sent_as):
|
||
|
|
assert required not in TENANT_ENGINE_ENVELOPE
|
||
|
|
assert sent_as in TENANT_ENGINE_ENVELOPE
|
||
|
|
|
||
|
|
|
||
|
|
def test_correlation_id_is_absent_entirely_and_cannot_be_synthesized():
|
||
|
|
"""The one that is not a rename.
|
||
|
|
|
||
|
|
A receiver-invented correlation_id would tie an event to an operation
|
||
|
|
audit-core never observed, which is worse than not having one.
|
||
|
|
"""
|
||
|
|
assert "correlation_id" not in TENANT_ENGINE_ENVELOPE
|
||
|
|
corrected = {
|
||
|
|
"id": TENANT_ENGINE_ENVELOPE["event_id"],
|
||
|
|
"type": TENANT_ENGINE_ENVELOPE["action"],
|
||
|
|
"source": TENANT_ENGINE_ENVELOPE["source"],
|
||
|
|
"subject": TENANT_ENGINE_ENVELOPE["resource"],
|
||
|
|
"tenant": TENANT_ENGINE_ENVELOPE["tenant"],
|
||
|
|
"occurred_at": TENANT_ENGINE_ENVELOPE["observed_at"],
|
||
|
|
"data": TENANT_ENGINE_ENVELOPE["details"],
|
||
|
|
}
|
||
|
|
with pytest.raises(ValueError, match="invalid_event"):
|
||
|
|
normalize(corrected, "e-1", IDENTITY)
|
||
|
|
|
||
|
|
|
||
|
|
def test_the_corrected_envelope_is_accepted():
|
||
|
|
"""What tenant-engine needs to send. The whole correction, in one place."""
|
||
|
|
corrected = {
|
||
|
|
"id": "e-1",
|
||
|
|
"type": "role.granted",
|
||
|
|
"source": "tenant-engine",
|
||
|
|
"subject": "tenant:acme",
|
||
|
|
"tenant": "tenant:acme",
|
||
|
|
"correlation_id": "req-9f21",
|
||
|
|
"occurred_at": "2026-09-10T00:00:00+00:00",
|
||
|
|
"data": {"role": "admin", "actor": "u-1"},
|
||
|
|
}
|
||
|
|
event = normalize(corrected, "e-1", IDENTITY)
|
||
|
|
assert event.event_id == "e-1"
|
||
|
|
assert event.action == "role.granted"
|
||
|
|
assert event.resource == "tenant:acme"
|
||
|
|
assert event.tenant == "tenant:acme"
|
||
|
|
assert event.details["data"]["role"] == "admin"
|
||
|
|
# Derived by the receiver, never taken from the sender.
|
||
|
|
assert event.outcome == "recorded"
|
||
|
|
assert event.actor is None
|
||
|
|
assert event.scope == "tenant"
|
||
|
|
|
||
|
|
|
||
|
|
def test_normalize_still_refuses_the_alternate_spellings():
|
||
|
|
"""Guards the decision not to relax the receiver.
|
||
|
|
|
||
|
|
Accepting event_id/action/resource as aliases would make audit-core choose
|
||
|
|
which sender key means which stored field. The mapping belongs to the
|
||
|
|
sender, which is the party whose assertion the record is.
|
||
|
|
"""
|
||
|
|
required = ("id", "type", "source", "subject", "tenant", "correlation_id", "occurred_at", "data")
|
||
|
|
accepted = {
|
||
|
|
"id": "e-2", "type": "t", "source": "tenant-engine", "subject": "s",
|
||
|
|
"tenant": "tenant:acme", "correlation_id": "c",
|
||
|
|
"occurred_at": "2026-09-10T00:00:00+00:00", "data": {"k": "v"},
|
||
|
|
}
|
||
|
|
for field in required:
|
||
|
|
broken = dict(accepted)
|
||
|
|
broken.pop(field)
|
||
|
|
with pytest.raises(ValueError, match="invalid_event"):
|
||
|
|
normalize(broken, "e-2", IDENTITY)
|