"""#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"]))