All checks were successful
Deploy Trade-In / deploy (push) Successful in 1m18s
Deploy Trade-In / changes (push) Successful in 7s
Deploy Trade-In / test (push) Has been skipped
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-backend (push) Has been skipped
Deploy Trade-In / build-browser (push) Successful in 2m2s
124 lines
5 KiB
Python
124 lines
5 KiB
Python
"""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
|