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>
This commit is contained in:
366
backend/tests/test_llm_gateway/test_tools_routes.py
Normal file
366
backend/tests/test_llm_gateway/test_tools_routes.py
Normal file
@@ -0,0 +1,366 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user