gendesign/tradein-mvp/browser/test_server_no_proxy_refusal.py
bot-backend 59c072fc4c
All checks were successful
Deploy Trade-In / changes (push) Successful in 10s
Deploy Trade-In / build-frontend (push) Successful in 2m13s
Deploy Trade-In / test (push) Successful in 2m45s
Deploy Trade-In / build-browser (push) Successful in 3m2s
Deploy Trade-In / build-backend (push) Successful in 1m38s
Deploy Trade-In / deploy (push) Successful in 2m29s
chore(tradein): удалить мёртвые mobileproxy env-переменные и rotate-ip (#2616 шаги 2-3) (#2650)
2026-08-05 09:35:54 +00:00

301 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) раньше в прод-контейнере, если ни тело
запроса, ни 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 "<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("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 "<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("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