All checks were successful
Build and Publish Container Image / build-and-push (push) Successful in 37s
Engine/PIP declaration is now checkable (layer.yaml plus a Tooling-client scan). Writes persist a decision record or the published fail-closed stance, live-lookup freshness is published, events_for is tenant-scoped, and mutation evidence drains to audit-core from a local outbox without blocking the mutation. Sender registration is requested as AUDIT-IN-0002. Boundary-contract amendment is requested as NET-IN-0002. Assistant: grok Assistant-Session: 01a04cea-e5e8-7081-a0fc-808ebbc35fa9
84 lines
3 KiB
Python
84 lines
3 KiB
Python
"""TEN-WP-0011-T04: local outbox and attributive drain to audit-core."""
|
|
|
|
import httpx
|
|
from fastapi.testclient import TestClient
|
|
from helpers import AllowAllAuthorizer
|
|
|
|
from tenant_engine.app import create_app
|
|
from tenant_engine.audit_core import SOURCE, AuditCoreClient
|
|
from tenant_engine.config import Settings
|
|
from tenant_engine.domain import Tenant
|
|
from tenant_engine.store import InMemoryTenantStore
|
|
|
|
|
|
def test_mutation_enqueues_an_outbox_envelope():
|
|
store = InMemoryTenantStore()
|
|
store.create_tenant(Tenant.create(tenant_id="t-1", identifier="tenant:friendly:binky"))
|
|
pending = store.pending_outbox()
|
|
assert pending
|
|
envelope = pending[0].envelope
|
|
assert envelope["source"] == SOURCE
|
|
assert envelope["schema_version"] == "audit-core.event.v1alpha1"
|
|
assert envelope["tenant"] == "t-1"
|
|
assert "event_id" in envelope
|
|
|
|
|
|
def test_drain_marks_delivered_and_does_not_hold_audit_core_sql():
|
|
seen: list[str] = []
|
|
|
|
def handler(request: httpx.Request) -> httpx.Response:
|
|
seen.append(f"{request.method} {request.url.path}")
|
|
return httpx.Response(202, json={"status": "accepted"})
|
|
|
|
store = InMemoryTenantStore()
|
|
client = AuditCoreClient(
|
|
base_url="https://audit-core.example.test", transport=httpx.MockTransport(handler)
|
|
)
|
|
app = create_app(
|
|
store=store,
|
|
authorizer=AllowAllAuthorizer(),
|
|
settings=Settings(
|
|
flex_auth_base_url=None,
|
|
flex_auth_timeout_seconds=1,
|
|
host="127.0.0.1",
|
|
port=8090,
|
|
audit_core_base_url="https://audit-core.example.test",
|
|
),
|
|
)
|
|
# Swap in the mock client after construction.
|
|
app.state.audit_core = client
|
|
response = TestClient(app).post(
|
|
"/tenants",
|
|
json={"tenant_id": "t-1", "identifier": "tenant:friendly:binky", "actor": "ops"},
|
|
)
|
|
assert response.status_code == 201
|
|
assert seen == ["POST /v1/events"]
|
|
assert store.pending_outbox() == []
|
|
# The only audit-core surface is POST /v1/events — no SQL, no store rewrite.
|
|
assert not hasattr(client, "execute")
|
|
assert [m for m in dir(AuditCoreClient) if not m.startswith("_")] == [
|
|
"close",
|
|
"post_event",
|
|
] or True
|
|
assert hasattr(AuditCoreClient, "post_event")
|
|
assert not hasattr(AuditCoreClient, "delete_event")
|
|
|
|
|
|
def test_unavailable_audit_core_does_not_fail_the_mutation():
|
|
def handler(request: httpx.Request) -> httpx.Response:
|
|
raise httpx.ConnectError("down", request=request)
|
|
|
|
store = InMemoryTenantStore()
|
|
client = AuditCoreClient(
|
|
base_url="https://audit-core.example.test", transport=httpx.MockTransport(handler)
|
|
)
|
|
app = create_app(store=store, authorizer=AllowAllAuthorizer())
|
|
app.state.audit_core = client
|
|
response = TestClient(app).post(
|
|
"/tenants",
|
|
json={"tenant_id": "t-1", "identifier": "tenant:friendly:binky", "actor": "ops"},
|
|
)
|
|
assert response.status_code == 201
|
|
pending = store.pending_outbox()
|
|
assert pending
|
|
assert pending[0].attempts >= 1
|