All checks were successful
Deploy Trade-In / changes (push) Successful in 10s
Deploy Trade-In / build-frontend (push) Successful in 2m13s
Deploy Trade-In / test (push) Successful in 2m45s
Deploy Trade-In / build-browser (push) Successful in 3m2s
Deploy Trade-In / build-backend (push) Successful in 1m38s
Deploy Trade-In / deploy (push) Successful in 2m29s
280 lines
12 KiB
Python
280 lines
12 KiB
Python
"""Тесты проброса browser-пула в SERP-скраперы avito/yandex/cian (#2160, #2616).
|
||
|
||
Изначально (#2164 P4) пул раздавался только в avito-путях, а yandex/cian SERP создавали
|
||
BrowserFetcher сами в __aenter__ без пула → их трафик шёл через env-прокси браузера —
|
||
покрыто ниже (секции Yandex/Cian). #2616 закрывает СИММЕТРИЧНЫЙ пробел: сам Avito SERP
|
||
(`AvitoScraper.__init__`/`__aenter__`) не принимал `proxy_provider` вообще (в отличие от
|
||
CianScraper/YandexRealtyScraper) — `build_browser_fetcher(config, "avito")` вызывался
|
||
без proxy_provider, поэтому браузерный путь Avito ходил ТОЛЬКО через мёртвый
|
||
`BROWSER_PROXY_AVITO` (issue #2613/#2616, root cause 52/53 banned-прогонов за 14 дней).
|
||
Секция Avito ниже проверяет тот же инвариант, что и Yandex/Cian.
|
||
|
||
Здесь проверяем, что AvitoScraper / 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 не происходит.
|
||
|
||
После F4-миграции (#2362 F4b cian + #2363 F4c yandex) ОБА скрапера
|
||
(`yandex/serp.py::__aenter__` и `cian/serp.py::__aenter__`) больше не конструируют
|
||
`BrowserFetcher(...)` напрямую — оба вызывают shared `build_browser_fetcher()` из
|
||
`scraper_kit.providers._base`, который ВНУТРИ себя ссылается на СВОЙ собственный
|
||
импорт `BrowserFetcher`. Патч локального имени `yandex_serp.BrowserFetcher` /
|
||
`cian_serp.BrowserFetcher` больше не перехватывает вызов (реальный
|
||
`BrowserFetcher.__init__` конструировался бы, а `calls` оставался пуст) — поэтому
|
||
для обоих провайдеров патчим `scraper_kit.providers._base.BrowserFetcher`
|
||
(реальную зависимость `build_browser_fetcher`), сохраняя все assertions как есть.
|
||
Проверяемый инвариант (какие 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.avito import serp as avito_serp
|
||
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,
|
||
environment: str = "dev",
|
||
) -> None:
|
||
calls.append(
|
||
{
|
||
"source": source,
|
||
"endpoint": endpoint,
|
||
"proxy_provider": proxy_provider,
|
||
"use_pool": use_pool,
|
||
"environment": environment,
|
||
}
|
||
)
|
||
|
||
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()
|
||
# #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
|
||
|
||
|
||
# ── Avito SERP (#2616) ──────────────────────────────────────────────────────
|
||
|
||
|
||
class _FakeCffiSession:
|
||
"""No-op curl_cffi AsyncSession stand-in — AvitoScraper.__aenter__ builds a
|
||
curl_cffi fallback session даже в browser-mode (#1805, через
|
||
providers._base.build_document_session → _base.AsyncSession — НЕ
|
||
avito_serp.AsyncSession, тот используется только в _rotate_ip), не относится
|
||
к проверяемому здесь инварианту (BrowserFetcher kwargs). Патчим
|
||
kit_base.AsyncSession, чтобы не конструировать реальную curl_cffi сессию."""
|
||
|
||
def __init__(self, *_args: Any, **_kwargs: Any) -> None:
|
||
pass
|
||
|
||
async def close(self) -> None:
|
||
return None
|
||
|
||
|
||
def _avito_config(*, use_pool: bool) -> types.SimpleNamespace:
|
||
return types.SimpleNamespace(
|
||
browser_http_endpoint="http://browser:3000",
|
||
use_proxy_pool_browser=use_pool,
|
||
scraper_fetch_mode="browser",
|
||
scraper_proxy_url=None,
|
||
)
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_avito_serp_wires_pool_when_flag_on(monkeypatch: pytest.MonkeyPatch) -> None:
|
||
"""proxy_provider + флаг on → BrowserFetcher получает провайдер + use_pool=True.
|
||
|
||
#2616 root-cause: до этого фикса AvitoScraper.__init__ не принимал
|
||
proxy_provider вовсе (AttributeError/TypeError на попытке передать) —
|
||
build_browser_fetcher(config, "avito") в __aenter__ вызывался БЕЗ
|
||
proxy_provider, поэтому браузерный путь Avito шёл только через мёртвый
|
||
BROWSER_PROXY_AVITO env-прокси.
|
||
"""
|
||
rec_fetcher, calls = _make_recorder()
|
||
monkeypatch.setattr(kit_base, "BrowserFetcher", rec_fetcher)
|
||
monkeypatch.setattr(kit_base, "AsyncSession", _FakeCffiSession)
|
||
provider = _FakeProxyProvider()
|
||
|
||
scraper = avito_serp.AvitoScraper(_avito_config(use_pool=True), proxy_provider=provider)
|
||
async with scraper:
|
||
pass
|
||
|
||
assert len(calls) == 1
|
||
assert calls[0]["source"] == "avito"
|
||
assert calls[0]["proxy_provider"] is provider
|
||
assert calls[0]["use_pool"] is True
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_avito_serp_no_pool_by_default(monkeypatch: pytest.MonkeyPatch) -> None:
|
||
"""Без provider/флага → BrowserFetcher(proxy_provider=None, use_pool=False) — ship-dark.
|
||
|
||
AvitoScraper(config) без явного proxy_provider= (позиционка совместимость со
|
||
старыми вызывающими) — поведение byte-в-byte как до #2616.
|
||
"""
|
||
rec_fetcher, calls = _make_recorder()
|
||
monkeypatch.setattr(kit_base, "BrowserFetcher", rec_fetcher)
|
||
monkeypatch.setattr(kit_base, "AsyncSession", _FakeCffiSession)
|
||
|
||
scraper = avito_serp.AvitoScraper(_avito_config(use_pool=False))
|
||
async with scraper:
|
||
pass
|
||
|
||
assert len(calls) == 1
|
||
assert calls[0]["source"] == "avito"
|
||
assert calls[0]["proxy_provider"] is None
|
||
assert calls[0]["use_pool"] is False
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_avito_serp_provider_without_flag_stays_dark(
|
||
monkeypatch: pytest.MonkeyPatch,
|
||
) -> None:
|
||
"""provider задан, но флаг off → use_pool=False (BrowserFetcher пул не трогает)."""
|
||
rec_fetcher, calls = _make_recorder()
|
||
monkeypatch.setattr(kit_base, "BrowserFetcher", rec_fetcher)
|
||
monkeypatch.setattr(kit_base, "AsyncSession", _FakeCffiSession)
|
||
provider = _FakeProxyProvider()
|
||
|
||
scraper = avito_serp.AvitoScraper(_avito_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
|