"""Write-path tests: external_valuations.house_id резолвится при сохранении (#2236). Cian и Yandex valuation-кэши теперь принимают house_id (уже разрезолвленный estimate-путём через match_house_readonly) и пишут его в external_valuations. Best-effort: house_id=None → колонка NULL, сохранение НЕ падает. DB — MagicMock, записываем параметры db.execute (никакого реального Postgres, как в test_yandex_valuation_save / test_backfill_house_coords). Легаси `app.services.scrapers.yandex_valuation` / `cian_valuation` удалены (#2277 / #2397 финальный шаг E, 0 runtime-импортёров) — `_yandex_result()` и cian-импорты переведены на kit-эквиваленты `scraper_kit.providers.yandex.valuation` / `scraper_kit.providers.cian.valuation` (те же типы, что `estimator.py` реально использует). """ from __future__ import annotations import os # Settings требует DATABASE_URL на import. Ставим фиктивный DSN заранее. os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost/test_db") from unittest.mock import AsyncMock, MagicMock, patch from scraper_kit.providers.cian.valuation import CianValuationResult, _save_to_cache # --------------------------------------------------------------------------- # Cian valuation — _save_to_cache прокидывает house_id в INSERT params # --------------------------------------------------------------------------- def _cian_result() -> CianValuationResult: return CianValuationResult( sale_price_rub=10_000_000, sale_accuracy=85.0, sale_price_from=9_000_000, sale_price_to=11_000_000, rent_price_rub=40_000, rent_accuracy=70.0, chart=[], chart_change_pct=None, chart_change_direction=None, external_house_id=54016, filters_hash="abc", raw_state=None, ) def _cian_save(db: MagicMock, house_id: int | None) -> dict: _save_to_cache( db, cache_key="ck", address="Екатеринбург, улица Малышева, 51", total_area=50.0, rooms_count=2, floor=3, total_floors=9, repair_type="cosmetic", deal_type="sale", result=_cian_result(), house_id=house_id, ) # _save_to_cache делает один db.execute(INSERT ...) + commit return db.execute.call_args_list[0].args[1] def test_cian_save_persists_house_id(): """Резолвленный house_id попадает в INSERT-параметры.""" db = MagicMock() params = _cian_save(db, house_id=777) assert params["hid"] == 777 db.commit.assert_called_once() def test_cian_save_null_house_id_no_exception(): """house_id=None → hid=None в params, сохранение не падает.""" db = MagicMock() params = _cian_save(db, house_id=None) assert params["hid"] is None db.commit.assert_called_once() # --------------------------------------------------------------------------- # Yandex valuation — _get_or_fetch_yandex_valuation_cached пишет house_id # --------------------------------------------------------------------------- def _make_db_cache_miss() -> MagicMock: """MagicMock db, у которого cache-lookup даёт miss (first() → None).""" db = MagicMock() db.execute.return_value.mappings.return_value.first.return_value = None return db def _yandex_result(): from scraper_kit.providers.yandex.valuation import ( ValuationHouseMeta, YandexValuationResult, ) return YandexValuationResult( address="Екатеринбург, улица Куйбышева, 106", offer_category="APARTMENT", offer_type="SELL", page=1, source_url="https://realty.yandex.ru/otsenka/?address=test&page=1", house=ValuationHouseMeta(year_built=2005, total_floors=25), history_items=[], ) def _insert_params(db: MagicMock) -> dict: """Найти params INSERT INTO external_valuations среди вызовов db.execute.""" for call in db.execute.call_args_list: args = call.args if len(args) >= 2 and "INSERT INTO external_valuations" in str(args[0]): return args[1] raise AssertionError("INSERT INTO external_valuations not found in db.execute calls") async def test_yandex_save_persists_house_id(): """Fresh fetch пишет house_id в INSERT external_valuations.""" from app.services import estimator db = _make_db_cache_miss() scraper = AsyncMock() scraper.fetch_house_history = AsyncMock(return_value=_yandex_result()) cm = MagicMock() cm.__aenter__ = AsyncMock(return_value=scraper) cm.__aexit__ = AsyncMock(return_value=False) with patch.object(estimator, "YandexValuationScraper", return_value=cm): res = await estimator._get_or_fetch_yandex_valuation_cached( db, address="Екатеринбург, улица Куйбышева, 106", house_id=4242 ) assert res is not None params = _insert_params(db) assert params["hid"] == 4242 async def test_yandex_save_null_house_id_no_exception(): """house_id=None → hid=None, INSERT проходит без исключения.""" from app.services import estimator db = _make_db_cache_miss() scraper = AsyncMock() scraper.fetch_house_history = AsyncMock(return_value=_yandex_result()) cm = MagicMock() cm.__aenter__ = AsyncMock(return_value=scraper) cm.__aexit__ = AsyncMock(return_value=False) with patch.object(estimator, "YandexValuationScraper", return_value=cm): res = await estimator._get_or_fetch_yandex_valuation_cached( db, address="Екатеринбург, улица Куйбышева, 106" ) assert res is not None params = _insert_params(db) assert params["hid"] is None