gendesign/tradein-mvp/browser/test_server_no_proxy_refusal.py
bot-backend 2fc4b1b0fc
All checks were successful
CI / changes (pull_request) Successful in 8s
CI Trade-In / changes (pull_request) Successful in 8s
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
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 2m44s
fix(tradein/scraper): не ходить через мёртвый запасной прокси — это не бан, а наша конфигурация
2026-08-02 16:56:23 +03:00

310 lines
12 KiB
Python
Raw 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_no_proxy_refusal.py — #2616 шаг 1: не идти напрямую с IP сервера.
tradein-browser (per-provider camoufox) раньше в прод-контейнере, если ни тело
запроса, ни ЛЮБАЯ env-переменная не несли proxy, лениво запускал camoufox БЕЗ
proxy — прямое подключение с IP сервера. Мертвые env-переменные (407/connection
refused, #2613) сегодня этого не триггерят (запуск падает раньше, на invalid-proxy
проверке camoufox) — но как только шаг 2 issue #2616 их удалит, сценарий станет
реальным. Этот файл фиксирует контракт ЗАРАНЕЕ (`_no_live_proxy` + `IS_PROD` guard
в fetch_handler/fetch_json_handler/login_handler), до того как #2616 шаг 2 сделает
его наблюдаемым в проде.
Проверяет:
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-прокси ни для одного провайдера.
Явно чистим все env-переменные, которые `_provider_proxy` умеет читать — тест
не должен зависеть от того, что реально задано в 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())
for var in (
"BROWSER_PROXY_AVITO",
"AVITO_PROXY_URL",
"BROWSER_PROXY_CIAN",
"CIAN_PROXY_URL",
"BROWSER_PROXY_YANDEX",
"YANDEX_PROXY_URL",
"SCRAPER_PROXY_URL",
):
monkeypatch.delenv(var, 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("BROWSER_PROXY_AVITO", "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 "<ok>"
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("BROWSER_PROXY_AVITO", "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 "<ok>"
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 "<ok>"
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("BROWSER_PROXY_AVITO", "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