fix country code
This commit is contained in:
@@ -48,8 +48,8 @@ class CDEKClient:
|
|||||||
self._http_client = http_client
|
self._http_client = http_client
|
||||||
self._auth_client = auth_client
|
self._auth_client = auth_client
|
||||||
normalized_base_url = base_url.rstrip("/")
|
normalized_base_url = base_url.rstrip("/")
|
||||||
self._city_lookup_url = f"{normalized_base_url}/location/cities"
|
self._city_lookup_url = f"{normalized_base_url}/location/suggest/cities"
|
||||||
self._tariff_url = f"{base_url.rstrip('/')}/calculator/tarifflist"
|
self._tariff_url = f"{normalized_base_url}/calculator/tarifflist"
|
||||||
self._timeout_seconds = timeout_seconds
|
self._timeout_seconds = timeout_seconds
|
||||||
self._retry_attempts = retry_attempts
|
self._retry_attempts = retry_attempts
|
||||||
self._retry_backoff_seconds = retry_backoff_seconds
|
self._retry_backoff_seconds = retry_backoff_seconds
|
||||||
@@ -103,8 +103,14 @@ class CDEKClient:
|
|||||||
return status_code == 429 or status_code >= 500
|
return status_code == 429 or status_code >= 500
|
||||||
|
|
||||||
async def _build_payload(self, request: DeliveryRequest) -> dict[str, Any]:
|
async def _build_payload(self, request: DeliveryRequest) -> dict[str, Any]:
|
||||||
from_city_code = await self._resolve_city_code(request.from_city)
|
from_city_code = await self._resolve_city_code(
|
||||||
to_city_code = await self._resolve_city_code(request.to_city)
|
request.from_city,
|
||||||
|
country_code=request.country_code,
|
||||||
|
)
|
||||||
|
to_city_code = await self._resolve_city_code(
|
||||||
|
request.to_city,
|
||||||
|
country_code=request.country_code,
|
||||||
|
)
|
||||||
return {
|
return {
|
||||||
"from_location": {"code": from_city_code},
|
"from_location": {"code": from_city_code},
|
||||||
"to_location": {"code": to_city_code},
|
"to_location": {"code": to_city_code},
|
||||||
@@ -118,21 +124,29 @@ class CDEKClient:
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
async def _resolve_city_code(self, city: str) -> int:
|
async def _resolve_city_code(self, city: str, *, country_code: str | None) -> int:
|
||||||
normalized_city = city.strip().casefold()
|
normalized_city = city.strip().casefold()
|
||||||
cached_code = self._city_code_cache.get(normalized_city)
|
normalized_country_code = (
|
||||||
|
country_code.strip().upper() if country_code is not None else ""
|
||||||
|
)
|
||||||
|
cache_key = f"{normalized_country_code}:{normalized_city}"
|
||||||
|
cached_code = self._city_code_cache.get(cache_key)
|
||||||
if cached_code is not None:
|
if cached_code is not None:
|
||||||
return cached_code
|
return cached_code
|
||||||
|
|
||||||
for attempt in range(self._retry_attempts + 1):
|
for attempt in range(self._retry_attempts + 1):
|
||||||
try:
|
try:
|
||||||
token = await self._auth_client.get_access_token()
|
token = await self._auth_client.get_access_token()
|
||||||
|
params = {"name": city}
|
||||||
|
if normalized_country_code:
|
||||||
|
params["country_code"] = normalized_country_code
|
||||||
response = await self._http_client.get(
|
response = await self._http_client.get(
|
||||||
self._city_lookup_url,
|
self._city_lookup_url,
|
||||||
params={"city": city, "country_codes": "RU", "size": 1},
|
params=params,
|
||||||
headers={"Authorization": f"Bearer {token}"},
|
headers={"Authorization": f"Bearer {token}"},
|
||||||
timeout=self._timeout_seconds,
|
timeout=self._timeout_seconds,
|
||||||
)
|
)
|
||||||
|
log.info(response.json())
|
||||||
except (httpx.TimeoutException, httpx.TransportError) as exc:
|
except (httpx.TimeoutException, httpx.TransportError) as exc:
|
||||||
if attempt < self._retry_attempts:
|
if attempt < self._retry_attempts:
|
||||||
await self._sleep(self._retry_delay(attempt))
|
await self._sleep(self._retry_delay(attempt))
|
||||||
@@ -148,7 +162,6 @@ class CDEKClient:
|
|||||||
raise CDEKClientError(
|
raise CDEKClientError(
|
||||||
f"CDEK city lookup failed with status {response.status_code}."
|
f"CDEK city lookup failed with status {response.status_code}."
|
||||||
)
|
)
|
||||||
log.info(response.json())
|
|
||||||
try:
|
try:
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
body = response.json()
|
body = response.json()
|
||||||
@@ -158,11 +171,17 @@ class CDEKClient:
|
|||||||
else:
|
else:
|
||||||
raise CDEKClientError("CDEK city lookup failed unexpectedly.")
|
raise CDEKClientError("CDEK city lookup failed unexpectedly.")
|
||||||
|
|
||||||
if not isinstance(body, list) or not body:
|
if isinstance(body, dict):
|
||||||
|
first_item = body
|
||||||
|
elif isinstance(body, list) and body:
|
||||||
|
first_item = body[0]
|
||||||
|
elif isinstance(body, list):
|
||||||
raise CDEKRequestError(
|
raise CDEKRequestError(
|
||||||
f"CDEK city lookup returned no matches for '{city}'."
|
f"CDEK city lookup returned no matches for '{city}'."
|
||||||
)
|
)
|
||||||
first_item = body[0]
|
else:
|
||||||
|
raise CDEKClientError("CDEK city lookup response must be an object or array.")
|
||||||
|
|
||||||
if not isinstance(first_item, dict):
|
if not isinstance(first_item, dict):
|
||||||
raise CDEKClientError("CDEK city lookup response entry must be an object.")
|
raise CDEKClientError("CDEK city lookup response entry must be an object.")
|
||||||
raw_city_code = first_item.get("code")
|
raw_city_code = first_item.get("code")
|
||||||
@@ -173,7 +192,7 @@ class CDEKClient:
|
|||||||
except (TypeError, ValueError) as exc:
|
except (TypeError, ValueError) as exc:
|
||||||
raise CDEKClientError("CDEK city lookup response city code is invalid.") from exc
|
raise CDEKClientError("CDEK city lookup response city code is invalid.") from exc
|
||||||
|
|
||||||
self._city_code_cache[normalized_city] = city_code
|
self._city_code_cache[cache_key] = city_code
|
||||||
return city_code
|
return city_code
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ class RequestLike(Protocol):
|
|||||||
entity: object
|
entity: object
|
||||||
from_city: object
|
from_city: object
|
||||||
to_city: object
|
to_city: object
|
||||||
|
country_code: object
|
||||||
weight_kg: object
|
weight_kg: object
|
||||||
length_cm: object
|
length_cm: object
|
||||||
width_cm: object
|
width_cm: object
|
||||||
@@ -29,6 +30,7 @@ class NormalizedDeliveryRequest:
|
|||||||
entity: str
|
entity: str
|
||||||
from_city: str
|
from_city: str
|
||||||
to_city: str
|
to_city: str
|
||||||
|
country_code: str | None
|
||||||
weight_kg: Decimal
|
weight_kg: Decimal
|
||||||
length_cm: Decimal
|
length_cm: Decimal
|
||||||
width_cm: Decimal
|
width_cm: Decimal
|
||||||
@@ -58,6 +60,7 @@ def normalize_delivery_request(
|
|||||||
entity=str(_get_required_attr(request, "entity")),
|
entity=str(_get_required_attr(request, "entity")),
|
||||||
from_city=_normalize_text(_get_required_attr(request, "from_city")),
|
from_city=_normalize_text(_get_required_attr(request, "from_city")),
|
||||||
to_city=_normalize_text(_get_required_attr(request, "to_city")),
|
to_city=_normalize_text(_get_required_attr(request, "to_city")),
|
||||||
|
country_code=_normalize_country_code(_get_optional_attr(request, "country_code")),
|
||||||
weight_kg=_normalize_weight(
|
weight_kg=_normalize_weight(
|
||||||
_to_decimal(_get_required_attr(request, "weight_kg")),
|
_to_decimal(_get_required_attr(request, "weight_kg")),
|
||||||
normalized_weight_scale,
|
normalized_weight_scale,
|
||||||
@@ -167,6 +170,15 @@ def _normalize_text(value: object) -> str:
|
|||||||
return str(value).strip()
|
return str(value).strip()
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_country_code(value: object) -> str | None:
|
||||||
|
if value is _MISSING or value is None:
|
||||||
|
return None
|
||||||
|
normalized = str(value).strip().upper()
|
||||||
|
if not normalized:
|
||||||
|
return None
|
||||||
|
return normalized
|
||||||
|
|
||||||
|
|
||||||
def _clamp_scale(scale: int) -> int:
|
def _clamp_scale(scale: int) -> int:
|
||||||
if scale < 0:
|
if scale < 0:
|
||||||
return 0
|
return 0
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ class DeliveryRequest(BaseModel):
|
|||||||
entity: DeliveryEntity
|
entity: DeliveryEntity
|
||||||
from_city: str = Field(min_length=1)
|
from_city: str = Field(min_length=1)
|
||||||
to_city: str = Field(min_length=1)
|
to_city: str = Field(min_length=1)
|
||||||
|
country_code: str | None = None
|
||||||
weight_kg: float = Field(gt=0)
|
weight_kg: float = Field(gt=0)
|
||||||
length_cm: float = Field(gt=0)
|
length_cm: float = Field(gt=0)
|
||||||
width_cm: float = Field(gt=0)
|
width_cm: float = Field(gt=0)
|
||||||
|
|||||||
@@ -142,6 +142,7 @@ class AggregatorService:
|
|||||||
entity=request.entity,
|
entity=request.entity,
|
||||||
from_city=request.from_city,
|
from_city=request.from_city,
|
||||||
to_city=request.to_city,
|
to_city=request.to_city,
|
||||||
|
country_code=request.country_code,
|
||||||
weight_kg=request.weight_kg,
|
weight_kg=request.weight_kg,
|
||||||
length_cm=request.length_cm,
|
length_cm=request.length_cm,
|
||||||
width_cm=request.width_cm,
|
width_cm=request.width_cm,
|
||||||
@@ -155,6 +156,7 @@ class AggregatorService:
|
|||||||
"entity": request.entity,
|
"entity": request.entity,
|
||||||
"from_city": request.from_city,
|
"from_city": request.from_city,
|
||||||
"to_city": request.to_city,
|
"to_city": request.to_city,
|
||||||
|
"country_code": request.country_code,
|
||||||
"weight_kg": str(request.weight_kg),
|
"weight_kg": str(request.weight_kg),
|
||||||
"length_cm": str(request.length_cm),
|
"length_cm": str(request.length_cm),
|
||||||
"width_cm": str(request.width_cm),
|
"width_cm": str(request.width_cm),
|
||||||
|
|||||||
@@ -94,11 +94,11 @@ class RecordingHTTPClient:
|
|||||||
request = httpx.Request("GET", url, params=params)
|
request = httpx.Request("GET", url, params=params)
|
||||||
self.city_lookup_timeouts.append(timeout)
|
self.city_lookup_timeouts.append(timeout)
|
||||||
assert headers == {"Authorization": "Bearer provider-token"}
|
assert headers == {"Authorization": "Bearer provider-token"}
|
||||||
city = params.get("city") if params else None
|
city = params.get("name") if params else None
|
||||||
code_by_city = {"Moscow": 44, "Kazan": 424}
|
code_by_city = {"Moscow": 44, "Kazan": 424}
|
||||||
return httpx.Response(
|
return httpx.Response(
|
||||||
200,
|
200,
|
||||||
json=[{"code": code_by_city.get(city, 44), "city": city}],
|
json={"code": code_by_city.get(city, 44), "full_name": city},
|
||||||
request=request,
|
request=request,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -140,29 +140,32 @@ class RecordingHTTPClient:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _make_request() -> DeliveryRequest:
|
def _make_request(**overrides: object) -> DeliveryRequest:
|
||||||
return DeliveryRequest(
|
payload: dict[str, object] = {
|
||||||
entity=DeliveryEntity.INDIVIDUAL,
|
"entity": DeliveryEntity.INDIVIDUAL,
|
||||||
from_city="Moscow",
|
"from_city": "Moscow",
|
||||||
to_city="Kazan",
|
"to_city": "Kazan",
|
||||||
weight_kg=1.2,
|
"country_code": None,
|
||||||
length_cm=20,
|
"weight_kg": 1.2,
|
||||||
width_cm=10,
|
"length_cm": 20,
|
||||||
height_cm=5,
|
"width_cm": 10,
|
||||||
)
|
"height_cm": 5,
|
||||||
|
}
|
||||||
|
payload.update(overrides)
|
||||||
|
return DeliveryRequest(**payload)
|
||||||
|
|
||||||
|
|
||||||
def test_cdek_client_retries_timeout_and_applies_timeout_seconds() -> None:
|
def test_cdek_client_retries_timeout_and_applies_timeout_seconds() -> None:
|
||||||
request = _make_request()
|
request = _make_request()
|
||||||
city_lookup_moscow = httpx.Response(
|
city_lookup_moscow = httpx.Response(
|
||||||
200,
|
200,
|
||||||
json=[{"code": 44, "city": "Moscow"}],
|
json={"code": 44, "full_name": "Moscow"},
|
||||||
request=httpx.Request("GET", "https://api.cdek.test/v2/location/cities"),
|
request=httpx.Request("GET", "https://api.cdek.test/v2/location/suggest/cities"),
|
||||||
)
|
)
|
||||||
city_lookup_kazan = httpx.Response(
|
city_lookup_kazan = httpx.Response(
|
||||||
200,
|
200,
|
||||||
json=[{"code": 424, "city": "Kazan"}],
|
json={"code": 424, "full_name": "Kazan"},
|
||||||
request=httpx.Request("GET", "https://api.cdek.test/v2/location/cities"),
|
request=httpx.Request("GET", "https://api.cdek.test/v2/location/suggest/cities"),
|
||||||
)
|
)
|
||||||
first_error = httpx.ReadTimeout(
|
first_error = httpx.ReadTimeout(
|
||||||
"read timeout",
|
"read timeout",
|
||||||
@@ -197,9 +200,7 @@ def test_cdek_client_retries_timeout_and_applies_timeout_seconds() -> None:
|
|||||||
assert len(http_client.calls) == 4
|
assert len(http_client.calls) == 4
|
||||||
assert http_client.calls[0]["method"] == "GET"
|
assert http_client.calls[0]["method"] == "GET"
|
||||||
assert http_client.calls[0]["params"] == {
|
assert http_client.calls[0]["params"] == {
|
||||||
"city": "Moscow",
|
"name": "Moscow",
|
||||||
"country_codes": "RU",
|
|
||||||
"size": 1,
|
|
||||||
}
|
}
|
||||||
assert http_client.calls[1]["method"] == "GET"
|
assert http_client.calls[1]["method"] == "GET"
|
||||||
assert http_client.calls[2]["method"] == "POST"
|
assert http_client.calls[2]["method"] == "POST"
|
||||||
@@ -209,17 +210,50 @@ def test_cdek_client_retries_timeout_and_applies_timeout_seconds() -> None:
|
|||||||
assert sleep_calls == [0.5]
|
assert sleep_calls == [0.5]
|
||||||
|
|
||||||
|
|
||||||
|
def test_cdek_client_passes_country_code_when_provided() -> None:
|
||||||
|
request = _make_request(country_code="KZ")
|
||||||
|
city_lookup_from = httpx.Response(
|
||||||
|
200,
|
||||||
|
json={"code": 88, "full_name": "Moscow"},
|
||||||
|
request=httpx.Request("GET", "https://api.cdek.test/v2/location/suggest/cities"),
|
||||||
|
)
|
||||||
|
city_lookup_to = httpx.Response(
|
||||||
|
200,
|
||||||
|
json={"code": 99, "full_name": "Kazan"},
|
||||||
|
request=httpx.Request("GET", "https://api.cdek.test/v2/location/suggest/cities"),
|
||||||
|
)
|
||||||
|
tariff_response = httpx.Response(
|
||||||
|
200,
|
||||||
|
json={"tariff_codes": [{"delivery_sum": 500}]},
|
||||||
|
request=httpx.Request("POST", "https://api.cdek.test/v2/calculator/tarifflist"),
|
||||||
|
)
|
||||||
|
http_client = SequenceHTTPClient([city_lookup_from, city_lookup_to, tariff_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=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
asyncio.run(client.get_raw_price(request))
|
||||||
|
|
||||||
|
assert http_client.calls[0]["params"] == {
|
||||||
|
"name": "Moscow",
|
||||||
|
"country_code": "KZ",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_cdek_client_raises_on_non_retriable_http_error() -> None:
|
def test_cdek_client_raises_on_non_retriable_http_error() -> None:
|
||||||
request = _make_request()
|
request = _make_request()
|
||||||
city_lookup_moscow = httpx.Response(
|
city_lookup_moscow = httpx.Response(
|
||||||
200,
|
200,
|
||||||
json=[{"code": 44, "city": "Moscow"}],
|
json={"code": 44, "full_name": "Moscow"},
|
||||||
request=httpx.Request("GET", "https://api.cdek.test/v2/location/cities"),
|
request=httpx.Request("GET", "https://api.cdek.test/v2/location/suggest/cities"),
|
||||||
)
|
)
|
||||||
city_lookup_kazan = httpx.Response(
|
city_lookup_kazan = httpx.Response(
|
||||||
200,
|
200,
|
||||||
json=[{"code": 424, "city": "Kazan"}],
|
json={"code": 424, "full_name": "Kazan"},
|
||||||
request=httpx.Request("GET", "https://api.cdek.test/v2/location/cities"),
|
request=httpx.Request("GET", "https://api.cdek.test/v2/location/suggest/cities"),
|
||||||
)
|
)
|
||||||
bad_response = httpx.Response(
|
bad_response = httpx.Response(
|
||||||
400,
|
400,
|
||||||
@@ -245,7 +279,7 @@ def test_cdek_client_raises_when_city_code_not_found() -> None:
|
|||||||
city_lookup_empty = httpx.Response(
|
city_lookup_empty = httpx.Response(
|
||||||
200,
|
200,
|
||||||
json=[],
|
json=[],
|
||||||
request=httpx.Request("GET", "https://api.cdek.test/v2/location/cities"),
|
request=httpx.Request("GET", "https://api.cdek.test/v2/location/suggest/cities"),
|
||||||
)
|
)
|
||||||
http_client = SequenceHTTPClient([city_lookup_empty])
|
http_client = SequenceHTTPClient([city_lookup_empty])
|
||||||
client = CDEKClient(
|
client = CDEKClient(
|
||||||
|
|||||||
@@ -177,6 +177,7 @@ def test_post_delivery_price_returns_prices_and_delegates_to_service() -> None:
|
|||||||
entity=DeliveryEntity.INDIVIDUAL,
|
entity=DeliveryEntity.INDIVIDUAL,
|
||||||
from_city="Moscow",
|
from_city="Moscow",
|
||||||
to_city="Kazan",
|
to_city="Kazan",
|
||||||
|
country_code=None,
|
||||||
weight_kg=2.5,
|
weight_kg=2.5,
|
||||||
length_cm=30.0,
|
length_cm=30.0,
|
||||||
width_cm=20.0,
|
width_cm=20.0,
|
||||||
@@ -184,6 +185,28 @@ def test_post_delivery_price_returns_prices_and_delegates_to_service() -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_post_delivery_price_accepts_optional_country_code() -> None:
|
||||||
|
service = StubAggregatorService(response=[])
|
||||||
|
app = create_app()
|
||||||
|
_install_service_override(app, service)
|
||||||
|
payload = _valid_payload()
|
||||||
|
payload["country_code"] = "kz"
|
||||||
|
|
||||||
|
async def run_request() -> httpx.Response:
|
||||||
|
transport = httpx.ASGITransport(app=app)
|
||||||
|
async with httpx.AsyncClient(
|
||||||
|
transport=transport,
|
||||||
|
base_url="http://testserver",
|
||||||
|
) as client:
|
||||||
|
return await client.post("/api/v1/delivery/price", json=payload)
|
||||||
|
|
||||||
|
response = asyncio.run(run_request())
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert len(service.calls) == 1
|
||||||
|
assert service.calls[0].country_code == "kz"
|
||||||
|
|
||||||
|
|
||||||
def test_post_delivery_price_rejects_invalid_payload() -> None:
|
def test_post_delivery_price_rejects_invalid_payload() -> None:
|
||||||
service = StubAggregatorService(response=[])
|
service = StubAggregatorService(response=[])
|
||||||
app = create_app()
|
app = create_app()
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ def _make_request(**overrides) -> DeliveryRequest:
|
|||||||
"entity": DeliveryEntity.INDIVIDUAL,
|
"entity": DeliveryEntity.INDIVIDUAL,
|
||||||
"from_city": " Moscow ",
|
"from_city": " Moscow ",
|
||||||
"to_city": " Saint Petersburg ",
|
"to_city": " Saint Petersburg ",
|
||||||
|
"country_code": " ru ",
|
||||||
"weight_kg": 1.235,
|
"weight_kg": 1.235,
|
||||||
"length_cm": 10.04,
|
"length_cm": 10.04,
|
||||||
"width_cm": 20.05,
|
"width_cm": 20.05,
|
||||||
@@ -45,15 +46,18 @@ def test_normalize_delivery_request_applies_rules_without_side_effects() -> None
|
|||||||
assert normalized is not request
|
assert normalized is not request
|
||||||
assert normalized.from_city == "Moscow"
|
assert normalized.from_city == "Moscow"
|
||||||
assert normalized.to_city == "Saint Petersburg"
|
assert normalized.to_city == "Saint Petersburg"
|
||||||
|
assert normalized.country_code == "RU"
|
||||||
assert normalized.weight_kg == Decimal("1.24")
|
assert normalized.weight_kg == Decimal("1.24")
|
||||||
assert normalized.length_cm == Decimal("10.0")
|
assert normalized.length_cm == Decimal("10.0")
|
||||||
assert normalized.width_cm == Decimal("20.1")
|
assert normalized.width_cm == Decimal("20.1")
|
||||||
assert normalized.height_cm == Decimal("30.1")
|
assert normalized.height_cm == Decimal("30.1")
|
||||||
assert request.from_city == " Moscow "
|
assert request.from_city == " Moscow "
|
||||||
|
assert request.country_code == " ru "
|
||||||
|
|
||||||
|
|
||||||
def test_normalize_delivery_request_handles_normalization_boundaries() -> None:
|
def test_normalize_delivery_request_handles_normalization_boundaries() -> None:
|
||||||
request = _make_request(
|
request = _make_request(
|
||||||
|
country_code=" ",
|
||||||
weight_kg=0.0001,
|
weight_kg=0.0001,
|
||||||
length_cm=0.01,
|
length_cm=0.01,
|
||||||
width_cm=0.04,
|
width_cm=0.04,
|
||||||
@@ -68,6 +72,7 @@ def test_normalize_delivery_request_handles_normalization_boundaries() -> None:
|
|||||||
assert low_scale.width_cm == Decimal("0.1")
|
assert low_scale.width_cm == Decimal("0.1")
|
||||||
assert low_scale.height_cm == Decimal("0.2")
|
assert low_scale.height_cm == Decimal("0.2")
|
||||||
assert high_scale.weight_kg == Decimal("0.01")
|
assert high_scale.weight_kg == Decimal("0.01")
|
||||||
|
assert low_scale.country_code is None
|
||||||
|
|
||||||
|
|
||||||
def test_filter_valid_prices_handles_empty_input() -> None:
|
def test_filter_valid_prices_handles_empty_input() -> None:
|
||||||
|
|||||||
@@ -58,16 +58,19 @@ class StubCache:
|
|||||||
self._storage[key] = value
|
self._storage[key] = value
|
||||||
|
|
||||||
|
|
||||||
def _make_request() -> DeliveryRequest:
|
def _make_request(**overrides: object) -> DeliveryRequest:
|
||||||
return DeliveryRequest(
|
payload: dict[str, object] = {
|
||||||
entity=DeliveryEntity.INDIVIDUAL,
|
"entity": DeliveryEntity.INDIVIDUAL,
|
||||||
from_city="Moscow",
|
"from_city": "Moscow",
|
||||||
to_city="Kazan",
|
"to_city": "Kazan",
|
||||||
weight_kg=1.234,
|
"country_code": None,
|
||||||
length_cm=30.0,
|
"weight_kg": 1.234,
|
||||||
width_cm=20.0,
|
"length_cm": 30.0,
|
||||||
height_cm=10.0,
|
"width_cm": 20.0,
|
||||||
)
|
"height_cm": 10.0,
|
||||||
|
}
|
||||||
|
payload.update(overrides)
|
||||||
|
return DeliveryRequest(**payload)
|
||||||
|
|
||||||
|
|
||||||
def _make_price(provider: str, price: str) -> DeliveryPrice:
|
def _make_price(provider: str, price: str) -> DeliveryPrice:
|
||||||
@@ -96,6 +99,17 @@ def test_get_all_prices_full_success_returns_sorted_and_updates_cache() -> None:
|
|||||||
assert [ttl for _, _, ttl in cache.set_calls] == [111, 222]
|
assert [ttl for _, _, ttl in cache.set_calls] == [111, 222]
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_all_prices_normalizes_and_forwards_country_code_to_providers() -> None:
|
||||||
|
provider = StubProvider(name="cdek", response=_make_price("cdek", "150.00"))
|
||||||
|
service = AggregatorService([provider], cache=StubCache())
|
||||||
|
|
||||||
|
result = asyncio.run(service.get_all_prices(_make_request(country_code=" kz ")))
|
||||||
|
|
||||||
|
assert [price.provider for price in result] == ["cdek"]
|
||||||
|
assert len(provider.calls) == 1
|
||||||
|
assert provider.calls[0].country_code == "KZ"
|
||||||
|
|
||||||
|
|
||||||
def test_get_all_prices_partial_failure_excludes_failed_provider() -> None:
|
def test_get_all_prices_partial_failure_excludes_failed_provider() -> None:
|
||||||
provider_ok = StubProvider(name="ok", response=_make_price("ok", "150.00"))
|
provider_ok = StubProvider(name="ok", response=_make_price("ok", "150.00"))
|
||||||
provider_failed = StubProvider(name="failed", error=RuntimeError("provider unavailable"))
|
provider_failed = StubProvider(name="failed", error=RuntimeError("provider unavailable"))
|
||||||
|
|||||||
Reference in New Issue
Block a user