gendesign/tradein-mvp/backend/tests/test_estimator_client_coords.py
bot-backend 99e2a5d75c
All checks were successful
CI / changes (pull_request) Successful in 6s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
feat(tradein): estimate accepts client-provided coords, skips geocode (Variant A)
2026-06-19 14:15:02 +03:00

129 lines
4.4 KiB
Python

"""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)