"""Tests for configuration management.
Requirements covered:
- AU-002: Support credential configuration via environment variables
- AU-003: Support pre-configured auth tokens as alternative to username/password
- 7.1-7.3: Configuration settings
"""
import os
from unittest.mock import patch
import pytest
from pydantic import SecretStr
from jana_mcp.config import Settings, get_settings
class TestSettingsDefaults:
"""Test default configuration values."""
def test_default_backend_url(self):
"""Test default Jana backend URL (7.1)."""
with patch.dict(os.environ, {}, clear=True):
settings = Settings()
assert settings.jana_backend_url == "http://web:8000"
def test_default_server_host(self):
"""Test default MCP server host (7.2)."""
with patch.dict(os.environ, {}, clear=True):
settings = Settings()
assert settings.mcp_server_host == "0.0.0.0"
def test_default_server_port(self):
"""Test default MCP server port (7.2)."""
with patch.dict(os.environ, {}, clear=True):
settings = Settings()
assert settings.mcp_server_port == 8080
def test_default_log_level(self):
"""Test default log level (7.2)."""
with patch.dict(os.environ, {}, clear=True):
settings = Settings()
assert settings.log_level == "INFO"
def test_default_timeout(self):
"""Test default timeout (7.3)."""
with patch.dict(os.environ, {}, clear=True):
settings = Settings()
assert settings.jana_timeout == 30
class TestSettingsFromEnvironment:
"""Test loading settings from environment variables (AU-002)."""
def test_load_backend_url_from_env(self):
"""Test JANA_BACKEND_URL from environment."""
with patch.dict(os.environ, {"JANA_BACKEND_URL": "http://localhost:9000"}):
settings = Settings()
assert settings.jana_backend_url == "http://localhost:9000"
def test_load_username_from_env(self):
"""Test JANA_USERNAME from environment."""
with patch.dict(os.environ, {"JANA_USERNAME": "testuser"}):
settings = Settings()
assert settings.jana_username == "testuser"
def test_load_password_from_env(self):
"""Test JANA_PASSWORD from environment."""
with patch.dict(os.environ, {"JANA_PASSWORD": "secretpass"}):
settings = Settings()
assert settings.jana_password is not None
assert settings.jana_password.get_secret_value() == "secretpass"
def test_load_token_from_env(self):
"""Test JANA_TOKEN from environment (AU-003)."""
with patch.dict(os.environ, {"JANA_TOKEN": "pre-configured-token"}):
settings = Settings()
assert settings.jana_token is not None
assert settings.jana_token.get_secret_value() == "pre-configured-token"
def test_load_server_port_from_env(self):
"""Test MCP_SERVER_PORT from environment."""
with patch.dict(os.environ, {"MCP_SERVER_PORT": "9090"}):
settings = Settings()
assert settings.mcp_server_port == 9090
def test_load_log_level_from_env(self):
"""Test LOG_LEVEL from environment."""
with patch.dict(os.environ, {"LOG_LEVEL": "DEBUG"}):
settings = Settings()
assert settings.log_level == "DEBUG"
def test_load_timeout_from_env(self):
"""Test JANA_TIMEOUT from environment."""
with patch.dict(os.environ, {"JANA_TIMEOUT": "60"}):
settings = Settings()
assert settings.jana_timeout == 60
class TestSettingsCredentials:
"""Test credential handling (AU-001, AU-003)."""
def test_has_credentials_with_username_password(self):
"""Test has_credentials with username/password."""
settings = Settings(jana_username="user", jana_password=SecretStr("pass"))
assert settings.has_credentials is True
def test_has_credentials_with_token_only(self):
"""Test has_credentials with token only (AU-003)."""
settings = Settings(jana_token=SecretStr("token123"))
assert settings.has_credentials is True
def test_has_credentials_missing(self, monkeypatch):
"""Test has_credentials when no credentials provided."""
# Clear env vars that might be set
monkeypatch.delenv("JANA_USERNAME", raising=False)
monkeypatch.delenv("JANA_PASSWORD", raising=False)
monkeypatch.delenv("JANA_TOKEN", raising=False)
settings = Settings()
assert settings.has_credentials is False
def test_has_credentials_partial_username_only(self, monkeypatch):
"""Test has_credentials with only username (incomplete)."""
monkeypatch.delenv("JANA_PASSWORD", raising=False)
monkeypatch.delenv("JANA_TOKEN", raising=False)
settings = Settings(jana_username="user")
assert settings.has_credentials is False
def test_has_credentials_partial_password_only(self, monkeypatch):
"""Test has_credentials with only password (incomplete)."""
monkeypatch.delenv("JANA_USERNAME", raising=False)
monkeypatch.delenv("JANA_TOKEN", raising=False)
settings = Settings(jana_password=SecretStr("pass"))
assert settings.has_credentials is False
def test_get_auth_token_from_token_setting(self):
"""Test get_auth_token returns token value (AU-003)."""
settings = Settings(jana_token=SecretStr("my-token"))
assert settings.get_auth_token() == "my-token"
def test_get_auth_token_when_not_set(self, monkeypatch):
"""Test get_auth_token returns None when not configured."""
monkeypatch.delenv("JANA_TOKEN", raising=False)
settings = Settings()
assert settings.get_auth_token() is None
def test_get_password_returns_value(self):
"""Test get_password returns password value."""
settings = Settings(jana_password=SecretStr("secret"))
assert settings.get_password() == "secret"
def test_get_password_when_not_set(self, monkeypatch):
"""Test get_password returns None when not configured."""
monkeypatch.delenv("JANA_PASSWORD", raising=False)
settings = Settings()
assert settings.get_password() is None
class TestSettingsValidation:
"""Test settings validation."""
def test_port_minimum_validation(self):
"""Test port number minimum validation."""
with pytest.raises(ValueError):
Settings(mcp_server_port=0)
def test_port_maximum_validation(self):
"""Test port number maximum validation."""
with pytest.raises(ValueError):
Settings(mcp_server_port=70000)
def test_timeout_minimum_validation(self):
"""Test timeout minimum validation."""
with pytest.raises(ValueError):
Settings(jana_timeout=0)
def test_timeout_maximum_validation(self):
"""Test timeout maximum validation."""
with pytest.raises(ValueError):
Settings(jana_timeout=400)
def test_valid_port_range(self):
"""Test valid port numbers are accepted."""
settings = Settings(mcp_server_port=3000)
assert settings.mcp_server_port == 3000
def test_valid_timeout_range(self):
"""Test valid timeout values are accepted."""
settings = Settings(jana_timeout=120)
assert settings.jana_timeout == 120
class TestGetSettings:
"""Test cached settings retrieval."""
def test_get_settings_returns_settings_instance(self):
"""Test get_settings returns Settings object."""
# Clear cache first
get_settings.cache_clear()
settings = get_settings()
assert isinstance(settings, Settings)
def test_get_settings_is_cached(self):
"""Test get_settings returns same cached instance."""
get_settings.cache_clear()
settings1 = get_settings()
settings2 = get_settings()
assert settings1 is settings2
class TestSettingsCaseSensitivity:
"""Test case insensitivity of environment variables."""
def test_lowercase_env_vars(self):
"""Test lowercase environment variable names work."""
with patch.dict(os.environ, {"jana_backend_url": "http://test:8000"}):
settings = Settings()
# Note: pydantic-settings handles case insensitivity
assert settings.jana_backend_url in ["http://web:8000", "http://test:8000"]
def test_mixed_case_env_vars(self):
"""Test mixed case environment variable names work."""
with patch.dict(os.environ, {"Jana_Backend_URL": "http://mixed:8000"}):
settings = Settings()
# Behavior depends on pydantic-settings configuration
assert settings.jana_backend_url is not None