gendesign/tradein-mvp/browser/test_server_smoke.py
Light1YT 74461e8611
All checks were successful
CI / changes (push) Successful in 7s
CI / backend-tests (push) Has been skipped
CI / frontend-tests (push) Has been skipped
CI / openapi-codegen-check (push) Has been skipped
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
fix(tradein-browser): non-fatal launch + background proxy retry
Прокси mproxy.site упал -> camoufox eager public_ip(proxy) для geoip
бросал InvalidProxy из app.startup -> exit(1) -> docker restart-loop
(RestartCount 3141). Теперь фейл launch'а не валит startup: контейнер
остаётся Up, фоновая задача ретраит launch с backoff (30-60с) пока прокси
не вернётся. /fetch и /login отдают 503 при недоступном браузере (caller
ловит как failed fetch, не краш). proxy+geoip сохранены — анти-бан
fingerprint на месте, никакого direct-connection fallback. Креды прокси
не логируются (только exc_type, без str(exc) с user:pass@host).

Refs #905
2026-06-14 20:16:17 +05:00

124 lines
5 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_smoke.py — смоук для resilience-логики tradein-browser.
Полный браузер (camoufox subprocess) юнит-тестить непрактично, поэтому здесь
только смоук: (1) модуль импортируется; (2) /fetch и /login отдают 503 когда
браузер не поднят (прокси лежит) — критичный resilience-инвариант, защищающий
от crash-storm. camoufox в окружении теста НЕ нужен: launch замокан no-op'ом,
имитируя «прокси всё ещё недоступен».
Запуск (из tradein-mvp/browser/)::
python -m pytest test_server_smoke.py -q
aiohttp.test_utils.make_mocked_request не поднимает реальный сервер/сокет —
хендлер вызывается напрямую, без event-loop-сервера.
"""
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.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)
def _json_body(response: Any) -> dict[str, Any]:
"""Достаёт JSON-тело из aiohttp Response (body — bytes после json_response)."""
return json.loads(response.body.decode())
def test_module_imports() -> None:
"""Смоук: модуль импортируется и экспонирует ключевые хендлеры."""
assert callable(server.fetch_handler)
assert callable(server.login_handler)
assert callable(server.health_handler)
def test_health_reports_browser_not_ready(monkeypatch: pytest.MonkeyPatch) -> None:
"""/health всегда 200, browser_ready=False когда браузер не поднят."""
monkeypatch.setattr(server, "_browser", None)
request = make_mocked_request("GET", "/health")
response = asyncio.run(server.health_handler(request))
assert response.status == 200
body = _json_body(response)
assert body["status"] == "ok"
assert body["browser_ready"] is False
def test_fetch_returns_503_when_browser_unavailable(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""/fetch отдаёт 503 (не 500/краш) когда браузер не поднят и launch не удался.
Имитирует «прокси лежит»: _browser=None + _try_launch_browser no-op,
оставляющий None. Никакого реального camoufox-launch'а / direct-fetch.
"""
monkeypatch.setattr(server, "_browser", None)
monkeypatch.setattr(server, "_fetch_lock", asyncio.Lock())
async def _no_op_launch() -> bool:
return False # прокси всё ещё недоступен → браузер остаётся None
monkeypatch.setattr(server, "_try_launch_browser", _no_op_launch)
request = make_mocked_request(
"POST", "/fetch", payload=b'{"url": "https://example.com"}'
)
async def _run() -> Any:
# make_mocked_request не парсит тело — подменяем .json() на нужный payload.
request.json = lambda: _coro({"url": "https://example.com"}) # type: ignore[method-assign]
return await server.fetch_handler(request)
response = asyncio.run(_run())
assert response.status == 503
body = _json_body(response)
assert "browser unavailable" in body["error"]
def test_login_returns_503_when_browser_unavailable(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""/login тоже отдаёт 503 когда браузер не поднят (без credential-логирования)."""
monkeypatch.setattr(server, "_browser", None)
monkeypatch.setattr(server, "_fetch_lock", asyncio.Lock())
async def _no_op_launch() -> bool:
return False
monkeypatch.setattr(server, "_try_launch_browser", _no_op_launch)
login_body = {
"url": "https://cian.example/login",
"email": "u@example.com",
"password": "secret",
"email_selector": "#email",
"password_selector": "#password",
"submit_selector": "#submit",
}
request = make_mocked_request("POST", "/login")
async def _run() -> Any:
request.json = lambda: _coro(login_body) # type: ignore[method-assign]
return await server.login_handler(request)
response = asyncio.run(_run())
assert response.status == 503
body = _json_body(response)
assert "browser unavailable" in body["error"]
async def _coro(value: Any) -> Any:
"""Хелпер: оборачивает значение в awaitable для подмены request.json()."""
return value