""" Integration Tests für Tools Routes. Testet die API-Endpoints /llm/tools/search und /llm/tools/health. """ import pytest from unittest.mock import AsyncMock, patch, MagicMock from fastapi.testclient import TestClient from fastapi import FastAPI from llm_gateway.routes.tools import router from llm_gateway.middleware.auth import verify_api_key from llm_gateway.services.tool_gateway import ( ToolGateway, SearchResponse, SearchResult, TavilyError, ToolGatewayError, get_tool_gateway, ) # Test App erstellen mit Auth-Override app = FastAPI() app.include_router(router, prefix="/tools") # Mock für Auth-Dependency def mock_verify_api_key(): return "test-user" class TestSearchEndpoint: """Tests für POST /tools/search.""" def setup_method(self): """Setup für jeden Test.""" # Auth-Dependency überschreiben für Tests app.dependency_overrides[verify_api_key] = mock_verify_api_key self.client = TestClient(app) def teardown_method(self): """Cleanup nach jedem Test.""" app.dependency_overrides.clear() def test_search_requires_auth(self): """Test dass Auth erforderlich ist.""" # Auth-Override entfernen für diesen Test app.dependency_overrides.clear() client = TestClient(app) response = client.post( "/tools/search", json={"query": "test"}, ) # Ohne API-Key sollte 401/403 kommen assert response.status_code in [401, 403] def test_search_invalid_query_too_short(self): """Test Validierung: Query zu kurz.""" response = self.client.post( "/tools/search", json={"query": ""}, ) assert response.status_code == 422 # Validation Error def test_search_invalid_max_results(self): """Test Validierung: max_results außerhalb Grenzen.""" response = self.client.post( "/tools/search", json={"query": "test", "max_results": 100}, # > 20 ) assert response.status_code == 422 def test_search_success(self): """Test erfolgreiche Suche.""" mock_gateway = MagicMock(spec=ToolGateway) mock_gateway.search = AsyncMock(return_value=SearchResponse( query="Datenschutz Schule", results=[ SearchResult( title="Datenschutz an Schulen", url="https://example.com", content="Informationen...", score=0.9, ) ], answer="Zusammenfassung", pii_detected=False, response_time_ms=1500, )) app.dependency_overrides[get_tool_gateway] = lambda: mock_gateway response = self.client.post( "/tools/search", json={"query": "Datenschutz Schule"}, ) assert response.status_code == 200 data = response.json() assert data["query"] == "Datenschutz Schule" assert len(data["results"]) == 1 assert data["results"][0]["title"] == "Datenschutz an Schulen" assert data["pii_detected"] is False def test_search_with_pii_redaction(self): """Test Suche mit PII-Erkennung.""" mock_gateway = MagicMock(spec=ToolGateway) mock_gateway.search = AsyncMock(return_value=SearchResponse( query="Kontakt test@example.com", redacted_query="Kontakt [EMAIL_REDACTED]", results=[], pii_detected=True, pii_types=["email"], response_time_ms=1000, )) app.dependency_overrides[get_tool_gateway] = lambda: mock_gateway response = self.client.post( "/tools/search", json={"query": "Kontakt test@example.com"}, ) assert response.status_code == 200 data = response.json() assert data["pii_detected"] is True assert "email" in data["pii_types"] assert data["redacted_query"] == "Kontakt [EMAIL_REDACTED]" def test_search_with_domain_filters(self): """Test Suche mit Domain-Filtern.""" mock_gateway = MagicMock(spec=ToolGateway) mock_gateway.search = AsyncMock(return_value=SearchResponse( query="test", results=[], pii_detected=False, response_time_ms=500, )) app.dependency_overrides[get_tool_gateway] = lambda: mock_gateway response = self.client.post( "/tools/search", json={ "query": "test", "include_domains": ["bayern.de"], "exclude_domains": ["wikipedia.org"], }, ) assert response.status_code == 200 # Prüfen dass Filter an Gateway übergeben wurden mock_gateway.search.assert_called_once() call_kwargs = mock_gateway.search.call_args.kwargs assert call_kwargs["include_domains"] == ["bayern.de"] assert call_kwargs["exclude_domains"] == ["wikipedia.org"] def test_search_gateway_error(self): """Test Fehlerbehandlung bei Gateway-Fehler.""" mock_gateway = MagicMock(spec=ToolGateway) mock_gateway.search = AsyncMock(side_effect=ToolGatewayError("Not configured")) app.dependency_overrides[get_tool_gateway] = lambda: mock_gateway response = self.client.post( "/tools/search", json={"query": "test"}, ) assert response.status_code == 503 assert "unavailable" in response.json()["detail"].lower() def test_search_tavily_error(self): """Test Fehlerbehandlung bei Tavily-Fehler.""" mock_gateway = MagicMock(spec=ToolGateway) mock_gateway.search = AsyncMock(side_effect=TavilyError("Rate limit")) app.dependency_overrides[get_tool_gateway] = lambda: mock_gateway response = self.client.post( "/tools/search", json={"query": "test"}, ) assert response.status_code == 502 assert "search service error" in response.json()["detail"].lower() class TestHealthEndpoint: """Tests für GET /tools/health.""" def setup_method(self): """Setup für jeden Test.""" app.dependency_overrides[verify_api_key] = mock_verify_api_key self.client = TestClient(app) def teardown_method(self): """Cleanup nach jedem Test.""" app.dependency_overrides.clear() def test_health_requires_auth(self): """Test dass Auth erforderlich ist.""" app.dependency_overrides.clear() client = TestClient(app) response = client.get("/tools/health") assert response.status_code in [401, 403] def test_health_success(self): """Test erfolgreicher Health Check.""" mock_gateway = MagicMock(spec=ToolGateway) mock_gateway.health_check = AsyncMock(return_value={ "tavily": { "configured": True, "healthy": True, "response_time_ms": 1500, }, "pii_redaction": { "enabled": True, }, }) app.dependency_overrides[get_tool_gateway] = lambda: mock_gateway response = self.client.get("/tools/health") assert response.status_code == 200 data = response.json() assert data["tavily"]["configured"] is True assert data["tavily"]["healthy"] is True assert data["pii_redaction"]["enabled"] is True def test_health_tavily_not_configured(self): """Test Health Check ohne Tavily-Konfiguration.""" mock_gateway = MagicMock(spec=ToolGateway) mock_gateway.health_check = AsyncMock(return_value={ "tavily": { "configured": False, "healthy": False, }, "pii_redaction": { "enabled": True, }, }) app.dependency_overrides[get_tool_gateway] = lambda: mock_gateway response = self.client.get("/tools/health") assert response.status_code == 200 data = response.json() assert data["tavily"]["configured"] is False class TestSearchRequestValidation: """Tests für Request-Validierung.""" def setup_method(self): """Setup für jeden Test.""" app.dependency_overrides[verify_api_key] = mock_verify_api_key self.client = TestClient(app) def teardown_method(self): """Cleanup nach jedem Test.""" app.dependency_overrides.clear() def test_query_max_length(self): """Test Query max length Validierung.""" # Query mit > 1000 Zeichen response = self.client.post( "/tools/search", json={"query": "x" * 1001}, ) assert response.status_code == 422 def test_search_depth_enum(self): """Test search_depth Enum Validierung.""" mock_gateway = MagicMock(spec=ToolGateway) mock_gateway.search = AsyncMock(return_value=SearchResponse( query="test", results=[], pii_detected=False, response_time_ms=100, )) app.dependency_overrides[get_tool_gateway] = lambda: mock_gateway # Gültiger Wert response = self.client.post( "/tools/search", json={"query": "test", "search_depth": "advanced"}, ) assert response.status_code == 200 def test_search_depth_invalid(self): """Test ungültiger search_depth Wert.""" response = self.client.post( "/tools/search", json={"query": "test", "search_depth": "invalid"}, ) assert response.status_code == 422 class TestSearchResponseFormat: """Tests für Response-Format.""" def setup_method(self): """Setup für jeden Test.""" app.dependency_overrides[verify_api_key] = mock_verify_api_key self.client = TestClient(app) def teardown_method(self): """Cleanup nach jedem Test.""" app.dependency_overrides.clear() def test_response_has_all_fields(self): """Test dass Response alle erforderlichen Felder hat.""" mock_gateway = MagicMock(spec=ToolGateway) mock_gateway.search = AsyncMock(return_value=SearchResponse( query="test query", redacted_query=None, results=[ SearchResult( title="Result 1", url="https://example.com/1", content="Content 1", score=0.95, published_date="2024-01-15", ), ], answer="AI Summary", pii_detected=False, pii_types=[], response_time_ms=2000, )) app.dependency_overrides[get_tool_gateway] = lambda: mock_gateway response = self.client.post( "/tools/search", json={"query": "test query"}, ) assert response.status_code == 200 data = response.json() # Pflichtfelder assert "query" in data assert "results" in data assert "pii_detected" in data assert "pii_types" in data assert "response_time_ms" in data # Optionale Felder assert "redacted_query" in data assert "answer" in data # Result-Felder result = data["results"][0] assert "title" in result assert "url" in result assert "content" in result assert "score" in result assert "published_date" in result