"""Variant A — /estimate accepts client-provided coords, skips server geocode(). The address autocomplete/suggest already resolves lat/lon for addresses that the server-side geocode() chain can't (DaData-format inputs, dead Yandex key). When the client passes valid in-EKB coords we build the GeocodeResult directly and skip geocode() entirely — fixing the `address_not_geocoded` failure class. Contracts locked here: 1. Valid in-EKB client coords → geocode() is NOT awaited, still returns an estimate. 2. Out-of-bbox coords (Moscow) → ignored, geocode() IS called. 3. lat/lon both None → unchanged, geocode() IS called. Style mirrors test_estimator_cian_budget.py. """ import contextlib import os # Settings requires DATABASE_URL at init time. Set dummy DSN before any app import. os.environ.setdefault("DATABASE_URL", "postgresql://test:test@localhost/test_db") from unittest.mock import AsyncMock, MagicMock, patch import anyio def _make_fake_geo(): from app.services.geocoder import GeocodeResult return GeocodeResult( lat=56.838, lon=60.595, full_address="Свердловская обл., Екатеринбург, ул. Учителей, 18", provider="nominatim", ) def _make_payload(lat=None, lon=None): from app.schemas.trade_in import TradeInEstimateInput return TradeInEstimateInput( address="ЕКБ, ул. Учителей, 18", area_m2=38.8, rooms=1, floor=4, total_floors=16, lat=lat, lon=lon, ) def _patches(geocode_mock): """Common downstream mocks so estimate_quality runs offline to completion.""" return ( patch("app.services.estimator.geocode", new=geocode_mock), patch("app.services.estimator.get_house_metadata", new=AsyncMock(return_value=None)), patch("app.services.estimator._fetch_analogs", return_value=([], False, "W")), patch("app.services.estimator._fetch_deals", return_value=[]), patch("app.services.estimator._get_or_fetch_imv_cached", new=AsyncMock(return_value=None)), patch( "app.services.estimator._get_or_fetch_yandex_valuation_cached", new=AsyncMock(return_value=None), ), patch( "app.services.estimator.estimate_via_cian_valuation", new=AsyncMock(return_value=None), ), patch("app.services.estimator._get_asking_sold_ratio", return_value=(None, None)), ) def test_client_coords_in_ekb_skip_geocode() -> None: """Valid in-EKB client coords → geocode() NOT awaited, estimate still returned.""" from app.services.estimator import estimate_quality db = MagicMock() payload = _make_payload(lat=56.838, lon=60.595) geocode_mock = AsyncMock(return_value=_make_fake_geo()) async def _run() -> None: with contextlib.ExitStack() as stack: for cm in _patches(geocode_mock): stack.enter_context(cm) result = await estimate_quality(payload, db) geocode_mock.assert_not_awaited() assert result.estimate_id is not None anyio.run(_run) def test_out_of_bbox_coords_ignored_geocode_called() -> None: """Moscow coords are outside the EKB bbox → ignored, geocode() IS called.""" from app.services.estimator import estimate_quality db = MagicMock() payload = _make_payload(lat=55.75, lon=37.61) # Moscow — out of EKB bbox geocode_mock = AsyncMock(return_value=_make_fake_geo()) async def _run() -> None: with contextlib.ExitStack() as stack: for cm in _patches(geocode_mock): stack.enter_context(cm) result = await estimate_quality(payload, db) geocode_mock.assert_awaited() assert result.estimate_id is not None anyio.run(_run) def test_no_coords_geocode_called() -> None: """lat/lon both None → unchanged behaviour, geocode() IS called.""" from app.services.estimator import estimate_quality db = MagicMock() payload = _make_payload(lat=None, lon=None) geocode_mock = AsyncMock(return_value=_make_fake_geo()) async def _run() -> None: with contextlib.ExitStack() as stack: for cm in _patches(geocode_mock): stack.enter_context(cm) result = await estimate_quality(payload, db) geocode_mock.assert_awaited() assert result.estimate_id is not None anyio.run(_run)