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_tools_routes.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

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