All checks were successful
Deploy Trade-In / changes (push) Successful in 10s
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 45s
Deploy Trade-In / build-backend (push) Successful in 58s
Deploy Trade-In / deploy (push) Successful in 47s
102 lines
3.8 KiB
Python
102 lines
3.8 KiB
Python
"""Mera-audit fix-3: cross-source dedup в _fetch_price_trend.
|
||
|
||
Проверяет что один объект на avito_imv + yandex_valuation с разными ext_item_id
|
||
не double-count'ится в месячной медиане (dedup всегда включён).
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import os
|
||
from typing import Any
|
||
from unittest.mock import MagicMock, patch
|
||
|
||
os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost/test_db")
|
||
|
||
import pytest
|
||
|
||
from app.services.estimator import _fetch_price_trend
|
||
|
||
|
||
def _make_mock_db(source1_rows: list[dict], source2_rows: list[dict]) -> MagicMock:
|
||
"""Возвращает mock db: Source1 (houses_price_dynamics) → source1_rows,
|
||
Source2 (house_placement_history) → source2_rows."""
|
||
mock_db = MagicMock()
|
||
|
||
def side_effect(*args: Any, **kwargs: Any) -> MagicMock:
|
||
call_text = str(args[0].text) if args else ""
|
||
result = MagicMock()
|
||
if "houses_price_dynamics" in call_text:
|
||
result.mappings.return_value.all.return_value = source1_rows
|
||
else:
|
||
result.mappings.return_value.all.return_value = source2_rows
|
||
return result
|
||
|
||
mock_db.execute.side_effect = side_effect
|
||
return mock_db
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Fix-3: dedup enabled (default True)
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
def test_price_trend_dedup_sql_uses_distinct_on() -> None:
|
||
"""SQL-запрос к house_placement_history всегда содержит DISTINCT ON (dedup)."""
|
||
mock_db = _make_mock_db([], [{"month": "2026-01", "ppm2": 150_000}])
|
||
|
||
with patch("app.services.estimator.settings") as mock_settings:
|
||
mock_settings.estimate_price_trend_max_age_months = 6
|
||
|
||
_fetch_price_trend(mock_db, target_house_id=42, min_points=1)
|
||
|
||
calls = mock_db.execute.call_args_list
|
||
# Второй вызов — Source 2 (house_placement_history)
|
||
assert len(calls) >= 2
|
||
second_sql = str(calls[1].args[0].text) if calls[1].args else ""
|
||
assert "DISTINCT ON" in second_sql.upper() or "distinct on" in second_sql.lower()
|
||
|
||
|
||
def test_price_trend_returns_result_with_dedup() -> None:
|
||
"""Функция корректно возвращает точки тренда (dedup включён)."""
|
||
rows = [
|
||
{"month": "2025-11", "ppm2": 148_000},
|
||
{"month": "2025-12", "ppm2": 152_000},
|
||
{"month": "2026-01", "ppm2": 155_000},
|
||
]
|
||
mock_db = _make_mock_db([], rows)
|
||
|
||
with patch("app.services.estimator.settings") as mock_settings:
|
||
mock_settings.estimate_price_trend_max_age_months = 6
|
||
|
||
result = _fetch_price_trend(mock_db, target_house_id=42, min_points=3)
|
||
|
||
assert result is not None
|
||
assert len(result) == 3
|
||
assert result[0]["month"] == "2025-11"
|
||
assert result[0]["ppm2"] == 148_000
|
||
|
||
|
||
def test_price_trend_source1_preferred_over_dedup_path() -> None:
|
||
"""Если Source1 (houses_price_dynamics) вернул ≥ min_points → Source2 не вызывается."""
|
||
source1_rows = [
|
||
{"month": "2025-11", "ppm2": 140_000},
|
||
{"month": "2025-12", "ppm2": 145_000},
|
||
{"month": "2026-01", "ppm2": 150_000},
|
||
]
|
||
mock_db = _make_mock_db(source1_rows, [])
|
||
|
||
with patch("app.services.estimator.settings") as mock_settings:
|
||
mock_settings.estimate_price_trend_max_age_months = 6
|
||
|
||
result = _fetch_price_trend(mock_db, target_house_id=42, min_points=3)
|
||
|
||
# Source1 вернул 3 точки — Source2 не должен вызываться
|
||
assert result is not None
|
||
assert len(result) == 3
|
||
calls = mock_db.execute.call_args_list
|
||
# Ровно один вызов (Source1)
|
||
assert len(calls) == 1
|
||
|
||
|
||
if __name__ == "__main__": # pragma: no cover
|
||
raise SystemExit(pytest.main([__file__, "-q"]))
|