Первоначальная версия VidConf

This commit is contained in:
2026-07-23 01:04:01 +03:00
commit 896455381a
335 changed files with 61527 additions and 0 deletions

View 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,
}