"""Несекретные настройки из OpenBao: `OPENBAO__APP__<ИМЯ>` (issue #22). Приоритет: обычный 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__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", } FLAT_KEYS = {"DEBUG", "NATS_ENABLED", "PARSE_TIMEOUT_SECONDS", "PARSE_MAX_BYTES", "PUBLISH_RETRIES"} TIMEOUT = 300 PORT = 9000 DOTENV_RETRIES = 5 @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__PARSE_TIMEOUT_SECONDS", str(TIMEOUT)) env.setenv("OPENBAO__APP__DEBUG", "true") s = _settings() assert s.PARSE_TIMEOUT_SECONDS == TIMEOUT assert s.DEBUG is True assert s.OPENBAO.NATS.NATS_URL == "nats://localhost:4222" def test_default_without_app(env: pytest.MonkeyPatch) -> None: assert _settings().PARSE_TIMEOUT_SECONDS == 120 # noqa: PLR2004 - значение по умолчанию def test_plain_env_overrides_app(env: pytest.MonkeyPatch) -> None: env.setenv("OPENBAO__APP__NATS_ENABLED", "false") env.setenv("NATS_ENABLED", "true") assert _settings().NATS_ENABLED is True def test_app_overrides_dotenv(env: pytest.MonkeyPatch, tmp_path: Path) -> None: dotenv = tmp_path / ".env" dotenv.write_text(f"NATS_ENABLED=true\nPUBLISH_RETRIES={DOTENV_RETRIES}\n", encoding="utf-8") env.setenv("OPENBAO__APP__NATS_ENABLED", "false") s = _settings(dotenv) assert s.NATS_ENABLED is False # OPENBAO__APP__ сильнее .env assert s.PUBLISH_RETRIES == DOTENV_RETRIES def test_types_parsed_like_plain_env(env: pytest.MonkeyPatch) -> None: env.setenv("OPENBAO__APP__NATS_ENABLED", "0") env.setenv("OPENBAO__APP__PARSE_MAX_BYTES", "1048576") s = _settings() assert s.NATS_ENABLED is False assert s.PARSE_MAX_BYTES == 1048576 # noqa: PLR2004 def test_invalid_value_rejected(env: pytest.MonkeyPatch) -> None: env.setenv("OPENBAO__APP__PARSE_TIMEOUT_SECONDS", "0") with pytest.raises(ValidationError, match="PARSE_TIMEOUT_SECONDS"): _settings() def test_unknown_name_is_config_error(env: pytest.MonkeyPatch) -> None: env.setenv("OPENBAO__APP__PARSE_TIMEOUT_SECONDZ", "300") with pytest.raises(ValueError, match="PARSE_TIMEOUT_SECONDZ"): _settings() def test_openbao_group_not_settable_via_app(env: pytest.MonkeyPatch) -> None: 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=false\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