gendesign/tradein-mvp/backend/tests/test_kit_serp_proxy_pool.py
bot-backend 59c072fc4c
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
chore(tradein): удалить мёртвые mobileproxy env-переменные и rotate-ip (#2616 шаги 2-3) (#2650)
2026-08-05 09:35:54 +00:00

280 lines
12 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-скраперы 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