This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
breakpilot-pwa/backend/tests/test_llm_gateway/test_tool_gateway.py
Benjamin Admin bfdaf63ba9 fix: Restore all files lost during destructive rebase
A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.

This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).

Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 09:51:32 +01:00

370 lines
12 KiB
Python

"""
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