"""test_server_no_proxy_refusal.py — #2616 шаг 1: не идти напрямую с IP сервера. tradein-browser (per-provider camoufox) раньше в прод-контейнере, если ни тело запроса, ни SCRAPER_PROXY_URL не несли proxy, лениво запускал camoufox БЕЗ proxy — прямое подключение с IP сервера. #2616 шаг 2 удалил мёртвые per-provider переменные (AVITO_PROXY_URL/BROWSER_PROXY_* и т.д., 407/connection refused, #2613) — SCRAPER_PROXY_URL остался единственным источником. Этот файл фиксирует контракт (`_no_live_proxy` + `IS_PROD` guard в fetch_handler/fetch_json_handler/login_handler). Проверяет: 1. `_no_live_proxy` — pure-function поведение (override / env приоритеты). 2. prod + нет override + нет env → 503 с распознаваемой причиной, `_ensure_browser` (и, соответственно, `_launch_browser`) НЕ вызывается вовсе — camoufox не стартует без прокси. 3. dev (или явный IS_PROD=False) + то же самое → прежнее поведение (lazy launch без прокси легитимен для локальной разработки). 4. prod + proxy присутствует (override ИЛИ env) → guard не триггерит, поведение без изменений (это НЕ случай отказа). 5. Текст отказа отличим от "browser unavailable (proxy may be down)" (launch failed на мёртвом-но-заданном прокси) — разные причины, разный текст. camoufox НЕ запускается: `_ensure_browser` мокается (см. test_server_proxy_override.py). Хендлер вызывается напрямую (make_mocked_request не поднимает сокет). Запуск (из tradein-mvp/browser/):: python -m pytest test_server_no_proxy_refusal.py -q """ from __future__ import annotations import asyncio import importlib.util import json from pathlib import Path from typing import Any import pytest from aiohttp.test_utils import make_mocked_request _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) def _json_body(response: Any) -> dict[str, Any]: return json.loads(response.body.decode()) async def _coro(value: Any) -> Any: return value def _make_request(path: str, body: dict[str, Any]) -> Any: request = make_mocked_request("POST", path) request.json = lambda: _coro(body) # type: ignore[method-assign] return request @pytest.fixture(autouse=True) def _reset_state(monkeypatch: pytest.MonkeyPatch) -> None: """Чистое per-provider состояние + НЕТ live env-прокси ни для одного провайдера. Явно чистим SCRAPER_PROXY_URL (единственная переменная, которую `_provider_proxy` читает, #2616 шаг 2) — тест не должен зависеть от того, что реально задано в shell/CI-раннере. """ monkeypatch.setattr(server, "_browsers", {}) monkeypatch.setattr(server, "_browser_cms", {}) monkeypatch.setattr(server, "_page_counters", {}) monkeypatch.setattr(server, "_locks", {}) monkeypatch.setattr(server, "_retry_tasks", {}) monkeypatch.setattr(server, "_last_goto_at", {}) monkeypatch.setattr(server, "_launched_proxy", {}) monkeypatch.setattr(server, "_locks_guard", asyncio.Lock()) monkeypatch.delenv("SCRAPER_PROXY_URL", raising=False) def _forbid_ensure_browser(monkeypatch: pytest.MonkeyPatch) -> list[str]: """Мок `_ensure_browser`, падающий AssertionError на любом вызове — falsification: если guard регрессирует (перестаёт отказывать ДО lock/launch), тест ловит это несовпадением типа исключения, а не просто другим статус-кодом. """ calls: list[str] = [] async def _boom(provider: str, proxy_override: str | None = None) -> bool: calls.append(provider) raise AssertionError("_ensure_browser must NOT be called — no proxy configured, prod") monkeypatch.setattr(server, "_ensure_browser", _boom) return calls # ── _no_live_proxy: pure-function поведение ───────────────────────────────────── def test_no_live_proxy_true_without_override_and_env() -> None: assert server._no_live_proxy("avito", None) is True def test_no_live_proxy_false_with_override() -> None: assert server._no_live_proxy("avito", "http://pool:8080") is False def test_no_live_proxy_false_with_env(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("SCRAPER_PROXY_URL", "http://env:8080") assert server._no_live_proxy("avito", None) is False # ── fetch_handler: prod-отказ, никакого launch'а ──────────────────────────────── def test_fetch_handler_prod_refuses_without_launch_attempt( monkeypatch: pytest.MonkeyPatch, ) -> None: monkeypatch.setattr(server, "IS_PROD", True) calls = _forbid_ensure_browser(monkeypatch) response = asyncio.run( server.fetch_handler(_make_request("/fetch", {"url": "https://www.avito.ru/x"})) ) assert response.status == 503 assert calls == [] # _ensure_browser НЕ вызывался вовсе body = _json_body(response) assert "no proxy configured" in body["error"] def test_fetch_json_handler_prod_refuses_without_launch_attempt( monkeypatch: pytest.MonkeyPatch, ) -> None: monkeypatch.setattr(server, "IS_PROD", True) calls = _forbid_ensure_browser(monkeypatch) response = asyncio.run( server.fetch_json_handler( _make_request("/fetch-json", {"url": "https://www.avito.ru/api/x", "source": "avito"}) ) ) assert response.status == 503 assert calls == [] body = _json_body(response) assert "no proxy configured" in body["error"] def test_login_handler_prod_refuses_without_launch_attempt( monkeypatch: pytest.MonkeyPatch, ) -> None: monkeypatch.setattr(server, "IS_PROD", True) calls = _forbid_ensure_browser(monkeypatch) response = asyncio.run( server.login_handler( _make_request( "/login", { "url": "https://www.cian.ru/login", "email": "a@b.ru", "password": "x", "email_selector": "#e", "password_selector": "#p", "submit_selector": "#s", "success_cookie": "sid", }, ) ) ) assert response.status == 503 assert calls == [] body = _json_body(response) assert "no proxy configured" in body["error"] # ── dev: прежнее поведение (lazy launch без прокси легитимен) ────────────────── def test_fetch_handler_dev_still_attempts_launch(monkeypatch: pytest.MonkeyPatch) -> None: """IS_PROD=False (dev) + нет прокси вообще → guard НЕ триггерит, _ensure_browser вызывается как раньше (dev/no-op direct connection — легитимно для локалки). """ monkeypatch.setattr(server, "IS_PROD", False) calls: list[tuple[str, str | None]] = [] async def _ensure(provider: str, proxy_override: str | None = None) -> bool: calls.append((provider, proxy_override)) return True monkeypatch.setattr(server, "_ensure_browser", _ensure) server._browsers["avito"] = object() # чтобы дальше не падать на _do_fetch async def _fake_do_fetch(*_a: Any, **_kw: Any) -> str: return "" monkeypatch.setattr(server, "_do_fetch", _fake_do_fetch) response = asyncio.run( server.fetch_handler(_make_request("/fetch", {"url": "https://www.avito.ru/x"})) ) assert response.status == 200 assert calls == [("avito", None)] # launch БЫЛ атакован — dev-путь не блокирует # ── prod + proxy присутствует (override ИЛИ env) → guard не триггерит ────────── def test_fetch_handler_prod_with_env_proxy_unaffected(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(server, "IS_PROD", True) monkeypatch.setenv("SCRAPER_PROXY_URL", "http://env:8080") # "живой" с т.з. guard'а calls: list[tuple[str, str | None]] = [] async def _ensure(provider: str, proxy_override: str | None = None) -> bool: calls.append((provider, proxy_override)) return True monkeypatch.setattr(server, "_ensure_browser", _ensure) server._browsers["avito"] = object() async def _fake_do_fetch(*_a: Any, **_kw: Any) -> str: return "" monkeypatch.setattr(server, "_do_fetch", _fake_do_fetch) response = asyncio.run( server.fetch_handler(_make_request("/fetch", {"url": "https://www.avito.ru/x"})) ) assert response.status == 200 assert calls == [("avito", None)] # guard не заблокировал — env-прокси "есть" def test_fetch_handler_prod_with_body_override_unaffected(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(server, "IS_PROD", True) calls: list[tuple[str, str | None]] = [] async def _ensure(provider: str, proxy_override: str | None = None) -> bool: calls.append((provider, proxy_override)) return True monkeypatch.setattr(server, "_ensure_browser", _ensure) server._browsers["avito"] = object() async def _fake_do_fetch(*_a: Any, **_kw: Any) -> str: return "" monkeypatch.setattr(server, "_do_fetch", _fake_do_fetch) response = asyncio.run( server.fetch_handler( _make_request( "/fetch", { "url": "https://www.avito.ru/x", "proxy": "http://pool:8080", "proxy_kind": "http", }, ) ) ) assert response.status == 200 assert calls == [("avito", "http://pool:8080")] # ── распознаваемость: отказ ≠ "browser unavailable (proxy may be down)" ──────── def test_refusal_message_distinguishable_from_launch_failure( monkeypatch: pytest.MonkeyPatch, ) -> None: """"нет прокси вообще" (наша инфраструктура) — другой текст, чем "browser unavailable (proxy may be down)" (launch упал на мёртвом-но-заданном прокси). """ monkeypatch.setattr(server, "IS_PROD", True) _forbid_ensure_browser(monkeypatch) no_proxy_resp = asyncio.run( server.fetch_handler(_make_request("/fetch", {"url": "https://www.avito.ru/x"})) ) no_proxy_text = _json_body(no_proxy_resp)["error"] # launch failure (дохлый-но-заданный env) — существующий, другой путь. monkeypatch.setenv("SCRAPER_PROXY_URL", "http://dead:8080") async def _fails_launch(provider: str, proxy_override: str | None = None) -> bool: return False # camoufox launch упал (InvalidProxy) — существующая семантика monkeypatch.setattr(server, "_ensure_browser", _fails_launch) launch_failed_resp = asyncio.run( server.fetch_handler(_make_request("/fetch", {"url": "https://www.avito.ru/x"})) ) launch_failed_text = _json_body(launch_failed_resp)["error"] assert no_proxy_text != launch_failed_text assert "no proxy configured" in no_proxy_text assert "browser unavailable" in launch_failed_text