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>
367 lines
11 KiB
Python
367 lines
11 KiB
Python
"""
|
|
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
|