test(browser): удалить устаревшие pool-routing тесты (CI red после #1793) #1802
1 changed files with 0 additions and 240 deletions
|
|
@ -1,240 +0,0 @@
|
|||
"""test_browser_pool_routing.py — per-source proxy pool routing (Phase 1, #browser-pool).
|
||||
|
||||
Юнит-тесты server.fetch_handler с включённым/выключенным FEATURE_BROWSER_POOL_ENABLED.
|
||||
camoufox не нужен: launch/_do_fetch_pool замоканы.
|
||||
|
||||
server.py живёт в tradein-mvp/browser/ (отдельный сервис) и импортит `from aiohttp
|
||||
import web`. aiohttp в backend-venv НЕТ (только в browser-образе), поэтому здесь в
|
||||
sys.modules инжектится минимальный стаб aiohttp.web ДО загрузки server.py — ровно те
|
||||
имена, что использует модуль (Application/Request/Response/json_response). Так
|
||||
routing-логика гоняется прямо в backend pytest-gate без тяжёлой aiohttp-зависимости.
|
||||
|
||||
Проверяем:
|
||||
- флаг ON → fetch_handler роутит по body["source"] на нужный proxy_url и держит
|
||||
его lock во время _do_fetch_pool (замоканного);
|
||||
- неизвестный/отсутствующий source → fallback на avito-прокси;
|
||||
- launch недоступного прокси → 503;
|
||||
- флаг OFF → берётся одиночный путь (_fetch_via_single), source игнорируется;
|
||||
- _build_proxy_map отбрасывает пустые env и применяет legacy-fallback.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import importlib.util
|
||||
import json
|
||||
import sys
|
||||
import types
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
# ── минимальный стаб aiohttp.web (server.py юзает только эти имена) ──────────────
|
||||
|
||||
|
||||
class _StubResponse:
|
||||
"""Имитация web.Response от json_response: хранит status + JSON-байты в .body."""
|
||||
|
||||
def __init__(self, payload: dict[str, Any], status: int = 200) -> None:
|
||||
self.status = status
|
||||
self.body = json.dumps(payload).encode()
|
||||
|
||||
|
||||
def _json_response(payload: dict[str, Any], status: int = 200) -> _StubResponse:
|
||||
return _StubResponse(payload, status=status)
|
||||
|
||||
|
||||
def _install_aiohttp_stub() -> None:
|
||||
"""Кладёт фейковый aiohttp + aiohttp.web в sys.modules, если настоящего нет."""
|
||||
try:
|
||||
import aiohttp # noqa: F401
|
||||
|
||||
return # настоящий aiohttp доступен — стаб не нужен
|
||||
except ModuleNotFoundError:
|
||||
pass
|
||||
|
||||
web = types.ModuleType("aiohttp.web")
|
||||
web.Application = type("Application", (), {}) # type: ignore[attr-defined]
|
||||
web.Request = type("Request", (), {}) # type: ignore[attr-defined]
|
||||
web.Response = _StubResponse # type: ignore[attr-defined]
|
||||
web.json_response = _json_response # type: ignore[attr-defined]
|
||||
|
||||
aiohttp_mod = types.ModuleType("aiohttp")
|
||||
aiohttp_mod.web = web # type: ignore[attr-defined]
|
||||
sys.modules["aiohttp"] = aiohttp_mod
|
||||
sys.modules["aiohttp.web"] = web
|
||||
|
||||
|
||||
_install_aiohttp_stub()
|
||||
|
||||
# server.py — отдельный сервис вне backend-пакета. Грузим по относительному пути.
|
||||
_SERVER_PATH = Path(__file__).resolve().parents[2] / "browser" / "server.py"
|
||||
_spec = importlib.util.spec_from_file_location("tradein_browser_server_pool", _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 _StubRequest:
|
||||
"""Минимальный stub aiohttp-request: handler дёргает только .json()."""
|
||||
|
||||
def __init__(self, body: dict[str, Any]) -> None:
|
||||
self._body = body
|
||||
|
||||
async def json(self) -> dict[str, Any]:
|
||||
return self._body
|
||||
|
||||
|
||||
def _json_body(response: Any) -> dict[str, Any]:
|
||||
return json.loads(response.body.decode())
|
||||
|
||||
|
||||
def _make_fetch_request(body: dict[str, Any]) -> Any:
|
||||
return _StubRequest(body)
|
||||
|
||||
|
||||
# ── флаг ON: routing по source ───────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_pool_routes_by_source_and_holds_proxy_lock(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""ON: source=yandex → fetch на yandex-прокси, держа именно его lock."""
|
||||
proxy_map = {
|
||||
"avito": "http://avito-proxy:1",
|
||||
"cian": "http://cian-proxy:2",
|
||||
"yandex": "http://yandex-proxy:3",
|
||||
}
|
||||
monkeypatch.setattr(server, "FEATURE_BROWSER_POOL_ENABLED", True)
|
||||
monkeypatch.setattr(server, "BROWSER_PROXY_MAP", proxy_map)
|
||||
monkeypatch.setattr(server, "_pool_locks", {})
|
||||
monkeypatch.setattr(server, "_pool_locks_guard", asyncio.Lock())
|
||||
|
||||
seen: dict[str, Any] = {}
|
||||
|
||||
async def _fake_get_or_launch(proxy_url: str) -> object:
|
||||
seen["launched_proxy"] = proxy_url
|
||||
return object() # «браузер поднят»
|
||||
|
||||
async def _fake_do_fetch_pool(url: str, proxy_url: str) -> str:
|
||||
seen["fetch_proxy"] = proxy_url
|
||||
# lock этого proxy_url должен быть удержан вызывающим (handler).
|
||||
seen["lock_held"] = server._pool_locks[proxy_url].locked()
|
||||
return "<html>yandex</html>"
|
||||
|
||||
monkeypatch.setattr(server, "_get_or_launch_pool_browser", _fake_get_or_launch)
|
||||
monkeypatch.setattr(server, "_do_fetch_pool", _fake_do_fetch_pool)
|
||||
|
||||
request = _make_fetch_request({"url": "https://realty.yandex.ru/x", "source": "yandex"})
|
||||
response = asyncio.run(server.fetch_handler(request))
|
||||
|
||||
assert response.status == 200
|
||||
assert _json_body(response)["html"] == "<html>yandex</html>"
|
||||
assert seen["launched_proxy"] == "http://yandex-proxy:3"
|
||||
assert seen["fetch_proxy"] == "http://yandex-proxy:3"
|
||||
assert seen["lock_held"] is True
|
||||
assert "http://yandex-proxy:3" in server._pool_locks
|
||||
|
||||
|
||||
def test_pool_unknown_source_falls_back_to_avito(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""ON: domclick без своего прокси → fallback на avito-прокси."""
|
||||
proxy_map = {"avito": "http://avito-proxy:1"} # нет domclick
|
||||
monkeypatch.setattr(server, "FEATURE_BROWSER_POOL_ENABLED", True)
|
||||
monkeypatch.setattr(server, "BROWSER_PROXY_MAP", proxy_map)
|
||||
monkeypatch.setattr(server, "_pool_locks", {})
|
||||
monkeypatch.setattr(server, "_pool_locks_guard", asyncio.Lock())
|
||||
|
||||
seen: dict[str, Any] = {}
|
||||
|
||||
async def _fake_get_or_launch(proxy_url: str) -> object:
|
||||
return object()
|
||||
|
||||
async def _fake_do_fetch_pool(url: str, proxy_url: str) -> str:
|
||||
seen["fetch_proxy"] = proxy_url
|
||||
return "<html>fallback</html>"
|
||||
|
||||
monkeypatch.setattr(server, "_get_or_launch_pool_browser", _fake_get_or_launch)
|
||||
monkeypatch.setattr(server, "_do_fetch_pool", _fake_do_fetch_pool)
|
||||
|
||||
request = _make_fetch_request({"url": "https://domclick.ru/x", "source": "domclick"})
|
||||
response = asyncio.run(server.fetch_handler(request))
|
||||
|
||||
assert response.status == 200
|
||||
assert seen["fetch_proxy"] == "http://avito-proxy:1"
|
||||
|
||||
|
||||
def test_pool_returns_503_when_browser_unavailable(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""ON: launch недоступного прокси → None → 503 (та же форма, что одиночный путь)."""
|
||||
monkeypatch.setattr(server, "FEATURE_BROWSER_POOL_ENABLED", True)
|
||||
monkeypatch.setattr(server, "BROWSER_PROXY_MAP", {"avito": "http://avito-proxy:1"})
|
||||
monkeypatch.setattr(server, "_pool_locks", {})
|
||||
monkeypatch.setattr(server, "_pool_locks_guard", asyncio.Lock())
|
||||
|
||||
async def _fake_get_or_launch(proxy_url: str) -> None:
|
||||
return None # прокси лежит
|
||||
|
||||
monkeypatch.setattr(server, "_get_or_launch_pool_browser", _fake_get_or_launch)
|
||||
|
||||
request = _make_fetch_request({"url": "https://avito.ru/x", "source": "avito"})
|
||||
response = asyncio.run(server.fetch_handler(request))
|
||||
|
||||
assert response.status == 503
|
||||
assert "browser unavailable" in _json_body(response)["error"]
|
||||
|
||||
|
||||
# ── флаг OFF: одиночный путь, source игнорируется ────────────────────────────────
|
||||
|
||||
|
||||
def test_flag_off_uses_single_path_and_ignores_source(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""OFF: берётся _fetch_via_single, pool-путь НЕ вызывается, source неважен."""
|
||||
monkeypatch.setattr(server, "FEATURE_BROWSER_POOL_ENABLED", False)
|
||||
|
||||
single_called = {"n": 0}
|
||||
|
||||
async def _fake_single(url: str) -> Any:
|
||||
single_called["n"] += 1
|
||||
return server.web.json_response({"html": "<html>single</html>"})
|
||||
|
||||
async def _fail_pool(url: str, source: str | None) -> Any:
|
||||
raise AssertionError("pool path не должен вызываться при OFF-флаге")
|
||||
|
||||
monkeypatch.setattr(server, "_fetch_via_single", _fake_single)
|
||||
monkeypatch.setattr(server, "_fetch_via_pool", _fail_pool)
|
||||
|
||||
request = _make_fetch_request({"url": "https://avito.ru/x", "source": "yandex"})
|
||||
response = asyncio.run(server.fetch_handler(request))
|
||||
|
||||
assert response.status == 200
|
||||
assert _json_body(response)["html"] == "<html>single</html>"
|
||||
assert single_called["n"] == 1
|
||||
|
||||
|
||||
def test_build_proxy_map_drops_empties(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""_build_proxy_map: пустые/None env не попадают в map; legacy-fallback работает."""
|
||||
for var in (
|
||||
"BROWSER_PROXY_AVITO",
|
||||
"AVITO_PROXY_URL",
|
||||
"SCRAPER_PROXY_URL",
|
||||
"BROWSER_PROXY_CIAN",
|
||||
"CIAN_PROXY_URL",
|
||||
"BROWSER_PROXY_YANDEX",
|
||||
"YANDEX_PROXY_URL",
|
||||
"BROWSER_PROXY_DOMCLICK",
|
||||
):
|
||||
monkeypatch.delenv(var, raising=False)
|
||||
|
||||
monkeypatch.setenv("AVITO_PROXY_URL", "http://avito:1") # avito через legacy-fallback
|
||||
monkeypatch.setenv("BROWSER_PROXY_CIAN", "http://cian:2")
|
||||
# yandex/domclick не заданы → отсутствуют в map.
|
||||
|
||||
result = server._build_proxy_map()
|
||||
assert result == {"avito": "http://avito:1", "cian": "http://cian:2"}
|
||||
assert "yandex" not in result
|
||||
assert "domclick" not in result
|
||||
Loading…
Add table
Reference in a new issue