"""test_server_resource_block.py — юниты для блокировки тяжёлых ресурсов (perf). Проверяет: 1. _parse_bool — разбор BROWSER_BLOCK_RESOURCES в bool; 2. _apply_resource_block навешивает route когда флаг включён и НЕ навешивает когда выключен (no-op); 3. сам route-handler аборт'ит image/media/font и continue'ит document/script/xhr; 4. сбой abort() деградирует в continue_() (fallback), не роняя навигацию. camoufox/Playwright НЕ поднимается: route + request мокаются заглушками. Хендлер вызывается напрямую через захваченный _apply_resource_block-callback. Запуск (из tradein-mvp/browser/):: python -m pytest test_server_resource_block.py -q """ from __future__ import annotations import asyncio import importlib.util from pathlib import Path from typing import Any import pytest # server.py — не пакет (отдельный сервис без __init__/pyproject). Грузим по пути. _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) # ── _parse_bool ────────────────────────────────────────────────────────────────── def test_parse_bool_truthy() -> None: for raw in ("true", "True", "1", "yes", "on", " TRUE "): assert server._parse_bool(raw, False) is True, raw def test_parse_bool_falsy() -> None: for raw in ("false", "0", "no", "off", "garbage", ""): assert server._parse_bool(raw, True) is False, raw def test_parse_bool_none_uses_default() -> None: assert server._parse_bool(None, True) is True assert server._parse_bool(None, False) is False def test_default_block_resources_on() -> None: """Дефолт: блокировка включена, типы — image/media/font.""" assert server._BLOCK_RESOURCES is True assert server._BLOCKED_TYPES == frozenset({"image", "media", "font"}) # ── route-interception заглушки ────────────────────────────────────────────────── class _FakeRequest: def __init__(self, resource_type: str) -> None: self.resource_type = resource_type class _FakeRoute: """Поддельный Playwright Route: считает abort/continue, мокает request.""" def __init__(self, resource_type: str, fail_abort: bool = False) -> None: self.request = _FakeRequest(resource_type) self.aborted = 0 self.continued = 0 self._fail_abort = fail_abort async def abort(self) -> None: if self._fail_abort: raise RuntimeError("route already handled") self.aborted += 1 async def continue_(self) -> None: self.continued += 1 class _FakePage: """Поддельная page: захватывает callback из page.route(pattern, handler).""" def __init__(self) -> None: self.route_pattern: str | None = None self.route_handler: Any = None async def route(self, pattern: str, handler: Any) -> None: self.route_pattern = pattern self.route_handler = handler def _capture_handler(monkeypatch: pytest.MonkeyPatch, *, enabled: bool, blocked: set[str]) -> Any: """Включает флаг/типы, прогоняет _apply_resource_block, возвращает page (с handler).""" monkeypatch.setattr(server, "_BLOCK_RESOURCES", enabled) monkeypatch.setattr(server, "_BLOCKED_TYPES", frozenset(blocked)) page = _FakePage() asyncio.run(server._apply_resource_block(page)) return page def test_apply_resource_block_noop_when_disabled(monkeypatch: pytest.MonkeyPatch) -> None: """BROWSER_BLOCK_RESOURCES=false → route НЕ навешивается.""" page = _capture_handler(monkeypatch, enabled=False, blocked={"image"}) assert page.route_handler is None assert page.route_pattern is None def test_apply_resource_block_registers_route_when_enabled( monkeypatch: pytest.MonkeyPatch, ) -> None: """Флаг включён → route на '**/*' с хендлером.""" page = _capture_handler(monkeypatch, enabled=True, blocked={"image", "media", "font"}) assert page.route_pattern == "**/*" assert callable(page.route_handler) def test_handler_aborts_image(monkeypatch: pytest.MonkeyPatch) -> None: """image (в _BLOCKED_TYPES) → abort, без continue.""" page = _capture_handler(monkeypatch, enabled=True, blocked={"image", "media", "font"}) route = _FakeRoute("image") asyncio.run(page.route_handler(route)) assert route.aborted == 1 assert route.continued == 0 def test_handler_aborts_media_and_font(monkeypatch: pytest.MonkeyPatch) -> None: page = _capture_handler(monkeypatch, enabled=True, blocked={"image", "media", "font"}) for rtype in ("media", "font"): route = _FakeRoute(rtype) asyncio.run(page.route_handler(route)) assert route.aborted == 1, rtype assert route.continued == 0, rtype def test_handler_continues_document_and_script(monkeypatch: pytest.MonkeyPatch) -> None: """document/script/stylesheet/xhr/fetch — НЕ блокируются (нужны для гидрации/stealth).""" page = _capture_handler(monkeypatch, enabled=True, blocked={"image", "media", "font"}) for rtype in ("document", "script", "stylesheet", "xhr", "fetch"): route = _FakeRoute(rtype) asyncio.run(page.route_handler(route)) assert route.aborted == 0, rtype assert route.continued == 1, rtype def test_handler_falls_back_to_continue_on_abort_error( monkeypatch: pytest.MonkeyPatch, ) -> None: """abort() кинул (route уже обработан) → fallback continue_(), навигация не падает.""" page = _capture_handler(monkeypatch, enabled=True, blocked={"image"}) route = _FakeRoute("image", fail_abort=True) # Не должно бросить наружу. asyncio.run(page.route_handler(route)) assert route.continued == 1, "после сбоя abort должен быть fallback-continue"