All checks were successful
Deploy Trade-In / changes (push) Successful in 10s
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / test (push) Successful in 1m33s
Deploy Trade-In / build-backend (push) Successful in 56s
Deploy Trade-In / deploy (push) Successful in 51s
113 lines
4.1 KiB
Python
113 lines
4.1 KiB
Python
"""#2002: premium_building флаг из MV `premium_houses`.
|
|
|
|
Покрывает read-only helper `_is_premium_building`:
|
|
- target резолвится в премиальный дом → (True, med_ppm2);
|
|
- target не в MV → (False, None);
|
|
- house_id is None → (False, None) без обращения к БД;
|
|
- MV отсутствует / SQL-ошибка → graceful (False, None), оценка не падает.
|
|
|
|
Плюс smoke-проверка дефолтов в response-модели AggregatedEstimate (флаг — метаданные,
|
|
сериализация без изменений, когда дом не премиальный).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from typing import Any
|
|
from unittest.mock import MagicMock
|
|
|
|
os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost/test_db")
|
|
|
|
import pytest
|
|
|
|
from app.services.estimator import _is_premium_building
|
|
|
|
|
|
def _make_mock_db(*, first_row: Any, raises: bool = False) -> MagicMock:
|
|
"""mock db.execute(...).first() → first_row; raises=True имитирует отсутствие MV."""
|
|
mock_db = MagicMock()
|
|
if raises:
|
|
mock_db.execute.side_effect = RuntimeError('relation "premium_houses" does not exist')
|
|
else:
|
|
mock_db.execute.return_value.first.return_value = first_row
|
|
return mock_db
|
|
|
|
|
|
def test_premium_house_yields_true_with_median() -> None:
|
|
"""house_id в premium_houses → (True, med_ppm2)."""
|
|
db = _make_mock_db(first_row=(255_000,))
|
|
|
|
is_premium, median = _is_premium_building(db, 4242)
|
|
|
|
assert is_premium is True
|
|
assert median == 255_000
|
|
# CAST(:hid AS bigint) — psycopg v3, не :hid::bigint
|
|
sql = str(db.execute.call_args.args[0].text)
|
|
assert "CAST(:hid AS bigint)" in sql
|
|
assert "::bigint" not in sql
|
|
|
|
|
|
def test_non_premium_house_yields_false_none() -> None:
|
|
"""house_id отсутствует в MV (first() → None) → (False, None)."""
|
|
db = _make_mock_db(first_row=None)
|
|
|
|
assert _is_premium_building(db, 9999) == (False, None)
|
|
|
|
|
|
def test_house_id_none_short_circuits_without_query() -> None:
|
|
"""house_id is None → (False, None), к БД не обращаемся."""
|
|
db = _make_mock_db(first_row=(255_000,))
|
|
|
|
assert _is_premium_building(db, None) == (False, None)
|
|
db.execute.assert_not_called()
|
|
|
|
|
|
def test_missing_mv_degrades_gracefully() -> None:
|
|
"""MV не существует / SQL-ошибка → (False, None), без проброса исключения."""
|
|
db = _make_mock_db(first_row=None, raises=True)
|
|
|
|
assert _is_premium_building(db, 4242) == (False, None)
|
|
# после ошибки откатываем poisoned-транзакцию
|
|
db.rollback.assert_called_once()
|
|
|
|
|
|
def test_premium_median_null_in_mv_is_handled() -> None:
|
|
"""Дом в MV, но med_ppm2 NULL → (True, None) (премиум-флаг есть, медианы нет)."""
|
|
db = _make_mock_db(first_row=(None,))
|
|
|
|
assert _is_premium_building(db, 4242) == (True, None)
|
|
|
|
|
|
def test_response_model_defaults_non_premium() -> None:
|
|
"""AggregatedEstimate по умолчанию: premium_building=False, median=None.
|
|
|
|
Гарантирует, что добавление флага не меняет сериализацию для не-премиум оценок.
|
|
"""
|
|
from datetime import UTC, datetime
|
|
from uuid import uuid4
|
|
|
|
from app.schemas.trade_in import AggregatedEstimate
|
|
|
|
est = AggregatedEstimate(
|
|
estimate_id=uuid4(),
|
|
median_price_rub=10_000_000,
|
|
range_low_rub=9_000_000,
|
|
range_high_rub=11_000_000,
|
|
median_price_per_m2=200_000,
|
|
confidence="medium",
|
|
n_analogs=12,
|
|
period_months=24,
|
|
analogs=[],
|
|
actual_deals=[],
|
|
expires_at=datetime.now(tz=UTC),
|
|
)
|
|
|
|
assert est.premium_building is False
|
|
assert est.premium_building_median_ppm2 is None
|
|
dumped = est.model_dump()
|
|
assert dumped["premium_building"] is False
|
|
assert dumped["premium_building_median_ppm2"] is None
|
|
|
|
|
|
if __name__ == "__main__": # pragma: no cover
|
|
raise SystemExit(pytest.main([__file__, "-q"]))
|