Принятые решения
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 — сборка письма, языки, готовность
compose — шаблон + данные + язык → готовое письмо
Кейсы
invite тема — имя пригласившего + продукт; в теле роль, ссылка, «48 часов»
reset тема, тело, ссылка, текст «1 час»
test тема и тело подтверждения — без кнопки и без ссылки
multipart/alternative HTML-часть И текстовая часть — обе присутствуют
ссылка дважды кнопка-действие + та же ссылка обычным текстом ниже
текстовая часть — plain без HTML-тегов, ссылка читаема
From-заголовок from_address → display-name + адрес (RFC 5322)
missing var → reject нет данных под ключ → ошибка рендера, не пустая подстановка
HTML-escaping имя пригласившего с <, & экранируется в HTML-части (инъекция)
типизированные ключи неизвестный ключ → ошибка, не строка-заглушка в коде
Выбор языка письма RU / EN
Кейсы
reset → адресат язык из профиля получателя
test → адресат язык профиля админа
invite → орг-дефолт язык организации (приглашаемого ещё нет в системе)
язык без перевода неподдерживаемый → фолбэк на дефолт
паритет ключей RU и EN — один набор ключей, ни одного пропуска
Готовность транспорта — is_available() + секрет
Кейсы
available → true is_enabled + host + port + from_address заполнены
disabled → false is_enabled=false даже при заполненных полях
нет обязательного → false NULL в host / port / from_address — каждое
last_test_ok=false — совет НЕ влияет на is_available (не ворота)
last_test_ok=NULL «не проверяли» ≠ «сломано» — не блокирует
пароль наружу не уходит сериализация → только флаг «задан / не задан», без password_enc и открытого пароля
Integration · P0
Доставка — два режима
Inline-отправка — тестовое письмо, вердикт сразу
Кейсы
success send вызван, вердикт OK, last_test_ok=true + last_test_at
SMTP-сбой типизированная ошибка наверх, last_test_ok=false, last_test_at записан
таймаут вердикт fail в пределах таймаута, запрос не зависает
письмо собрано верно получатель = текущий админ, тема тестового, без ссылки
пароль расшифрован в send spy: crypto-ядро decrypt вызвано в момент отправки; открытый пароль ушёл в SMTP-auth, не в БД / лог
Очередь SAQ — обработчик задачи напрямую
Кейсы
обработчик: compose + send письмо собрано (адрес, тема, ссылка, язык), send вызван
enqueue → мгновенный ответ задача поставлена, потребитель отвечает сразу, не дожидаясь доставки
идемпотентность повторный enqueue с тем же job_id → одна задача, письмо не дублируется
потребитель не пакетит N писем = N задач; дробление на пачки — забота воркера
успешная доставка задача завершена, ретраи не запускались
Integration · P1
Ошибки доставки, анти-энумерация, темп
Таксономия ошибок — 4xx vs 5xx, ретраи, «не доставлено»
Кейсы
4xx / сеть → ретрай временный сбой → повтор с экспоненциальным backoff
5xx → без ретраев неверный адрес, ящик отклонил → сразу «не доставлено»
ретраи исчерпаны задача в «не доставлено», статус виден потребителю
статус инвайта провал доставки приглашения отражается на модели admission
backoff растёт задержка между попытками увеличивается, ≤ предел
Сброс пароля — сбой доставки не раскрывается
Кейсы
сбой → проглатывается провал доставки reset НЕ пробрасывается наверх
единый ответ исход неотличим от успеха — анти-энумерация (механика в Auth)
провал в лог и аудит сбой фиксируется внутри, наружу — ничего
Темп рассылки — ответственность воркера
Кейсы
ограничение скорости воркер держит заданный темп отправки (конфиг)
размер пачки за раз берётся не больше пачки (конфиг)
параметры — конфиг скорость, пачка, параллельность из настроек, не хардкод
залп задач много задач разом → воркер разгребает в темпе, не одним залпом
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сбои, безопасность, темп