← Notifications

Тест-кейсы

notifications · workzone
Принятые решения
Конфиг-матрица здесь не дублируется Каналы, маршруты, дедуп серии и засев builtin проверяются со стороны Admin (admin-panel/test_notifications.py): delete-builtin → reject, залоченный маршрут (CHECK (enabled OR NOT locked)), in_app не выключить. Здесь — слой диспетчера и доставки: идемпотентность, расщепление FK, две модели адресации.
FK ondelete расщеплён намеренно Аудит доставки переживает удаление канала (channel_idSET NULL, строка цела), но уходит за уведомлением и пользователем (CASCADE). Легко перепутать — держим отдельным тестом с явным противопоставлением.
Стек и инфраструктура
Имеется done pytest, pytest-asyncio, pytest-cov
Тест-БД testcontainers (PostgreSQL) — реальные CHECK / FK / триггеры; per-test rollback
Фабрики factory_boy — уведомление, канал, доставка; builtin-каналы из миграции
Маркеры @pytest.mark.integration — весь слой доставки идёт против реальной БД
Integration · P0 Идемпотентность доставки и расщепление FK
test_delivery_idempotency.py
notification_deliveries — ключ идемпотентности
Кейсы
UNIQUE(notification, channel, user)повтор диспетчера / ретрай SAQ по тому же адресату и каналу → одна строка, апдейт, не дубль
partial UNIQUE WHERE user_id IS NULLwebhook-broadcast (user_id = NULL) идемпотентен без пользователя — UNIQUE(notification, channel) держит одну строку
два канала — две строкиодно уведомление на in_app и email → две строки доставки (разный channel_id), не конфликт
два адресата — две строкиbroadcast одному каналу двум людям → две строки (разный user_id), уникальность per-user
test_delivery_fk.py
Расщепление ondelete — SET NULL (аудит) vs CASCADE (жизненный цикл)
Кейсы
DELETE канал → SET NULLудалён webhook-канал → deliveries.channel_id = NULL, строка-аудит цела, факт доставки сохранён
один DELETE канала — два исходаудаление notification_channels: его notification_routes уходят каскадом (channel_idCASCADE, маршрут — конфиг), а deliveries.channel_idNULL (SET NULL, аудит цел); тот же родитель, разный ondelete у двух детей → Каналы и маршруты
DELETE уведомление → CASCADEудалено notifications → его строки доставки уходят каскадом
DELETE пользователь → CASCADEудалён users → его личные строки доставки уходят каскадом
противопоставление в одном прогонеудаление канала оставляет строку (аудит), удаление события / юзера — сносит (жизненный цикл); разный ondelete у трёх FK одной таблицы
Integration · P1 Адресация, личное сужение, машина состояния
test_addressing.py
notifications — две модели адресации через target_user_id
Кейсы
broadcast — target_user_id NULLадресаты — срез по роли (owner / admin, status=active), веер вычисляется запросом, не колонкой
targeted — target_user_id = Nagent / account адресованы одному пользователю; FK → users, ON DELETE CASCADE
DELETE юзер → каскадудалён адресат → его targeted-уведомления и их доставки уходят каскадом; broadcast-факты целы
severity CHECKinfo / warning / critical — вне словаря → отказ БД; ортогонален event_type
event_type CHECKsync · security · budget · system · discovery · agent · account; чужое значение → отказ БД
test_prefs.py
notification_prefs — личное сужение по типу
Кейсы
UNIQUE(user, event_type)вторая настройка того же типа для юзера → конфликт, дубль отбит
user_id FK CASCADEудалён users → его строки настроек уходят каскадом
строки нет → дефолт каталогаэффективная настройка = дефолт каталога типа (личные agent · account: email opt-in false), НЕ глобальный «оба включены»
email_enabled без колоночного DEFAULTу колонки нет server_default; значение при INSERT пишет код из каталога типа — платформенные true, личные false
test_delivery_state.py
notification_deliveries — машина состояния
Кейсы
state CHECKqueued / sent / failed / read; чужое значение → отказ БД
read только у in_appread допустим лишь для in_app-доставки; email / webhook в read не переходят
триггер set_updated_atсмена state двигает updated_at строго вперёд (новое > прежнего)
failed несёт errorпереход в failed сопровождается заполненным error
sent несёт sent_atsent проставляет sent_at; read проставляет read_at
API · P1 Личная лента — HTTP-контракт httpx · ASGI-приложение · → HTTP API · → Conformance
test_api_inbox.py
Чтение своей ленты, счётчик, пометка прочтения
Кейсы
только своя лентаGET → in_app-доставки вызывающего: targeted ему + broadcast под его роль; чужие targeted в выдачу не попадают
счётчик непрочитанногоGET unread → число in_app-доставок в состоянии не-read; после пометки уменьшается
пометка прочтенияпереход доставки в readread_at; идемпотентно (повтор — no-op); только для in_app
лента под единым контрактомпагинация / сортировка по свежести — общий list-контракт свода, не своя схема
чужое пометить нельзяпометка доставки другого пользователя → 404 (не раскрываем существование); аноним → 401
Structure Файловая структура тестов

Слой доставки целиком интеграционный — CHECK, FK и триггеры проверяются против реального PostgreSQL. Конфиг-матрица (каналы, маршруты, дедуп, засев builtin) живёт в admin-panel/tests.html и здесь не дублируется.

  • tests/notifications/каталог модуля
    • conftest.pytestcontainers PG, фабрики уведомление / канал / доставка
    • integration/против реальной БД
      • delivery-idempotency · delivery-fkключ идемпотентности, расщепление ondelete
      • addressing · prefs · delivery-stateдве модели адресации, личное сужение, машина состояния