From dff1e050b9efc44c36374085ab4841ea5a7904ca Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sat, 3 Oct 2026 18:17:19 +0400 Subject: [PATCH] =?UTF-8?q?feat:=20Settings=20=D1=87=D0=B8=D1=82=D0=B0?= =?UTF-8?q?=D0=B5=D1=82=20ENV=5FFILE,=20=D0=BF=D0=BE=D0=BB=D1=8F=20auth/NA?= =?UTF-8?q?TS/outbox,=20.env.example=20=D0=B8=20.env.test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit NATS_SEED/NATS_JWT необязательны (пусто — подключение без nkeys); в app.py разбор seed защищён от пустого значения (подключение переедет в задаче 22). Co-Authored-By: Claude Fable 5.1 --- api/.env.example | 41 ++++++++++++++++++++++++++++++++++++++ api/.env.test | 34 +++++++++++++++++++++++++++++++ api/src/app.py | 4 +++- api/src/core/config.py | 41 ++++++++++++++++++++++++++++++-------- api/tests/test_config.py | 43 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 154 insertions(+), 9 deletions(-) create mode 100644 api/.env.example create mode 100644 api/.env.test create mode 100644 api/tests/test_config.py diff --git a/api/.env.example b/api/.env.example new file mode 100644 index 0000000..11c2988 --- /dev/null +++ b/api/.env.example @@ -0,0 +1,41 @@ +# Пример локального окружения. Скопируйте в api/.env: cp .env.example .env +# Порты — стандартные для docs/dev/docker-compose.yml. + +DEBUG=true + +# PostgreSQL (драйвер asyncpg) +OPENBAO__DATABASE__DATABASE_URL=postgresql+asyncpg://armka:armka@localhost:5432/armka + +# MinIO (адрес без схемы — так ждёт miniopy-async) +OPENBAO__MINIO__MINIO_URL=localhost:9000 +OPENBAO__MINIO__MINIO_ADMIN_LOGIN=minioadmin +OPENBAO__MINIO__MINIO_ADMIN_PASSWORD=minioadmin +OPENBAO__MINIO__MINIO_BUCKET=arm-ka + +# NATS без JetStream; пустые NATS_SEED/NATS_JWT — подключение без nkeys +OPENBAO__NATS__NATS_URL=nats://localhost:4222 +OPENBAO__NATS__NATS_SEED= +OPENBAO__NATS__NATS_JWT= + +# SMB — заглушки (в Срезе 0 не используется) +OPENBAO__SMB__SMB_HOST=localhost +OPENBAO__SMB__SMB_PATH=share +OPENBAO__SMB__SMB_LOGIN=user +OPENBAO__SMB__SMB_PASSWORD=password + +# Аутентификация. Dev-эмитент работает только при AUTH_DEV_ISSUER=true И DEBUG=true. +AUTH_DEV_ISSUER=true +# ТОЛЬКО ДЛЯ РАЗРАБОТКИ: секрет HS256 dev-эмитента, в проде не использовать +AUTH_DEV_SECRET=dev-only-secret-do-not-use-in-production-0123456789 +# Проверка токенов РАИСА (прод): JWKS по URL либо публичный ключ PEM +AUTH_JWKS_URL= +AUTH_PUBLIC_KEY= +AUTH_ISSUER= +AUTH_AUDIENCE= +AUTH_EMAIL_CLAIM=email + +# Обмен с сервисами распознавания +NATS_ENABLED=true +OUTBOX_POLL_SECONDS=5 +ACK_TIMEOUT_SECONDS=30 +OUTBOX_MAX_REPUBLISH=3 diff --git a/api/.env.test b/api/.env.test new file mode 100644 index 0000000..e348f24 --- /dev/null +++ b/api/.env.test @@ -0,0 +1,34 @@ +# Окружение тестов (pytest.ini: ENV_FILE=.env.test). Только тестовые значения, секретов здесь нет. +# Порты — временная тестовая инфраструктура: PostgreSQL 55432, NATS 54222, MinIO 59000. + +DEBUG=true + +OPENBAO__DATABASE__DATABASE_URL=postgresql+asyncpg://armka:armka@localhost:55432/armka + +OPENBAO__MINIO__MINIO_URL=localhost:59000 +OPENBAO__MINIO__MINIO_ADMIN_LOGIN=minioadmin +OPENBAO__MINIO__MINIO_ADMIN_PASSWORD=minioadmin +OPENBAO__MINIO__MINIO_BUCKET=arm-ka + +OPENBAO__NATS__NATS_URL=nats://localhost:54222 +OPENBAO__NATS__NATS_SEED= +OPENBAO__NATS__NATS_JWT= + +OPENBAO__SMB__SMB_HOST=localhost +OPENBAO__SMB__SMB_PATH=share +OPENBAO__SMB__SMB_LOGIN=user +OPENBAO__SMB__SMB_PASSWORD=password + +AUTH_DEV_ISSUER=true +AUTH_DEV_SECRET=test-only-secret-not-for-production-0123456789 +AUTH_JWKS_URL= +AUTH_PUBLIC_KEY= +AUTH_ISSUER= +AUTH_AUDIENCE= +AUTH_EMAIL_CLAIM=email + +# Интеграционные тесты включают NATS сами +NATS_ENABLED=false +OUTBOX_POLL_SECONDS=5 +ACK_TIMEOUT_SECONDS=30 +OUTBOX_MAX_REPUBLISH=3 diff --git a/api/src/app.py b/api/src/app.py index 529b7d3..f8136c7 100644 --- a/api/src/app.py +++ b/api/src/app.py @@ -36,7 +36,8 @@ UP_TIME = datetime.now(UTC) # --- NATS: аутентификация по nkeys/JWT --------------------------------------------------------- -kp = nkeys.from_seed(settings.OPENBAO.NATS.NATS_SEED.encode("utf-8")) +# Пустой NATS_SEED — подключение без nkeys (разработка, тесты); подключение переедет в core/nats.py (задача 22) +kp = nkeys.from_seed(settings.OPENBAO.NATS.NATS_SEED.encode("utf-8")) if settings.OPENBAO.NATS.NATS_SEED else None def jwt_cb(): @@ -44,6 +45,7 @@ def jwt_cb(): def sig_cb(nonce): + assert kp is not None if isinstance(nonce, str): nonce = nonce.encode("utf-8") return base64.b64encode(kp.sign(nonce)) diff --git a/api/src/core/config.py b/api/src/core/config.py index f2dd1cf..7648784 100644 --- a/api/src/core/config.py +++ b/api/src/core/config.py @@ -1,4 +1,6 @@ -from pydantic_settings import BaseSettings +import os + +from pydantic_settings import BaseSettings, SettingsConfigDict class DBSettings(BaseSettings): @@ -14,8 +16,8 @@ class MinioSettings(BaseSettings): class NATSSettings(BaseSettings): NATS_URL: str - NATS_SEED: str - NATS_JWT: str + NATS_SEED: str = "" # пусто — подключение к NATS без nkeys (локальная разработка, тесты) + NATS_JWT: str = "" class SMBSettings(BaseSettings): @@ -38,17 +40,40 @@ class ServerSettings(BaseSettings): class Settings(BaseSettings): + """Настройки приложения. + + Файл окружения выбирается переменной ``ENV_FILE`` (по умолчанию ``.env``; значение читается при импорте модуля, + относительный путь считается от текущего каталога — ``api/``). Тесты используют ``.env.test``. + """ + OPENBAO: OpenBAOSettings DEBUG: bool = False server: ServerSettings = ServerSettings() ROOT_PATH: str = "/" VERSION: str = "1.0.0" - class Config: - extra = "ignore" - env_file = ".env" - env_nested_delimiter = "__" - nested_model_default_partial_update = False + # Аутентификация: dev-эмитент (только для разработки; роутер подключается при AUTH_DEV_ISSUER и DEBUG одновременно) + AUTH_DEV_ISSUER: bool = False + AUTH_DEV_SECRET: str = "" + # Проверка токенов РАИСА: JWKS по URL либо статический публичный ключ (PEM) + AUTH_JWKS_URL: str = "" + AUTH_PUBLIC_KEY: str = "" + AUTH_ISSUER: str = "" + AUTH_AUDIENCE: str = "" + AUTH_EMAIL_CLAIM: str = "email" # claim токена с email пользователя (сопоставляется с app_user.email) + + # NATS и outbox/inbox обмена recognition_job + NATS_ENABLED: bool = False + OUTBOX_POLL_SECONDS: int = 5 # период опроса outbox + ACK_TIMEOUT_SECONDS: int = 30 # ожидание ack сервиса распознавания до повторной публикации + OUTBOX_MAX_REPUBLISH: int = 3 # число повторных публикаций до перевода в dlq + + model_config = SettingsConfigDict( + extra="ignore", + env_file=os.environ.get("ENV_FILE", ".env"), + env_nested_delimiter="__", + nested_model_default_partial_update=False, + ) def get_settings() -> Settings: diff --git a/api/tests/test_config.py b/api/tests/test_config.py new file mode 100644 index 0000000..42fadda --- /dev/null +++ b/api/tests/test_config.py @@ -0,0 +1,43 @@ +"""Настройки: чтение ENV_FILE и значения по умолчанию (без БД и сети).""" + +import os +from pathlib import Path + +import pytest + +from src.core.config import Settings + +API_DIR = Path(__file__).resolve().parent.parent + + +@pytest.fixture +def clean_env(monkeypatch: pytest.MonkeyPatch) -> pytest.MonkeyPatch: + """Убирает из окружения процесса переменные, которые перебили бы значения из env-файла.""" + for key in list(os.environ): + if key.startswith(("OPENBAO__", "AUTH_", "OUTBOX_", "ACK_")) or key in {"NATS_ENABLED", "DEBUG"}: + monkeypatch.delenv(key) + return monkeypatch + + +def test_env_test_is_read(clean_env: pytest.MonkeyPatch) -> None: + """.env.test читается: порты временной тестовой инфраструктуры.""" + s = Settings(_env_file=API_DIR / ".env.test") # type: ignore[call-arg] + assert s.OPENBAO.DATABASE.DATABASE_URL.endswith("localhost:55432/armka") + assert s.OPENBAO.NATS.NATS_URL == "nats://localhost:54222" + assert s.AUTH_DEV_ISSUER is True + assert s.NATS_ENABLED is False + + +def test_defaults(clean_env: pytest.MonkeyPatch) -> None: + """Значения по умолчанию: claim email и параметры outbox.""" + s = Settings(_env_file=API_DIR / ".env.test") # type: ignore[call-arg] + assert s.AUTH_EMAIL_CLAIM == "email" + assert (s.OUTBOX_POLL_SECONDS, s.ACK_TIMEOUT_SECONDS, s.OUTBOX_MAX_REPUBLISH) == (5, 30, 3) + + +def test_empty_nats_seed_and_jwt_allowed(clean_env: pytest.MonkeyPatch) -> None: + """Пустые NATS_SEED/NATS_JWT допустимы (подключение без nkeys).""" + s = Settings(_env_file=API_DIR / ".env.example") # type: ignore[call-arg] + assert s.OPENBAO.NATS.NATS_SEED == "" + assert s.OPENBAO.NATS.NATS_JWT == "" + assert s.NATS_ENABLED is True