gendesign/tradein-mvp/backend/tests/scrapers/test_yandex_valuation_kit_migration.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

120 lines
5.5 KiB
Python

"""Issue #2336 (Group E3 of scraper_kit migration epic #2277/#2308).
`scraper_kit.providers.yandex.valuation.YandexValuationScraper` is already a
strangler-copy (#2133) of the (now deleted, #2277 final step)
`app.services.scrapers.yandex_valuation`, and already used via the kit path in
`admin.py::scrape_yandex_valuation` (#2305, Group A) with the established DI
convention: `config=RealScraperConfig()`, `delay_provider=get_scraper_delay`,
`proxy_provider=_kit_proxy_provider()`.
This file proves #2336's two confirmed risk patterns for any future call site that
constructs the kit scraper directly, so a regression is caught in CI rather than
silently in prod:
1. **Mandatory `config: ScraperConfig`** (same shape as the cian_valuation footgun,
#2335) — omitting it raises `TypeError`.
2. **Optional `delay_provider`** — if omitted, silently falls back to the hardcoded
class-default `request_delay_sec = 5.0`, discarding whatever an admin configured via
the DB-backed `scraper_settings` table (`get_scraper_delay`, cached 60s,
`max(per_source, global)`). No exception, no log — just quietly weaker anti-ban
throttling if an admin had raised it after ban trouble.
Legacy-vs-kit parity coverage (previously here, using
`app.services.scrapers.yandex_valuation`) was removed together with the legacy module
deletion — `.parse()` correctness itself is still covered kit-only by
`tests/test_yandex_valuation.py` / `tests/test_yandex_valuation_save.py`.
"""
from __future__ import annotations
import os
from unittest.mock import patch
import pytest
# app.services.scraper_adapters imports app.core.config.settings=Settings(), which
# requires DATABASE_URL. Offline construction never touches a real DB — a fake DSN
# is enough.
os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost/test_db")
from scraper_kit.providers.yandex.valuation import (
YandexValuationScraper as KitYandexValuationScraper,
)
from app.services import scraper_settings
from app.services.scraper_adapters import RealScraperConfig
# ---------------------------------------------------------------------------
# Footgun #1 — mandatory `config`
# ---------------------------------------------------------------------------
def test_kit_valuation_requires_config() -> None:
"""Omitting `config=` raises TypeError — documents the mandatory-DI contract.
Guards the exact risk flagged for #2336: if a future call site (#2337, switching
estimator.py to the kit class) forgets `config=`, this must fail loud in CI, not
silently in prod inside estimator.py's blanket except-block.
"""
with pytest.raises(TypeError):
KitYandexValuationScraper() # type: ignore[call-arg]
# ---------------------------------------------------------------------------
# Footgun #2 — `delay_provider` silently falls back to hardcoded 5.0s
# ---------------------------------------------------------------------------
def test_kit_valuation_without_delay_provider_uses_hardcoded_default() -> None:
"""No `delay_provider=` -> `request_delay_sec` stays at the class-default 5.0s.
This is the regression this test guards: a real call site that forgets to wire
`delay_provider=get_scraper_delay` silently reverts an admin-tuned anti-ban delay
(e.g. raised after a ban) to 5.0s -- no error, no log, just quietly weaker
throttling.
"""
scraper = KitYandexValuationScraper(config=RealScraperConfig())
assert scraper.request_delay_sec == 5.0
assert KitYandexValuationScraper.request_delay_sec == 5.0 # class default untouched
def test_kit_valuation_with_delay_provider_uses_configured_value() -> None:
"""`delay_provider=get_scraper_delay` -> instance picks up whatever it returns.
Controls the return value via patch so the assertion doesn't depend on a real DB
row / DB availability -- this proves the WIRING (delay_provider is actually called
and its return value actually lands in request_delay_sec), not
`scraper_settings`'s own DB-fallback behaviour (covered elsewhere, e.g.
`tests/test_yandex_scrapers_delay_wiring.py` for the legacy module).
"""
with patch("app.services.scraper_settings.get_scraper_delay", return_value=12.5) as mock_delay:
# Attribute lookup on the MODULE (not the bare name imported at file-top)
# so this resolves to the patched mock, not the original function object
# `from ... import get_scraper_delay` would have bound at import time.
scraper = KitYandexValuationScraper(
config=RealScraperConfig(), delay_provider=scraper_settings.get_scraper_delay
)
mock_delay.assert_called_once_with("yandex_valuation")
assert scraper.request_delay_sec == 12.5
assert scraper.request_delay_sec != 5.0 # explicitly NOT the hardcoded fallback
def test_kit_valuation_delay_provider_receives_scraper_name() -> None:
"""delay_provider is invoked with `self.name` ('yandex_valuation'), not a typo'd string.
`get_scraper_delay` aliases 'yandex_valuation' to the shared 'yandex' DB row today
(`_KEY_ALIASES` in scraper_settings.py), but a hardcoded/mismatched source string
passed at the call site would be a latent bug if that alias ever changes.
"""
calls: list[str] = []
def _spy_delay_provider(source: str) -> float:
calls.append(source)
return 9.0
scraper = KitYandexValuationScraper(
config=RealScraperConfig(), delay_provider=_spy_delay_provider
)
assert calls == ["yandex_valuation"]
assert scraper.request_delay_sec == 9.0