Files
g2s-aggregator/tests/services/test_waybill_email_sender.py
T
Раис Юсупалиев 5e750ca71c
Deploy / deploy (push) Failing after 59s
Рефактор
2026-06-26 20:03:05 +03:00

241 lines
7.2 KiB
Python

import asyncio
from dataclasses import dataclass
from datetime import datetime, timezone
from typing import Any
from app.services.waybill_email_sender import WaybillEmailSenderService
@dataclass
class StoredOrder:
order_uuid: str
account_email: str
provider_waybill_url: str | None = None
waybill_email_sent_at: datetime | None = None
class StubSessionContext:
def __init__(self, session: object) -> None:
self._session = session
async def __aenter__(self) -> object:
return self._session
async def __aexit__(self, exc_type, exc, traceback) -> None:
return None
class StubRepository:
def __init__(self, orders: list[StoredOrder]) -> None:
self._orders = {order.order_uuid: order for order in orders}
self.session_value = object()
self.calls: list[tuple[str, dict[str, Any]]] = []
def session(self) -> StubSessionContext:
return StubSessionContext(self.session_value)
async def list_orders_pending_waybill_email(
self, session: object, *, limit: int
) -> list[StoredOrder]:
self.calls.append(("list", {"limit": limit}))
return [
order
for order in self._orders.values()
if order.provider_waybill_url is not None
and order.waybill_email_sent_at is None
]
async def record_waybill_email_sent(
self,
session: object,
*,
order_uuid: str,
sent_at: datetime,
) -> StoredOrder | None:
self.calls.append(
("record", {"order_uuid": order_uuid, "sent_at": sent_at})
)
order = self._orders.get(order_uuid)
if order is None:
return None
if order.waybill_email_sent_at is None:
order.waybill_email_sent_at = sent_at
return order
class StubDownloader:
def __init__(self, results: dict[str, bytes | Exception]) -> None:
self._results = results
self.calls: list[str] = []
async def download_waybill_pdf(self, url: str) -> bytes:
self.calls.append(url)
result = self._results[url]
if isinstance(result, Exception):
raise result
return result
class StubEmailSender:
def __init__(self, raise_for: set[str] | None = None) -> None:
self.raise_for = raise_for or set()
self.calls: list[dict[str, Any]] = []
async def send_email(
self,
*,
to: str,
subject: str,
body: str,
attachment_bytes: bytes,
attachment_filename: str,
) -> None:
self.calls.append(
{
"to": to,
"subject": subject,
"body": body,
"attachment_bytes": attachment_bytes,
"attachment_filename": attachment_filename,
}
)
if to in self.raise_for:
raise RuntimeError(f"smtp blew up for {to}")
_SENT_AT = datetime(2026, 5, 24, 12, 0, tzinfo=timezone.utc)
def _make_service(
*,
repository: StubRepository,
downloader: StubDownloader | None = None,
email_sender: StubEmailSender | None = None,
) -> WaybillEmailSenderService:
return WaybillEmailSenderService(
order_repository=repository,
waybill_downloader=downloader or StubDownloader({}),
email_sender=email_sender or StubEmailSender(),
batch_size=10,
datetime_now=lambda: _SENT_AT,
)
def test_poll_once_downloads_pdf_sends_email_and_marks_sent() -> None:
order = StoredOrder(
order_uuid="o-1",
account_email="client@example.com",
provider_waybill_url="https://cdek.test/1.pdf",
)
repo = StubRepository([order])
downloader = StubDownloader({"https://cdek.test/1.pdf": b"%PDF"})
email_sender = StubEmailSender()
service = _make_service(
repository=repo, downloader=downloader, email_sender=email_sender
)
summary = asyncio.run(service.poll_once())
assert summary.processed == 1
assert summary.succeeded == 1
assert summary.failed == 0
assert downloader.calls == ["https://cdek.test/1.pdf"]
assert len(email_sender.calls) == 1
call = email_sender.calls[0]
assert call["to"] == "client@example.com"
assert call["subject"] == "Накладная по заказу o-1"
assert "o-1" in call["body"]
assert "https://cdek.test/1.pdf" in call["body"]
assert call["attachment_bytes"] == b"%PDF"
assert call["attachment_filename"] == "waybill_o-1.pdf"
assert order.waybill_email_sent_at == _SENT_AT
def test_poll_once_download_error_keeps_order_pending_and_skips_send() -> None:
order = StoredOrder(
order_uuid="o-1",
account_email="client@example.com",
provider_waybill_url="https://cdek.test/1.pdf",
)
repo = StubRepository([order])
downloader = StubDownloader({"https://cdek.test/1.pdf": RuntimeError("cdek 500")})
email_sender = StubEmailSender()
service = _make_service(
repository=repo, downloader=downloader, email_sender=email_sender
)
summary = asyncio.run(service.poll_once())
assert summary.processed == 1
assert summary.succeeded == 0
assert summary.failed == 1
assert email_sender.calls == []
assert order.waybill_email_sent_at is None
def test_poll_once_smtp_error_keeps_order_pending() -> None:
order = StoredOrder(
order_uuid="o-1",
account_email="bad@example.com",
provider_waybill_url="https://cdek.test/1.pdf",
)
repo = StubRepository([order])
downloader = StubDownloader({"https://cdek.test/1.pdf": b"%PDF"})
email_sender = StubEmailSender(raise_for={"bad@example.com"})
service = _make_service(
repository=repo, downloader=downloader, email_sender=email_sender
)
summary = asyncio.run(service.poll_once())
assert summary.failed == 1
assert order.waybill_email_sent_at is None
assert len(email_sender.calls) == 1
def test_poll_once_failure_on_one_order_does_not_break_batch() -> None:
bad = StoredOrder(
order_uuid="bad",
account_email="bad@example.com",
provider_waybill_url="https://cdek.test/bad.pdf",
)
good = StoredOrder(
order_uuid="good",
account_email="good@example.com",
provider_waybill_url="https://cdek.test/good.pdf",
)
repo = StubRepository([bad, good])
downloader = StubDownloader(
{
"https://cdek.test/bad.pdf": RuntimeError("cdek 500"),
"https://cdek.test/good.pdf": b"%PDF",
}
)
email_sender = StubEmailSender()
service = _make_service(
repository=repo, downloader=downloader, email_sender=email_sender
)
summary = asyncio.run(service.poll_once())
assert summary.processed == 2
assert summary.succeeded == 1
assert summary.failed == 1
assert good.waybill_email_sent_at == _SENT_AT
assert bad.waybill_email_sent_at is None
def test_run_forever_exits_when_stop_event_is_set() -> None:
repo = StubRepository([])
service = _make_service(repository=repo)
async def run() -> None:
stop_event = asyncio.Event()
stop_event.set()
await asyncio.wait_for(
service.run_forever(interval_seconds=0.01, stop_event=stop_event),
timeout=1.0,
)
asyncio.run(run())