"""Быстрые тесты каркаса приложения: аутентификация NATS, /healthcheck, префикс API (без БД и NATS).""" import importlib import os from http import HTTPStatus from types import SimpleNamespace import httpx import nkeys import pytest from src.api.router import router from src.core.config import NATSSettings # src/__init__.py реэкспортирует объект app под именем модуля, поэтому модуль берём через importlib app_module = importlib.import_module("src.app") def _nats_settings(seed: str = "", jwt: str = "") -> NATSSettings: return NATSSettings(NATS_URL="nats://localhost:4222", NATS_SEED=seed, NATS_JWT=jwt) def _valid_seed() -> str: return nkeys.encode_seed(os.urandom(32), nkeys.PREFIX_BYTE_USER).decode("utf-8") def test_build_nats_auth_empty_credentials_gives_no_kwargs(): assert app_module._build_nats_auth(_nats_settings()) == {} @pytest.mark.parametrize( ("seed", "jwt"), [("SU-only-seed", ""), ("", "only-jwt")], ) def test_build_nats_auth_partial_credentials_is_config_error(seed: str, jwt: str): with pytest.raises(RuntimeError, match="NATS_SEED"): app_module._build_nats_auth(_nats_settings(seed, jwt)) def test_build_nats_auth_valid_credentials_give_callbacks(): seed = _valid_seed() auth = app_module._build_nats_auth(_nats_settings(seed, "header.payload.signature")) assert set(auth) == {"user_jwt_cb", "signature_cb"} assert auth["user_jwt_cb"]() == b"header.payload.signature" signature = auth["signature_cb"](b"nonce") assert isinstance(signature, bytes) assert signature @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.closed = False async def close(self) -> None: self.closed = 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, "_connect_nats", unavailable) 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_closes_created_resources(monkeypatch: pytest.MonkeyPatch): fake_nats = _FakeNats() s3_client = _FakeS3Client() storage = SimpleNamespace(client=s3_client) async def connect_s3() -> SimpleNamespace: return storage async def connect_nats() -> _FakeNats: return fake_nats monkeypatch.setattr(app_module, "_connect_s3", connect_s3) monkeypatch.setattr(app_module, "_connect_nats", connect_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 not fake_nats.closed assert not s3_client.session_closed assert fake_nats.closed assert s3_client.session_closed