gendesign/tradein-mvp/backend/tests/test_extval_house_id_write_path.py
bot-backend a9b87aa311
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
chore(tradein/scraper-kit): удалить 5 orphaned legacy-модулей (#2277 финальный шаг, частичный)
Удалены (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 — чисто на всех изменённых файлах.
2026-07-04 10:41:46 +03:00

161 lines
6.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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