Первоначальная версия VidConf
This commit is contained in:
170
backend/tests/test_smtp_backend.py
Normal file
170
backend/tests/test_smtp_backend.py
Normal file
@@ -0,0 +1,170 @@
|
||||
"""Тесты SMTP-бэкенда email (`services.email`).
|
||||
|
||||
Маппинг ошибок aiosmtplib в
|
||||
`EmailSendError(retryable=...)`, вложение `.ics` в MIME, console-бэкенд по
|
||||
умолчанию. `_env_file=None` в конструкторе `Settings` — тесты не должны
|
||||
зависеть от содержимого реального `backend/.env` (изолированный конфиг).
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
import aiosmtplib
|
||||
import pytest
|
||||
|
||||
from core.config import Settings
|
||||
from services.email import (
|
||||
ConsoleEmailBackend,
|
||||
EmailAttachment,
|
||||
EmailSendError,
|
||||
SmtpEmailBackend,
|
||||
_build_message,
|
||||
create_email_backend,
|
||||
)
|
||||
|
||||
|
||||
def test_create_email_backend_defaults_to_console() -> None:
|
||||
backend = create_email_backend(Settings(_env_file=None))
|
||||
|
||||
assert isinstance(backend, ConsoleEmailBackend)
|
||||
|
||||
|
||||
def test_create_email_backend_smtp() -> None:
|
||||
backend = create_email_backend(
|
||||
Settings(
|
||||
_env_file=None,
|
||||
email_backend="smtp",
|
||||
smtp_host="smtp.example.com",
|
||||
smtp_port=2525,
|
||||
)
|
||||
)
|
||||
|
||||
assert isinstance(backend, SmtpEmailBackend)
|
||||
|
||||
|
||||
async def test_console_backend_logs_message(caplog: pytest.LogCaptureFixture) -> None:
|
||||
backend = ConsoleEmailBackend()
|
||||
|
||||
with caplog.at_level(logging.INFO):
|
||||
await backend.send(
|
||||
to="user@example.com",
|
||||
subject="Тестовая тема",
|
||||
body="Тестовое тело письма",
|
||||
attachments=[
|
||||
EmailAttachment(filename="invite.ics", content=b"BEGIN", mime_type="text/calendar")
|
||||
],
|
||||
)
|
||||
|
||||
assert "user@example.com" in caplog.text
|
||||
assert "invite.ics" in caplog.text
|
||||
|
||||
|
||||
def test_build_message_includes_html_alternative_and_ics_attachment() -> None:
|
||||
message = _build_message(
|
||||
sender="VidConf <no-reply@vidconf.example>",
|
||||
to="user@example.com",
|
||||
subject="Саммари встречи",
|
||||
body="Текстовая версия",
|
||||
html_body="<p>HTML-версия</p>",
|
||||
attachments=[
|
||||
EmailAttachment(
|
||||
filename="invite.ics",
|
||||
content=b"BEGIN:VCALENDAR\r\nEND:VCALENDAR\r\n",
|
||||
mime_type="text/calendar; method=REQUEST",
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
assert message["Subject"] == "Саммари встречи"
|
||||
plain_part = message.get_body(preferencelist=("plain",))
|
||||
assert plain_part is not None
|
||||
assert "Текстовая версия" in plain_part.get_content()
|
||||
|
||||
html_part = message.get_body(preferencelist=("html",))
|
||||
assert html_part is not None
|
||||
assert "HTML-версия" in html_part.get_content()
|
||||
|
||||
attachments = list(message.iter_attachments())
|
||||
assert len(attachments) == 1
|
||||
assert attachments[0].get_filename() == "invite.ics"
|
||||
assert attachments[0].get_content_type() == "text/calendar"
|
||||
assert attachments[0].get_payload(decode=True) == b"BEGIN:VCALENDAR\r\nEND:VCALENDAR\r\n"
|
||||
|
||||
|
||||
def _backend() -> SmtpEmailBackend:
|
||||
return SmtpEmailBackend(
|
||||
hostname="smtp.example.com",
|
||||
port=587,
|
||||
username=None,
|
||||
password=None,
|
||||
start_tls=True,
|
||||
use_tls=False,
|
||||
timeout=30,
|
||||
sender="VidConf <no-reply@vidconf.example>",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"error",
|
||||
[
|
||||
aiosmtplib.SMTPConnectError("не удалось подключиться"),
|
||||
aiosmtplib.SMTPServerDisconnected("сервер оборвал соединение"),
|
||||
aiosmtplib.SMTPTimeoutError("таймаут"),
|
||||
aiosmtplib.SMTPAuthenticationError(535, "неверные учётные данные"),
|
||||
],
|
||||
)
|
||||
async def test_smtp_backend_maps_transport_errors_to_retryable(
|
||||
monkeypatch: pytest.MonkeyPatch, error: Exception
|
||||
) -> None:
|
||||
async def _raise(*args: object, **kwargs: object) -> None:
|
||||
raise error
|
||||
|
||||
monkeypatch.setattr(aiosmtplib, "send", _raise)
|
||||
backend = _backend()
|
||||
|
||||
with pytest.raises(EmailSendError) as excinfo:
|
||||
await backend.send(to="user@example.com", subject="Тема", body="Тело")
|
||||
|
||||
assert excinfo.value.retryable is True
|
||||
|
||||
|
||||
async def test_smtp_backend_maps_recipients_refused_to_non_retryable(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
refused = aiosmtplib.SMTPRecipientsRefused(
|
||||
[aiosmtplib.SMTPRecipientRefused(550, "мусор", "user@example.com")]
|
||||
)
|
||||
|
||||
async def _raise(*args: object, **kwargs: object) -> None:
|
||||
raise refused
|
||||
|
||||
monkeypatch.setattr(aiosmtplib, "send", _raise)
|
||||
backend = _backend()
|
||||
|
||||
with pytest.raises(EmailSendError) as excinfo:
|
||||
await backend.send(to="user@example.com", subject="Тема", body="Тело")
|
||||
|
||||
assert excinfo.value.retryable is False
|
||||
|
||||
|
||||
async def test_smtp_backend_send_success_calls_aiosmtplib(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
captured: dict[str, object] = {}
|
||||
|
||||
async def _fake_send(message: object, **kwargs: object) -> tuple[dict[str, object], str]:
|
||||
captured["message"] = message
|
||||
captured["kwargs"] = kwargs
|
||||
return {}, "OK"
|
||||
|
||||
monkeypatch.setattr(aiosmtplib, "send", _fake_send)
|
||||
backend = _backend()
|
||||
|
||||
await backend.send(to="user@example.com", subject="Тема", body="Тело")
|
||||
|
||||
assert captured["kwargs"] == {
|
||||
"hostname": "smtp.example.com",
|
||||
"port": 587,
|
||||
"username": None,
|
||||
"password": None,
|
||||
"start_tls": True,
|
||||
"use_tls": False,
|
||||
"timeout": 30,
|
||||
}
|
||||
Reference in New Issue
Block a user