""" Tests für Tool Gateway Service. """ import pytest from unittest.mock import AsyncMock, patch, MagicMock import httpx from llm_gateway.services.tool_gateway import ( ToolGateway, ToolGatewayConfig, SearchDepth, SearchResult, SearchResponse, TavilyError, ToolGatewayError, get_tool_gateway, ) from llm_gateway.services.pii_detector import PIIDetector, RedactionResult class TestToolGatewayConfig: """Tests für ToolGatewayConfig.""" def test_default_config(self): """Test Standardkonfiguration.""" config = ToolGatewayConfig() assert config.tavily_api_key is None assert config.tavily_base_url == "https://api.tavily.com" assert config.timeout == 30 assert config.max_results == 5 assert config.search_depth == SearchDepth.BASIC assert config.include_answer is True assert config.pii_redaction_enabled is True def test_config_from_env(self): """Test Konfiguration aus Umgebungsvariablen.""" with patch.dict("os.environ", { "TAVILY_API_KEY": "test-key", "TAVILY_BASE_URL": "https://custom.tavily.com", "TAVILY_TIMEOUT": "60", "TAVILY_MAX_RESULTS": "10", "TAVILY_SEARCH_DEPTH": "advanced", "PII_REDACTION_ENABLED": "false", }): config = ToolGatewayConfig.from_env() assert config.tavily_api_key == "test-key" assert config.tavily_base_url == "https://custom.tavily.com" assert config.timeout == 60 assert config.max_results == 10 assert config.search_depth == SearchDepth.ADVANCED assert config.pii_redaction_enabled is False class TestToolGatewayAvailability: """Tests für Gateway-Verfügbarkeit.""" def test_tavily_not_available_without_key(self): """Test Tavily nicht verfügbar ohne API Key.""" config = ToolGatewayConfig(tavily_api_key=None) gateway = ToolGateway(config=config) assert gateway.tavily_available is False def test_tavily_available_with_key(self): """Test Tavily verfügbar mit API Key.""" config = ToolGatewayConfig(tavily_api_key="test-key") gateway = ToolGateway(config=config) assert gateway.tavily_available is True class TestToolGatewayPIIRedaction: """Tests für PII-Redaktion im Gateway.""" def test_redact_query_with_email(self): """Test Redaktion von E-Mail in Query.""" config = ToolGatewayConfig(pii_redaction_enabled=True) gateway = ToolGateway(config=config) result = gateway._redact_query("Kontakt test@example.com Datenschutz") assert result.pii_found is True assert "test@example.com" not in result.redacted_text assert "[EMAIL_REDACTED]" in result.redacted_text def test_no_redaction_when_disabled(self): """Test keine Redaktion wenn deaktiviert.""" config = ToolGatewayConfig(pii_redaction_enabled=False) gateway = ToolGateway(config=config) result = gateway._redact_query("test@example.com") assert result.pii_found is False assert result.redacted_text == "test@example.com" class TestToolGatewaySearch: """Tests für Suche mit Gateway.""" @pytest.mark.asyncio async def test_search_raises_error_without_key(self): """Test Fehler bei Suche ohne API Key.""" config = ToolGatewayConfig(tavily_api_key=None) gateway = ToolGateway(config=config) with pytest.raises(ToolGatewayError, match="not configured"): await gateway.search("test query") @pytest.mark.asyncio async def test_search_success(self): """Test erfolgreiche Suche.""" config = ToolGatewayConfig(tavily_api_key="test-key") gateway = ToolGateway(config=config) mock_response = MagicMock() mock_response.status_code = 200 mock_response.json.return_value = { "results": [ { "title": "Test Result", "url": "https://example.com", "content": "Test content", "score": 0.95, } ], "answer": "Test answer", } with patch.object(gateway, "_get_client") as mock_client: mock_http = AsyncMock() mock_http.post.return_value = mock_response mock_client.return_value = mock_http result = await gateway.search("Schulrecht Bayern") assert result.query == "Schulrecht Bayern" assert len(result.results) == 1 assert result.results[0].title == "Test Result" assert result.answer == "Test answer" @pytest.mark.asyncio async def test_search_with_pii_redaction(self): """Test Suche mit PII-Redaktion.""" config = ToolGatewayConfig( tavily_api_key="test-key", pii_redaction_enabled=True, ) gateway = ToolGateway(config=config) mock_response = MagicMock() mock_response.status_code = 200 mock_response.json.return_value = { "results": [], "answer": None, } with patch.object(gateway, "_get_client") as mock_client: mock_http = AsyncMock() mock_http.post.return_value = mock_response mock_client.return_value = mock_http result = await gateway.search("Kontakt test@example.com Datenschutz") assert result.pii_detected is True assert "email" in result.pii_types assert result.redacted_query is not None assert "test@example.com" not in result.redacted_query # Prüfen dass redaktierte Query an Tavily gesendet wurde call_args = mock_http.post.call_args sent_query = call_args.kwargs["json"]["query"] assert "test@example.com" not in sent_query @pytest.mark.asyncio async def test_search_http_error(self): """Test HTTP-Fehler bei Suche.""" config = ToolGatewayConfig(tavily_api_key="test-key") gateway = ToolGateway(config=config) mock_response = MagicMock() mock_response.status_code = 429 mock_response.text = "Rate limit exceeded" mock_response.raise_for_status.side_effect = httpx.HTTPStatusError( "Rate limit", request=MagicMock(), response=mock_response, ) with patch.object(gateway, "_get_client") as mock_client: mock_http = AsyncMock() mock_http.post.return_value = mock_response mock_client.return_value = mock_http with pytest.raises(TavilyError, match="429"): await gateway.search("test") @pytest.mark.asyncio async def test_search_with_domain_filters(self): """Test Suche mit Domain-Filtern.""" config = ToolGatewayConfig(tavily_api_key="test-key") gateway = ToolGateway(config=config) mock_response = MagicMock() mock_response.status_code = 200 mock_response.json.return_value = {"results": [], "answer": None} with patch.object(gateway, "_get_client") as mock_client: mock_http = AsyncMock() mock_http.post.return_value = mock_response mock_client.return_value = mock_http await gateway.search( "test", include_domains=["gov.de", "schule.de"], exclude_domains=["wikipedia.org"], ) call_args = mock_http.post.call_args payload = call_args.kwargs["json"] assert payload["include_domains"] == ["gov.de", "schule.de"] assert payload["exclude_domains"] == ["wikipedia.org"] class TestSearchResult: """Tests für SearchResult Dataclass.""" def test_search_result_creation(self): """Test SearchResult erstellen.""" result = SearchResult( title="Test", url="https://example.com", content="Content", score=0.9, published_date="2024-01-15", ) assert result.title == "Test" assert result.url == "https://example.com" assert result.score == 0.9 def test_search_result_defaults(self): """Test SearchResult Standardwerte.""" result = SearchResult( title="Test", url="https://example.com", content="Content", ) assert result.score == 0.0 assert result.published_date is None class TestSearchResponse: """Tests für SearchResponse Dataclass.""" def test_search_response_creation(self): """Test SearchResponse erstellen.""" response = SearchResponse( query="test query", results=[], pii_detected=False, ) assert response.query == "test query" assert len(response.results) == 0 assert response.pii_detected is False def test_search_response_with_pii(self): """Test SearchResponse mit PII.""" response = SearchResponse( query="original query", redacted_query="redacted query", results=[], pii_detected=True, pii_types=["email", "phone"], ) assert response.pii_detected is True assert "email" in response.pii_types class TestSearchDepthEnum: """Tests für SearchDepth Enum.""" def test_search_depth_values(self): """Test SearchDepth Werte.""" assert SearchDepth.BASIC.value == "basic" assert SearchDepth.ADVANCED.value == "advanced" class TestGetToolGatewaySingleton: """Tests für Singleton Pattern.""" def test_singleton_returns_same_instance(self): """Test dass get_tool_gateway Singleton zurückgibt.""" gateway1 = get_tool_gateway() gateway2 = get_tool_gateway() assert gateway1 is gateway2 class TestToolGatewayHealthCheck: """Tests für Health Check.""" @pytest.mark.asyncio async def test_health_check_without_tavily(self): """Test Health Check ohne Tavily.""" config = ToolGatewayConfig(tavily_api_key=None) gateway = ToolGateway(config=config) status = await gateway.health_check() assert status["tavily"]["configured"] is False assert status["tavily"]["healthy"] is False @pytest.mark.asyncio async def test_health_check_with_tavily_success(self): """Test Health Check mit erfolgreichem Tavily.""" config = ToolGatewayConfig(tavily_api_key="test-key") gateway = ToolGateway(config=config) with patch.object(gateway, "search") as mock_search: mock_search.return_value = SearchResponse( query="test", results=[], response_time_ms=100, ) status = await gateway.health_check() assert status["tavily"]["configured"] is True assert status["tavily"]["healthy"] is True assert status["tavily"]["response_time_ms"] == 100 @pytest.mark.asyncio async def test_health_check_with_tavily_failure(self): """Test Health Check mit Tavily-Fehler.""" config = ToolGatewayConfig(tavily_api_key="test-key") gateway = ToolGateway(config=config) with patch.object(gateway, "search") as mock_search: mock_search.side_effect = TavilyError("Connection failed") status = await gateway.health_check() assert status["tavily"]["configured"] is True assert status["tavily"]["healthy"] is False assert "error" in status["tavily"] class TestToolGatewayClose: """Tests für Gateway-Cleanup.""" @pytest.mark.asyncio async def test_close_client(self): """Test Client-Cleanup.""" config = ToolGatewayConfig(tavily_api_key="test-key") gateway = ToolGateway(config=config) # Simuliere dass Client erstellt wurde mock_client = AsyncMock() gateway._client = mock_client await gateway.close() mock_client.aclose.assert_called_once() assert gateway._client is None