fix: SMB необязателен, остановка lifespan по независимым шагам (I-3, T4.5, M-3)

- OPENBAO__SMB__* необязательны (по умолчанию пусто); при пустом SMB_HOST
  сессия SMB не регистрируется (DEBUG «SMB не настроен»), в .env.example
  и .env.test SMB выключен — в логе dev-запуска больше нет трейсбэка SMB.
- Задан SMB_HOST: register_session и reset_connection_cache вызываются
  через asyncio.to_thread и не блокируют цикл событий.
- Остановка: задачи обмена -> NATS -> S3 -> SMB -> engine.dispose();
  каждый шаг в своём try/except с логом step=..., ошибка одного шага не
  отменяет следующие. Пул соединений БД закрывается последним (M-3).
- README: «что в логе при запуске», таблица настроек SMB, порядок
  остановки.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Andrey Epifancev 2026-10-04 01:53:23 +04:00
parent 6a6ffb54f4
commit 3d31c1eac5
7 changed files with 226 additions and 50 deletions

View File

@ -155,21 +155,23 @@ Dev-пользователи (`api/seed/dev/users.yaml`, создаются `mak
### Что вы увидите в логе при запуске ### Что вы увидите в логе при запуске
Старт занимает около секунды; в логе один трейсбэк — это ожидаемо в dev, приложение при этом работает: Старт занимает около секунды; трейсбэков в логе нет. SMB (горячие папки ИС Сканирование из шаблона) необязателен и по
умолчанию выключен: в `.env.example` `OPENBAO__SMB__SMB_HOST` пуст, сессия SMB не регистрируется (сообщение об этом —
- Трейсбэк SMB: `Ошибка при регистрации сессии SMB ... Failed to connect to 'localhost:445': Connection refused`. только на уровне DEBUG). Если `SMB_HOST` задан, регистрация выполняется в отдельном потоке (цикл событий не
Настройки `OPENBAO__SMB__*` — заглушки, SMB в Срезе 0 не используется; игнорируйте. Пока на `localhost:445` ничего блокируется; старт ждёт её завершения не дольше тайм-аута `smbclient`, 60 с); ошибка подключения логируется трейсбэком
не слушает, отказ приходит сразу (старт не задерживается); если порт отвечает, но молчит, ожидание ограничено `Ошибка при регистрации сессии SMB`, приложение продолжает работу.
тайм-аутом `smbclient` (60 с).
Строки `Бакет проверен`, `Подключение к NATS установлено` и `Подписка NATS активна. subject=recognition.result, Строки `Бакет проверен`, `Подключение к NATS установлено` и `Подписка NATS активна. subject=recognition.result,
queue=arm-core` означают, что S3 и NATS из compose подключены (MinIO из compose слушает `http`, поэтому в `.env` queue=arm-core` означают, что S3 и NATS из compose подключены (MinIO из compose слушает `http`, поэтому в `.env`
задано `OPENBAO__MINIO__MINIO_SECURE=false`). задано `OPENBAO__MINIO__MINIO_SECURE=false`).
Недоступность SMB, S3 или NATS при старте приложение не роняет и не задерживает: ошибка логируется, состояние NATS Недоступность S3 или NATS при старте приложение не роняет и не задерживает (недоступный SMB, если он настроен, — не
видно в `/healthcheck`. К NATS ядро подключается в фоне и повторяет попытки само (`nats=down`, пока связи нет; warning роняет): ошибка логируется, состояние NATS видно в `/healthcheck`. К NATS ядро подключается в фоне и повторяет попытки
в логе — на первую ошибку и дальше не чаще раза в минуту); после разрыва nats-py переподключается и восстанавливает само (`nats=down`, пока связи нет; warning в логе — на первую ошибку и дальше не чаще раза в минуту); после разрыва
подписку. nats-py переподключается и восстанавливает подписку.
При остановке ресурсы закрываются по шагам: фоновые задачи обмена → NATS → S3 → SMB → пул соединений БД. Ошибка
одного шага логируется (`Ошибка при остановке приложения. step=...`) и не отменяет следующие.
Предупреждение «Включён dev-эмитент токенов … только для разработки» появляется не при старте, а при первом Предупреждение «Включён dev-эмитент токенов … только для разработки» появляется не при старте, а при первом
запросе к защищённому эндпоинту (`/api/v1/me`): проверяющий токены создаётся лениво (штатно, см. «Безопасность запросе к защищённому эндпоинту (`/api/v1/me`): проверяющий токены создаётся лениво (штатно, см. «Безопасность
@ -207,10 +209,10 @@ docker compose -f ../docs/dev/docker-compose.yml down -v # полный сб
| `OPENBAO__NATS__NATS_URL` | — (обязательно) | адрес NATS (`nats://localhost:4222`) | | `OPENBAO__NATS__NATS_URL` | — (обязательно) | адрес NATS (`nats://localhost:4222`) |
| `OPENBAO__NATS__NATS_SEED` | пусто | nkey seed; пусто — подключение без учётных данных (compose, тесты). Задаётся вместе с `NATS_JWT` или не задаётся вовсе | | `OPENBAO__NATS__NATS_SEED` | пусто | nkey seed; пусто — подключение без учётных данных (compose, тесты). Задаётся вместе с `NATS_JWT` или не задаётся вовсе |
| `OPENBAO__NATS__NATS_JWT` | пусто | JWT NATS; см. `NATS_SEED` | | `OPENBAO__NATS__NATS_JWT` | пусто | JWT NATS; см. `NATS_SEED` |
| `OPENBAO__SMB__SMB_HOST` | — (обязательно) | заглушка SMB (в Срезе 0 не используется, блок обязателен) | | `OPENBAO__SMB__SMB_HOST` | пусто | SMB из шаблона, необязателен (ядру в Срезе 0 не нужен). Пусто — сессия SMB не регистрируется; задан — регистрация при старте (в отдельном потоке) и сброс кэша соединений при остановке |
| `OPENBAO__SMB__SMB_PATH` | — (обязательно) | заглушка SMB | | `OPENBAO__SMB__SMB_PATH` | пусто | путь SMB (в Срезе 0 не используется) |
| `OPENBAO__SMB__SMB_LOGIN` | — (обязательно) | заглушка SMB | | `OPENBAO__SMB__SMB_LOGIN` | пусто | логин SMB |
| `OPENBAO__SMB__SMB_PASSWORD` | — (обязательно) | заглушка SMB | | `OPENBAO__SMB__SMB_PASSWORD` | пусто | пароль SMB |
| `NATS_ENABLED` | `false` | подключение к NATS и обмен через `recognition_job`: фоновые outbox-публикатор и вахтёр сроков ответа (период 30 с, `processing`/`published` с прошедшим `reply_deadline` → `timeout`); без него фоновых задач нет, `/healthcheck` показывает `nats=off` | | `NATS_ENABLED` | `false` | подключение к NATS и обмен через `recognition_job`: фоновые outbox-публикатор и вахтёр сроков ответа (период 30 с, `processing`/`published` с прошедшим `reply_deadline` → `timeout`); без него фоновых задач нет, `/healthcheck` показывает `nats=off` |
| `OUTBOX_POLL_SECONDS` | `5` | период опроса outbox (`queued` → публикация → `published`) | | `OUTBOX_POLL_SECONDS` | `5` | период опроса outbox (`queued` → публикация → `published`) |
| `ACK_TIMEOUT_SECONDS` | `30` | ожидание ack сервиса распознавания до повторной публикации | | `ACK_TIMEOUT_SECONDS` | `30` | ожидание ack сервиса распознавания до повторной публикации |

View File

@ -19,11 +19,11 @@ OPENBAO__NATS__NATS_URL=nats://localhost:4222
OPENBAO__NATS__NATS_SEED= OPENBAO__NATS__NATS_SEED=
OPENBAO__NATS__NATS_JWT= OPENBAO__NATS__NATS_JWT=
# SMB — заглушки (в Срезе 0 не используется) # SMB необязателен (ядру в Срезе 0 не нужен): пустой SMB_HOST — сессия SMB не регистрируется
OPENBAO__SMB__SMB_HOST=localhost OPENBAO__SMB__SMB_HOST=
OPENBAO__SMB__SMB_PATH=share OPENBAO__SMB__SMB_PATH=
OPENBAO__SMB__SMB_LOGIN=user OPENBAO__SMB__SMB_LOGIN=
OPENBAO__SMB__SMB_PASSWORD=password OPENBAO__SMB__SMB_PASSWORD=
# Аутентификация. Dev-эмитент работает только при AUTH_DEV_ISSUER=true И DEBUG=true. # Аутентификация. Dev-эмитент работает только при AUTH_DEV_ISSUER=true И DEBUG=true.
AUTH_DEV_ISSUER=true AUTH_DEV_ISSUER=true

View File

@ -17,10 +17,11 @@ OPENBAO__NATS__NATS_URL=nats://localhost:4222
OPENBAO__NATS__NATS_SEED= OPENBAO__NATS__NATS_SEED=
OPENBAO__NATS__NATS_JWT= OPENBAO__NATS__NATS_JWT=
OPENBAO__SMB__SMB_HOST=localhost # SMB необязателен: пустой SMB_HOST — сессия SMB не регистрируется
OPENBAO__SMB__SMB_PATH=share OPENBAO__SMB__SMB_HOST=
OPENBAO__SMB__SMB_LOGIN=user OPENBAO__SMB__SMB_PATH=
OPENBAO__SMB__SMB_PASSWORD=password OPENBAO__SMB__SMB_LOGIN=
OPENBAO__SMB__SMB_PASSWORD=
AUTH_DEV_ISSUER=true AUTH_DEV_ISSUER=true
AUTH_DEV_SECRET=test-only-secret-not-for-production-0123456789 AUTH_DEV_SECRET=test-only-secret-not-for-production-0123456789

View File

@ -2,17 +2,21 @@
Что здесь есть: настройки, подключение S3 (MinIO; `MINIO_SECURE` — https или http), NATS (`src/core/nats.py`: core Что здесь есть: настройки, подключение S3 (MinIO; `MINIO_SECURE` — https или http), NATS (`src/core/nats.py`: core
NATS без JetStream, подключение в фоне при NATS_ENABLED=true независимо от DEBUG, подписка `recognition.result` в queue NATS без JetStream, подключение в фоне при NATS_ENABLED=true независимо от DEBUG, подписка `recognition.result` в queue
group `arm-core`), SMB (горячие папки ИС Сканирование), middleware (gzip, proxy headers, CORS; при DEBUG=False пакет group `arm-core`), SMB (горячие папки ИС Сканирование; необязателен — при пустом `SMB_HOST` не подключается),
РАИСА лишь условно импортируется, middleware не подключается), healthcheck (uptime, version, nats: ok|down|off), раздача middleware (gzip, proxy headers, CORS; при DEBUG=False пакет РАИСА лишь условно импортируется, middleware не
собранного SPA. подключается), healthcheck (uptime, version, nats: ok|down|off), раздача собранного SPA.
Недоступность S3 или NATS при старте не роняет и не задерживает приложение: ошибка логируется, healthcheck показывает Недоступность S3 или NATS при старте не роняет и не задерживает приложение: ошибка логируется, healthcheck показывает
состояние. При NATS_ENABLED=true lifespan запускает фоновые задачи обмена `recognition_job` — outbox-публикатор и состояние. При NATS_ENABLED=true lifespan запускает фоновые задачи обмена `recognition_job` — outbox-публикатор и
вахтёр сроков (`src/services/recognition_exchange.py`, design D-6) — и останавливает их до закрытия NATS. Сообщения вахтёр сроков (`src/services/recognition_exchange.py`, design D-6) — и останавливает их до закрытия NATS. Сообщения
`recognition.result` (ack и результаты сервисов) обрабатывает inbox `handle_message` того же модуля с фабрикой сессий `recognition.result` (ack и результаты сервисов) обрабатывает inbox `handle_message` того же модуля с фабрикой сессий
приложения и `NoopMapper` (маппинг результата в доменные таблицы — Срез 2). приложения и `NoopMapper` (маппинг результата в доменные таблицы — Срез 2).
Остановка: фоновые задачи обмена → NATS → S3 → SMB → пул соединений БД; каждый шаг независим — ошибка одного
логируется и не отменяет остальные.
""" """
import asyncio
import os import os
from collections.abc import Awaitable, Callable
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from datetime import UTC, datetime from datetime import UTC, datetime
from functools import partial from functools import partial
@ -33,7 +37,7 @@ from src.core.clients import S3Storage
from src.core.config import get_settings from src.core.config import get_settings
from src.core.exception_handlers import register_exception_handlers from src.core.exception_handlers import register_exception_handlers
from src.core.nats import MessageHandler, NatsConnection from src.core.nats import MessageHandler, NatsConnection
from src.db.db import async_session_maker from src.db.db import async_session_maker, engine
from src.services.recognition_exchange import ExchangeTasks, NoopMapper, handle_message from src.services.recognition_exchange import ExchangeTasks, NoopMapper, handle_message
settings = get_settings() settings = get_settings()
@ -118,18 +122,57 @@ def _start_exchange(nats: NatsConnection) -> ExchangeTasks:
return ExchangeTasks.start(async_session_maker, nats, settings) return ExchangeTasks.start(async_session_maker, nats, settings)
def _smb_configured() -> bool:
return bool(settings.OPENBAO.SMB.SMB_HOST)
async def _register_smb() -> None:
"""Сессия SMB (шаблон) — только если задан `SMB_HOST`; вызов блокирующий, поэтому в отдельном потоке."""
smb = settings.OPENBAO.SMB
if not _smb_configured():
logger.debug("SMB не настроен (OPENBAO__SMB__SMB_HOST пуст): сессия SMB не регистрируется")
return
try:
await asyncio.to_thread(smbclient.register_session, smb.SMB_HOST, smb.SMB_LOGIN, smb.SMB_PASSWORD)
except Exception:
logger.exception("Ошибка при регистрации сессии SMB")
async def _reset_smb() -> None:
"""Закрывает кэш соединений SMB (блокирующий вызов — в отдельном потоке); без SMB ничего не делает."""
if _smb_configured():
await asyncio.to_thread(smbclient.reset_connection_cache)
async def _cleanup_step(name: str, action: Callable[[], Awaitable[object]]) -> None:
"""Один шаг остановки: исключение логируется и не мешает следующим шагам."""
try:
await action()
except Exception:
logger.exception("Ошибка при остановке приложения. step={step}", step=name)
async def _shutdown(app: FastAPI) -> None:
"""Остановка по шагам: задачи обмена (до NATS: публикатор не пишет в закрытое соединение) → NATS → S3 → SMB →
пул соединений БД (последним: задачи обмена и inbox к этому моменту остановлены)."""
exchange, nats, storage = app.state.exchange, app.state.nats, app.state.storage
if exchange is not None:
await _cleanup_step("exchange", exchange.stop)
if nats is not None:
await _cleanup_step("nats", nats.stop)
if storage:
await _cleanup_step("s3", storage.client.close_session)
await _cleanup_step("smb", _reset_smb)
await _cleanup_step("db", engine.dispose)
@asynccontextmanager @asynccontextmanager
async def lifespan(app: FastAPI): async def lifespan(app: FastAPI):
app.state.storage = None app.state.storage = None
app.state.nats = None app.state.nats = None
app.state.exchange = None app.state.exchange = None
try: await _register_smb()
smbclient.register_session(
settings.OPENBAO.SMB.SMB_HOST, settings.OPENBAO.SMB.SMB_LOGIN, settings.OPENBAO.SMB.SMB_PASSWORD
)
except Exception:
logger.exception("Ошибка при регистрации сессии SMB")
try: try:
app.state.storage = await _connect_s3() app.state.storage = await _connect_s3()
@ -142,13 +185,7 @@ async def lifespan(app: FastAPI):
yield yield
finally: finally:
logger.info("Приложение закрывается") logger.info("Приложение закрывается")
smbclient.reset_connection_cache() await _shutdown(app)
if app.state.exchange is not None: # до закрытия NATS: публикатор не должен писать в закрытое соединение
await app.state.exchange.stop()
if app.state.nats is not None:
await app.state.nats.stop()
if app.state.storage:
await app.state.storage.client.close_session()
app = FastAPI(title="АРМ КА", version=VERSION, root_path=settings.ROOT_PATH, lifespan=lifespan) app = FastAPI(title="АРМ КА", version=VERSION, root_path=settings.ROOT_PATH, lifespan=lifespan)

View File

@ -22,17 +22,20 @@ class NATSSettings(BaseSettings):
class SMBSettings(BaseSettings): class SMBSettings(BaseSettings):
SMB_HOST: str """SMB (горячие папки ИС Сканирование, шаблон). Необязательны: ядру в Срезе 0 SMB не нужен; пустой `SMB_HOST` —
SMB_PATH: str сессия SMB не регистрируется."""
SMB_LOGIN: str
SMB_PASSWORD: str SMB_HOST: str = ""
SMB_PATH: str = ""
SMB_LOGIN: str = ""
SMB_PASSWORD: str = ""
class OpenBAOSettings(BaseSettings): class OpenBAOSettings(BaseSettings):
DATABASE: DBSettings DATABASE: DBSettings
MINIO: MinioSettings MINIO: MinioSettings
NATS: NATSSettings NATS: NATSSettings
SMB: SMBSettings SMB: SMBSettings = SMBSettings() # все поля необязательны (см. SMBSettings)
class ServerSettings(BaseSettings): class ServerSettings(BaseSettings):

View File

@ -5,12 +5,14 @@
import asyncio import asyncio
import importlib import importlib
import threading
from http import HTTPStatus from http import HTTPStatus
from types import SimpleNamespace from types import SimpleNamespace
from typing import Any from typing import Any
import httpx import httpx
import pytest import pytest
from loguru import logger
from src.api.router import router from src.api.router import router
from src.core.nats import NatsConnection from src.core.nats import NatsConnection
@ -94,6 +96,27 @@ def _no_smb(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(app_module.smbclient, "reset_connection_cache", lambda: None) monkeypatch.setattr(app_module.smbclient, "reset_connection_cache", lambda: None)
class _FakeEngine:
"""Подмена движка БД приложения: lifespan вызывает `dispose()` последним шагом остановки (финальное ревью M-3)."""
def __init__(self, events: list[str] | None = None) -> None:
self.disposed = 0
self.events = events
async def dispose(self) -> None:
self.disposed += 1
if self.events is not None:
self.events.append("db")
@pytest.fixture(autouse=True)
def fake_engine(monkeypatch: pytest.MonkeyPatch) -> _FakeEngine:
"""Lifespan в быстрых тестах не трогает пул реального движка БД."""
engine = _FakeEngine()
monkeypatch.setattr(app_module, "engine", engine)
return engine
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def exchange_loops(monkeypatch: pytest.MonkeyPatch) -> list[str]: def exchange_loops(monkeypatch: pytest.MonkeyPatch) -> list[str]:
"""Циклы публикатора и вахтёра подменены ожиданием: быстрые тесты lifespan не ходят в БД.""" """Циклы публикатора и вахтёра подменены ожиданием: быстрые тесты lifespan не ходят в БД."""
@ -300,3 +323,113 @@ async def test_lifespan_without_nats_starts_no_exchange_tasks(
await asyncio.sleep(0) await asyncio.sleep(0)
assert exchange_loops == [] assert exchange_loops == []
assert asyncio.all_tasks() - tasks_before == set() assert asyncio.all_tasks() - tasks_before == set()
# --- SMB необязателен, остановка по независимым шагам (финальное ревью I-3, T4.5, M-3) -------------------------------
async def _unavailable_s3() -> None:
return None
def test_env_files_leave_smb_off():
"""В `.env.test` (и, значит, в тестах) SMB выключен: пустой SMB_HOST."""
assert app_module.settings.OPENBAO.SMB.SMB_HOST == ""
async def test_lifespan_skips_smb_when_host_empty(monkeypatch: pytest.MonkeyPatch, fake_engine: _FakeEngine):
def forbidden(*args: Any, **kwargs: Any) -> None:
raise AssertionError("SMB не настроен — вызова быть не должно")
records: list[Any] = []
handler_id = logger.add(lambda message: records.append(message.record), level="DEBUG")
monkeypatch.setattr(app_module.settings.OPENBAO.SMB, "SMB_HOST", "")
monkeypatch.setattr(app_module.smbclient, "register_session", forbidden)
monkeypatch.setattr(app_module.smbclient, "reset_connection_cache", forbidden)
monkeypatch.setattr(app_module, "_connect_s3", _unavailable_s3)
monkeypatch.setattr(app_module, "_create_nats", lambda: None)
try:
async with app_module.lifespan(app_module.app):
pass
finally:
logger.remove(handler_id)
assert [r for r in records if "SMB не настроен" in r["message"] and r["level"].name == "DEBUG"]
assert not [r for r in records if r["level"].name == "ERROR"]
assert fake_engine.disposed == 1
async def test_lifespan_registers_smb_in_worker_thread_when_configured(monkeypatch: pytest.MonkeyPatch):
"""Задан SMB_HOST: `register_session` и `reset_connection_cache` — вне потока цикла событий (`to_thread`)."""
loop_thread = threading.get_ident()
calls: list[tuple[str, Any, bool]] = []
def register_session(*args: Any, **kwargs: Any) -> None:
calls.append(("register", args, threading.get_ident() != loop_thread))
def reset_connection_cache() -> None:
calls.append(("reset", (), threading.get_ident() != loop_thread))
smb = app_module.settings.OPENBAO.SMB
monkeypatch.setattr(smb, "SMB_HOST", "smb.local")
monkeypatch.setattr(smb, "SMB_LOGIN", "user")
monkeypatch.setattr(smb, "SMB_PASSWORD", "password")
monkeypatch.setattr(app_module.smbclient, "register_session", register_session)
monkeypatch.setattr(app_module.smbclient, "reset_connection_cache", reset_connection_cache)
monkeypatch.setattr(app_module, "_connect_s3", _unavailable_s3)
monkeypatch.setattr(app_module, "_create_nats", lambda: None)
async with app_module.lifespan(app_module.app):
assert calls == [("register", ("smb.local", "user", "password"), True)]
assert calls[-1] == ("reset", (), True)
class _FailingNats(_FakeNats):
def __init__(self, events: list[str]) -> None:
super().__init__()
self.events = events
async def stop(self) -> None:
self.events.append("nats")
raise RuntimeError("подставная ошибка остановки NATS")
class _FailingS3Client:
def __init__(self, events: list[str]) -> None:
self.events = events
async def close_session(self) -> None:
self.events.append("s3")
raise RuntimeError("подставная ошибка закрытия S3")
async def test_shutdown_steps_are_independent_and_engine_disposed_last(
monkeypatch: pytest.MonkeyPatch, exchange_loops: list[str]
):
"""Ошибки `nats.stop()` и закрытия S3 не отменяют сброс SMB и `engine.dispose()`; порядок шагов сохраняется."""
events: list[str] = []
monkeypatch.setattr(app_module, "engine", _FakeEngine(events))
monkeypatch.setattr(app_module.settings.OPENBAO.SMB, "SMB_HOST", "smb.local")
monkeypatch.setattr(app_module.smbclient, "register_session", lambda *args, **kwargs: None)
monkeypatch.setattr(app_module.smbclient, "reset_connection_cache", lambda: events.append("smb"))
storage = SimpleNamespace(client=_FailingS3Client(events))
async def connect_s3() -> SimpleNamespace:
return storage
monkeypatch.setattr(app_module, "_connect_s3", connect_s3)
monkeypatch.setattr(app_module, "_create_nats", lambda: _FailingNats(events))
records: list[Any] = []
handler_id = logger.add(lambda message: records.append(message.record), level="ERROR")
try:
async with app_module.lifespan(app_module.app):
exchange = app_module.app.state.exchange
assert exchange is not None
finally:
logger.remove(handler_id)
assert all(task.done() for task in exchange.tasks)
assert events == ["nats", "s3", "smb", "db"]
failed_steps = sorted(r["extra"].get("step") for r in records)
assert failed_steps == ["nats", "s3"]

View File

@ -27,10 +27,11 @@ def test_env_test_is_read(clean_env: pytest.MonkeyPatch) -> None:
assert s.AUTH_DEV_ISSUER is True assert s.AUTH_DEV_ISSUER is True
assert s.NATS_ENABLED is False assert s.NATS_ENABLED is False
assert s.OPENBAO.MINIO.MINIO_SECURE is False assert s.OPENBAO.MINIO.MINIO_SECURE is False
assert s.OPENBAO.SMB.SMB_HOST == "" # SMB выключен: в логе запуска нет трейсбэка SMB
def test_defaults(clean_env: pytest.MonkeyPatch) -> None: def test_defaults(clean_env: pytest.MonkeyPatch) -> None:
"""Значения по умолчанию классов настроек: env-файла нет, заданы только обязательные OPENBAO__*.""" """Значения по умолчанию классов настроек: env-файла нет, заданы только обязательные OPENBAO__* (SMB не нужен)."""
required = { required = {
"OPENBAO__DATABASE__DATABASE_URL": "postgresql+asyncpg://u:p@localhost:5432/d", "OPENBAO__DATABASE__DATABASE_URL": "postgresql+asyncpg://u:p@localhost:5432/d",
"OPENBAO__MINIO__MINIO_URL": "localhost:9000", "OPENBAO__MINIO__MINIO_URL": "localhost:9000",
@ -38,10 +39,6 @@ def test_defaults(clean_env: pytest.MonkeyPatch) -> None:
"OPENBAO__MINIO__MINIO_ADMIN_PASSWORD": "password", "OPENBAO__MINIO__MINIO_ADMIN_PASSWORD": "password",
"OPENBAO__MINIO__MINIO_BUCKET": "bucket", "OPENBAO__MINIO__MINIO_BUCKET": "bucket",
"OPENBAO__NATS__NATS_URL": "nats://localhost:4222", "OPENBAO__NATS__NATS_URL": "nats://localhost:4222",
"OPENBAO__SMB__SMB_HOST": "host",
"OPENBAO__SMB__SMB_PATH": "path",
"OPENBAO__SMB__SMB_LOGIN": "login",
"OPENBAO__SMB__SMB_PASSWORD": "password",
} }
for key, value in required.items(): for key, value in required.items():
clean_env.setenv(key, value) clean_env.setenv(key, value)
@ -59,6 +56,8 @@ def test_defaults(clean_env: pytest.MonkeyPatch) -> None:
assert s.OPENBAO.NATS.NATS_SEED == "" assert s.OPENBAO.NATS.NATS_SEED == ""
assert s.OPENBAO.NATS.NATS_JWT == "" assert s.OPENBAO.NATS.NATS_JWT == ""
assert s.OPENBAO.MINIO.MINIO_SECURE is True assert s.OPENBAO.MINIO.MINIO_SECURE is True
smb = s.OPENBAO.SMB
assert (smb.SMB_HOST, smb.SMB_PATH, smb.SMB_LOGIN, smb.SMB_PASSWORD) == ("", "", "", "")
def test_empty_nats_seed_and_jwt_allowed(clean_env: pytest.MonkeyPatch) -> None: def test_empty_nats_seed_and_jwt_allowed(clean_env: pytest.MonkeyPatch) -> None:
@ -67,3 +66,4 @@ def test_empty_nats_seed_and_jwt_allowed(clean_env: pytest.MonkeyPatch) -> None:
assert s.OPENBAO.NATS.NATS_SEED == "" assert s.OPENBAO.NATS.NATS_SEED == ""
assert s.OPENBAO.NATS.NATS_JWT == "" assert s.OPENBAO.NATS.NATS_JWT == ""
assert s.NATS_ENABLED is True assert s.NATS_ENABLED is True
assert s.OPENBAO.SMB.SMB_HOST == ""