"""#3051: скоуп региона в estimate — fast-path и вызов geocode(). ПОЧЕМУ ЭТО ТЕСТ. Оба тира geocode() ограничены регионом ЖЁСТКИМ фильтром (DaData `locations.region`, Nominatim `viewbox`+`bounded=1`), а не бустом: промах региона не даёт ошибки — выдача схлопывается в пустую, и estimate возвращает `_empty_estimate(reason='address_not_geocoded')`, неотличимую от «такого адреса нет». Значит проверять надо не результат, а ЧТО именно уходит в geocode(). Второй предмет — регресс-нейтральность 66: без московских данных поведение обязано быть прежним. Сеть и БД не дёргаем: geocode и _empty_estimate мокаются, db — MagicMock. """ from __future__ import annotations import os from unittest.mock import AsyncMock, MagicMock, patch os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost/test_db") import pytest from app.schemas.trade_in import TradeInEstimateInput from app.services.estimator import _request_region_code, estimate_quality pytestmark = pytest.mark.anyio # Тверская 6 (Москва) и Малышева 30 (Екатеринбург) — точки внутри bbox_region # соответствующих регионов реестра. MSK = (55.7605, 37.6100) EKB = (56.8380, 60.6000) def _payload(**kw) -> TradeInEstimateInput: base = {"address": "Тверская 6", "area_m2": 50.0, "rooms": 2} base.update(kw) return TradeInEstimateInput(**base) # ── _request_region_code: приоритет координаты → city_hint → 66 ────────────── def test_region_from_moscow_coords() -> None: assert _request_region_code(_payload(lat=MSK[0], lon=MSK[1])) == 77 def test_region_from_ekb_coords() -> None: assert _request_region_code(_payload(lat=EKB[0], lon=EKB[1])) == 66 def test_region_without_coords_defaults_to_66() -> None: """Нет координат и нет узнаваемого города — прежний дефолт 66.""" assert _request_region_code(_payload()) == 66 assert _request_region_code(_payload(city_hint="Урюпинск")) == 66 def test_region_from_city_hint() -> None: assert _request_region_code(_payload(city_hint="Москва")) == 77 assert _request_region_code(_payload(city_hint="Нижний Тагил")) == 66 def test_region_coords_outside_any_region_default_66() -> None: """Точка вне охвата (Сочи) → 66, а НЕ None: NULL обнулил бы фильтр.""" assert _request_region_code(_payload(lat=43.6, lon=39.7)) == 66 # ── fast-path клиентских координат: региононезависимость ───────────────────── async def _run_estimate(payload: TradeInEstimateInput): """estimate_quality до первой развилки: geocode → None → _empty_estimate. Если fast-path принял клиентские координаты, функция идёт дальше, в счёт по БД, и спотыкается о MagicMock-сессию — это ожидаемо и подавляется: предмет проверки здесь ровно один, БЫЛ ли вызван geocode() и с каким регионом. """ geocode_mock = AsyncMock(return_value=None) empty_mock = MagicMock(return_value="EMPTY") result = None with ( patch("app.services.estimator.geocode", new=geocode_mock), patch("app.services.estimator._empty_estimate", new=empty_mock), ): try: result = await estimate_quality(payload, MagicMock()) except Exception: # дальше по функции живая БД, см. докстринг pass return geocode_mock, empty_mock, result async def test_fast_path_accepts_moscow_coords() -> None: """Московские координаты принимаются как клиентские — geocode не зовём.""" geocode_mock, _, _ = await _run_estimate(_payload(lat=MSK[0], lon=MSK[1])) geocode_mock.assert_not_awaited() async def test_fast_path_accepts_ekb_coords_unchanged() -> None: """Регресс 66: координаты области по-прежнему минуют geocode().""" geocode_mock, _, _ = await _run_estimate(_payload(lat=EKB[0], lon=EKB[1])) geocode_mock.assert_not_awaited() async def test_fast_path_ignores_coords_outside_coverage() -> None: """Точка вне охвата (Сочи) — как и раньше, честный geocode().""" geocode_mock, _, _ = await _run_estimate(_payload(lat=43.6, lon=39.7)) geocode_mock.assert_awaited_once() # ── geocode() получает регион запроса ──────────────────────────────────────── async def test_geocode_gets_region_77_for_moscow() -> None: geocode_mock, empty_mock, result = await _run_estimate( _payload(address="Тверская 6", city_hint="Москва") ) assert geocode_mock.await_args.kwargs["region_code"] == 77 assert result == "EMPTY" assert empty_mock.call_args.kwargs["reason"] == "address_not_geocoded" async def test_geocode_gets_region_66_for_ekb() -> None: geocode_mock, _, _ = await _run_estimate( _payload(address="Малышева 30", city_hint="Екатеринбург") ) assert geocode_mock.await_args.kwargs["region_code"] == 66 async def test_geocode_default_region_66_without_hints() -> None: """Регресс-нейтральность: без координат и city_hint — прежний скоуп 66.""" geocode_mock, _, _ = await _run_estimate(_payload(address="Малышева 30")) assert geocode_mock.await_args.kwargs["region_code"] == 66 assert geocode_mock.await_args.kwargs["city_hint"] is None