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