"""#3051: `suggest()` умеет регион — и по умолчанию остаётся свердловским. ПОЧЕМУ ЭТО ВООБЩЕ ТЕСТ. Оба внешних тира подсказок ограничены регионом ЖЁСТКИМ фильтром, а не бустом: DaData `locations.region` и Nominatim `viewbox`+`bounded=1`. Промах региона не даёт ни ошибки, ни warning'а от провайдера — выдача схлопывается в ПУСТОЙ список, неотличимый от «такого адреса нет». Ровно так московский адрес молча возвращал ноль подсказок при свердловском констрейнте. Значит проверять надо не результат, а то, ЧТО именно уходит провайдеру. Второй, более важный предмет проверки — регресс-нейтральность: вызов без `region_code` обязан слать провайдерам те же самые значения, что и до правки. Сеть не дёргаем: тиры мокаются по образцу `test_geocoder_city_hint`. """ from __future__ import annotations import os from unittest.mock import AsyncMock, MagicMock, patch os.environ.setdefault("DATABASE_URL", "postgresql://test:test@localhost/test_db") import pytest from app.api.v1 import geocode as geocode_module from app.api.v1.geocode import SuggestResponse, effective_region_code from app.services.geocoder import ( OBLAST66_VIEWBOX, SVERDLOVSK_OBLAST_REGION, _dadata_suggest, _nominatim_suggest, _viewbox_for_region, suggest, ) from app.services.regions import REGIONS pytestmark = pytest.mark.anyio # ── DaData-тир: имя региона в hard-констрейнте ─────────────────────────────── async def test_dadata_suggest_default_region_unchanged() -> None: """Без region_code — прежняя константа «Свердловская» (БЕЗ типа).""" mock = AsyncMock(return_value=[]) with patch("app.services.geocoder.dadata.suggest_addresses", new=mock): assert await _dadata_suggest("Малышева 30", limit=5) == [] assert mock.await_args.kwargs["region"] == SVERDLOVSK_OBLAST_REGION assert mock.await_args.kwargs["region"] == "Свердловская" assert mock.await_args.kwargs["city"] is None async def test_dadata_suggest_region_77_sends_moscow() -> None: """region_code=77 — в DaData уходит «Москва», а не свердловский констрейнт.""" mock = AsyncMock(return_value=[]) with patch("app.services.geocoder.dadata.suggest_addresses", new=mock): assert await _dadata_suggest("Тверская 6", limit=5, region_code=77) == [] assert mock.await_args.kwargs["region"] == "Москва" async def test_dadata_suggest_unknown_region_raises() -> None: """Регион вне реестра — явная ошибка, а не молчаливый пустой список.""" with patch("app.services.geocoder.dadata.suggest_addresses", new=AsyncMock(return_value=[])): with pytest.raises(ValueError, match="unknown region_code"): await _dadata_suggest("Ленина 1", limit=5, region_code=99) # ── Nominatim-тир: рамка региона ───────────────────────────────────────────── def test_viewbox_for_region_66_is_literal_constant() -> None: """Для 66 рамка — историческая константа, не пересчёт из bbox реестра.""" assert _viewbox_for_region(66) == OBLAST66_VIEWBOX["viewbox"] def test_viewbox_for_region_77_covers_moscow() -> None: """Рамка 77 строится из bbox_region реестра: lon_min,lat_max,lon_max,lat_min.""" lat_min, lat_max, lon_min, lon_max = REGIONS[77].bbox_region assert _viewbox_for_region(77) == f"{lon_min},{lat_max},{lon_max},{lat_min}" assert _viewbox_for_region(77) != OBLAST66_VIEWBOX["viewbox"] async def test_nominatim_suggest_default_viewbox_and_suffix_unchanged() -> None: """Дефолтный вызов: свердловская рамка + ЕКБ-суффикс dual-query (#2580 C2).""" seen: list[tuple[str, str]] = [] async def fake_get(url, params=None, **_kw): seen.append((params["q"], params["viewbox"])) response = MagicMock() response.json.return_value = [] response.raise_for_status.return_value = None return response with ( patch("httpx.AsyncClient.get", new=AsyncMock(side_effect=fake_get)), patch("app.services.geocoder._nominatim_throttle", new=AsyncMock()), ): await _nominatim_suggest("Ленина, 1", limit=5) queries = [q for q, _ in seen] assert "Ленина, 1, Екатеринбург" in queries assert "Ленина, 1" in queries assert {vb for _, vb in seen} == {OBLAST66_VIEWBOX["viewbox"]} async def test_nominatim_suggest_region_77_sends_moscow_frame() -> None: """region_code=77: московская рамка и московский суффикс, ЕКБ не упоминается.""" seen: list[tuple[str, str]] = [] async def fake_get(url, params=None, **_kw): seen.append((params["q"], params["viewbox"])) response = MagicMock() response.json.return_value = [] response.raise_for_status.return_value = None return response with ( patch("httpx.AsyncClient.get", new=AsyncMock(side_effect=fake_get)), patch("app.services.geocoder._nominatim_throttle", new=AsyncMock()), ): await _nominatim_suggest("Тверская, 6", limit=5, region_code=77) queries = [q for q, _ in seen] assert "Тверская, 6, Москва" in queries assert not any("Екатеринбург" in q for q in queries) assert {vb for _, vb in seen} == {_viewbox_for_region(77)} # ── suggest(): прокидывание региона и гейт локальных ЕКБ-тиров ─────────────── async def test_suggest_passes_region_to_both_tiers() -> None: """region_code доезжает и до DaData, и до Nominatim-фолбэка.""" dadata_mock = AsyncMock(return_value=[]) nominatim_mock = AsyncMock(return_value=[]) with ( patch("app.services.geocoder._dadata_suggest", new=dadata_mock), patch("app.services.geocoder._nominatim_suggest", new=nominatim_mock), patch("app.services.geocoder.settings") as mock_settings, ): mock_settings.dadata_api_token = "token" await suggest("Тверская 6", db=None, limit=5, region_code=77) assert dadata_mock.await_args.args[2] == 77 assert nominatim_mock.await_args.kwargs["region_code"] == 77 async def test_suggest_region_77_skips_cadastral_tier() -> None: """Кадастровый тир (ЕКБ-FDW) для 77 не зовётся вовсе — данных там нет.""" db = MagicMock() with ( patch("app.services.geocoder._cadastral_house_match") as house_mock, patch("app.services.geocoder._cadastral_forward_sync") as forward_mock, patch("app.services.geocoder._nominatim_suggest", new=AsyncMock(return_value=[])), patch("app.services.geocoder.settings") as mock_settings, ): mock_settings.dadata_api_token = None await suggest("Тверская 6", db=db, limit=5, region_code=77) house_mock.assert_not_called() forward_mock.assert_not_called() async def test_suggest_default_still_uses_cadastral_tier() -> None: """Регресс-контроль: дефолтный (66) вызов кадастровый тир по-прежнему зовёт.""" db = MagicMock() with ( patch("app.services.geocoder._cadastral_house_match", return_value=None) as house_mock, patch("app.services.geocoder._cadastral_forward_sync", return_value=[]) as forward_mock, patch("app.services.geocoder._nominatim_suggest", new=AsyncMock(return_value=[])), patch("app.services.geocoder.settings") as mock_settings, ): mock_settings.dadata_api_token = None await suggest("Малышева 30", db=db, limit=5) assert house_mock.called or forward_mock.called async def test_suggest_unknown_region_raises() -> None: """Неизвестный регион — ValueError до похода к провайдерам.""" with pytest.raises(ValueError, match="unknown region_code"): await suggest("Ленина 1", db=None, limit=5, region_code=99) # ── API-слой: effective_region_code и хендлер /suggest ────────────────────── # # Всё выше проверяет, что `suggest()` в geocoder.py УМЕЕТ регион. Ниже — # СЛЕДУЮЩИЙ слой той же истории #3051: сам HTTP-хендлер `/suggest` (и его # публичный прокси в mera.py) до сих пор звал `suggest()` с `region_code=66` # по умолчанию БЕЗУСЛОВНО, даже когда `city_hint='Москва'` уже прямо говорил, # какой регион нужен. Приоритет: явный `region_code` > вывод из `city_hint` # (реестр `app.services.regions`) > 66. Инварианты ниже — на хелпере # `effective_region_code`, который эту логику и вносит. def test_effective_region_no_hint_no_explicit_defaults_to_66() -> None: assert effective_region_code(None, None) == 66 def test_effective_region_sverdlovsk_city_hint_stays_66() -> None: assert effective_region_code(None, "Серов") == 66 def test_effective_region_moscow_city_hint_infers_77() -> None: assert effective_region_code(None, "Москва") == 77 def test_effective_region_explicit_wins_over_moscow_hint() -> None: assert effective_region_code(66, "Москва") == 66 async def test_suggest_handler_resolves_region_from_moscow_hint() -> None: """`suggest_addresses` без явного `region_code` реально передаёт вниз 77, а не только хелпер в изоляции.""" captured: dict[str, object] = {} async def _fake_suggest(q, db=None, limit=8, city_hint=None, region_code=66): captured["region_code"] = region_code return [] with patch.object(geocode_module, "suggest", _fake_suggest): result = await geocode_module.suggest_addresses( q="Тверская 6", limit=8, db=MagicMock(), city_hint="Москва", region_code=None, ) assert isinstance(result, SuggestResponse) assert captured["region_code"] == 77