import asyncio import httpx import pytest from app.controllers.v1 import delivery as delivery_controller from app.controllers.v1.delivery import get_aggregator_service from app.main import create_app from app.schemas.request import AddressSuggestRequest, SuggestAddressRequest from app.schemas.response import AddressSuggestion from app.services.aggregator import ( AddressSuggestionsUnavailableError, InvalidAddressSuggestRequestError, UnsupportedAddressSuggestionCountryError, ) class StubAggregatorService: def __init__(self, *, response: object, error: Exception | None = None) -> None: self._response = response self._error = error self.calls: list[SuggestAddressRequest] = [] async def suggest_addresses(self, request: SuggestAddressRequest) -> object: self.calls.append(request) if self._error is not None: raise self._error return self._response def _install_service_override(app, service: StubAggregatorService) -> None: async def override_service() -> StubAggregatorService: return service app.dependency_overrides[get_aggregator_service] = override_service def _valid_payload(**overrides: object) -> dict[str, object]: payload = { "city": "4097", "query": "Lenina", "limit": 5, } payload.update(overrides) return payload def test_post_address_suggest_uses_registered_provider_in_default_dependency( monkeypatch: pytest.MonkeyPatch, ) -> None: class StubHttpClient: async def aclose(self) -> None: return None class StubCDEKProvider: name = "stub-cdek" cache_ttl_seconds = 900 @classmethod def from_adapter_config(cls, *, http_client, adapter_config) -> "StubCDEKProvider": assert http_client is stub_http_client _ = adapter_config return cls() class StubAddressProvider: def __init__(self, name: str, response: list[AddressSuggestion]) -> None: self.name = name self._response = response self.calls: list[AddressSuggestRequest] = [] async def suggest( self, request: AddressSuggestRequest ) -> list[AddressSuggestion]: self.calls.append(request) return self._response class StubDadataAddressSuggestionProvider: @classmethod def from_config( cls, *, http_client, config, ) -> StubAddressProvider: assert http_client is stub_http_client _ = config return stub_dadata_provider class StubYandexGeosuggestAddressSuggestionProvider: @classmethod def from_config( cls, *, http_client, config, ) -> StubAddressProvider: assert http_client is stub_http_client _ = config return stub_yandex_provider class StubTomTomAddressSuggestionProvider: @classmethod def from_config( cls, *, http_client, config, ) -> StubAddressProvider: assert http_client is stub_http_client _ = config return stub_tomtom_provider class StubCache: async def get(self, key: str) -> object | None: _ = key return None async def set(self, key: str, value: object, ttl: int | None = None) -> None: _ = key _ = value _ = ttl class StubPriceCache: @classmethod def from_repository_config(cls, repository_config) -> StubCache: _ = repository_config return StubCache() stub_http_client = StubHttpClient() stub_dadata_provider = StubAddressProvider( "dadata", [ AddressSuggestion( address="107241, Moscow, Khabarovskaya 1", street="Khabarovskaya", house="1", flat="25", postal_code="107241", ) ], ) stub_yandex_provider = StubAddressProvider( "yandex_geosuggest", [ AddressSuggestion( address="Tumanyan Street, 12, Yerevan", street="Tumanyan Street", house="12", ) ], ) stub_tomtom_provider = StubAddressProvider( "tomtom", [ AddressSuggestion( address="Alexanderplatz 1, 10178 Berlin", street="Alexanderplatz", house="1", postal_code="10178", ) ], ) http_client_timeouts: list[float] = [] def fake_build_controller_http_client(timeout_seconds: float) -> StubHttpClient: http_client_timeouts.append(timeout_seconds) return stub_http_client monkeypatch.setattr( delivery_controller, "build_controller_http_client", fake_build_controller_http_client, ) monkeypatch.setattr(delivery_controller, "CDEKProvider", StubCDEKProvider) monkeypatch.setattr( delivery_controller, "DadataAddressSuggestionProvider", StubDadataAddressSuggestionProvider, ) monkeypatch.setattr( delivery_controller, "YandexGeosuggestAddressSuggestionProvider", StubYandexGeosuggestAddressSuggestionProvider, ) monkeypatch.setattr( delivery_controller, "TomTomAddressSuggestionProvider", StubTomTomAddressSuggestionProvider, ) monkeypatch.setattr(delivery_controller, "PriceCache", StubPriceCache) app = create_app() request_payload = _valid_payload( city="1124", query="Alexanderplatz 1", ) 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/suggest-address", json=request_payload, ) response = asyncio.run(run_request()) assert response.status_code == 200 assert response.json() == [ { "address": "Alexanderplatz 1, 10178 Berlin", "street": "Alexanderplatz", "house": "1", "flat": None, "postal_code": "10178", } ] assert http_client_timeouts == [10.0] assert stub_dadata_provider.calls == [] assert stub_yandex_provider.calls == [] assert stub_tomtom_provider.calls == [ AddressSuggestRequest( country_code="DE", city="Берлин", query="Alexanderplatz 1", limit=5, ) ] def test_post_address_suggest_returns_response_and_delegates_to_service() -> None: expected_response = [ AddressSuggestion( address="107241, Moscow, Khabarovskaya 1", street="Khabarovskaya", house="1", flat="25", postal_code="107241", ) ] service = StubAggregatorService(response=expected_response) app = create_app() _install_service_override(app, service) 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/suggest-address", json=_valid_payload(), ) response = asyncio.run(run_request()) assert response.status_code == 200 assert response.json() == [item.model_dump(mode="json") for item in expected_response] assert service.calls == [SuggestAddressRequest(**_valid_payload())] @pytest.mark.parametrize( ("field_name", "field_value"), [("city", ""), ("limit", 0), ("query", "")], ) def test_post_address_suggest_rejects_invalid_payload( field_name: str, field_value: object, ) -> None: service = StubAggregatorService(response=[]) app = create_app() _install_service_override(app, service) payload = _valid_payload() payload[field_name] = field_value 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/suggest-address", json=payload, ) response = asyncio.run(run_request()) assert response.status_code == 422 assert service.calls == [] def test_post_address_suggest_maps_unsupported_country_to_400() -> None: service = StubAggregatorService( response=None, error=UnsupportedAddressSuggestionCountryError("unsupported country"), ) app = create_app() _install_service_override(app, service) async def run_request() -> httpx.Response: transport = httpx.ASGITransport(app=app, raise_app_exceptions=False) async with httpx.AsyncClient( transport=transport, base_url="http://testserver", ) as client: return await client.post( "/api/v1/delivery/suggest-address", json=_valid_payload(), ) response = asyncio.run(run_request()) assert response.status_code == 400 assert response.json() == { "detail": { "code": "unsupported_address_suggestion_country", "message": "Address suggestions are not configured for the requested country.", } } def test_post_address_suggest_maps_invalid_request_to_400() -> None: service = StubAggregatorService( response=None, error=InvalidAddressSuggestRequestError("provider rejected request"), ) app = create_app() _install_service_override(app, service) async def run_request() -> httpx.Response: transport = httpx.ASGITransport(app=app, raise_app_exceptions=False) async with httpx.AsyncClient( transport=transport, base_url="http://testserver", ) as client: return await client.post( "/api/v1/delivery/suggest-address", json=_valid_payload(), ) response = asyncio.run(run_request()) assert response.status_code == 400 assert response.json() == { "detail": { "code": "invalid_address_suggest_request", "message": "Address suggestion request contains invalid or unsupported provider data.", } } def test_post_address_suggest_maps_service_exception_to_503() -> None: service = StubAggregatorService( response=None, error=AddressSuggestionsUnavailableError("provider unavailable"), ) app = create_app() _install_service_override(app, service) async def run_request() -> httpx.Response: transport = httpx.ASGITransport(app=app, raise_app_exceptions=False) async with httpx.AsyncClient( transport=transport, base_url="http://testserver", ) as client: return await client.post( "/api/v1/delivery/suggest-address", json=_valid_payload(), ) response = asyncio.run(run_request()) assert response.status_code == 503 assert response.json() == { "detail": { "code": "address_suggestions_unavailable", "message": "Address suggestions are temporarily unavailable.", } }