"""Несекретные настройки из 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