"""#3257: предзаполнение формы оценки фактами дома. Две независимые правки: 1. TradeInEstimateInput — валидатор floor <= total_floors (когда оба заданы). 2. GET /api/v1/geocode/house-facts — предзаполнение total_floors/year_built/ house_type из справочника `houses` (переиспользует _lookup_house_facts). Стиль моков БД/эндпоинта — как в test_geocode_reverse_api.py / test_3234_house_facts_fallback.py. """ from __future__ import annotations import os import sys from unittest.mock import MagicMock, patch # DATABASE_URL required by config before any app import. os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost:5432/test") # WeasyPrint stub — not installed in CI without GTK. _wp_mock = MagicMock() sys.modules.setdefault("weasyprint", _wp_mock) import pytest # noqa: E402 from fastapi import FastAPI # noqa: E402 from fastapi.testclient import TestClient # noqa: E402 from pydantic import ValidationError # noqa: E402 from app.api.v1 import geocode as geocode_module # noqa: E402 from app.core.db import get_db # noqa: E402 from app.schemas.trade_in import TradeInEstimateInput # noqa: E402 from app.services.estimator import _HouseFacts # noqa: E402 # ── A. TradeInEstimateInput.floor <= total_floors ─────────────────────────── def _payload(**overrides: object) -> dict: base: dict[str, object] = {"address": "ЕКБ, ул. Учителей, 18", "area_m2": 38.8, "rooms": 1} base.update(overrides) return base def test_floor_equal_total_floors_is_allowed() -> None: model = TradeInEstimateInput(**_payload(floor=9, total_floors=9)) assert model.floor == model.total_floors == 9 def test_floor_less_than_total_floors_is_allowed() -> None: model = TradeInEstimateInput(**_payload(floor=4, total_floors=9)) assert model.floor == 4 @pytest.mark.parametrize( "kwargs", [ {"floor": 5}, # total_floors не задан {"total_floors": 9}, # floor не задан {}, # оба не заданы ], ) def test_partial_input_skips_validator(kwargs: dict) -> None: """Частичный ввод легален — гейт не срабатывает, пока не заданы ОБА поля.""" model = TradeInEstimateInput(**_payload(**kwargs)) assert model is not None def test_floor_greater_than_total_floors_is_rejected() -> None: with pytest.raises(ValidationError) as exc_info: TradeInEstimateInput(**_payload(floor=12, total_floors=9)) message = str(exc_info.value) assert "12" in message assert "9" in message # ── B. GET /api/v1/geocode/house-facts ────────────────────────────────────── @pytest.fixture def app() -> FastAPI: application = FastAPI() application.include_router(geocode_module.router, prefix="/api/v1/geocode") application.dependency_overrides[get_db] = lambda: MagicMock() return application def test_house_facts_not_found_returns_200_found_false(app: FastAPI) -> None: client = TestClient(app) with patch( "app.api.v1.geocode._lookup_house_facts", return_value=None, ): r = client.get("/api/v1/geocode/house-facts?lat=56.838&lon=60.595") assert r.status_code == 200 body = r.json() assert body == { "found": False, "total_floors": None, "year_built": None, "house_type": None, "source": None, } def test_house_facts_found_returns_facts(app: FastAPI) -> None: facts = _HouseFacts( house_id=42, total_floors=9, year_built=1975, house_type="panel", material_walls=None, ) client = TestClient(app) with patch( "app.api.v1.geocode._lookup_house_facts", return_value=facts, ) as lookup_mock: r = client.get("/api/v1/geocode/house-facts?lat=56.838&lon=60.595") assert r.status_code == 200 body = r.json() assert body == { "found": True, "total_floors": 9, "year_built": 1975, "house_type": "panel", "source": "houses", } # target_house_id не резолвился (fias_id не передан) → None прокинут дальше assert lookup_mock.call_args.kwargs["target_house_id"] is None assert lookup_mock.call_args.kwargs["lat"] == 56.838 assert lookup_mock.call_args.kwargs["lon"] == 60.595 def test_house_facts_resolves_house_id_by_fias(app: FastAPI) -> None: """fias_id передан и резолвится в houses.id → он уезжает в _lookup_house_facts как target_house_id (обходя geo-фолбэк по lat/lon).""" facts = _HouseFacts( house_id=42, total_floors=9, year_built=1975, house_type="panel", material_walls=None, ) client = TestClient(app) with ( patch( "app.api.v1.geocode._resolve_house_id_by_fias", return_value=42, ), patch( "app.api.v1.geocode._lookup_house_facts", return_value=facts, ) as lookup_mock, ): r = client.get("/api/v1/geocode/house-facts?lat=56.838&lon=60.595&fias_id=some-guid") assert r.status_code == 200 assert lookup_mock.call_args.kwargs["target_house_id"] == 42 def test_house_facts_missing_required_query_params_is_422(app: FastAPI) -> None: client = TestClient(app) r = client.get("/api/v1/geocode/house-facts?lat=56.838") assert r.status_code == 422