241 lines
7.2 KiB
Python
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
|
|
cdek_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.cdek_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",
|
|
cdek_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",
|
|
cdek_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",
|
|
cdek_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",
|
|
cdek_waybill_url="https://cdek.test/bad.pdf",
|
|
)
|
|
good = StoredOrder(
|
|
order_uuid="good",
|
|
account_email="good@example.com",
|
|
cdek_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())
|