"""#3264: свип берёт BFF подзапросом из прогретой страницы, а не навигацией. BFF-ручка Домклика — не страница, а JSON-эндпоинт SPA. Навигацией браузера на неё через мобильные узлы пула приходит только ChallengeTimeout (прод, прогоны 5330 и 5351: 9/9 и 6/6 запросов зависли на челлендже), а подзапросом из прогретого контекста — HTTP 200 за 6 секунд. Замер 30.08.2026: все четыре узла пула, все шесть комнатных корзин, суммарно 6359 офферов против 6367, снятых напрямую с резидентного IP. Тестируется контракт, а не обход защиты: 1. оба вызова свипа идут с fetch_mode="subresource" И с непустым origin — без прогрева рукопожатие в куки контекста не попадёт, режим бесполезен; 2. BrowserFetcher кладёт fetch_mode в payload ТОЛЬКО когда он не дефолтный — сайдкар прежней версии не должен получать незнакомый ключ. """ from __future__ import annotations import os from types import SimpleNamespace from unittest.mock import AsyncMock, MagicMock os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost:5432/test") import pytest from scraper_kit.providers.domclick.serp import _SWEEP_ORIGIN, DomClickScraper def _scraper() -> DomClickScraper: return DomClickScraper( SimpleNamespace(scraper_fetch_mode="browser", browser_http_endpoint="http://b/fetch"), cookies={"qrator_jsid2": "x"}, ) @pytest.mark.asyncio async def test_count_probe_uses_subresource_with_warmed_origin() -> None: s = _scraper() fetcher = MagicMock() fetcher.fetch = AsyncMock(return_value='{"result":{"offersCount":2202}}') await s._count(fetcher, "2", None, None) # type: ignore[attr-defined] kw = fetcher.fetch.await_args.kwargs assert kw["fetch_mode"] == "subresource" assert kw["origin"] == _SWEEP_ORIGIN and kw["origin"] assert kw["cookies"] == {"qrator_jsid2": "x"} @pytest.mark.asyncio async def test_pagination_uses_subresource_with_warmed_origin() -> None: """Второй вызов — листалка; режим и прогрев должны совпадать с count-пробой.""" s = _scraper() fetcher = MagicMock() fetcher.fetch = AsyncMock(return_value='{"result":{"items":[]}}') await s._paginate(fetcher, "2", None, None, set(), [], 1) # type: ignore[attr-defined] assert fetcher.fetch.await_count >= 1 kw = fetcher.fetch.await_args.kwargs assert kw["fetch_mode"] == "subresource" assert kw["origin"] == _SWEEP_ORIGIN @pytest.mark.asyncio @pytest.mark.parametrize( ("mode", "ожидается_в_payload"), [("subresource", True), ("navigate", False), (None, False)], ) async def test_fetch_mode_reaches_payload_only_when_meaningful( mode: str | None, ожидается_в_payload: bool ) -> None: """Дефолтный режим в payload не попадает: старый сайдкар не знает такого ключа.""" from scraper_kit.browser_fetcher import BrowserFetcher bf = BrowserFetcher(source="domclick", endpoint="http://b/fetch") bf._client = MagicMock() captured: dict = {} class _Resp: status_code = 200 @staticmethod def json() -> dict: return {"html": "ok", "status": 200} @staticmethod def raise_for_status() -> None: return None async def _post(_url: str, json: dict, **_kw: object) -> _Resp: captured.update(json) return _Resp() bf._client.post = _post await bf._post_fetch("https://bff-search-web.domclick.ru/api/offers/v1", _SWEEP_ORIGIN, {"c": "1"}, False, _SWEEP_ORIGIN, mode) assert ("fetch_mode" in captured) is ожидается_в_payload if ожидается_в_payload: assert captured["fetch_mode"] == mode