"""Tests for FastAPI routes.""" from unittest.mock import AsyncMock, patch import pytest from fastapi.testclient import TestClient from llm_inference.api.app import create_app from llm_inference.api.dependencies import init_concurrency_limiter from llm_inference.client import LLMClient from llm_inference.config import LLMSettings, SettingsCache from llm_inference.schemas import CompletionResponse @pytest.fixture def app_client(test_settings: LLMSettings) -> TestClient: """Create a test client with mocked client.""" # Inject test settings into cache BEFORE creating app SettingsCache.set(test_settings) # Initialize concurrency limiter for tests init_concurrency_limiter(test_settings.max_concurrent_completions) app = create_app() app.state.settings = test_settings app.state.client = LLMClient(settings=test_settings) return TestClient(app) class TestHealthRoutes: """Tests for health check routes.""" def test_health_endpoint(self, app_client: TestClient) -> None: """Test /health endpoint.""" response = app_client.get("/health") assert response.status_code == 200 data = response.json() assert "status" in data assert "backends" in data assert data["status"] in ["healthy", "degraded", "unhealthy"] def test_ready_endpoint(self, app_client: TestClient) -> None: """Test /ready endpoint.""" response = app_client.get("/ready") assert response.status_code == 200 data = response.json() assert data["ready"] is True class TestModelsRoutes: """Tests for model management routes.""" def test_list_models(self, app_client: TestClient) -> None: """Test GET /v1/models.""" response = app_client.get("/v1/models") assert response.status_code == 200 data = response.json() assert "data" in data assert "object" in data assert data["object"] == "list" def test_list_models_with_backend_filter(self, app_client: TestClient) -> None: """Test GET /v1/models with backend filter.""" response = app_client.get("/v1/models?backend=litellm") assert response.status_code == 200 data = response.json() # All models should be from litellm backend for model in data["data"]: assert model["backend"] == "litellm" def test_list_models_invalid_backend(self, app_client: TestClient) -> None: """Test GET /v1/models with invalid backend.""" response = app_client.get("/v1/models?backend=invalid") assert response.status_code == 400 def test_list_backends(self, app_client: TestClient) -> None: """Test GET /v1/backends.""" response = app_client.get("/v1/backends") assert response.status_code == 200 data = response.json() assert "backends" in data assert "litellm" in data["backends"] class TestCompletionsRoutes: """Tests for completion routes.""" def test_chat_completions( self, app_client: TestClient, mock_completion_response: CompletionResponse, ) -> None: """Test POST /v1/chat/completions.""" # Mock the client's complete method with patch.object( app_client.app.state.client, "complete", new_callable=AsyncMock, return_value=mock_completion_response, ): response = app_client.post( "/v1/chat/completions", json={ "messages": [{"role": "user", "content": "Hello"}], "model": "gpt-3.5-turbo", }, ) assert response.status_code == 200 data = response.json() assert "id" in data assert "choices" in data assert "model" in data def test_chat_completions_with_parameters( self, app_client: TestClient, mock_completion_response: CompletionResponse, ) -> None: """Test completion with optional parameters.""" with patch.object( app_client.app.state.client, "complete", new_callable=AsyncMock, return_value=mock_completion_response, ): response = app_client.post( "/v1/chat/completions", json={ "messages": [{"role": "user", "content": "Hello"}], "model": "gpt-4", "temperature": 0.5, "max_tokens": 100, "top_p": 0.9, }, ) assert response.status_code == 200 def test_chat_completions_with_backend_override( self, app_client: TestClient, mock_completion_response: CompletionResponse, ) -> None: """Test completion with backend override.""" with patch.object( app_client.app.state.client, "complete", new_callable=AsyncMock, return_value=mock_completion_response, ): response = app_client.post( "/v1/chat/completions", json={ "messages": [{"role": "user", "content": "Hello"}], "model": "gpt-3.5-turbo", "backend": "litellm", }, ) assert response.status_code == 200 def test_chat_completions_missing_messages(self, app_client: TestClient) -> None: """Test completion without messages returns error.""" response = app_client.post( "/v1/chat/completions", json={"model": "gpt-3.5-turbo"}, ) assert response.status_code == 422 # Validation error def test_chat_completions_missing_model(self, app_client: TestClient) -> None: """Test completion without model returns error.""" response = app_client.post( "/v1/chat/completions", json={"messages": [{"role": "user", "content": "Hello"}]}, ) assert response.status_code == 422 # Validation error class TestModelLoadRoutes: """Tests for model load/unload routes.""" def test_load_model_unsupported_backend(self, app_client: TestClient) -> None: """Test loading model on backend that doesn't support it.""" response = app_client.post( "/v1/models/load", json={"model": "test-model", "backend": "litellm"}, ) # LiteLLM doesn't support model loading assert response.status_code == 400 def test_unload_model_unsupported_backend(self, app_client: TestClient) -> None: """Test unloading model on backend that doesn't support it.""" response = app_client.post( "/v1/models/unload", json={"model": "test-model", "backend": "litellm"}, ) # LiteLLM doesn't support model unloading assert response.status_code == 400