gMAS / tests /test_config_settings.py
Артём Боярских
chore: initial commit
3193174
"""Tests for src/config/settings.py and src/config/logging.py"""
import os
import tempfile
from pathlib import Path
import pytest
from config.settings import FrameworkSettings, load_env_file, load_settings
# ─────────────────────────── config/logging.py ───────────────────────────────
class TestConfigLogging:
def test_as_bool_truthy_values(self):
from config.logging import _as_bool
assert _as_bool("1") is True
assert _as_bool("true") is True
assert _as_bool("True") is True
assert _as_bool("TRUE") is True
assert _as_bool("yes") is True
assert _as_bool("on") is True
def test_as_bool_falsy_values(self):
from config.logging import _as_bool
assert _as_bool("0") is False
assert _as_bool("false") is False
assert _as_bool("no") is False
assert _as_bool("") is False
assert _as_bool("random") is False
def test_as_bool_none(self):
from config.logging import _as_bool
assert _as_bool(None) is False
def test_setup_logging_basic(self):
from config.logging import setup_logging
# Should not raise
setup_logging(level="INFO")
def test_setup_logging_debug_level(self):
from config.logging import setup_logging
setup_logging(level="DEBUG")
def test_setup_logging_with_format(self):
from config.logging import setup_logging
setup_logging(format_string="{time} | {message}")
def test_setup_logging_with_backtrace(self):
from config.logging import setup_logging
setup_logging(backtrace=True)
setup_logging(backtrace=False)
def test_setup_logging_with_log_file(self, tmp_path):
from config.logging import setup_logging
log_file = str(tmp_path / "test.log")
setup_logging(level="INFO", log_file=log_file)
def test_setup_logging_from_env(self, monkeypatch):
from config.logging import setup_logging
monkeypatch.setenv("RWXF_LOG_LEVEL", "WARNING")
monkeypatch.setenv("RWXF_LOG_BACKTRACE", "true")
setup_logging() # Should read from env vars
def test_setup_logging_log_file_from_env(self, monkeypatch, tmp_path):
from config.logging import setup_logging
log_file = str(tmp_path / "env.log")
monkeypatch.setenv("RWXF_LOG_FILE", log_file)
setup_logging()
monkeypatch.delenv("RWXF_LOG_FILE", raising=False)
def test_logger_is_available(self):
from config.logging import logger
assert logger is not None
# ─────────────────────────── FrameworkSettings ───────────────────────────────
class TestFrameworkSettings:
def test_create_with_api_key(self, monkeypatch):
monkeypatch.setenv("RWXF_API_KEY", "test-key-123")
settings = FrameworkSettings()
assert settings.resolved_api_key == "test-key-123"
def test_default_values(self, monkeypatch):
monkeypatch.setenv("RWXF_API_KEY", "test-key")
settings = FrameworkSettings()
assert settings.model_name == "gpt-4o-mini"
assert settings.default_timeout == 60
assert settings.max_retries == 3
assert settings.log_level == "INFO"
assert settings.embedding_normalize is True
def test_missing_api_key_raises(self, monkeypatch):
monkeypatch.delenv("RWXF_API_KEY", raising=False)
monkeypatch.delenv("RWXF_API_KEY_FILE", raising=False)
with pytest.raises(Exception):
FrameworkSettings()
def test_custom_model_name(self, monkeypatch):
monkeypatch.setenv("RWXF_API_KEY", "key")
monkeypatch.setenv("RWXF_MODEL_NAME", "gpt-4")
settings = FrameworkSettings()
assert settings.model_name == "gpt-4"
def test_custom_timeout(self, monkeypatch):
monkeypatch.setenv("RWXF_API_KEY", "key")
monkeypatch.setenv("RWXF_DEFAULT_TIMEOUT", "120")
settings = FrameworkSettings()
assert settings.default_timeout == 120
def test_valid_hash_embedding_model(self, monkeypatch):
monkeypatch.setenv("RWXF_API_KEY", "key")
monkeypatch.setenv("RWXF_EMBEDDING_MODEL", "hash:768")
settings = FrameworkSettings()
assert settings.embedding_model == "hash:768"
def test_valid_sentence_transformers_model(self, monkeypatch):
monkeypatch.setenv("RWXF_API_KEY", "key")
monkeypatch.setenv("RWXF_EMBEDDING_MODEL", "sentence-transformers/all-MiniLM-L6-v2")
settings = FrameworkSettings()
assert "sentence-transformers" in settings.embedding_model
def test_invalid_embedding_model(self, monkeypatch):
monkeypatch.setenv("RWXF_API_KEY", "key")
monkeypatch.setenv("RWXF_EMBEDDING_MODEL", "openai/text-embedding")
with pytest.raises(Exception):
FrameworkSettings()
def test_api_key_file(self, monkeypatch, tmp_path):
key_file = tmp_path / "api_key.txt"
key_file.write_text("secret-key-from-file", encoding="utf-8")
monkeypatch.delenv("RWXF_API_KEY", raising=False)
monkeypatch.setenv("RWXF_API_KEY_FILE", str(key_file))
settings = FrameworkSettings()
assert settings.resolved_api_key == "secret-key-from-file"
def test_api_key_file_not_found(self, monkeypatch, tmp_path):
monkeypatch.delenv("RWXF_API_KEY", raising=False)
monkeypatch.setenv("RWXF_API_KEY_FILE", str(tmp_path / "nonexistent.txt"))
with pytest.raises(Exception):
FrameworkSettings()
def test_resolved_api_key_none_raises(self, monkeypatch):
monkeypatch.setenv("RWXF_API_KEY", "key")
settings = FrameworkSettings()
# Manually set api_key to None to test RuntimeError
object.__setattr__(settings, "api_key", None)
with pytest.raises(RuntimeError, match="API key is not configured"):
_ = settings.resolved_api_key
def test_empty_string_api_key_treated_as_none(self, monkeypatch):
monkeypatch.setenv("RWXF_API_KEY", " ")
monkeypatch.delenv("RWXF_API_KEY_FILE", raising=False)
# Empty string should be treated as None β†’ raises
with pytest.raises(Exception):
FrameworkSettings()
def test_log_level_custom(self, monkeypatch):
monkeypatch.setenv("RWXF_API_KEY", "key")
monkeypatch.setenv("RWXF_LOG_LEVEL", "DEBUG")
settings = FrameworkSettings()
assert settings.log_level == "DEBUG"
# ─────────────────────────── load_env_file ───────────────────────────────────
class TestLoadEnvFile:
def test_load_from_file(self, tmp_path, monkeypatch):
env_file = tmp_path / ".env"
env_file.write_text(
"MY_TEST_VAR=hello\n"
"ANOTHER_VAR=world\n"
"# This is a comment\n"
"\n",
encoding="utf-8",
)
# Remove env vars before loading
monkeypatch.delenv("MY_TEST_VAR", raising=False)
monkeypatch.delenv("ANOTHER_VAR", raising=False)
load_env_file(env_file)
assert os.environ.get("MY_TEST_VAR") == "hello"
assert os.environ.get("ANOTHER_VAR") == "world"
def test_load_nonexistent_file(self, tmp_path):
# Should not raise
load_env_file(tmp_path / "nonexistent.env")
def test_does_not_overwrite_existing_env(self, tmp_path, monkeypatch):
env_file = tmp_path / ".env"
env_file.write_text("EXISTING_VAR=from_file\n", encoding="utf-8")
monkeypatch.setenv("EXISTING_VAR", "original")
load_env_file(env_file)
assert os.environ.get("EXISTING_VAR") == "original"
def test_handles_quoted_values(self, tmp_path, monkeypatch):
env_file = tmp_path / ".env"
env_file.write_text('QUOTED_VAR="quoted_value"\n', encoding="utf-8")
monkeypatch.delenv("QUOTED_VAR", raising=False)
load_env_file(env_file)
assert os.environ.get("QUOTED_VAR") == "quoted_value"
def test_handles_single_quoted_values(self, tmp_path, monkeypatch):
env_file = tmp_path / ".env"
env_file.write_text("SQ_VAR='single_quoted'\n", encoding="utf-8")
monkeypatch.delenv("SQ_VAR", raising=False)
load_env_file(env_file)
assert os.environ.get("SQ_VAR") == "single_quoted"
def test_skips_lines_without_equals(self, tmp_path, monkeypatch):
env_file = tmp_path / ".env"
env_file.write_text("NOEQUALS\nVALID=value\n", encoding="utf-8")
monkeypatch.delenv("VALID", raising=False)
monkeypatch.delenv("NOEQUALS", raising=False)
load_env_file(env_file)
assert os.environ.get("VALID") == "value"
assert os.environ.get("NOEQUALS") is None
def test_skips_lines_with_empty_key(self, tmp_path, monkeypatch):
"""Line 123: continue when key is empty after stripping (e.g. '=value')."""
env_file = tmp_path / ".env"
env_file.write_text("=value_with_no_key\nVALID2=ok\n", encoding="utf-8")
monkeypatch.delenv("VALID2", raising=False)
load_env_file(env_file)
assert os.environ.get("VALID2") == "ok"
# Empty key line should be skipped without error
assert os.environ.get("") is None or True # no key named "" should be set
# ─────────────────────────── load_settings ───────────────────────────────────
class TestLoadSettings:
def test_load_settings_success(self, monkeypatch):
monkeypatch.setenv("RWXF_API_KEY", "test-key")
settings = load_settings()
assert settings is not None
assert settings.resolved_api_key == "test-key"
def test_load_settings_with_env_file(self, tmp_path, monkeypatch):
env_file = tmp_path / ".env"
env_file.write_text("RWXF_API_KEY=key-from-file\n", encoding="utf-8")
monkeypatch.delenv("RWXF_API_KEY", raising=False)
settings = load_settings(env_file)
assert settings.resolved_api_key == "key-from-file"
monkeypatch.delenv("RWXF_API_KEY", raising=False)
def test_load_settings_failure_raises_runtime_error(self, monkeypatch):
monkeypatch.delenv("RWXF_API_KEY", raising=False)
monkeypatch.delenv("RWXF_API_KEY_FILE", raising=False)
with pytest.raises(RuntimeError):
load_settings()