forked from cardosofelipe/fast-next-template
Implements complete LLM Gateway MCP Server with: - FastMCP server with 4 tools: chat_completion, list_models, get_usage, count_tokens - LiteLLM Router with multi-provider failover chains - Circuit breaker pattern for fault tolerance - Redis-based cost tracking per project/agent - Comprehensive test suite (209 tests, 92% coverage) Model groups defined per ADR-004: - reasoning: claude-opus-4 → gpt-4.1 → gemini-2.5-pro - code: claude-sonnet-4 → gpt-4.1 → deepseek-coder - fast: claude-haiku → gpt-4.1-mini → gemini-2.0-flash 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
201 lines
6.6 KiB
Python
201 lines
6.6 KiB
Python
"""
|
|
Tests for config module.
|
|
"""
|
|
|
|
import os
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
|
|
from config import Settings, get_settings
|
|
|
|
|
|
class TestSettings:
|
|
"""Tests for Settings class."""
|
|
|
|
def test_default_values(self) -> None:
|
|
"""Test default configuration values."""
|
|
settings = Settings()
|
|
|
|
assert settings.host == "0.0.0.0"
|
|
assert settings.port == 8001
|
|
assert settings.debug is False
|
|
assert settings.redis_url == "redis://localhost:6379/0"
|
|
assert settings.litellm_timeout == 120
|
|
assert settings.circuit_failure_threshold == 5
|
|
|
|
def test_custom_values(self) -> None:
|
|
"""Test custom configuration values."""
|
|
settings = Settings(
|
|
host="127.0.0.1",
|
|
port=9000,
|
|
debug=True,
|
|
redis_url="redis://custom:6380/1",
|
|
litellm_timeout=60,
|
|
)
|
|
|
|
assert settings.host == "127.0.0.1"
|
|
assert settings.port == 9000
|
|
assert settings.debug is True
|
|
assert settings.redis_url == "redis://custom:6380/1"
|
|
assert settings.litellm_timeout == 60
|
|
|
|
def test_port_validation_valid(self) -> None:
|
|
"""Test valid port numbers."""
|
|
settings = Settings(port=1)
|
|
assert settings.port == 1
|
|
|
|
settings = Settings(port=65535)
|
|
assert settings.port == 65535
|
|
|
|
settings = Settings(port=8080)
|
|
assert settings.port == 8080
|
|
|
|
def test_port_validation_invalid(self) -> None:
|
|
"""Test invalid port numbers."""
|
|
with pytest.raises(ValueError, match="Port must be between"):
|
|
Settings(port=0)
|
|
|
|
with pytest.raises(ValueError, match="Port must be between"):
|
|
Settings(port=65536)
|
|
|
|
with pytest.raises(ValueError, match="Port must be between"):
|
|
Settings(port=-1)
|
|
|
|
def test_ttl_validation_valid(self) -> None:
|
|
"""Test valid TTL values."""
|
|
settings = Settings(redis_ttl_hours=1)
|
|
assert settings.redis_ttl_hours == 1
|
|
|
|
settings = Settings(redis_ttl_hours=168) # 1 week
|
|
assert settings.redis_ttl_hours == 168
|
|
|
|
def test_ttl_validation_invalid(self) -> None:
|
|
"""Test invalid TTL values."""
|
|
with pytest.raises(ValueError, match="Redis TTL must be positive"):
|
|
Settings(redis_ttl_hours=0)
|
|
|
|
with pytest.raises(ValueError, match="Redis TTL must be positive"):
|
|
Settings(redis_ttl_hours=-1)
|
|
|
|
def test_failure_threshold_validation(self) -> None:
|
|
"""Test circuit failure threshold validation."""
|
|
settings = Settings(circuit_failure_threshold=1)
|
|
assert settings.circuit_failure_threshold == 1
|
|
|
|
settings = Settings(circuit_failure_threshold=100)
|
|
assert settings.circuit_failure_threshold == 100
|
|
|
|
with pytest.raises(ValueError, match="Failure threshold must be between"):
|
|
Settings(circuit_failure_threshold=0)
|
|
|
|
with pytest.raises(ValueError, match="Failure threshold must be between"):
|
|
Settings(circuit_failure_threshold=101)
|
|
|
|
def test_timeout_validation(self) -> None:
|
|
"""Test timeout validation."""
|
|
settings = Settings(litellm_timeout=1)
|
|
assert settings.litellm_timeout == 1
|
|
|
|
settings = Settings(litellm_timeout=600)
|
|
assert settings.litellm_timeout == 600
|
|
|
|
with pytest.raises(ValueError, match="Timeout must be between"):
|
|
Settings(litellm_timeout=0)
|
|
|
|
with pytest.raises(ValueError, match="Timeout must be between"):
|
|
Settings(litellm_timeout=601)
|
|
|
|
def test_get_available_providers_none(self) -> None:
|
|
"""Test getting available providers with none configured."""
|
|
settings = Settings()
|
|
providers = settings.get_available_providers()
|
|
assert providers == []
|
|
|
|
def test_get_available_providers_some(self) -> None:
|
|
"""Test getting available providers with some configured."""
|
|
settings = Settings(
|
|
anthropic_api_key="test-key",
|
|
openai_api_key="test-key",
|
|
)
|
|
providers = settings.get_available_providers()
|
|
|
|
assert "anthropic" in providers
|
|
assert "openai" in providers
|
|
assert "google" not in providers
|
|
assert len(providers) == 2
|
|
|
|
def test_get_available_providers_all(self) -> None:
|
|
"""Test getting available providers with all configured."""
|
|
settings = Settings(
|
|
anthropic_api_key="test-key",
|
|
openai_api_key="test-key",
|
|
google_api_key="test-key",
|
|
alibaba_api_key="test-key",
|
|
deepseek_api_key="test-key",
|
|
)
|
|
providers = settings.get_available_providers()
|
|
|
|
assert len(providers) == 5
|
|
assert "anthropic" in providers
|
|
assert "openai" in providers
|
|
assert "google" in providers
|
|
assert "alibaba" in providers
|
|
assert "deepseek" in providers
|
|
|
|
def test_has_any_provider_false(self) -> None:
|
|
"""Test has_any_provider when none configured."""
|
|
settings = Settings()
|
|
assert settings.has_any_provider() is False
|
|
|
|
def test_has_any_provider_true(self) -> None:
|
|
"""Test has_any_provider when at least one configured."""
|
|
settings = Settings(anthropic_api_key="test-key")
|
|
assert settings.has_any_provider() is True
|
|
|
|
def test_deepseek_base_url_counts_as_provider(self) -> None:
|
|
"""Test that DeepSeek base URL alone counts as provider."""
|
|
settings = Settings(deepseek_base_url="http://localhost:8000")
|
|
providers = settings.get_available_providers()
|
|
assert "deepseek" in providers
|
|
|
|
|
|
class TestGetSettings:
|
|
"""Tests for get_settings function."""
|
|
|
|
def test_get_settings_returns_settings(self) -> None:
|
|
"""Test that get_settings returns a Settings instance."""
|
|
# Clear the cache first
|
|
get_settings.cache_clear()
|
|
|
|
settings = get_settings()
|
|
assert isinstance(settings, Settings)
|
|
|
|
def test_get_settings_is_cached(self) -> None:
|
|
"""Test that get_settings returns cached instance."""
|
|
get_settings.cache_clear()
|
|
|
|
settings1 = get_settings()
|
|
settings2 = get_settings()
|
|
|
|
assert settings1 is settings2
|
|
|
|
def test_env_var_override(self) -> None:
|
|
"""Test that environment variables override defaults."""
|
|
get_settings.cache_clear()
|
|
|
|
with patch.dict(
|
|
os.environ,
|
|
{
|
|
"LLM_GATEWAY_HOST": "192.168.1.1",
|
|
"LLM_GATEWAY_PORT": "9999",
|
|
"LLM_GATEWAY_DEBUG": "true",
|
|
},
|
|
):
|
|
get_settings.cache_clear()
|
|
settings = get_settings()
|
|
|
|
assert settings.host == "192.168.1.1"
|
|
assert settings.port == 9999
|
|
assert settings.debug is True
|