admin-panel/test_notifications.py):
delete-builtin → reject, a locked route
(CHECK (enabled OR NOT locked)), in_app can’t be disabled.
Here it’s the dispatcher and delivery layer: idempotency,
the FK split, the two addressing models.
ondelete is split deliberately
The delivery audit survives channel deletion
(channel_id → SET NULL, row intact), but
goes with the notification and the user (CASCADE). Easy
to confuse — kept as a dedicated test with an explicit contrast.
@pytest.mark.integration — the whole delivery layer runs
against a real DB
user_id = NULL) is idempotent without a user — UNIQUE(notification, channel) holds one rowchannel_id), no conflictuser_id), uniqueness per-userdeliveries.channel_id = NULL, audit row intact, the delivery fact preservednotification_channels row: its notification_routes go by cascade (channel_id → CASCADE, a route is config), while deliveries.channel_id → NULL (SET NULL, audit intact); same parent, different ondelete on two children → Channels and routesnotifications row deleted → its delivery rows go by cascadeusers row deleted → their personal delivery rows go by cascadeondelete across three FKs of one tableowner / admin, status=active), the fan-out is computed by query, not a columnagent / account addressed to one user; FK → users, ON DELETE CASCADEinfo / warning / critical — outside the set → DB reject; orthogonal to event_typesync · security · budget · system · discovery · agent · account; any other value → DB rejectusers row deleted → their preference rows go by cascadeagent · account: email opt-in false), NOT a global “both on”true, personal falsequeued / sent / failed / read; any other value → DB rejectread is allowed only for in_app delivery; email / webhook don’t transition to readstate change moves updated_at strictly forward (new > previous)failed comes with a populated errorsent sets sent_at; read sets read_atread state; decreases after markingread → read_at; idempotent (a repeat is a no-op); in_app only404 (we don’t disclose existence); anonymous → 401
The delivery layer is fully integration — CHECK, FK and triggers are verified
against a real PostgreSQL. The config matrix (channels, routes, dedup,
builtin seed) lives in
admin-panel/tests.html and isn’t duplicated here.
tests/notifications/module catalog
conftest.pytestcontainers PG, factories notification / channel / deliveryintegration/against a real DB
delivery-idempotency · delivery-fkidempotency key, ondelete splitaddressing · prefs · delivery-statetwo addressing models, personal narrowing, state machine