Добавлен поллер накладной

This commit is contained in:
Раис Юсупалиев
2026-05-23 20:24:23 +03:00
parent c494d50566
commit 5526f90cb3
23 changed files with 1481 additions and 61 deletions
@@ -0,0 +1,173 @@
import asyncio
from typing import Any
import httpx
import pytest
from app.adapters.delivery_providers.cdek.client import (
CDEKClient,
CDEKClientError,
CDEKRequestError,
)
from app.adapters.delivery_providers.cdek.order_mapper import (
CDEKOrderInfo,
CDEKWaybillInfo,
)
class StubAuthClient:
async def get_access_token(self) -> str:
return "test-token"
class SequenceHTTPClient:
def __init__(self, results: list[Any]) -> None:
self._results = results
self.calls: list[dict[str, Any]] = []
def _next_result(self) -> Any:
return self._results[len(self.calls) - 1]
async def get(
self,
url: str,
*,
headers: dict[str, str] | None = None,
timeout: float | None = None,
) -> httpx.Response:
self.calls.append(
{"method": "GET", "url": url, "headers": headers, "timeout": timeout}
)
result = self._next_result()
if isinstance(result, Exception):
raise result
return result
def _make_client(http_client: SequenceHTTPClient, **kwargs: Any) -> CDEKClient:
kwargs.setdefault("retry_attempts", 0)
return CDEKClient(
http_client=http_client, # type: ignore[arg-type]
auth_client=StubAuthClient(), # type: ignore[arg-type]
base_url="https://api.cdek.test/v2",
timeout_seconds=7.5,
**kwargs,
)
def test_get_order_parses_status_and_waybill_uuid() -> None:
response = httpx.Response(
200,
json={
"entity": {
"uuid": "cdek-order-uuid",
"statuses": [
{"code": "ACCEPTED", "date_time": "2026-05-24T10:00:00+0000"}
],
},
"related_entities": [
{"type": "waybill", "uuid": "waybill-uuid-1"}
],
},
request=httpx.Request("GET", "https://api.cdek.test/v2/orders/cdek-order-uuid"),
)
http_client = SequenceHTTPClient([response])
client = _make_client(http_client)
result = asyncio.run(client.get_order("cdek-order-uuid"))
assert result == CDEKOrderInfo(
order_uuid="cdek-order-uuid",
status_code="ACCEPTED",
waybill_uuid="waybill-uuid-1",
)
assert http_client.calls[0]["url"] == (
"https://api.cdek.test/v2/orders/cdek-order-uuid"
)
assert http_client.calls[0]["headers"] == {"Authorization": "Bearer test-token"}
def test_get_order_retries_on_5xx_and_succeeds() -> None:
flaky = httpx.Response(
503,
json={"message": "temporary"},
request=httpx.Request("GET", "https://api.cdek.test/v2/orders/x"),
)
success = httpx.Response(
200,
json={"entity": {"uuid": "x", "statuses": []}},
request=httpx.Request("GET", "https://api.cdek.test/v2/orders/x"),
)
http_client = SequenceHTTPClient([flaky, success])
sleep_calls: list[float] = []
async def fake_sleep(seconds: float) -> None:
sleep_calls.append(seconds)
client = _make_client(
http_client,
retry_attempts=1,
retry_backoff_seconds=0.1,
sleep=fake_sleep,
)
result = asyncio.run(client.get_order("x"))
assert result.order_uuid == "x"
assert len(http_client.calls) == 2
assert sleep_calls == [0.1]
def test_get_order_raises_request_error_on_4xx() -> None:
response = httpx.Response(
404,
json={"requests": [{"errors": [{"code": "v2_not_found"}]}]},
request=httpx.Request("GET", "https://api.cdek.test/v2/orders/missing"),
)
http_client = SequenceHTTPClient([response])
client = _make_client(http_client)
with pytest.raises(CDEKRequestError, match="status 404"):
asyncio.run(client.get_order("missing"))
def test_get_waybill_returns_url_when_present() -> None:
response = httpx.Response(
200,
json={
"entity": {
"uuid": "waybill-uuid-1",
"url": "https://cdek.test/waybill/1.pdf",
}
},
request=httpx.Request(
"GET", "https://api.cdek.test/v2/print/orders/waybill-uuid-1"
),
)
http_client = SequenceHTTPClient([response])
client = _make_client(http_client)
result = asyncio.run(client.get_waybill("waybill-uuid-1"))
assert result == CDEKWaybillInfo(
waybill_uuid="waybill-uuid-1",
url="https://cdek.test/waybill/1.pdf",
)
assert http_client.calls[0]["url"] == (
"https://api.cdek.test/v2/print/orders/waybill-uuid-1"
)
def test_get_waybill_raises_client_error_for_invalid_payload() -> None:
response = httpx.Response(
200,
json={"entity": {}},
request=httpx.Request(
"GET", "https://api.cdek.test/v2/print/orders/waybill-uuid-1"
),
)
http_client = SequenceHTTPClient([response])
client = _make_client(http_client)
with pytest.raises(CDEKClientError, match="response payload is invalid"):
asyncio.run(client.get_waybill("waybill-uuid-1"))
@@ -1,10 +1,16 @@
from app.adapters.delivery_providers.cdek.order_mapper import (
CDEKOrderInfo,
CDEKOrderMappingError,
CDEKOrderRegistrationResult,
CDEKWaybillInfo,
compose_address_line,
map_cdek_existing_order_response,
map_cdek_order_info_response,
map_cdek_order_request,
map_cdek_order_response,
map_cdek_waybill_info_response,
)
import pytest
from app.schemas.payment import Address
from tests.payment_fixtures import make_init_payment_request
@@ -207,3 +213,72 @@ def test_compose_address_line_handles_empty_apartment() -> None:
)
assert compose_address_line(address) == "Москва, Lenina, 1"
def test_map_cdek_order_info_response_picks_latest_status_by_date_time() -> None:
result = map_cdek_order_info_response(
{
"entity": {
"uuid": "cdek-order-uuid",
"statuses": [
{"code": "ACCEPTED", "date_time": "2026-05-24T10:00:00+0000"},
{"code": "INVALID", "date_time": "2026-05-24T10:00:05+0000"},
],
},
"related_entities": [
{"type": "waybill", "uuid": "waybill-uuid-1"}
],
}
)
assert result == CDEKOrderInfo(
order_uuid="cdek-order-uuid",
status_code="INVALID",
waybill_uuid="waybill-uuid-1",
)
def test_map_cdek_order_info_response_returns_none_status_when_statuses_empty() -> None:
result = map_cdek_order_info_response(
{"entity": {"uuid": "cdek-order-uuid", "statuses": []}}
)
assert result == CDEKOrderInfo(
order_uuid="cdek-order-uuid",
status_code=None,
waybill_uuid=None,
)
def test_map_cdek_order_info_response_raises_for_missing_entity() -> None:
with pytest.raises(CDEKOrderMappingError):
map_cdek_order_info_response({"requests": []})
def test_map_cdek_waybill_info_response_returns_url_when_present() -> None:
result = map_cdek_waybill_info_response(
{
"entity": {
"uuid": "waybill-uuid-1",
"url": "https://cdek.test/waybill/1.pdf",
}
}
)
assert result == CDEKWaybillInfo(
waybill_uuid="waybill-uuid-1",
url="https://cdek.test/waybill/1.pdf",
)
def test_map_cdek_waybill_info_response_returns_none_url_when_missing() -> None:
result = map_cdek_waybill_info_response(
{"entity": {"uuid": "waybill-uuid-1"}}
)
assert result == CDEKWaybillInfo(waybill_uuid="waybill-uuid-1", url=None)
def test_map_cdek_waybill_info_response_raises_for_missing_uuid() -> None:
with pytest.raises(CDEKOrderMappingError):
map_cdek_waybill_info_response({"entity": {}})