Контур платформы передаёт в под только OPENBAO__*. Свой источник настроек (settings_customise_sources): приоритет обычный env -> OPENBAO__APP__* -> .env -> умолчание; неизвестное имя — ошибка при старте; ключ APP не ломает OpenBAOSettings. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EAKRDNzXtRrwmWz9z6PZu3
133 lines
5.4 KiB
Python
133 lines
5.4 KiB
Python
"""Несекретные настройки из OpenBao: `OPENBAO__APP__<ИМЯ>` (issue #44; без БД и сети).
|
||
|
||
Приоритет: обычный env → `OPENBAO__APP__*` → `.env` → значение по умолчанию.
|
||
"""
|
||
|
||
import os
|
||
from pathlib import Path
|
||
|
||
import pytest
|
||
from pydantic import ValidationError
|
||
|
||
from src.core.config import Settings
|
||
|
||
REQUIRED_ENV = {
|
||
"OPENBAO__DATABASE__DATABASE_URL": "postgresql+asyncpg://u:p@localhost:5432/d",
|
||
"OPENBAO__MINIO__MINIO_URL": "localhost:9000",
|
||
"OPENBAO__MINIO__MINIO_ADMIN_LOGIN": "login",
|
||
"OPENBAO__MINIO__MINIO_ADMIN_PASSWORD": "password",
|
||
"OPENBAO__MINIO__MINIO_BUCKET": "bucket",
|
||
"OPENBAO__NATS__NATS_URL": "nats://localhost:4222",
|
||
}
|
||
# Плоские настройки, которые тесты ниже задают/проверяют
|
||
POLL = 7
|
||
PORT = 9000
|
||
FLAT_KEYS = {"DEBUG", "NATS_ENABLED", "AUTH_DEV_ISSUER", "FORWARDED_ALLOW_IPS", "CORS_ORIGINS", "OUTBOX_POLL_SECONDS"}
|
||
|
||
|
||
@pytest.fixture
|
||
def env(monkeypatch: pytest.MonkeyPatch) -> pytest.MonkeyPatch:
|
||
"""Чистое окружение с обязательными OPENBAO__*; остальное из процесса убрано."""
|
||
for key in list(os.environ):
|
||
if key.startswith(("OPENBAO__", "SERVER__")) or key in FLAT_KEYS:
|
||
monkeypatch.delenv(key)
|
||
for key, value in REQUIRED_ENV.items():
|
||
monkeypatch.setenv(key, value)
|
||
return monkeypatch
|
||
|
||
|
||
def _settings(env_file: Path | None = None) -> Settings:
|
||
return Settings(_env_file=env_file) # type: ignore[call-arg]
|
||
|
||
|
||
def test_app_value_applied(env: pytest.MonkeyPatch) -> None:
|
||
env.setenv("OPENBAO__APP__NATS_ENABLED", "true")
|
||
env.setenv("OPENBAO__APP__DEBUG", "true")
|
||
s = _settings()
|
||
assert s.NATS_ENABLED is True
|
||
assert s.DEBUG is True
|
||
# Секреты OpenBao по-прежнему читаются во вложенные группы
|
||
assert s.OPENBAO.NATS.NATS_URL == "nats://localhost:4222"
|
||
|
||
|
||
def test_default_without_app(env: pytest.MonkeyPatch) -> None:
|
||
assert _settings().NATS_ENABLED is False
|
||
|
||
|
||
def test_plain_env_overrides_app(env: pytest.MonkeyPatch) -> None:
|
||
env.setenv("OPENBAO__APP__NATS_ENABLED", "true")
|
||
env.setenv("NATS_ENABLED", "false")
|
||
assert _settings().NATS_ENABLED is False
|
||
|
||
|
||
def test_app_overrides_dotenv(env: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
||
dotenv = tmp_path / ".env"
|
||
dotenv.write_text("NATS_ENABLED=false\nFORWARDED_ALLOW_IPS=1.1.1.1\n", encoding="utf-8")
|
||
env.setenv("OPENBAO__APP__NATS_ENABLED", "true")
|
||
s = _settings(dotenv)
|
||
assert s.NATS_ENABLED is True # OPENBAO__APP__ сильнее .env
|
||
assert s.FORWARDED_ALLOW_IPS == "1.1.1.1" # то, чего нет в OPENBAO__APP__, остаётся из .env
|
||
|
||
|
||
def test_types_parsed_like_plain_env(env: pytest.MonkeyPatch) -> None:
|
||
env.setenv("OPENBAO__APP__OUTBOX_POLL_SECONDS", str(POLL))
|
||
env.setenv("OPENBAO__APP__AUTH_DEV_ISSUER", "1")
|
||
env.setenv("OPENBAO__APP__FORWARDED_ALLOW_IPS", "10.0.0.1,10.0.0.2")
|
||
s = _settings()
|
||
assert s.OUTBOX_POLL_SECONDS == POLL
|
||
assert s.AUTH_DEV_ISSUER is True
|
||
assert s.FORWARDED_ALLOW_IPS == "10.0.0.1,10.0.0.2"
|
||
|
||
|
||
@pytest.mark.parametrize(
|
||
("raw", "expected"),
|
||
[
|
||
("https://a.example.ru,https://b.example.ru", ["https://a.example.ru", "https://b.example.ru"]),
|
||
('["https://a.example.ru"]', ["https://a.example.ru"]),
|
||
("", []),
|
||
],
|
||
ids=["comma", "json", "empty"],
|
||
)
|
||
def test_cors_origins_list_parsed_like_plain_env(env: pytest.MonkeyPatch, raw: str, expected: list[str]) -> None:
|
||
"""NoDecode-список разбирается валидатором так же, как из обычного env."""
|
||
env.setenv("OPENBAO__APP__CORS_ORIGINS", raw)
|
||
assert _settings().CORS_ORIGINS == expected
|
||
|
||
|
||
def test_invalid_value_rejected(env: pytest.MonkeyPatch) -> None:
|
||
env.setenv("OPENBAO__APP__OUTBOX_POLL_SECONDS", "0")
|
||
with pytest.raises(ValidationError, match="OUTBOX_POLL_SECONDS"):
|
||
_settings()
|
||
|
||
|
||
def test_unknown_name_is_config_error(env: pytest.MonkeyPatch) -> None:
|
||
"""Опечатка не игнорируется молча: ошибка при создании Settings (то есть при старте)."""
|
||
env.setenv("OPENBAO__APP__NATS_ENABLD", "true")
|
||
with pytest.raises(ValueError, match="NATS_ENABLD"):
|
||
_settings()
|
||
|
||
|
||
def test_openbao_group_not_settable_via_app(env: pytest.MonkeyPatch) -> None:
|
||
"""Секреты — только `OPENBAO__<ГРУППА>__*`; `OPENBAO__APP__OPENBAO__...` — не имя настройки."""
|
||
env.setenv("OPENBAO__APP__OPENBAO__NATS__NATS_URL", "nats://x:4222")
|
||
with pytest.raises(ValueError, match="OPENBAO"):
|
||
_settings()
|
||
|
||
|
||
def test_app_key_does_not_break_openbao_validation(env: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
||
"""Ключ APP не попадает в OpenBAOSettings (там extra=forbid) — ни из env, ни из env-файла."""
|
||
dotenv = tmp_path / ".env"
|
||
dotenv.write_text("OPENBAO__APP__NATS_ENABLED=true\n", encoding="utf-8")
|
||
env.setenv("OPENBAO__APP__DEBUG", "true")
|
||
s = _settings(dotenv)
|
||
assert s.DEBUG is True
|
||
assert not hasattr(s.OPENBAO, "APP")
|
||
|
||
|
||
def test_nested_group_still_works(env: pytest.MonkeyPatch) -> None:
|
||
env.setenv("OPENBAO__MINIO__MINIO_SECURE", "false")
|
||
env.setenv("OPENBAO__APP__SERVER__PORT", str(PORT))
|
||
s = _settings()
|
||
assert s.OPENBAO.MINIO.MINIO_SECURE is False
|
||
assert s.server.PORT == PORT
|