Files
g2s-aggregator/tests/services/test_aggregator.py
T
Раис Юсупалиев 2d68aff34d fix cdek client
2026-03-08 23:35:04 +03:00

171 lines
5.7 KiB
Python

import asyncio
from decimal import Decimal
import pytest
from app.adapters.delivery_providers.base import ProviderRequestError
from app.schemas.request import DeliveryEntity, DeliveryRequest
from app.schemas.response import DeliveryPrice
from app.services.aggregator import AggregatorService, InvalidDeliveryRequestError
_MISSING = object()
class StubProvider:
def __init__(
self,
name: str,
*,
response: DeliveryPrice | None = None,
error: Exception | None = None,
cache_ttl_seconds: int = 900,
) -> None:
self.name = name
self._response = response
self._error = error
self.cache_ttl_seconds = cache_ttl_seconds
self.calls: list[DeliveryRequest] = []
async def get_price(self, request: DeliveryRequest) -> DeliveryPrice:
self.calls.append(request)
if self._error is not None:
raise self._error
if self._response is None:
raise RuntimeError("Stub provider has no response configured.")
return self._response
class StubCache:
def __init__(
self,
*,
forced_get_value: object = _MISSING,
) -> None:
self._storage: dict[str, object] = {}
self._forced_get_value = forced_get_value
self.get_calls: list[str] = []
self.set_calls: list[tuple[str, object, int | None]] = []
async def get(self, key: str) -> object | None:
self.get_calls.append(key)
if self._forced_get_value is not _MISSING:
return self._forced_get_value
return self._storage.get(key)
async def set(self, key: str, value: object, ttl: int | None = None) -> None:
self.set_calls.append((key, value, ttl))
self._storage[key] = value
def _make_request() -> DeliveryRequest:
return DeliveryRequest(
entity=DeliveryEntity.INDIVIDUAL,
from_city="Moscow",
to_city="Kazan",
weight_kg=1.234,
length_cm=30.0,
width_cm=20.0,
height_cm=10.0,
)
def _make_price(provider: str, price: str) -> DeliveryPrice:
return DeliveryPrice(
provider=provider,
service_name="standard",
price=Decimal(price),
currency="RUB",
delivery_days_min=2,
delivery_days_max=4,
)
def test_get_all_prices_full_success_returns_sorted_and_updates_cache() -> None:
provider_a = StubProvider(name="a", response=_make_price("a", "300.00"), cache_ttl_seconds=111)
provider_b = StubProvider(name="b", response=_make_price("b", "100.00"), cache_ttl_seconds=222)
cache = StubCache()
service = AggregatorService([provider_a, provider_b], cache=cache)
result = asyncio.run(service.get_all_prices(_make_request()))
assert [price.provider for price in result] == ["b", "a"]
assert len(provider_a.calls) == 1
assert len(provider_b.calls) == 1
assert len(cache.get_calls) == 2
assert [ttl for _, _, ttl in cache.set_calls] == [111, 222]
def test_get_all_prices_partial_failure_excludes_failed_provider() -> None:
provider_ok = StubProvider(name="ok", response=_make_price("ok", "150.00"))
provider_failed = StubProvider(name="failed", error=RuntimeError("provider unavailable"))
service = AggregatorService([provider_ok, provider_failed], cache=StubCache())
result = asyncio.run(service.get_all_prices(_make_request()))
assert [price.provider for price in result] == ["ok"]
assert len(provider_ok.calls) == 1
assert len(provider_failed.calls) == 1
def test_get_all_prices_all_failures_returns_empty_list() -> None:
provider_a = StubProvider(name="a", error=RuntimeError("a down"))
provider_b = StubProvider(name="b", error=RuntimeError("b down"))
service = AggregatorService([provider_a, provider_b], cache=StubCache())
result = asyncio.run(service.get_all_prices(_make_request()))
assert result == []
assert len(provider_a.calls) == 1
assert len(provider_b.calls) == 1
def test_get_all_prices_raises_invalid_request_for_provider_request_errors() -> None:
provider = StubProvider(
name="cdek",
error=ProviderRequestError("city not found"),
)
service = AggregatorService([provider], cache=StubCache())
with pytest.raises(InvalidDeliveryRequestError):
asyncio.run(service.get_all_prices(_make_request()))
assert len(provider.calls) == 1
def test_get_all_prices_cache_hit_skips_provider_call() -> None:
cached_payload = _make_price("cdek", "99.00").model_dump(mode="json")
cache = StubCache(forced_get_value=cached_payload)
provider = StubProvider(name="cdek", response=_make_price("cdek", "150.00"))
service = AggregatorService([provider], cache=cache)
result = asyncio.run(service.get_all_prices(_make_request()))
assert [price.provider for price in result] == ["cdek"]
assert result[0].price == Decimal("99.00")
assert provider.calls == []
assert len(cache.get_calls) == 1
assert cache.set_calls == []
def test_get_all_prices_delegates_filtering_and_sorting_to_domain_logic() -> None:
provider_a = StubProvider(name="a", response=_make_price("a", "300.00"))
provider_b = StubProvider(name="b", response=_make_price("b", "100.00"))
delegated_inputs: list[list[DeliveryPrice]] = []
def fake_filter_and_sort(prices):
price_list = list(prices)
delegated_inputs.append(price_list)
return [price_list[0]]
service = AggregatorService(
[provider_a, provider_b],
cache=StubCache(),
filter_and_sort_prices_fn=fake_filter_and_sort,
)
result = asyncio.run(service.get_all_prices(_make_request()))
assert [price.provider for price in delegated_inputs[0]] == ["a", "b"]
assert [price.provider for price in result] == ["a"]