gendesign/tradein-mvp/backend/tests/test_estimator_premium_building.py
lekss361 840d64a4a0
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
feat(tradein): premium_houses MV + premium_building flag (#2002)
2026-06-27 16:40:39 +00:00

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