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