gendesign/tradein-mvp/backend/tests/test_kit_serp_proxy_pool.py
bot-backend 96dd27c4ae
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
refactor(tradein/scraper-kit): cian providers on shared _base.py (#2362)
Мигрирует 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 чисто на всех изменённых файлах.
2026-07-04 09:29:49 +03:00

173 lines
7.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.

"""Тесты проброса 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