← Email

Тест-кейсы

email · workzone
Принятые решения
SMTP мокается Реальная отправка не происходит. Проверяем, что письмо собрано верно (адрес, тема, ссылка, язык, multipart/alternative) и что send вызван. Тот же подход уже применён в auth-тестах.
SAQ-задача — вызов обработчика напрямую Без поднятия воркера: тестируем логику задачи (compose + send, ретраи, разметка «не доставлено»), не инфраструктуру очереди.
Стек и инфраструктура
Имеется done pytest, pytest-asyncio, pytest-cov, httpx
Добавить testcontainers (PostgreSQL), мок SMTP (подмена aiosmtplib), time-machine (TTL ссылок и таймауты)
Тест-БД singleton smtp_settings — реальная строка в тест-БД, per-test rollback
Маркеры @pytest.mark.unit, @pytest.mark.integration — фильтрация по типу
Unit Без БД и SMTP — сборка письма, языки, готовность
test_compose.py
compose — шаблон + данные + язык → готовое письмо
Кейсы
inviteтема — имя пригласившего + продукт; в теле роль, ссылка, «48 часов»
resetтема, тело, ссылка, текст «1 час»
testтема и тело подтверждения — без кнопки и без ссылки
multipart/alternativeHTML-часть И текстовая часть — обе присутствуют
ссылка дваждыкнопка-действие + та же ссылка обычным текстом ниже
текстовая часть — plainбез HTML-тегов, ссылка читаема
From-заголовокfrom_address → display-name + адрес (RFC 5322)
missing var → rejectнет данных под ключ → ошибка рендера, не пустая подстановка
HTML-escapingимя пригласившего с <, & экранируется в HTML-части (инъекция)
типизированные ключинеизвестный ключ → ошибка, не строка-заглушка в коде
test_i18n.py
Выбор языка письма RU / EN
Кейсы
reset → адресатязык из профиля получателя
test → адресатязык профиля админа
invite → орг-дефолтязык организации (приглашаемого ещё нет в системе)
язык без переводанеподдерживаемый → фолбэк на дефолт
паритет ключейRU и EN — один набор ключей, ни одного пропуска
test_availability.py
Готовность транспорта — is_available() + секрет
Кейсы
available → trueis_enabled + host + port + from_address заполнены
disabled → falseis_enabled=false даже при заполненных полях
нет обязательного → falseNULL в host / port / from_address — каждое
last_test_ok=false — советНЕ влияет на is_available (не ворота)
last_test_ok=NULL«не проверяли» ≠ «сломано» — не блокирует
пароль наружу не уходитсериализация → только флаг «задан / не задан», без password_enc и открытого пароля
Integration · P0 Доставка — два режима
test_send_inline.py
Inline-отправка — тестовое письмо, вердикт сразу
Кейсы
successsend вызван, вердикт OK, last_test_ok=true + last_test_at
SMTP-сбойтипизированная ошибка наверх, last_test_ok=false, last_test_at записан
таймаутвердикт fail в пределах таймаута, запрос не зависает
письмо собрано вернополучатель = текущий админ, тема тестового, без ссылки
пароль расшифрован в sendspy: crypto-ядро decrypt вызвано в момент отправки; открытый пароль ушёл в SMTP-auth, не в БД / лог
test_queue_task.py
Очередь SAQ — обработчик задачи напрямую
Кейсы
обработчик: compose + sendписьмо собрано (адрес, тема, ссылка, язык), send вызван
enqueue → мгновенный ответзадача поставлена, потребитель отвечает сразу, не дожидаясь доставки
идемпотентностьповторный enqueue с тем же job_id → одна задача, письмо не дублируется
потребитель не пакетитN писем = N задач; дробление на пачки — забота воркера
успешная доставказадача завершена, ретраи не запускались
Integration · P1 Ошибки доставки, анти-энумерация, темп
test_delivery_errors.py
Таксономия ошибок — 4xx vs 5xx, ретраи, «не доставлено»
Кейсы
4xx / сеть → ретрайвременный сбой → повтор с экспоненциальным backoff
5xx → без ретраевневерный адрес, ящик отклонил → сразу «не доставлено»
ретраи исчерпанызадача в «не доставлено», статус виден потребителю
статус инвайтапровал доставки приглашения отражается на модели admission
backoff растётзадержка между попытками увеличивается, ≤ предел
test_reset_anti_enum.py
Сброс пароля — сбой доставки не раскрывается
Кейсы
сбой → проглатываетсяпровал доставки reset НЕ пробрасывается наверх
единый ответисход неотличим от успеха — анти-энумерация (механика в Auth)
провал в лог и аудитсбой фиксируется внутри, наружу — ничего
test_throttling.py
Темп рассылки — ответственность воркера
Кейсы
ограничение скоростиворкер держит заданный темп отправки (конфиг)
размер пачкиза раз берётся не больше пачки (конфиг)
параметры — конфигскорость, пачка, параллельность из настроек, не хардкод
залп задачмного задач разом → воркер разгребает в темпе, не одним залпом
v2 Отложенный охват
Security-алерты fan-out по получателям (Owner / Admin) тем же queue-путём, дедуп повторов, общий шаблонный layout
Structure Файловая структура тестов

Приоритет (P0–P1) ортогонален каталогам и задаётся маркерами (pytest -m p0), а не отдельными папками.

  • tests/email/каталог модуля
    • conftest.pyмок SMTP, singleton smtp_settings, фабрики
    • unit/без БД и SMTP
      • compose · i18n · availabilityсборка письма, языки, предикат
    • integration/с БД и мок-SMTP
      • send-inline · queue-taskдва режима доставки
      • delivery-errors · reset-anti-enum · throttlingсбои, безопасность, темп