MinioSettings.MINIO_SECURE (по умолчанию true — https, как в шаблоне и на платформе) передаётся в Minio(secure=...); .env.example и .env.test — false (MinIO из docker compose слушает http). README: трейсбэк S3 из раздела «что в логе» убран, переменная добавлена в таблицу конфигурации. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
201 lines
7.0 KiB
Python
201 lines
7.0 KiB
Python
"""Быстрые тесты каркаса приложения: lifespan, /healthcheck, префикс API, клиент S3 (без БД и NATS).
|
|
|
|
Аутентификация и supervisor NATS — tests/test_nats_core.py (подключение переехало в src/core/nats.py, рулинг R-3).
|
|
"""
|
|
|
|
import asyncio
|
|
import importlib
|
|
from http import HTTPStatus
|
|
from types import SimpleNamespace
|
|
from typing import Any
|
|
|
|
import httpx
|
|
import pytest
|
|
|
|
from src.api.router import router
|
|
from src.core.nats import NatsConnection
|
|
|
|
# src/__init__.py реэкспортирует объект app под именем модуля, поэтому модуль берём через importlib
|
|
app_module = importlib.import_module("src.app")
|
|
|
|
|
|
@pytest.fixture
|
|
async def client():
|
|
# Без lifespan: app.state.nats не установлен — healthcheck обязан это пережить
|
|
transport = httpx.ASGITransport(app=app_module.app)
|
|
async with httpx.AsyncClient(transport=transport, base_url="http://test") as c:
|
|
yield c
|
|
|
|
|
|
async def test_healthcheck_nats_off_when_disabled(client: httpx.AsyncClient, monkeypatch: pytest.MonkeyPatch):
|
|
monkeypatch.setattr(app_module.settings, "NATS_ENABLED", False)
|
|
|
|
response = await client.get("/healthcheck")
|
|
|
|
assert response.status_code == HTTPStatus.OK
|
|
body = response.json()
|
|
assert body["nats"] == "off"
|
|
assert {"uptime", "version"} <= set(body)
|
|
|
|
|
|
async def test_healthcheck_nats_down_when_enabled_without_connection(
|
|
client: httpx.AsyncClient, monkeypatch: pytest.MonkeyPatch
|
|
):
|
|
monkeypatch.setattr(app_module.settings, "NATS_ENABLED", True)
|
|
|
|
response = await client.get("/healthcheck")
|
|
|
|
assert response.status_code == HTTPStatus.OK
|
|
assert response.json()["nats"] == "down"
|
|
|
|
|
|
@pytest.mark.parametrize(("connected", "expected"), [(True, "ok"), (False, "down")])
|
|
async def test_healthcheck_nats_follows_connection_state(
|
|
client: httpx.AsyncClient, monkeypatch: pytest.MonkeyPatch, connected: bool, expected: str
|
|
):
|
|
monkeypatch.setattr(app_module.settings, "NATS_ENABLED", True)
|
|
monkeypatch.setattr(app_module.app.state, "nats", SimpleNamespace(is_connected=connected), raising=False)
|
|
|
|
response = await client.get("/healthcheck")
|
|
|
|
assert response.status_code == HTTPStatus.OK
|
|
assert response.json()["nats"] == expected
|
|
|
|
|
|
def test_router_prefix():
|
|
assert router.prefix == "/api/v1"
|
|
|
|
|
|
class _FakeNats:
|
|
def __init__(self) -> None:
|
|
self.started = False
|
|
self.stopped = False
|
|
self.is_connected = False
|
|
|
|
async def start(self) -> None:
|
|
self.started = True
|
|
|
|
async def stop(self) -> None:
|
|
self.stopped = True
|
|
|
|
|
|
class _FakeS3Client:
|
|
def __init__(self) -> None:
|
|
self.session_closed = False
|
|
|
|
async def close_session(self) -> None:
|
|
self.session_closed = True
|
|
|
|
|
|
@pytest.fixture
|
|
def _no_smb(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
monkeypatch.setattr(app_module.smbclient, "register_session", lambda *args, **kwargs: None)
|
|
monkeypatch.setattr(app_module.smbclient, "reset_connection_cache", lambda: None)
|
|
|
|
|
|
@pytest.mark.usefixtures("_no_smb")
|
|
async def test_lifespan_starts_when_s3_and_nats_unavailable(monkeypatch: pytest.MonkeyPatch):
|
|
async def unavailable() -> None:
|
|
return None
|
|
|
|
monkeypatch.setattr(app_module, "_connect_s3", unavailable)
|
|
monkeypatch.setattr(app_module, "_create_nats", lambda: None)
|
|
reached_yield = False
|
|
|
|
async with app_module.lifespan(app_module.app):
|
|
reached_yield = True
|
|
assert app_module.app.state.storage is None
|
|
assert app_module.app.state.nats is None
|
|
|
|
assert reached_yield
|
|
|
|
|
|
@pytest.mark.usefixtures("_no_smb")
|
|
async def test_lifespan_starts_and_stops_nats_and_closes_s3(monkeypatch: pytest.MonkeyPatch):
|
|
fake_nats = _FakeNats()
|
|
s3_client = _FakeS3Client()
|
|
storage = SimpleNamespace(client=s3_client)
|
|
|
|
async def connect_s3() -> SimpleNamespace:
|
|
return storage
|
|
|
|
monkeypatch.setattr(app_module, "_connect_s3", connect_s3)
|
|
monkeypatch.setattr(app_module, "_create_nats", lambda: fake_nats)
|
|
|
|
async with app_module.lifespan(app_module.app):
|
|
assert app_module.app.state.storage is storage
|
|
assert app_module.app.state.nats is fake_nats
|
|
assert fake_nats.started
|
|
assert not fake_nats.stopped
|
|
assert not s3_client.session_closed
|
|
|
|
assert fake_nats.stopped
|
|
assert s3_client.session_closed
|
|
|
|
|
|
class _NeverConnectingClient:
|
|
"""Клиент nats-py, который никогда не подключается: connect висит (как nats-py без сервера)."""
|
|
|
|
is_connected = False
|
|
is_closed = False
|
|
|
|
async def connect(self, servers: str, **options: Any) -> None:
|
|
await asyncio.Event().wait()
|
|
|
|
async def close(self) -> None:
|
|
self.is_closed = True
|
|
|
|
|
|
@pytest.mark.usefixtures("_no_smb")
|
|
async def test_lifespan_does_not_wait_for_nats_and_healthcheck_is_down(monkeypatch: pytest.MonkeyPatch):
|
|
"""Регрессия: недоступный NATS не задерживает старт; до первого подключения healthcheck показывает down."""
|
|
|
|
async def unavailable() -> None:
|
|
return None
|
|
|
|
nats = NatsConnection(app_module.settings.OPENBAO.NATS, client_factory=_NeverConnectingClient)
|
|
monkeypatch.setattr(app_module, "_connect_s3", unavailable)
|
|
monkeypatch.setattr(app_module, "_create_nats", lambda: nats)
|
|
monkeypatch.setattr(app_module.settings, "NATS_ENABLED", True)
|
|
|
|
async with app_module.lifespan(app_module.app):
|
|
transport = httpx.ASGITransport(app=app_module.app)
|
|
async with httpx.AsyncClient(transport=transport, base_url="http://test") as c:
|
|
response = await c.get("/healthcheck")
|
|
assert response.json()["nats"] == "down"
|
|
|
|
assert not nats.is_connected
|
|
|
|
|
|
def test_create_nats_disabled_returns_none(monkeypatch: pytest.MonkeyPatch):
|
|
monkeypatch.setattr(app_module.settings, "NATS_ENABLED", False)
|
|
assert app_module._create_nats() is None
|
|
|
|
|
|
def test_create_nats_enabled_returns_connection_for_results(monkeypatch: pytest.MonkeyPatch):
|
|
monkeypatch.setattr(app_module.settings, "NATS_ENABLED", True)
|
|
nats = app_module._create_nats()
|
|
assert isinstance(nats, NatsConnection)
|
|
assert (nats.result_subject, nats.queue) == ("recognition.result", "arm-core")
|
|
assert not nats.is_connected # создание не подключает: подключение — в start()
|
|
|
|
|
|
@pytest.mark.parametrize("secure", [True, False])
|
|
async def test_connect_s3_passes_secure_flag(monkeypatch: pytest.MonkeyPatch, secure: bool):
|
|
created: dict[str, Any] = {}
|
|
|
|
class _FakeMinio:
|
|
def __init__(self, endpoint: str, **kwargs: Any) -> None:
|
|
created.update(kwargs, endpoint=endpoint)
|
|
|
|
async def bucket_exists(self, bucket: str) -> bool:
|
|
return True
|
|
|
|
monkeypatch.setattr(app_module, "Minio", _FakeMinio)
|
|
monkeypatch.setattr(app_module.settings.OPENBAO.MINIO, "MINIO_SECURE", secure)
|
|
|
|
storage = await app_module._connect_s3()
|
|
|
|
assert storage is not None
|
|
assert created["secure"] is secure
|