import asyncio import pytest from app.adapters.address_suggestions.base import ( AddressSuggestionClientError, AddressSuggestionRequestError, ) from app.cities import cities_map from app.schemas.request import AddressSuggestRequest, SuggestAddressRequest from app.schemas.response import AddressSuggestion from app.services.aggregator import ( AddressSuggestionsUnavailableError, AggregatorService, InvalidAddressSuggestRequestError, UnsupportedAddressSuggestionCountryError, ) _DADATA_COUNTRIES = ("RU", "BY", "KZ") _YANDEX_COUNTRIES = ("AM", "AZ", "KG", "MD", "TJ", "TM", "UZ") # UA is intentionally absent: cities_map has no Ukrainian city to resolve. _TOMTOM_COUNTRIES = ( "AL", "AT", "BE", "BG", "CH", "CZ", "DE", "DK", "EE", "ES", "FI", "FR", "GB", "GR", "HR", "HU", "IE", "IT", "LT", "LV", "NL", "NO", "PL", "PT", "RO", "RS", "SE", "SI", "SK", ) def _first_city(country_code: str) -> tuple[str, str]: for city_id, entry in cities_map.items(): if isinstance(entry, dict) and entry.get("country") == country_code: return city_id, entry["city"] raise AssertionError(f"cities_map has no city for country {country_code}") class StubAddressSuggestionProvider: def __init__( self, name: str, *, response: list[AddressSuggestion] | None = None, error: Exception | None = None, ) -> None: self.name = name self._response = response if response is not None else [] self._error = error self.calls: list[AddressSuggestRequest] = [] async def suggest(self, request: AddressSuggestRequest) -> list[AddressSuggestion]: self.calls.append(request) if self._error is not None: raise self._error return self._response def _make_request(country_code: str, *, query: str = "Lenina") -> SuggestAddressRequest: city_id, _ = _first_city(country_code) return SuggestAddressRequest(city=city_id, query=query, limit=5) def _expected_provider_request( country_code: str, *, query: str = "Lenina" ) -> AddressSuggestRequest: _, city_name = _first_city(country_code) return AddressSuggestRequest( country_code=country_code, city=city_name, query=query, limit=5, ) def _make_suggestion( address: str, *, street: str | None = None, house: str | None = None, flat: str | None = None, postal_code: str | None = None, ) -> AddressSuggestion: return AddressSuggestion( address=address, street=street, house=house, flat=flat, postal_code=postal_code, ) def _make_country_mapping() -> dict[str, str]: return { **{country_code: "dadata" for country_code in _DADATA_COUNTRIES}, **{ country_code: "yandex_geosuggest" for country_code in _YANDEX_COUNTRIES }, **{country_code: "tomtom" for country_code in _TOMTOM_COUNTRIES}, } @pytest.mark.parametrize("country_code", _DADATA_COUNTRIES) def test_suggest_addresses_routes_dadata_countries_to_dadata( country_code: str, ) -> None: dadata = StubAddressSuggestionProvider( "dadata", response=[ _make_suggestion( "107241, Moscow, Khabarovskaya 1", street="Khabarovskaya", house="1", flat="25", postal_code="107241", ) ], ) yandex_geosuggest = StubAddressSuggestionProvider( "yandex_geosuggest", response=[_make_suggestion("Yerevan, Tumanyan 1")], ) tomtom = StubAddressSuggestionProvider( "tomtom", response=[_make_suggestion("Alexanderplatz 1, 10178 Berlin")], ) service = AggregatorService( providers=[], address_suggestion_providers=[dadata, yandex_geosuggest, tomtom], address_suggestion_country_to_provider=_make_country_mapping(), ) request = _make_request(country_code, query="Khabarovskaya") result = asyncio.run(service.suggest_addresses(request)) assert result == [ AddressSuggestion( address="107241, Moscow, Khabarovskaya 1", street="Khabarovskaya", house="1", flat="25", postal_code="107241", ) ] assert dadata.calls == [ _expected_provider_request(country_code, query="Khabarovskaya") ] assert yandex_geosuggest.calls == [] assert tomtom.calls == [] @pytest.mark.parametrize("country_code", _YANDEX_COUNTRIES) def test_suggest_addresses_routes_cis_countries_to_yandex_geosuggest( country_code: str, ) -> None: dadata = StubAddressSuggestionProvider( "dadata", response=[_make_suggestion("Moscow, Lenina 1")], ) yandex_geosuggest = StubAddressSuggestionProvider( "yandex_geosuggest", response=[_make_suggestion("Yerevan, Tumanyan 1")], ) tomtom = StubAddressSuggestionProvider( "tomtom", response=[_make_suggestion("Alexanderplatz 1, 10178 Berlin")], ) service = AggregatorService( providers=[], address_suggestion_providers=[dadata, yandex_geosuggest, tomtom], address_suggestion_country_to_provider=_make_country_mapping(), ) request = _make_request(country_code, query="Tumanyan") result = asyncio.run(service.suggest_addresses(request)) assert result == [AddressSuggestion(address="Yerevan, Tumanyan 1")] assert dadata.calls == [] assert yandex_geosuggest.calls == [ _expected_provider_request(country_code, query="Tumanyan") ] assert tomtom.calls == [] @pytest.mark.parametrize("country_code", _TOMTOM_COUNTRIES) def test_suggest_addresses_routes_european_countries_to_tomtom( country_code: str, ) -> None: dadata = StubAddressSuggestionProvider( "dadata", response=[_make_suggestion("Moscow, Lenina 1")], ) yandex_geosuggest = StubAddressSuggestionProvider( "yandex_geosuggest", response=[_make_suggestion("Yerevan, Tumanyan 1")], ) tomtom = StubAddressSuggestionProvider( "tomtom", response=[ _make_suggestion( "Alexanderplatz 1, 10178 Berlin", street="Alexanderplatz", house="1", postal_code="10178", ) ], ) service = AggregatorService( providers=[], address_suggestion_providers=[dadata, yandex_geosuggest, tomtom], address_suggestion_country_to_provider=_make_country_mapping(), ) request = _make_request(country_code, query="Alexanderplatz 1") result = asyncio.run(service.suggest_addresses(request)) assert result == [ AddressSuggestion( address="Alexanderplatz 1, 10178 Berlin", street="Alexanderplatz", house="1", flat=None, postal_code="10178", ) ] assert dadata.calls == [] assert yandex_geosuggest.calls == [] assert tomtom.calls == [ _expected_provider_request(country_code, query="Alexanderplatz 1") ] def test_suggest_addresses_raises_for_unknown_city_id() -> None: service = AggregatorService( providers=[], address_suggestion_providers=[ StubAddressSuggestionProvider( "dadata", response=[_make_suggestion("Moscow, Lenina 1")], ) ], address_suggestion_country_to_provider={"RU": "dadata"}, ) with pytest.raises(InvalidAddressSuggestRequestError): asyncio.run( service.suggest_addresses( SuggestAddressRequest(city="nonexistent-city-id", query="Lenina") ) ) def test_suggest_addresses_raises_for_unsupported_country() -> None: service = AggregatorService( providers=[], address_suggestion_providers=[ StubAddressSuggestionProvider( "dadata", response=[_make_suggestion("Moscow, Lenina 1")], ) ], address_suggestion_country_to_provider={"RU": "dadata"}, ) with pytest.raises(UnsupportedAddressSuggestionCountryError): asyncio.run( service.suggest_addresses(_make_request("DE", query="Alexanderplatz")) ) def test_suggest_addresses_raises_for_unregistered_provider_mapping() -> None: service = AggregatorService( providers=[], address_suggestion_providers=[ StubAddressSuggestionProvider( "dadata", response=[_make_suggestion("Moscow, Lenina 1")], ) ], address_suggestion_country_to_provider={"AM": "yandex_geosuggest"}, ) with pytest.raises(UnsupportedAddressSuggestionCountryError): asyncio.run(service.suggest_addresses(_make_request("AM", query="Tumanyan"))) def test_suggest_addresses_maps_provider_request_error_to_invalid_request() -> None: service = AggregatorService( providers=[], address_suggestion_providers=[ StubAddressSuggestionProvider( "dadata", error=AddressSuggestionRequestError("provider rejected request"), ) ], address_suggestion_country_to_provider={"RU": "dadata"}, ) with pytest.raises(InvalidAddressSuggestRequestError): asyncio.run(service.suggest_addresses(_make_request("RU"))) def test_suggest_addresses_maps_provider_client_error_to_unavailable() -> None: service = AggregatorService( providers=[], address_suggestion_providers=[ StubAddressSuggestionProvider( "dadata", error=AddressSuggestionClientError("provider unavailable"), ) ], address_suggestion_country_to_provider={"RU": "dadata"}, ) with pytest.raises(AddressSuggestionsUnavailableError): asyncio.run(service.suggest_addresses(_make_request("RU")))