Добавлена валидация цены

This commit is contained in:
Раис Юсупалиев
2026-05-13 16:35:22 +03:00
parent 6b66af5eb3
commit 39cd5ddc7a
24 changed files with 1021 additions and 56 deletions
@@ -10,6 +10,7 @@ def test_map_cdek_response_maps_all_tariffs_to_unified_model() -> None:
"currency": "usd",
"tariff_codes": [
{
"tariff_code": 7,
"tariff_name": "Express",
"delivery_sum": "1234.50",
"currency": "rub",
@@ -33,6 +34,25 @@ def test_map_cdek_response_maps_all_tariffs_to_unified_model() -> None:
assert [price.currency for price in result] == ["RUB", "USD"]
assert [price.delivery_days_min for price in result] == [2, 5]
assert [price.delivery_days_max for price in result] == [4, 7]
assert [price.tariff_code for price in result] == [7, 136]
def test_map_cdek_response_returns_none_tariff_code_when_missing() -> None:
payload = {
"tariff_codes": [
{
"tariff_name": "Express",
"delivery_sum": "100.00",
"currency": "RUB",
"period_min": 1,
"period_max": 2,
}
]
}
result = map_cdek_response(payload)
assert result[0].tariff_code is None
def test_map_cdek_response_raises_for_missing_tariff_codes() -> None:
@@ -0,0 +1,265 @@
import asyncio
from decimal import Decimal
from typing import Any
import httpx
import pytest
from app.adapters.delivery_providers.cdek.client import (
CDEKClient,
CDEKClientError,
CDEKProvider,
CDEKRequestError,
)
from app.schemas.payment import InitPaymentRequest
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 post(
self,
url: str,
*,
json: dict[str, Any] | None = None,
data: dict[str, Any] | None = None,
headers: dict[str, str] | None = None,
timeout: float | None = None,
) -> httpx.Response:
self.calls.append(
{
"method": "POST",
"url": url,
"json": json,
"data": data,
"headers": headers,
"timeout": timeout,
}
)
result = self._next_result()
if isinstance(result, Exception):
raise result
return result
def _make_request(**overrides: object) -> InitPaymentRequest:
payload: dict[str, object] = {
"order_uuid": "order-uuid-1",
"price": 125000,
"type": 2,
"tariff_code": 535,
"comment": "Test payment",
"sender": {
"name": "Petr Petrov",
"email": "sender@example.com",
"phone": {"number": "+79009876543"},
},
"recipient": {
"name": "Ivan Ivanov",
"email": "ivan@example.com",
"phone": {"number": "+79001234567"},
},
"from_location": {
"address": "Lenina 1",
"city": "Moscow",
"country_code": "RU",
},
"to_location": {
"address": "Pushkina 10",
"city": "Novosibirsk",
"country_code": "RU",
},
"services": [{"code": "INSURANCE", "parameter": "1000"}],
"packages": [
{
"number": "1",
"weight": 1,
"length": 20,
"width": 15,
"height": 10,
"comment": "Package 1",
}
],
}
payload.update(overrides)
return InitPaymentRequest(**payload)
def test_provider_get_payment_price_posts_tarifflist_payload_and_maps_requested_tariff() -> None:
response = httpx.Response(
200,
json={
"tariff_codes": [
{
"tariff_code": 234,
"tariff_name": "Other tariff",
"delivery_sum": "999.00",
"currency": "RUB",
"period_min": 3,
"period_max": 5,
},
{
"tariff_code": 535,
"tariff_name": "CDEK tariff",
"delivery_sum": "1250.00",
"currency": "RUB",
"period_min": 1,
"period_max": 2,
},
]
},
request=httpx.Request("POST", "https://api.cdek.test/v2/calculator/tarifflist"),
)
http_client = SequenceHTTPClient([response])
provider = CDEKProvider(
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,
retry_attempts=0,
)
)
result = asyncio.run(provider.get_payment_price(_make_request()))
assert result is not None
assert result.provider == "cdek"
assert result.service_name == "CDEK tariff"
assert result.price == Decimal("1250.00")
assert http_client.calls == [
{
"method": "POST",
"url": "https://api.cdek.test/v2/calculator/tarifflist",
"json": {
"type": 2,
"from_location": {
"address": "Lenina 1",
"city": "Moscow",
"country_code": "RU",
},
"to_location": {
"address": "Pushkina 10",
"city": "Novosibirsk",
"country_code": "RU",
},
"packages": [
{
"weight": 1000,
"length": 20,
"width": 15,
"height": 10,
}
],
"services": [{"code": "INSURANCE", "parameter": "1000"}],
},
"data": None,
"headers": {"Authorization": "Bearer test-token"},
"timeout": 7.5,
}
]
def test_provider_get_payment_price_omits_services_when_none() -> None:
response = httpx.Response(
200,
json={"tariff_codes": []},
request=httpx.Request("POST", "https://api.cdek.test/v2/calculator/tarifflist"),
)
http_client = SequenceHTTPClient([response])
provider = CDEKProvider(
CDEKClient(
http_client=http_client, # type: ignore[arg-type]
auth_client=StubAuthClient(), # type: ignore[arg-type]
base_url="https://api.cdek.test/v2",
retry_attempts=0,
)
)
result = asyncio.run(provider.get_payment_price(_make_request(services=None)))
assert result is None
assert "services" not in http_client.calls[0]["json"]
def test_client_get_raw_payment_price_maps_4xx_to_request_error() -> None:
rejected_response = httpx.Response(
422,
json={"errors": [{"message": "bad request"}]},
request=httpx.Request("POST", "https://api.cdek.test/v2/calculator/tarifflist"),
)
http_client = SequenceHTTPClient([rejected_response])
client = CDEKClient(
http_client=http_client, # type: ignore[arg-type]
auth_client=StubAuthClient(), # type: ignore[arg-type]
base_url="https://api.cdek.test/v2",
retry_attempts=2,
)
with pytest.raises(CDEKRequestError, match="status 422"):
asyncio.run(client.get_raw_payment_price(_make_request()))
assert len(http_client.calls) == 1
def test_client_get_raw_payment_price_retries_5xx_and_raises_client_error() -> None:
first_response = httpx.Response(
503,
json={"message": "temporary failure"},
request=httpx.Request("POST", "https://api.cdek.test/v2/calculator/tarifflist"),
)
second_response = httpx.Response(
503,
json={"message": "temporary failure"},
request=httpx.Request("POST", "https://api.cdek.test/v2/calculator/tarifflist"),
)
http_client = SequenceHTTPClient([first_response, second_response])
sleep_calls: list[float] = []
async def fake_sleep(seconds: float) -> None:
sleep_calls.append(seconds)
client = CDEKClient(
http_client=http_client, # type: ignore[arg-type]
auth_client=StubAuthClient(), # type: ignore[arg-type]
base_url="https://api.cdek.test/v2",
retry_attempts=1,
retry_backoff_seconds=0.25,
sleep=fake_sleep,
)
with pytest.raises(CDEKClientError, match="status 503"):
asyncio.run(client.get_raw_payment_price(_make_request()))
assert len(http_client.calls) == 2
assert sleep_calls == [0.25]
def test_provider_get_payment_price_maps_invalid_success_payload_to_client_error() -> None:
invalid_response = httpx.Response(
200,
json={"tariff_codes": [{"tariff_code": 535}]},
request=httpx.Request("POST", "https://api.cdek.test/v2/calculator/tarifflist"),
)
http_client = SequenceHTTPClient([invalid_response])
provider = CDEKProvider(
CDEKClient(
http_client=http_client, # type: ignore[arg-type]
auth_client=StubAuthClient(), # type: ignore[arg-type]
base_url="https://api.cdek.test/v2",
retry_attempts=0,
)
)
with pytest.raises(CDEKClientError, match="response payload is invalid"):
asyncio.run(provider.get_payment_price(_make_request()))