migrations/0004_breach_records.sql models a case's lifecycle as
append-only events (alleged/cured/determined/terminated) grouped by
case_id rather than one mutable row - resolution is always a new,
later event, never an edit. A CHECK constraint makes the
anonymized-default rule (License V1C1 §7.4) a database fact:
named_entitlement_holder can be set if and only if anonymized = false.
src/target_revenue/breach_record.py's publish_breach_event() enforces
per-Licensor phase ownership and rejects named-disclosure requests
that don't also set named_disclosure_authorized_under_cua: true - the
Trust Service records the Licensor's assertion that the CUA's naming
clause authorizes it, it never verifies the underlying CUA text
itself. Signs every event with the same instance Ed25519 key already
used for Ledger entries and Attestations.
Adds POST/GET /phases/{id}/breach-records. Guarded the .registry
import behind a lazy in-function import (matching attestation.py's
TYPE_CHECKING pattern) so tests/test_breach_record.py (7 tests) runs
under plain system Python with no psycopg dependency. 5 new
Docker-gated tests cover the default-anonymized lifecycle, the
named-disclosure authorization gate, cross-Licensor rejection,
signature verification, and DB-level UPDATE/DELETE rejection.
This closes WP-0006 again - all 9 tasks done.
69 lines
2.2 KiB
Python
69 lines
2.2 KiB
Python
"""Pure, offline tests for target_revenue.breach_record._validate
|
|
(WP-0006-T09). No Docker/Postgres required — persistence/publication
|
|
round-trips are covered by the Docker-gated tests in
|
|
tests/test_ledger_hosting.py.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from target_revenue import breach_record
|
|
|
|
|
|
def _base_event(**overrides):
|
|
event = {
|
|
"id": "trsl:breach:example0010001",
|
|
"case_id": "trsl:case:example001",
|
|
"event_type": "alleged",
|
|
"category": "unauthorized-commercial-use",
|
|
"event_at": "2026-08-01T00:00:00Z",
|
|
}
|
|
event.update(overrides)
|
|
return event
|
|
|
|
|
|
def test_valid_anonymized_alleged_event_has_no_errors():
|
|
assert breach_record._validate(_base_event()) == []
|
|
|
|
|
|
def test_missing_required_field_rejected():
|
|
event = _base_event()
|
|
del event["category"]
|
|
errors = breach_record._validate(event)
|
|
assert any("category" in e for e in errors)
|
|
|
|
|
|
def test_unknown_event_type_rejected():
|
|
errors = breach_record._validate(_base_event(event_type="dismissed"))
|
|
assert any("event_type" in e for e in errors)
|
|
|
|
|
|
def test_named_disclosure_requires_holder_and_authorization():
|
|
errors = breach_record._validate(_base_event(anonymized=False))
|
|
assert any("named_entitlement_holder" in e for e in errors)
|
|
assert any("named_disclosure_authorized_under_cua" in e for e in errors)
|
|
|
|
|
|
def test_named_disclosure_with_holder_but_no_authorization_still_rejected():
|
|
errors = breach_record._validate(
|
|
_base_event(anonymized=False, named_entitlement_holder="Acme Corp")
|
|
)
|
|
assert any("named_disclosure_authorized_under_cua" in e for e in errors)
|
|
|
|
|
|
def test_named_disclosure_with_holder_and_authorization_accepted():
|
|
errors = breach_record._validate(
|
|
_base_event(
|
|
anonymized=False,
|
|
named_entitlement_holder="Acme Corp",
|
|
named_disclosure_authorized_under_cua=True,
|
|
)
|
|
)
|
|
assert errors == []
|
|
|
|
|
|
def test_anonymized_event_with_holder_set_rejected():
|
|
"""A caller must not be able to sneak a name in while anonymized=True."""
|
|
errors = breach_record._validate(
|
|
_base_event(anonymized=True, named_entitlement_holder="Acme Corp")
|
|
)
|
|
assert any("must not be set" in e for e in errors)
|