"""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)