AUDIT-WP-0009-T08 — assert the approval-validity prohibition by test
§9.4 forbids audit-core answering whether an approval is still valid: a consumer branching on such an answer would route an authorization decision through the audit fabric. The prohibition was honoured by absence, which is not the estate's idiom — §6.4 obligation 3 requires a published stance to equal shipped behaviour asserted by test. Written against shapes rather than today's route list, so adding a validity surface later fails here rather than passing quietly. Plausible verdict paths must 404 rather than 403: a distinguishable forbidden would imply a surface exists behind auth. No backend class, Postgres included, may carry a verdict-shaped method name. The declaration must stay in layer.yaml, INTENT.md and SCOPE.md. The last test guards the opposite error — an approval-shaped event class is still ingestible, because the prohibition bounds the verdict, not the record. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0185wifnLzCxjEY2MT1XbK7L Assistant: claude-code Assistant-Model: opus Assistant-Process: 713962@bnt-lap001 Assistant-Session: 2718d99d-d3ff-478f-83a2-3a30f01a02fc
This commit is contained in:
parent
2f7f475e85
commit
948e8a3e8f
2 changed files with 123 additions and 1 deletions
122
tests/test_approval_validity_prohibition.py
Normal file
122
tests/test_approval_validity_prohibition.py
Normal file
|
|
@ -0,0 +1,122 @@
|
||||||
|
"""AUDIT-WP-0009-T08 — the §9.4 approval-validity prohibition, asserted.
|
||||||
|
|
||||||
|
The prohibition has until now been honoured by absence, which is not the
|
||||||
|
estate's idiom: §6.4 obligation 3 requires a published stance to equal shipped
|
||||||
|
behaviour *asserted by test*. A prohibition stated in `INTENT.md`, `SCOPE.md`
|
||||||
|
and `layer.yaml` is worth asserting in `tests/`.
|
||||||
|
|
||||||
|
Audit Core records approval events. It renders no verdict on whether an
|
||||||
|
approval is still valid — a consumer branching on such an answer would route an
|
||||||
|
authorization decision through the audit fabric, and `access-engine` is the
|
||||||
|
estate's only decision point (§6). Operative approval state belongs to
|
||||||
|
`approval-engine`.
|
||||||
|
|
||||||
|
These tests are deliberately written against *shapes* rather than a fixed list
|
||||||
|
of today's routes, so that adding a validity surface later fails here rather
|
||||||
|
than passing quietly.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import inspect
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from audit_core import interface
|
||||||
|
from audit_core.ingestion import IngestionApplication
|
||||||
|
from audit_core.sqlite_backend import SQLiteAuditBackend
|
||||||
|
|
||||||
|
from test_ingestion import invoke
|
||||||
|
|
||||||
|
REPO = Path(__file__).resolve().parents[1]
|
||||||
|
|
||||||
|
# Paths a consumer looking for a verdict would plausibly try. None may answer.
|
||||||
|
VERDICT_PATHS = [
|
||||||
|
"/v1/approvals",
|
||||||
|
"/v1/approvals/appr-1",
|
||||||
|
"/v1/approvals/appr-1/valid",
|
||||||
|
"/v1/approvals/appr-1/validity",
|
||||||
|
"/v1/approvals/appr-1/status",
|
||||||
|
"/v1/approval-validity",
|
||||||
|
"/v1/approval-status",
|
||||||
|
"/v1/is-approved",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Method-name fragments that would mean a backend answers the question.
|
||||||
|
VERDICT_NAME_FRAGMENTS = (
|
||||||
|
"approval_valid",
|
||||||
|
"approval_validity",
|
||||||
|
"is_approved",
|
||||||
|
"approval_status",
|
||||||
|
"check_approval",
|
||||||
|
"approval_state",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def app(tmp_path):
|
||||||
|
return IngestionApplication(SQLiteAuditBackend(str(tmp_path / "events.db")), "opaque")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("path", VERDICT_PATHS)
|
||||||
|
def test_no_route_answers_whether_an_approval_is_valid(app, path):
|
||||||
|
status, _ = invoke(app, None, path=path, method="GET", body=b"")
|
||||||
|
# 404 is the right answer: the surface does not exist. Anything in the 2xx
|
||||||
|
# range would be a verdict, and a 403 would imply one exists behind auth.
|
||||||
|
assert status.startswith("404"), f"{path} answered {status}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_no_backend_exposes_an_approval_verdict_method():
|
||||||
|
"""The contract and every implementation, not only the one in use."""
|
||||||
|
from audit_core import mock_file_backend, sqlite_backend
|
||||||
|
|
||||||
|
modules = [interface, sqlite_backend, mock_file_backend]
|
||||||
|
try: # optional dependency; skipping it would weaken the assertion silently
|
||||||
|
from audit_core import postgres_backend
|
||||||
|
|
||||||
|
modules.append(postgres_backend)
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
offenders = []
|
||||||
|
for module in modules:
|
||||||
|
for _, obj in inspect.getmembers(module, inspect.isclass):
|
||||||
|
if obj.__module__ != module.__name__:
|
||||||
|
continue
|
||||||
|
for name in dir(obj):
|
||||||
|
lowered = name.lower()
|
||||||
|
if any(fragment in lowered for fragment in VERDICT_NAME_FRAGMENTS):
|
||||||
|
offenders.append(f"{module.__name__}.{obj.__name__}.{name}")
|
||||||
|
assert not offenders, f"approval-validity surface appeared: {offenders}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_the_prohibition_is_declared_where_a_reader_would_look():
|
||||||
|
"""Shipped behaviour above is only half of §6.4 obligation 3."""
|
||||||
|
layer = (REPO / "layer.yaml").read_text()
|
||||||
|
assert "approval_validity_query: forbidden" in layer
|
||||||
|
|
||||||
|
intent = (REPO / "INTENT.md").read_text()
|
||||||
|
assert "no approval-validity query" in intent.lower()
|
||||||
|
|
||||||
|
scope = (REPO / "SCOPE.md").read_text()
|
||||||
|
assert "approval-validity query" in scope.lower()
|
||||||
|
|
||||||
|
|
||||||
|
def test_recording_an_approval_event_is_still_allowed(app):
|
||||||
|
"""The prohibition bounds the verdict, not the record. Guards over-reading.
|
||||||
|
|
||||||
|
Emitted here from the one registered sender: `approval-engine` is not yet
|
||||||
|
admitted as a source (AUDIT-WP-0009-T09), and this test asserts that an
|
||||||
|
approval-shaped *event class* is ingestible, not that its future sender is
|
||||||
|
already registered. A 400 `source_not_allowed` is sender admission working,
|
||||||
|
which is a different control.
|
||||||
|
"""
|
||||||
|
from test_ingestion import event
|
||||||
|
|
||||||
|
status, _ = invoke(
|
||||||
|
app,
|
||||||
|
event(id="evt-approval-1", type="approval.revoked"),
|
||||||
|
key="evt-approval-1",
|
||||||
|
)
|
||||||
|
assert status.startswith("202")
|
||||||
|
|
@ -169,7 +169,7 @@ than adding a new shape.
|
||||||
|
|
||||||
```task
|
```task
|
||||||
id: AUDIT-WP-0009-T08
|
id: AUDIT-WP-0009-T08
|
||||||
status: todo
|
status: done
|
||||||
priority: medium
|
priority: medium
|
||||||
state_hub_task_id: "f7b513ee-af48-5c5a-a34e-3e9922d29b76"
|
state_hub_task_id: "f7b513ee-af48-5c5a-a34e-3e9922d29b76"
|
||||||
```
|
```
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue