All checks were successful
CI Trade-In / changes (pull_request) Successful in 12s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / changes (pull_request) Successful in 15s
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 1m41s
Мигрирует call-site'ы cian/{serp,detail,newbuilding,session}.py на shared
фабрики Foundation-модуля (#2358): build_browser_fetcher() (serp.py __aenter__)
и build_curl_cffi_session() (detail.py own-session path, newbuilding.py
resolve_cian_zhk_url[_via_search], session.py verify_session). Headers/timeout/
proxy-resolution сохранены байт-в-байт — чистый call-site swap, без изменения
поведения.
Caveat (config Optional footgun): newbuilding.py::fetch_newbuilding() НЕ
переведена на build_browser_fetcher() — build_browser_fetcher() требует
ScraperConfig mandatory (весь смысл фабрики, закрывает класс багов #2322/#2330),
но здесь config: ScraperConfig | None = None легитимно Optional (admin.py
debug-роут и test_scraper_kit_newbuilding_endpoint.py вызывают функцию без
config= и ожидают graceful endpoint=None). Подмена уронила бы AttributeError
на config=None вместо текущего degrade — реальная регрессия, не call-site swap.
Оставлено как есть с комментарием в коде.
Companion test updates (не ослабление, retarget под рефактор): CianScraper
теперь строит BrowserFetcher внутри _base.py, а не в serp.py — два теста в
test_kit_serp_proxy_pool.py, патчившие cian_serp.BrowserFetcher напрямую,
переключены на scraper_kit.providers._base.BrowserFetcher (тот же инвариант,
другая точка патча). Аналогично test_scraper_kit_pricehistory_session_parity.py
::test_fetch_detail_own_session_proxy_wiring_parity — патч AsyncSession
переключён с cian.detail на _base (тот же assert на итоговый proxies=dict).
Tests: full backend suite 3278 passed, 1 pre-existing flake
(test_search_api.py::test_search_cache_hit, #2208), 6 skipped. Все
parity/regression тесты (test_scraper_kit_pipeline_parity2.py,
test_scraper_kit_newbuilding_endpoint.py, test_scraper_kit_cian_golden_parity.py,
test_kit_serp_proxy_pool.py, test_scraper_kit_pricehistory_session_parity.py,
test_cian_serp_scraper.py, test_cian_session.py, test_cian_detail.py) зелёные.
ruff check + format --check чисто на всех изменённых файлах.
173 lines
7.1 KiB
Python
173 lines
7.1 KiB
Python
"""Тесты проброса 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 SERP всё ещё конструирует BrowserFetcher
|
||
напрямую в своём модуле — патчим `yandex_serp.BrowserFetcher`. Cian SERP
|
||
(#2362, F4b) переведён на общую фабрику `scraper_kit.providers._base.
|
||
build_browser_fetcher()` — конструктор теперь вызывается ИЗ _base.py, поэтому
|
||
патчим `BrowserFetcher` там, а не в `cian_serp` (там больше нет своей ссылки на
|
||
класс). Проверяемый инвариант (какие kwargs долетают до BrowserFetcher)
|
||
не меняется — меняется только точка патча, отражающая реальное место вызова.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import types
|
||
from typing import Any
|
||
|
||
import pytest
|
||
from scraper_kit.providers import _base as kit_base
|
||
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(yandex_serp, "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(yandex_serp, "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(yandex_serp, "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()
|
||
# #2362: CianScraper.__aenter__ теперь строит BrowserFetcher через
|
||
# _base.build_browser_fetcher() — патчим класс там, не в cian_serp.
|
||
monkeypatch.setattr(kit_base, "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(kit_base, "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
|