DB_SCHEMA (по умолчанию dkb, валидация идентификатора) читают models/base.py (SCHEMA), db/db.py (search_path),
alembic/env.py и миграции 0001-0005 (SCHEMA и строковые FK f"{SCHEMA}.x.id" вместо литералов). В контуре -
OPENBAO__APP__DB_SCHEMA. Офлайн-SQL `alembic upgrade head --sql` при значении по умолчанию совпадает побайтно.
Тесты: валидация настройки, миграции и сессия приложения в схеме dkb_alt; .env.example/.env.test/README/CLAUDE.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EAKRDNzXtRrwmWz9z6PZu3
156 lines
6.2 KiB
Python
156 lines
6.2 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",
|
||
"DB_SCHEMA",
|
||
}
|
||
|
||
|
||
@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_db_schema_default_and_app(env: pytest.MonkeyPatch) -> None:
|
||
"""`DB_SCHEMA`: по умолчанию `dkb`; в контуре задаётся как `OPENBAO__APP__DB_SCHEMA`."""
|
||
assert _settings().DB_SCHEMA == "dkb"
|
||
env.setenv("OPENBAO__APP__DB_SCHEMA", "dkb_alt")
|
||
assert _settings().DB_SCHEMA == "dkb_alt"
|
||
|
||
|
||
@pytest.mark.parametrize("bad", ["", "Dkb", "1dkb", "dkb-alt", 'dkb"; DROP SCHEMA x; --', "pg_dkb", "a" * 64, "схема"])
|
||
def test_db_schema_rejects_unsafe_identifier(env: pytest.MonkeyPatch, bad: str) -> None:
|
||
"""Имя подставляется в DDL: не идентификатор `[a-z_][a-z0-9_]{0,62}` или `pg_*` отвергается при старте."""
|
||
env.setenv("DB_SCHEMA", bad)
|
||
with pytest.raises(ValidationError):
|
||
_settings()
|
||
|
||
|
||
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
|