"""Unit tests for Configuration module.
Tests settings loading, validation, and error handling.
"""
from __future__ import annotations
from pathlib import Path
import pytest
from sso_mcp_server.config import ConfigurationError, Settings, reset_settings
class TestConfigurationError:
"""Tests for ConfigurationError exception."""
def test_configuration_error_message_only(self) -> None:
"""Test ConfigurationError with message only."""
error = ConfigurationError("Test error")
assert str(error) == "Test error"
assert error.message == "Test error"
assert error.action is None
def test_configuration_error_with_action(self) -> None:
"""Test ConfigurationError with action guidance."""
error = ConfigurationError("Missing value", action="Set the VALUE env var")
assert "Missing value" in str(error)
assert "Action: Set the VALUE env var" in str(error)
assert error.message == "Missing value"
assert error.action == "Set the VALUE env var"
class TestSettingsValidation:
"""Tests for Settings validation."""
def test_raises_for_missing_azure_client_id(self, temp_dir: Path) -> None:
"""Test that missing AZURE_CLIENT_ID raises ConfigurationError."""
with pytest.raises(ConfigurationError) as exc_info:
Settings(
azure_client_id="",
azure_tenant_id="test-tenant",
checklist_dir=temp_dir,
)
assert "AZURE_CLIENT_ID" in exc_info.value.message
def test_raises_for_missing_azure_tenant_id(self, temp_dir: Path) -> None:
"""Test that missing AZURE_TENANT_ID raises ConfigurationError."""
with pytest.raises(ConfigurationError) as exc_info:
Settings(
azure_client_id="test-client",
azure_tenant_id="",
checklist_dir=temp_dir,
)
assert "AZURE_TENANT_ID" in exc_info.value.message
def test_raises_for_missing_checklist_dir(self, temp_dir: Path) -> None:
"""Test that non-existent CHECKLIST_DIR raises ConfigurationError."""
with pytest.raises(ConfigurationError) as exc_info:
Settings(
azure_client_id="test-client",
azure_tenant_id="test-tenant",
checklist_dir=temp_dir / "nonexistent",
)
assert "CHECKLIST_DIR does not exist" in exc_info.value.message
def test_raises_for_invalid_checklist_dir(self, temp_dir: Path) -> None:
"""Test that file as CHECKLIST_DIR raises ConfigurationError."""
file_path = temp_dir / "file.txt"
file_path.write_text("content")
with pytest.raises(ConfigurationError) as exc_info:
Settings(
azure_client_id="test-client",
azure_tenant_id="test-tenant",
checklist_dir=file_path,
)
assert "is not a directory" in exc_info.value.message
def test_raises_for_invalid_port_low(self, temp_dir: Path) -> None:
"""Test that port below 1024 raises ConfigurationError."""
with pytest.raises(ConfigurationError) as exc_info:
Settings(
azure_client_id="test-client",
azure_tenant_id="test-tenant",
checklist_dir=temp_dir,
mcp_port=80,
)
assert "1024 and 65535" in exc_info.value.message
def test_raises_for_invalid_port_high(self, temp_dir: Path) -> None:
"""Test that port above 65535 raises ConfigurationError."""
with pytest.raises(ConfigurationError) as exc_info:
Settings(
azure_client_id="test-client",
azure_tenant_id="test-tenant",
checklist_dir=temp_dir,
mcp_port=70000,
)
assert "1024 and 65535" in exc_info.value.message
def test_raises_for_invalid_log_level(self, temp_dir: Path) -> None:
"""Test that invalid LOG_LEVEL raises ConfigurationError."""
with pytest.raises(ConfigurationError) as exc_info:
Settings(
azure_client_id="test-client",
azure_tenant_id="test-tenant",
checklist_dir=temp_dir,
log_level="INVALID",
)
assert "LOG_LEVEL" in exc_info.value.message
def test_valid_settings_accepted(self, temp_dir: Path) -> None:
"""Test that valid settings are accepted."""
settings = Settings(
azure_client_id="test-client",
azure_tenant_id="test-tenant",
checklist_dir=temp_dir,
mcp_port=9000,
log_level="DEBUG",
)
assert settings.azure_client_id == "test-client"
assert settings.mcp_port == 9000
class TestSettingsFromEnv:
"""Tests for Settings.from_env() method."""
def test_from_env_loads_environment_variables(
self, temp_dir: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Test that from_env loads from environment variables."""
reset_settings()
monkeypatch.setenv("AZURE_CLIENT_ID", "env-client-id")
monkeypatch.setenv("AZURE_TENANT_ID", "env-tenant-id")
monkeypatch.setenv("CHECKLIST_DIR", str(temp_dir))
monkeypatch.setenv("MCP_PORT", "9001")
monkeypatch.setenv("LOG_LEVEL", "DEBUG")
settings = Settings.from_env()
assert settings.azure_client_id == "env-client-id"
assert settings.azure_tenant_id == "env-tenant-id"
assert settings.checklist_dir == temp_dir.resolve()
assert settings.mcp_port == 9001
assert settings.log_level == "DEBUG"
def test_from_env_uses_defaults(self, temp_dir: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""Test that from_env uses default values."""
reset_settings()
# Clear potentially conflicting env vars
monkeypatch.delenv("MCP_PORT", raising=False)
monkeypatch.delenv("LOG_LEVEL", raising=False)
monkeypatch.setenv("AZURE_CLIENT_ID", "test-client")
monkeypatch.setenv("AZURE_TENANT_ID", "test-tenant")
monkeypatch.setenv("CHECKLIST_DIR", str(temp_dir))
settings = Settings.from_env()
assert settings.mcp_port == 8080
assert settings.log_level == "INFO"
def test_from_env_raises_for_invalid_port(
self, temp_dir: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Test that from_env raises for non-integer port."""
reset_settings()
monkeypatch.setenv("AZURE_CLIENT_ID", "test-client")
monkeypatch.setenv("AZURE_TENANT_ID", "test-tenant")
monkeypatch.setenv("CHECKLIST_DIR", str(temp_dir))
monkeypatch.setenv("MCP_PORT", "invalid")
with pytest.raises(ConfigurationError) as exc_info:
Settings.from_env()
assert "must be an integer" in exc_info.value.message
class TestSettingsDefaults:
"""Tests for Settings default values."""
def test_token_cache_path_default(self, temp_dir: Path) -> None:
"""Test that token_cache_path has correct default."""
settings = Settings(
azure_client_id="test-client",
azure_tenant_id="test-tenant",
checklist_dir=temp_dir,
)
expected = Path.home() / ".sso-mcp-server" / "token_cache.bin"
assert settings.token_cache_path == expected