radius_m (schema + estimate_quality comp-search) уже в main (e23dabe4). Здесь —
остаток: два derived-роута принимают radius_m как optional query-param.
- GET /estimate/{id}/house-analytics и /sell-time-sensitivity: += radius_m
(int|None). None → байт-идентичная авто-логика (расширение до 300 м только при
<8 in-house записей). Задан → явный радиус расширения выборки домов, клампится
в [100,5000], возвращается в radius_m ответа.
Refs #2044
61 lines
2.4 KiB
Python
61 lines
2.4 KiB
Python
"""Unit tests for #2044 (BE-2) optional radius_m input.
|
|
|
|
The schema field + estimate_quality wiring (base_radius_m / fallback_radius_m)
|
|
landed in e23dabe4; these tests lock the contract: radius_m is an optional,
|
|
bounded field whose None value preserves the byte-identical two-tier default
|
|
radii, while an explicit value overrides both search radii.
|
|
|
|
NOTE: importing app.services.estimator pulls app.core.config.Settings which
|
|
requires DATABASE_URL. Set it BEFORE importing app modules.
|
|
"""
|
|
|
|
import os
|
|
|
|
os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost:5432/test")
|
|
|
|
import pytest
|
|
from pydantic import ValidationError
|
|
|
|
from app.schemas.trade_in import TradeInEstimateInput
|
|
from app.services import estimator
|
|
|
|
|
|
def test_radius_m_defaults_to_none() -> None:
|
|
payload = TradeInEstimateInput(address="ул. Тестовая, 1", area_m2=50, rooms=2)
|
|
assert payload.radius_m is None
|
|
|
|
|
|
def test_radius_m_accepts_valid_int() -> None:
|
|
payload = TradeInEstimateInput(address="ул. Тестовая, 1", area_m2=50, rooms=2, radius_m=1500)
|
|
assert payload.radius_m == 1500
|
|
|
|
|
|
def test_radius_m_out_of_range_rejected() -> None:
|
|
with pytest.raises(ValidationError):
|
|
TradeInEstimateInput(address="ул. Тестовая, 1", area_m2=50, rooms=2, radius_m=50)
|
|
with pytest.raises(ValidationError):
|
|
TradeInEstimateInput(address="ул. Тестовая, 1", area_m2=50, rooms=2, radius_m=9000)
|
|
|
|
|
|
def test_radius_none_preserves_default_radii() -> None:
|
|
# estimate_quality resolves `payload.radius_m or DEFAULT_RADIUS_M`. None must
|
|
# keep the byte-identical two-tier defaults; an explicit value overrides both.
|
|
assert (None or estimator.DEFAULT_RADIUS_M) == estimator.DEFAULT_RADIUS_M
|
|
assert (None or estimator.FALLBACK_RADIUS_M) == estimator.FALLBACK_RADIUS_M
|
|
assert (500 or estimator.DEFAULT_RADIUS_M) == 500
|
|
assert (500 or estimator.FALLBACK_RADIUS_M) == 500
|
|
|
|
|
|
def test_api_radius_expand_clamp_bounds() -> None:
|
|
# The house-analytics / sell-time endpoints clamp an explicit radius into
|
|
# [100, 5000] before ST_DWithin. Mirror that guard so the contract is locked.
|
|
def _clamp(r: int) -> int:
|
|
return max(100, min(r, 5000))
|
|
|
|
assert _clamp(50) == 100
|
|
assert _clamp(300) == 300
|
|
assert _clamp(9000) == 5000
|
|
|
|
|
|
if __name__ == "__main__": # pragma: no cover
|
|
raise SystemExit(pytest.main([__file__, "-q"]))
|