Some checks failed
Deploy Trade-In / perimeter-smoke (push) Blocked by required conditions
Deploy Trade-In / deploy-status (push) Blocked by required conditions
Deploy Trade-In / changes (push) Successful in 12s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Successful in 4m2s
Deploy Trade-In / build-backend (push) Successful in 1m4s
Deploy Trade-In / deploy (push) Has been cancelled
166 lines
5.6 KiB
Python
166 lines
5.6 KiB
Python
"""#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
|