129 lines
4.4 KiB
Python
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)
|