gendesign/tradein-mvp/backend/tests/test_kit_serp_proxy_pool.py
bot-backend 802b114e0d
All checks were successful
CI Trade-In / changes (pull_request) Successful in 10s
CI / changes (pull_request) Successful in 12s
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 1m45s
refactor(tradein/scraper-kit): yandex providers on shared _base.py (#2363)
F4c-yandex (epic #2277 Group F, parent #2352): migrate the SERP scraper's
BrowserFetcher construction onto build_browser_fetcher() from the Foundation
module (#2358, providers/_base.py) instead of local ad-hoc construction.

- yandex/serp.py::__aenter__ now calls build_browser_fetcher(config, source=,
  proxy_provider=, fetch_timeout_s=) instead of constructing BrowserFetcher(...)
  directly. Preserves the existing fetch_timeout_s=30 (see caveat below),
  endpoint/proxy_provider/use_pool resolution byte-for-byte. The retry/tarpit
  logic in fetch_around() (rotate_ip + sleep on status==0/JSON-error) is
  untouched, per the Foundation module's documented exclusion.

Left unchanged (deliberate, not an oversight):
- yandex/serp.py::_rotate_ip's curl_cffi AsyncSession(timeout=30) — this hits
  the mobile-proxy provider's own changeip API, not Yandex; it intentionally
  carries no impersonate/headers/proxy (same as the mirrored avito/serp.py
  rotate-ip session). Not listed among the _base.py docstring's documented
  extraction sites -- consistent exclusion, not a new one.
- yandex/detail.py -- no BrowserFetcher/curl_cffi construction exists in this
  file (uses BaseScraper's httpx-based _http_get). Nothing to migrate.
- yandex/newbuilding.py -- both BrowserFetcher(...) call sites take an
  OPTIONAL `config: ScraperConfig | None`, a genuinely different contract from
  build_browser_fetcher's mandatory `config`. Migrating would require making
  config mandatory (breaking admin.py:1511, yandex_newbuilding_sweep.py:353,
  and several tests that call without config -- out of scope for a call-site
  swap) and breaks test_scraper_kit_newbuilding_endpoint.py's "without config,
  endpoint=None" regression case (its SimpleNamespace mock lacks
  use_proxy_pool_browser, and the mock config path can never satisfy a
  mandatory-config helper). Left untouched; the class-bug #2322/#2330 this
  file already fixed (config now threaded, just not via the shared helper)
  remains fixed.

Also updates test_kit_serp_proxy_pool.py's 3 yandex-specific tests: they
monkeypatched the module-local `yandex_serp.BrowserFetcher` name, which the
migrated call path no longer references directly (it now goes through
scraper_kit.providers._base.build_browser_fetcher -> _base's own BrowserFetcher
import). Patch target moved to scraper_kit.providers._base.BrowserFetcher;
every assertion is unchanged -- this adapts the mock to the new (intentional)
call path, it does not weaken what the test verifies. Cian's tests in the same
file are untouched (cian/serp.py is not migrated yet, separate F4b issue).
2026-07-04 09:29:36 +03:00

174 lines
7.2 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.

"""Тесты проброса browser-пула в SERP-скраперы yandex/cian (#2160).
Зеркало P4 (#2164) для avito: там пул раздавался только в avito-путях, а yandex/cian
SERP создавали BrowserFetcher сами в __aenter__ без пула → весь трафик шёл через
env-прокси браузера. Здесь проверяем, что YandexRealtyScraper / CianScraper прокидывают
proxy_provider + use_pool=config.use_proxy_pool_browser в BrowserFetcher.
Инвариант ship-dark:
- proxy_provider=None ИЛИ флаг off → BrowserFetcher(..., proxy_provider=None, use_pool=False),
т.е. env-прокси браузера, поведение байт-в-байт как до фикса.
- proxy_provider задан + флаг on → BrowserFetcher получает провайдер + use_pool=True.
BrowserFetcher монки-патчится на recording-заглушку в модуле каждого скрапера, так что
реального HTTP к tradein-browser не происходит.
Yandex (#2363, F4c миграция): `yandex/serp.py::__aenter__` больше не конструирует
`BrowserFetcher(...)` напрямую — вызывает shared `build_browser_fetcher()` из
`scraper_kit.providers._base`, который ВНУТРИ себя ссылается на СВОЙ собственный
импорт `BrowserFetcher`. Патч локального имени `yandex_serp.BrowserFetcher` больше
не перехватывает вызов (реальный `BrowserFetcher.__init__` конструировался бы, а
`calls` оставался пуст) — патчим `scraper_kit.providers._base.BrowserFetcher`
(реальную зависимость `build_browser_fetcher`), сохраняя все assertions как есть.
Cian (`cian/serp.py`) пока НЕ мигрирован (F4b, отдельный issue) — там патч
локального имени остаётся корректным.
"""
from __future__ import annotations
import types
from typing import Any
import pytest
from scraper_kit.providers.cian import serp as cian_serp
from scraper_kit.providers.yandex import serp as yandex_serp
class _FakeProxyProvider:
"""ProxyProvider-заглушка: методы no-op (в этих тестах пул не вызывается —
BrowserFetcher замокан, acquire/release не срабатывают)."""
def acquire(self, provider: str) -> None:
return None
def release(self, lease: object) -> None:
return None
def mark_health(self, lease: object, ok: bool) -> None:
return None
def _make_recorder() -> tuple[type, list[dict[str, Any]]]:
"""Recording-заглушка BrowserFetcher: пишет init-kwargs в общий список."""
calls: list[dict[str, Any]] = []
class _RecFetcher:
def __init__(
self,
source: str = "avito",
fetch_timeout_s: float = 120.0,
*,
endpoint: str,
proxy_provider: object | None = None,
use_pool: bool = False,
) -> None:
calls.append(
{
"source": source,
"endpoint": endpoint,
"proxy_provider": proxy_provider,
"use_pool": use_pool,
}
)
async def __aenter__(self) -> _RecFetcher:
return self
async def __aexit__(self, *args: object) -> None:
return None
return _RecFetcher, calls
def _config(*, use_pool: bool) -> types.SimpleNamespace:
return types.SimpleNamespace(
browser_http_endpoint="http://browser:3000",
use_proxy_pool_browser=use_pool,
)
# ── Yandex SERP ──────────────────────────────────────────────────────────────
@pytest.mark.asyncio
async def test_yandex_serp_wires_pool_when_flag_on(monkeypatch: pytest.MonkeyPatch) -> None:
"""proxy_provider + флаг on → BrowserFetcher получает провайдер + use_pool=True."""
rec_fetcher, calls = _make_recorder()
monkeypatch.setattr("scraper_kit.providers._base.BrowserFetcher", rec_fetcher)
provider = _FakeProxyProvider()
scraper = yandex_serp.YandexRealtyScraper(_config(use_pool=True), proxy_provider=provider)
async with scraper:
pass
assert len(calls) == 1
assert calls[0]["source"] == "yandex"
assert calls[0]["proxy_provider"] is provider
assert calls[0]["use_pool"] is True
@pytest.mark.asyncio
async def test_yandex_serp_no_pool_by_default(monkeypatch: pytest.MonkeyPatch) -> None:
"""Без provider/флага → BrowserFetcher(proxy_provider=None, use_pool=False) — ship-dark."""
rec_fetcher, calls = _make_recorder()
monkeypatch.setattr("scraper_kit.providers._base.BrowserFetcher", rec_fetcher)
scraper = yandex_serp.YandexRealtyScraper(_config(use_pool=False))
async with scraper:
pass
assert len(calls) == 1
assert calls[0]["proxy_provider"] is None
assert calls[0]["use_pool"] is False
@pytest.mark.asyncio
async def test_yandex_serp_provider_without_flag_stays_dark(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""provider задан, но флаг off → use_pool=False (BrowserFetcher пул не трогает)."""
rec_fetcher, calls = _make_recorder()
monkeypatch.setattr("scraper_kit.providers._base.BrowserFetcher", rec_fetcher)
provider = _FakeProxyProvider()
scraper = yandex_serp.YandexRealtyScraper(_config(use_pool=False), proxy_provider=provider)
async with scraper:
pass
assert calls[0]["proxy_provider"] is provider
assert calls[0]["use_pool"] is False
# ── Cian SERP ────────────────────────────────────────────────────────────────
@pytest.mark.asyncio
async def test_cian_serp_wires_pool_when_flag_on(monkeypatch: pytest.MonkeyPatch) -> None:
"""proxy_provider + флаг on → BrowserFetcher получает провайдер + use_pool=True."""
rec_fetcher, calls = _make_recorder()
monkeypatch.setattr(cian_serp, "BrowserFetcher", rec_fetcher)
provider = _FakeProxyProvider()
scraper = cian_serp.CianScraper(_config(use_pool=True), proxy_provider=provider)
async with scraper:
pass
assert len(calls) == 1
assert calls[0]["source"] == "cian"
assert calls[0]["proxy_provider"] is provider
assert calls[0]["use_pool"] is True
@pytest.mark.asyncio
async def test_cian_serp_no_pool_by_default(monkeypatch: pytest.MonkeyPatch) -> None:
"""Без provider/флага → BrowserFetcher(proxy_provider=None, use_pool=False) — ship-dark."""
rec_fetcher, calls = _make_recorder()
monkeypatch.setattr(cian_serp, "BrowserFetcher", rec_fetcher)
scraper = cian_serp.CianScraper(_config(use_pool=False))
async with scraper:
pass
assert len(calls) == 1
assert calls[0]["proxy_provider"] is None
assert calls[0]["use_pool"] is False