"""test_server_fetch_mode.py — режимы получения тела: навигация против подзапроса (#3264). BFF-ручка Домклика и карта офферов страницами не являются. Навигацией браузера на BFF через мобильные узлы пула приходит только ChallengeTimeout, подзапросом из прогретого контекста — HTTP 200 за 6 секунд (замер 30.08.2026, четыре узла, шесть корзин). Здесь проверяется контракт режимов, а не поведение площадки. Тестируется: 1. дефолт остаётся навигацией — goto зовётся, подзапрос нет; 2. subresource НЕ навигирует на целевой url и берёт тело запросом контекста; 3. gzip-файл распаковывается (карта офферов приезжает application/gzip); 4. неизвестный режим отбивается 400, а не падает внутри. camoufox НЕ запускается — browser/context/page подделаны. """ from __future__ import annotations import asyncio import gzip import importlib.util from pathlib import Path from typing import Any import pytest _SERVER_PATH = Path(__file__).resolve().parent / "server.py" _spec = importlib.util.spec_from_file_location("tradein_browser_server", _SERVER_PATH) assert _spec is not None and _spec.loader is not None server = importlib.util.module_from_spec(_spec) _spec.loader.exec_module(server) class _Resp: def __init__(self, raw: bytes, status: int = 200) -> None: self._raw, self.status = raw, status async def body(self) -> bytes: return self._raw class _Request: def __init__(self, raw: bytes) -> None: self._raw = raw self.calls: list[tuple[str, Any]] = [] async def get(self, url: str, **kwargs: Any) -> _Resp: self.calls.append((url, kwargs)) return _Resp(self._raw) class _Context: def __init__(self, raw: bytes) -> None: self.request = _Request(raw) class _Page: def __init__(self, raw: bytes = b"{}") -> None: self.context = _Context(raw) self.goto_urls: list[str] = [] self.evaluated: list[str] = [] async def goto(self, url: str, **_kw: Any) -> None: self.goto_urls.append(url) async def evaluate(self, _script: str, url: str) -> dict: self.evaluated.append(url) return {"status": 200, "b64": ""} def test_gzip_body_is_decompressed() -> None: """Карта офферов приезжает Content-Type: application/gzip — стек её не распакует.""" raw = gzip.compress("x".encode()) assert server._decode_body(raw) == "x" def test_plain_body_passes_through() -> None: assert server._decode_body(b'{"result":{"offersCount":2202}}') == ( '{"result":{"offersCount":2202}}' ) def test_subresource_does_not_navigate_to_target() -> None: """Смысл режима: на целевой url НЕ ходим навигацией, берём его подзапросом.""" page = _Page(b'{"result":{"offersCount":2202}}') raw, status = asyncio.run( server._fetch_subresource(page, "https://bff-search-web.domclick.ru/api/x", "subresource", "https://ekaterinburg.domclick.ru/serp") ) assert page.goto_urls == [] assert status == 200 assert b"offersCount" in raw url, kwargs = page.context.request.calls[0] assert url == "https://bff-search-web.domclick.ru/api/x" assert kwargs["headers"] == {"Referer": "https://ekaterinburg.domclick.ru/serp"} def test_subresource_without_referer_sends_no_header() -> None: page = _Page() asyncio.run(server._fetch_subresource(page, "https://x.domclick.ru/api", "subresource", None)) assert page.context.request.calls[0][1]["headers"] is None def test_unknown_mode_raises_named_error() -> None: page = _Page() with pytest.raises(ValueError, match="fetch_mode"): asyncio.run(server._fetch_subresource(page, "https://x/api", "телепортация", None)) def test_navigate_is_the_default_and_stays_a_valid_mode() -> None: assert "navigate" in server.FETCH_MODES assert {"navigate", "subresource", "page_fetch"} == set(server.FETCH_MODES)