All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 8s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 1m36s
Удалены (0 runtime importers в app/, scripts/, packages/; проверено grep'ом, включая intra-legacy импорты в оставшихся 21 legacy-модуле scrapers/): - backend/app/services/scrapers/avito_imv.py - backend/app/services/scrapers/domclick_detail.py - backend/app/services/scrapers/ekb_geoportal_client.py - backend/app/services/scrapers/yandex_detail.py - backend/app/services/scrapers/yandex_valuation.py Тесты — удалены (pure legacy-vs-kit parity, kit-only coverage уже есть в другом месте): - tests/scrapers/test_ekb_geoportal_client_kit_parity.py - tests/scrapers/test_domclick_detail_kit_parity.py Тесты — конвертированы в kit-only (импорт легаси заменён на scraper_kit.*, покрытие сохранено без потерь): - tests/scrapers/test_avito_imv_kit_parity.py (parity-тесты убраны, config-footgun regression на kit-стороне оставлен) - tests/scrapers/test_avito_imv_browser_transport.py - tests/scrapers/test_yandex_valuation_kit_migration.py (parity убрана, footgun- регрессия mandatory config/delay_provider оставлена) - tests/scrapers/test_domclick_detail.py (+ exceptions переведены на scraper_kit.domclick_exceptions, т.к. kit parse_detail_html поднимает их) - tests/test_avito_imv_parse.py - tests/test_ekb_geoportal_ingest.py (client-часть; ingest/geocoder-часть не трогали — не зависят от удалённого модуля) - tests/test_yandex_detail.py - tests/test_yandex_detail_structural.py - tests/test_yandex_valuation.py (YandexValuationScraper() → config=_KIT_CONFIG, kit конструктор требует обязательный config) - tests/test_yandex_valuation_save.py - tests/test_extval_house_id_write_path.py (только yandex_valuation-часть; cian_valuation остался нетронутым — живой легаси-модуль) - tests/test_yandex_history_area_filter.py Тесты — частично отредактированы (убраны только части про удалённые модули, живые легаси-модули/их parity не тронуты): - tests/scrapers/test_admin_domclick_ingest_kit_parity.py (base.py/ScrapedLot parity остался) - tests/scrapers/test_admin_yandex_kit_parity.py (yandex_realty parity остался) - tests/scrapers/test_admin_avito_kit_parity.py (avito.py/avito_houses.py parity остался; _parse_price parity убран) - tests/scrapers/test_avito_unix_date_tz_consistency.py (IMV-ветка переведена на kit avito.imv, легаси avito/avito_houses/avito_shared/avito_detail не тронуты) - tests/tasks/test_yandex_detail_backfill.py (subject — kit-задача, уже вызывает scraper_kit.providers.yandex.detail; save_detail_enrichment coverage-тесты внизу файла переведены на kit) - tests/test_scraper_kit_yandex_golden_parity.py (detail/valuation parity убраны; serp/newbuilding/house_type_normalizer/build_url для yandex_realty/ yandex_newbuilding не тронуты) - tests/test_scraper_proxy.py (YandexValuation proxy-тесты переведены на kit config= DI; test_avito_imv_own_session_receives_proxies удалён — дублирует test_avito_imv_kit_parity.py footgun-тесты) - tests/test_yandex_scrapers_delay_wiring.py (yandex_detail/yandex_valuation → kit delay_provider=; yandex_realty/yandex_newbuilding не тронуты) - tests/test_scraper_kit_group_c_backfill_kit_parity.py (docstring уточнение, без функциональных изменений) Полный backend-suite зелёный (3253 passed, 6 skipped) кроме известного pre-existing flake test_search_api.py::test_search_cache_hit (#2208, не регрессия этого PR). ruff check + ruff format — чисто на всех изменённых файлах.
161 lines
6.1 KiB
Python
161 lines
6.1 KiB
Python
"""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` удалён (#2277 финальный шаг
|
||
scraper_kit-миграции) — `_yandex_result()` переведён на kit-эквивалент
|
||
`scraper_kit.providers.yandex.valuation` (тот же тип, что `estimator.py` реально
|
||
использует через `YandexValuationScraper`, который здесь пропатчен). `cian_valuation`
|
||
остаётся легаси (не мигрирован) — его импорт не тронут.
|
||
"""
|
||
|
||
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 app.services.scrapers.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
|