test(email): контактный адрес, Reply-To и тестовая отправка письма
Покрытие валидации/сохранения contact_email, простановки Reply-To в письмах регистрации/приглашений/саммари при включённом и выключенном контактном адресе, и эндпоинта тестовой отправки (успех, дефолтный получатель, сбой транспорта без утечки логина/пароля).
This commit is contained in:
@@ -24,6 +24,7 @@ from models.team import Team
|
||||
from models.user import User
|
||||
from services import ai_levels
|
||||
from services.conference_ids import generate_number, generate_slug
|
||||
from services.email import EmailSendError
|
||||
|
||||
FUTURE = datetime.now(UTC) + timedelta(days=3)
|
||||
|
||||
@@ -85,6 +86,7 @@ async def test_all_admin_endpoints_forbidden_for_non_admin(
|
||||
("PATCH", f"/api/v1/admin/users/{other_user.id}", {"role": "admin"}),
|
||||
("GET", "/api/v1/admin/settings", None),
|
||||
("PUT", "/api/v1/admin/settings", {}),
|
||||
("POST", "/api/v1/admin/settings/test-email", {}),
|
||||
]
|
||||
for method, path, body in requests:
|
||||
response = await client.request(method, path, json=body, headers=headers)
|
||||
@@ -606,3 +608,153 @@ async def test_put_settings_unavailable_ai_level_returns_400(
|
||||
headers=_auth_headers(admin),
|
||||
)
|
||||
assert response.status_code == 400
|
||||
|
||||
|
||||
async def test_put_settings_contact_email_enable_and_persist(
|
||||
client: httpx.AsyncClient, db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
monkeypatch.setattr(admin_module, "transcription_queue_served", lambda: False)
|
||||
admin = await _make_user(db_session, role="admin")
|
||||
await db_session.commit()
|
||||
|
||||
response = await client.put(
|
||||
"/api/v1/admin/settings",
|
||||
json={"contact_email_enabled": True, "contact_email": "Contact@VidConf.RU"},
|
||||
headers=_auth_headers(admin),
|
||||
)
|
||||
assert response.status_code == 200, response.text
|
||||
body = response.json()
|
||||
assert body["contact_email_enabled"] is True
|
||||
assert body["contact_email"] == "contact@vidconf.ru"
|
||||
|
||||
reloaded = await client.get("/api/v1/admin/settings", headers=_auth_headers(admin))
|
||||
assert reloaded.json()["contact_email"] == "contact@vidconf.ru"
|
||||
|
||||
|
||||
async def test_put_settings_contact_email_invalid_returns_400(
|
||||
client: httpx.AsyncClient, db_session: AsyncSession
|
||||
) -> None:
|
||||
admin = await _make_user(db_session, role="admin")
|
||||
await db_session.commit()
|
||||
|
||||
response = await client.put(
|
||||
"/api/v1/admin/settings",
|
||||
json={"contact_email_enabled": True, "contact_email": "not an email"},
|
||||
headers=_auth_headers(admin),
|
||||
)
|
||||
assert response.status_code == 400
|
||||
|
||||
|
||||
# --- Тестовое письмо ----------------------------------------------------------------
|
||||
|
||||
|
||||
async def test_send_test_email_defaults_to_admin_email(
|
||||
client: httpx.AsyncClient, db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""Без явного `to` тестовое письмо уходит на email текущего администратора."""
|
||||
admin = await _make_user(db_session, role="admin")
|
||||
await db_session.commit()
|
||||
|
||||
sent: dict[str, object] = {}
|
||||
|
||||
class _FakeBackend:
|
||||
async def send(self, **kwargs: object) -> None:
|
||||
sent.update(kwargs)
|
||||
|
||||
monkeypatch.setattr(admin_module, "create_email_backend", lambda settings: _FakeBackend())
|
||||
|
||||
response = await client.post(
|
||||
"/api/v1/admin/settings/test-email", json={}, headers=_auth_headers(admin)
|
||||
)
|
||||
|
||||
assert response.status_code == 200, response.text
|
||||
body = response.json()
|
||||
assert body["success"] is True
|
||||
assert sent["to"] == admin.email
|
||||
|
||||
|
||||
async def test_send_test_email_to_explicit_recipient(
|
||||
client: httpx.AsyncClient, db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
admin = await _make_user(db_session, role="admin")
|
||||
await db_session.commit()
|
||||
|
||||
sent: dict[str, object] = {}
|
||||
|
||||
class _FakeBackend:
|
||||
async def send(self, **kwargs: object) -> None:
|
||||
sent.update(kwargs)
|
||||
|
||||
monkeypatch.setattr(admin_module, "create_email_backend", lambda settings: _FakeBackend())
|
||||
|
||||
response = await client.post(
|
||||
"/api/v1/admin/settings/test-email",
|
||||
json={"to": "other@example.com"},
|
||||
headers=_auth_headers(admin),
|
||||
)
|
||||
|
||||
assert response.status_code == 200, response.text
|
||||
assert sent["to"] == "other@example.com"
|
||||
|
||||
|
||||
async def test_send_test_email_uses_contact_email_as_reply_to(
|
||||
client: httpx.AsyncClient, db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
admin = await _make_user(db_session, role="admin")
|
||||
await db_session.commit()
|
||||
await client.put(
|
||||
"/api/v1/admin/settings",
|
||||
json={"contact_email_enabled": True, "contact_email": "contact@vidconf.example"},
|
||||
headers=_auth_headers(admin),
|
||||
)
|
||||
|
||||
sent: dict[str, object] = {}
|
||||
|
||||
class _FakeBackend:
|
||||
async def send(self, **kwargs: object) -> None:
|
||||
sent.update(kwargs)
|
||||
|
||||
monkeypatch.setattr(admin_module, "create_email_backend", lambda settings: _FakeBackend())
|
||||
|
||||
response = await client.post(
|
||||
"/api/v1/admin/settings/test-email", json={}, headers=_auth_headers(admin)
|
||||
)
|
||||
|
||||
assert response.status_code == 200, response.text
|
||||
assert sent["reply_to"] == "contact@vidconf.example"
|
||||
|
||||
|
||||
async def test_send_test_email_reports_transport_failure_without_leaking_secrets(
|
||||
client: httpx.AsyncClient, db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""Сбой транспорта — понятный текст ошибки; хост/порт видны, логин/пароль — нет."""
|
||||
monkeypatch.setenv("EMAIL_BACKEND", "smtp")
|
||||
monkeypatch.setenv("SMTP_HOST", "smtp.example.com")
|
||||
monkeypatch.setenv("SMTP_PORT", "2525")
|
||||
monkeypatch.setenv("SMTP_USERNAME", "secret-user")
|
||||
monkeypatch.setenv("SMTP_PASSWORD", "super-secret-password")
|
||||
get_settings.cache_clear()
|
||||
|
||||
class _FailingBackend:
|
||||
async def send(self, **kwargs: object) -> None:
|
||||
raise EmailSendError("временный сбой SMTP: сервер недоступен", retryable=True)
|
||||
|
||||
monkeypatch.setattr(admin_module, "create_email_backend", lambda settings: _FailingBackend())
|
||||
try:
|
||||
admin = await _make_user(db_session, role="admin")
|
||||
await db_session.commit()
|
||||
|
||||
response = await client.post(
|
||||
"/api/v1/admin/settings/test-email", json={}, headers=_auth_headers(admin)
|
||||
)
|
||||
|
||||
assert response.status_code == 200, response.text
|
||||
body = response.json()
|
||||
assert body["success"] is False
|
||||
assert "сбой SMTP" in body["message"]
|
||||
assert body["smtp_host"] == "smtp.example.com"
|
||||
assert body["smtp_port"] == 2525
|
||||
assert "secret-user" not in response.text
|
||||
assert "super-secret-password" not in response.text
|
||||
finally:
|
||||
get_settings.cache_clear()
|
||||
|
||||
@@ -48,6 +48,7 @@ class _CapturingEmailBackend:
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.sent: list[tuple[str, str, str]] = []
|
||||
self.reply_to: list[str | None] = []
|
||||
|
||||
async def send(
|
||||
self,
|
||||
@@ -57,8 +58,10 @@ class _CapturingEmailBackend:
|
||||
body: str,
|
||||
html_body: str | None = None,
|
||||
attachments: Sequence[EmailAttachment] = (),
|
||||
reply_to: str | None = None,
|
||||
) -> None:
|
||||
self.sent.append((to, subject, body))
|
||||
self.reply_to.append(reply_to)
|
||||
|
||||
|
||||
def _extract_verification_token(email_body: str) -> str:
|
||||
@@ -432,3 +435,34 @@ async def test_register_any_domain_allowed_when_verification_disabled(
|
||||
},
|
||||
)
|
||||
assert response.status_code == 201, response.text
|
||||
|
||||
|
||||
async def test_register_sets_reply_to_when_contact_email_enabled(
|
||||
client: httpx.AsyncClient, db_session: AsyncSession, email_backend: _CapturingEmailBackend
|
||||
) -> None:
|
||||
"""Контактный адрес инстанса включён — письмо подтверждения регистрации несёт `Reply-To`."""
|
||||
await InstanceSettingsService(db_session).update(
|
||||
SettingsUpdateIn(contact_email_enabled=True, contact_email="contact@vidconf.example")
|
||||
)
|
||||
await db_session.commit()
|
||||
|
||||
response = await client.post(
|
||||
"/api/v1/auth/register",
|
||||
json={"email": "dave@example.com", "name_user": "Dave", "password": "supersecret1"},
|
||||
)
|
||||
|
||||
assert response.status_code == 201, response.text
|
||||
assert email_backend.reply_to[-1] == "contact@vidconf.example"
|
||||
|
||||
|
||||
async def test_register_no_reply_to_when_contact_email_disabled(
|
||||
client: httpx.AsyncClient, email_backend: _CapturingEmailBackend
|
||||
) -> None:
|
||||
"""Контактный адрес выключен (дефолт) — `Reply-To` не проставляется."""
|
||||
response = await client.post(
|
||||
"/api/v1/auth/register",
|
||||
json={"email": "erin@example.com", "name_user": "Erin", "password": "supersecret1"},
|
||||
)
|
||||
|
||||
assert response.status_code == 201, response.text
|
||||
assert email_backend.reply_to[-1] is None
|
||||
|
||||
@@ -48,6 +48,7 @@ from services.instance_settings import (
|
||||
BootstrapOverrides,
|
||||
InstanceSettingsService,
|
||||
InvalidAiLevelError,
|
||||
InvalidContactEmailError,
|
||||
InvalidEmailDomainError,
|
||||
InvalidTimezoneError,
|
||||
SettingsUpdateIn,
|
||||
@@ -69,6 +70,7 @@ _MANAGED_KEYS = (
|
||||
"display_timezone",
|
||||
"registration_team_choice",
|
||||
"registration_email_domain",
|
||||
"contact_email",
|
||||
)
|
||||
|
||||
|
||||
@@ -123,6 +125,7 @@ async def test_ensure_bootstrapped_imports_yaml_defaults(
|
||||
"display_timezone",
|
||||
"registration_team_choice",
|
||||
"registration_email_domain",
|
||||
"contact_email",
|
||||
}
|
||||
cfg = await service.get()
|
||||
assert cfg.transcriber.provider == "faster_whisper_cpu"
|
||||
@@ -132,6 +135,8 @@ async def test_ensure_bootstrapped_imports_yaml_defaults(
|
||||
assert cfg.registration_team_choice is False
|
||||
assert cfg.registration_email_domain_enabled is False
|
||||
assert cfg.registration_email_domain is None
|
||||
assert cfg.contact_email_enabled is False
|
||||
assert cfg.contact_email is None
|
||||
|
||||
|
||||
async def test_ensure_bootstrapped_is_idempotent_and_keeps_admin_edits(
|
||||
@@ -384,6 +389,68 @@ async def test_registration_email_domain_can_be_disabled_keeping_stored_domain(
|
||||
assert cfg.registration_email_domain == "acme.io"
|
||||
|
||||
|
||||
async def test_contact_email_enable_without_email_rejected(
|
||||
db_session: AsyncSession, clean_instance_settings: None
|
||||
) -> None:
|
||||
"""Включение контактного адреса без email (ни в патче, ни ранее сохранённого) → 400."""
|
||||
service = InstanceSettingsService(db_session)
|
||||
await service.ensure_bootstrapped(PLUGINS_YAML)
|
||||
|
||||
with pytest.raises(InvalidContactEmailError):
|
||||
await service.update(SettingsUpdateIn(contact_email_enabled=True))
|
||||
|
||||
cfg = await service.get()
|
||||
assert cfg.contact_email_enabled is False
|
||||
assert cfg.contact_email is None
|
||||
|
||||
|
||||
async def test_contact_email_rejects_invalid_format(
|
||||
db_session: AsyncSession, clean_instance_settings: None
|
||||
) -> None:
|
||||
service = InstanceSettingsService(db_session)
|
||||
await service.ensure_bootstrapped(PLUGINS_YAML)
|
||||
|
||||
with pytest.raises(InvalidContactEmailError):
|
||||
await service.update(SettingsUpdateIn(contact_email="not an email"))
|
||||
|
||||
cfg = await service.get()
|
||||
assert cfg.contact_email is None
|
||||
|
||||
|
||||
async def test_contact_email_normalizes_input(
|
||||
db_session: AsyncSession, clean_instance_settings: None
|
||||
) -> None:
|
||||
"""` Contact@VidConf.RU ` нормализуется в `contact@vidconf.ru` (strip, lower)."""
|
||||
service = InstanceSettingsService(db_session)
|
||||
await service.ensure_bootstrapped(PLUGINS_YAML)
|
||||
|
||||
cfg = await service.update(
|
||||
SettingsUpdateIn(contact_email_enabled=True, contact_email=" Contact@VidConf.RU ")
|
||||
)
|
||||
|
||||
assert cfg.contact_email_enabled is True
|
||||
assert cfg.contact_email == "contact@vidconf.ru"
|
||||
|
||||
reloaded = await service.get()
|
||||
assert reloaded.contact_email == "contact@vidconf.ru"
|
||||
|
||||
|
||||
async def test_contact_email_can_be_disabled_keeping_stored_email(
|
||||
db_session: AsyncSession, clean_instance_settings: None
|
||||
) -> None:
|
||||
"""Выключение контактного адреса без передачи email не требует его и не роняет валидацию."""
|
||||
service = InstanceSettingsService(db_session)
|
||||
await service.ensure_bootstrapped(PLUGINS_YAML)
|
||||
await service.update(
|
||||
SettingsUpdateIn(contact_email_enabled=True, contact_email="contact@acme.io")
|
||||
)
|
||||
|
||||
cfg = await service.update(SettingsUpdateIn(contact_email_enabled=False))
|
||||
|
||||
assert cfg.contact_email_enabled is False
|
||||
assert cfg.contact_email == "contact@acme.io"
|
||||
|
||||
|
||||
async def test_transcription_enabled_flag_toggles_both_transcriber_and_summarizer(
|
||||
db_session: AsyncSession, clean_instance_settings: None
|
||||
) -> None:
|
||||
|
||||
@@ -19,6 +19,7 @@ from celery.exceptions import MaxRetriesExceededError
|
||||
from sqlalchemy import text
|
||||
|
||||
from core.db import engine
|
||||
from core.plugins.config import ChatConfig, InstanceConfig, SummarizerConfig, TranscriberConfig
|
||||
from services.conference_ids import generate_number, generate_slug
|
||||
from services.email import EmailAttachment, EmailSendError
|
||||
from workers.tasks import invitations as invitations_module
|
||||
@@ -65,10 +66,13 @@ class _FakeEmailBackend:
|
||||
body: str,
|
||||
html_body: str | None = None,
|
||||
attachments: tuple[EmailAttachment, ...] = (),
|
||||
reply_to: str | None = None,
|
||||
) -> None:
|
||||
if to in self._fail_for:
|
||||
raise EmailSendError(f"сбой отправки {to}", retryable=self._retryable)
|
||||
self.sent.append({"to": to, "subject": subject, "attachments": attachments})
|
||||
self.sent.append(
|
||||
{"to": to, "subject": subject, "attachments": attachments, "reply_to": reply_to}
|
||||
)
|
||||
|
||||
|
||||
class _Fixture:
|
||||
@@ -364,6 +368,48 @@ async def test_pinned_conference_sends_to_owner_and_past_participants_with_email
|
||||
assert delivered == sent_to
|
||||
|
||||
|
||||
def _cfg(
|
||||
*, contact_email_enabled: bool = False, contact_email: str | None = None
|
||||
) -> InstanceConfig:
|
||||
return InstanceConfig(
|
||||
transcriber=TranscriberConfig(),
|
||||
summarizer=SummarizerConfig(),
|
||||
chat=ChatConfig(),
|
||||
contact_email_enabled=contact_email_enabled,
|
||||
contact_email=contact_email,
|
||||
)
|
||||
|
||||
|
||||
async def test_invitations_set_reply_to_when_contact_email_enabled(
|
||||
fx: _Fixture, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""Включённый контактный адрес инстанса — приглашение несёт `Reply-To`."""
|
||||
mock_backend = _FakeEmailBackend()
|
||||
monkeypatch.setattr(invitations_module, "create_email_backend", lambda settings: mock_backend)
|
||||
|
||||
await send_invitations_async(
|
||||
_FakeTask(),
|
||||
fx.conference_id,
|
||||
plugins_config=_cfg(contact_email_enabled=True, contact_email="contact@vidconf.example"),
|
||||
)
|
||||
|
||||
assert mock_backend.sent
|
||||
assert all(call["reply_to"] == "contact@vidconf.example" for call in mock_backend.sent)
|
||||
|
||||
|
||||
async def test_invitations_no_reply_to_when_contact_email_disabled(
|
||||
fx: _Fixture, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""Контактный адрес выключен (дефолт) — `Reply-To` не проставляется."""
|
||||
mock_backend = _FakeEmailBackend()
|
||||
monkeypatch.setattr(invitations_module, "create_email_backend", lambda settings: mock_backend)
|
||||
|
||||
await send_invitations_async(_FakeTask(), fx.conference_id, plugins_config=_cfg())
|
||||
|
||||
assert mock_backend.sent
|
||||
assert all(call["reply_to"] is None for call in mock_backend.sent)
|
||||
|
||||
|
||||
async def test_explicit_emails_override_default_recipients(
|
||||
fx: _Fixture, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
|
||||
@@ -60,6 +60,7 @@ class _FakeEmailBackend:
|
||||
|
||||
def __init__(self, *, fail_for: set[str] | None = None, retryable: bool = True) -> None:
|
||||
self.sent: list[str] = []
|
||||
self.reply_to: list[str | None] = []
|
||||
self._fail_for = fail_for or set()
|
||||
self._retryable = retryable
|
||||
|
||||
@@ -71,10 +72,12 @@ class _FakeEmailBackend:
|
||||
body: str,
|
||||
html_body: str | None = None,
|
||||
attachments: Any = (),
|
||||
reply_to: str | None = None,
|
||||
) -> None:
|
||||
if to in self._fail_for:
|
||||
raise EmailSendError(f"сбой отправки для {to}", retryable=self._retryable)
|
||||
self.sent.append(to)
|
||||
self.reply_to.append(reply_to)
|
||||
|
||||
|
||||
class _Fixture:
|
||||
@@ -221,12 +224,19 @@ async def fx() -> AsyncGenerator[_Fixture, None]:
|
||||
await conn.commit()
|
||||
|
||||
|
||||
def _cfg(*, summary_recipients: str = "all") -> InstanceConfig:
|
||||
def _cfg(
|
||||
*,
|
||||
summary_recipients: str = "all",
|
||||
contact_email_enabled: bool = False,
|
||||
contact_email: str | None = None,
|
||||
) -> InstanceConfig:
|
||||
return InstanceConfig(
|
||||
transcriber=TranscriberConfig(),
|
||||
summarizer=SummarizerConfig(),
|
||||
chat=ChatConfig(),
|
||||
summary_recipients=cast("Any", summary_recipients),
|
||||
contact_email_enabled=contact_email_enabled,
|
||||
contact_email=contact_email,
|
||||
)
|
||||
|
||||
|
||||
@@ -291,6 +301,40 @@ async def test_notify_session_mode_owner_sends_only_to_owner(
|
||||
assert await _fetch_session_status(fx.session_id) == "notified"
|
||||
|
||||
|
||||
async def test_notify_session_sets_reply_to_when_contact_email_enabled(
|
||||
fx: _Fixture, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""Включённый контактный адрес инстанса — саммари-письмо несёт `Reply-To`."""
|
||||
backend = _FakeEmailBackend()
|
||||
monkeypatch.setattr(notify_module, "create_email_backend", lambda settings: backend)
|
||||
|
||||
await notify_session_async(
|
||||
_FakeTask(),
|
||||
fx.session_id,
|
||||
plugins_config=_cfg(
|
||||
summary_recipients="owner",
|
||||
contact_email_enabled=True,
|
||||
contact_email="contact@vidconf.example",
|
||||
),
|
||||
)
|
||||
|
||||
assert backend.reply_to == ["contact@vidconf.example"]
|
||||
|
||||
|
||||
async def test_notify_session_no_reply_to_when_contact_email_disabled(
|
||||
fx: _Fixture, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""Контактный адрес выключен (дефолт) — `Reply-To` не проставляется."""
|
||||
backend = _FakeEmailBackend()
|
||||
monkeypatch.setattr(notify_module, "create_email_backend", lambda settings: backend)
|
||||
|
||||
await notify_session_async(
|
||||
_FakeTask(), fx.session_id, plugins_config=_cfg(summary_recipients="owner")
|
||||
)
|
||||
|
||||
assert backend.reply_to == [None]
|
||||
|
||||
|
||||
async def test_notify_session_conference_override_wins_over_instance_default(
|
||||
fx: _Fixture, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
"""
|
||||
|
||||
import logging
|
||||
from email.message import EmailMessage
|
||||
from typing import cast
|
||||
|
||||
import aiosmtplib
|
||||
import pytest
|
||||
@@ -90,6 +92,33 @@ def test_build_message_includes_html_alternative_and_ics_attachment() -> None:
|
||||
assert attachments[0].get_payload(decode=True) == b"BEGIN:VCALENDAR\r\nEND:VCALENDAR\r\n"
|
||||
|
||||
|
||||
def test_build_message_sets_reply_to_when_given() -> None:
|
||||
message = _build_message(
|
||||
sender="VidConf <no-reply@vidconf.example>",
|
||||
to="user@example.com",
|
||||
subject="Тема",
|
||||
body="Тело",
|
||||
html_body=None,
|
||||
attachments=(),
|
||||
reply_to="contact@vidconf.example",
|
||||
)
|
||||
|
||||
assert message["Reply-To"] == "contact@vidconf.example"
|
||||
|
||||
|
||||
def test_build_message_omits_reply_to_when_not_given() -> None:
|
||||
message = _build_message(
|
||||
sender="VidConf <no-reply@vidconf.example>",
|
||||
to="user@example.com",
|
||||
subject="Тема",
|
||||
body="Тело",
|
||||
html_body=None,
|
||||
attachments=(),
|
||||
)
|
||||
|
||||
assert message["Reply-To"] is None
|
||||
|
||||
|
||||
def _backend() -> SmtpEmailBackend:
|
||||
return SmtpEmailBackend(
|
||||
hostname="smtp.example.com",
|
||||
@@ -168,3 +197,23 @@ async def test_smtp_backend_send_success_calls_aiosmtplib(monkeypatch: pytest.Mo
|
||||
"use_tls": False,
|
||||
"timeout": 30,
|
||||
}
|
||||
|
||||
|
||||
async def test_smtp_backend_send_passes_reply_to_into_message(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
captured: dict[str, object] = {}
|
||||
|
||||
async def _fake_send(message: object, **kwargs: object) -> tuple[dict[str, object], str]:
|
||||
captured["message"] = message
|
||||
return {}, "OK"
|
||||
|
||||
monkeypatch.setattr(aiosmtplib, "send", _fake_send)
|
||||
backend = _backend()
|
||||
|
||||
await backend.send(
|
||||
to="user@example.com", subject="Тема", body="Тело", reply_to="contact@vidconf.example"
|
||||
)
|
||||
|
||||
message = cast(EmailMessage, captured["message"])
|
||||
assert message["Reply-To"] == "contact@vidconf.example"
|
||||
|
||||
Reference in New Issue
Block a user