gendesign/tradein-mvp/browser/test_server_resource_block.py
bot-backend 59b5d15752
All checks were successful
CI / changes (pull_request) Successful in 6s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
feat(browser): block font/media + per-provider page pacing — снизить proxy fan-out (#1812)
2026-06-19 21:28:09 +03:00

162 lines
6.7 KiB
Python
Raw Permalink 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.

"""test_server_resource_block.py — юниты для блокировки тяжёлых ресурсов (perf).
Проверяет:
1. _parse_bool — общий разбор булевых env-флагов;
2. _parse_block_types — разбор BROWSER_BLOCK_RESOURCE_TYPES CSV (дефолт font,media);
3. _apply_resource_block навешивает route когда _BLOCKED_TYPES непуст и НЕ навешивает
когда пуст (no-op);
4. сам route-handler аборт'ит блокируемые типы и continue'ит document/script/xhr;
5. сбой 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_parse_block_types_default_font_media() -> None:
"""Дефолт BROWSER_BLOCK_RESOURCE_TYPES (env не задана) — font/media."""
assert server._parse_block_types(None) == frozenset({"font", "media"})
def test_parse_block_types_empty_string_disables() -> None:
"""Пустая строка → пустой набор (блокировка выключена)."""
assert server._parse_block_types("") == frozenset()
def test_parse_block_types_csv_trim_lower() -> None:
"""CSV парсится с trim/lower, дубликаты схлопываются."""
assert server._parse_block_types(" Font , MEDIA ,font") == frozenset({"font", "media"})
# ── 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, *, blocked: set[str]) -> Any:
"""Ставит _BLOCKED_TYPES, прогоняет _apply_resource_block, возвращает page (с handler).
Пустой blocked → блокировка выключена (no-op), route не навешивается.
"""
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_empty(monkeypatch: pytest.MonkeyPatch) -> None:
"""Пустой _BLOCKED_TYPES (BROWSER_BLOCK_RESOURCE_TYPES="") → route НЕ навешивается."""
page = _capture_handler(monkeypatch, blocked=set())
assert page.route_handler is None
assert page.route_pattern is None
def test_apply_resource_block_registers_route_when_nonempty(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Непустой набор → route на '**/*' с хендлером."""
page = _capture_handler(monkeypatch, blocked={"font", "media"})
assert page.route_pattern == "**/*"
assert callable(page.route_handler)
def test_handler_aborts_blocked_types(monkeypatch: pytest.MonkeyPatch) -> None:
"""Дефолтные font/media (в _BLOCKED_TYPES) → abort, без continue."""
page = _capture_handler(monkeypatch, blocked={"font", "media"})
for rtype in ("font", "media"):
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/image — НЕ блокируются (гидрация/fingerprint)."""
page = _capture_handler(monkeypatch, blocked={"font", "media"})
for rtype in ("document", "script", "stylesheet", "xhr", "fetch", "image"):
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, blocked={"font"})
route = _FakeRoute("font", fail_abort=True)
# Не должно бросить наружу.
asyncio.run(page.route_handler(route))
assert route.continued == 1, "после сбоя abort должен быть fallback-continue"