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_alerts_agent/test_api_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

595 lines
18 KiB
Python

"""
Tests für Alerts Agent API Routes.
Testet alle Endpoints: ingest, run, inbox, feedback, profile, stats.
"""
import pytest
from datetime import datetime
from fastapi import FastAPI
from fastapi.testclient import TestClient
from alerts_agent.api.routes import router, _alerts_store, _profile_store
from alerts_agent.models.alert_item import AlertStatus
# Test App erstellen
app = FastAPI()
app.include_router(router, prefix="/api")
class TestIngestEndpoint:
"""Tests für POST /alerts/ingest."""
def setup_method(self):
"""Setup für jeden Test."""
_alerts_store.clear()
_profile_store.clear()
self.client = TestClient(app)
def test_ingest_minimal(self):
"""Test minimaler Alert-Import."""
response = self.client.post(
"/api/alerts/ingest",
json={
"title": "Test Alert",
"url": "https://example.com/test",
},
)
assert response.status_code == 200
data = response.json()
assert data["status"] == "created"
assert "id" in data
assert len(data["id"]) == 36 # UUID
def test_ingest_full(self):
"""Test vollständiger Alert-Import."""
response = self.client.post(
"/api/alerts/ingest",
json={
"title": "Neue Inklusions-Richtlinie",
"url": "https://example.com/inklusion",
"snippet": "Das Kultusministerium hat neue Richtlinien...",
"topic_label": "Inklusion Bayern",
"published_at": "2024-01-15T10:30:00",
},
)
assert response.status_code == 200
data = response.json()
assert "Inklusions-Richtlinie" in data["message"]
def test_ingest_stores_alert(self):
"""Test dass Alert gespeichert wird."""
response = self.client.post(
"/api/alerts/ingest",
json={
"title": "Stored Alert",
"url": "https://example.com/stored",
},
)
alert_id = response.json()["id"]
assert alert_id in _alerts_store
assert _alerts_store[alert_id].title == "Stored Alert"
def test_ingest_validation_missing_title(self):
"""Test Validierung: Titel fehlt."""
response = self.client.post(
"/api/alerts/ingest",
json={
"url": "https://example.com/test",
},
)
assert response.status_code == 422
def test_ingest_validation_missing_url(self):
"""Test Validierung: URL fehlt."""
response = self.client.post(
"/api/alerts/ingest",
json={
"title": "Test",
},
)
assert response.status_code == 422
def test_ingest_validation_empty_title(self):
"""Test Validierung: Leerer Titel."""
response = self.client.post(
"/api/alerts/ingest",
json={
"title": "",
"url": "https://example.com",
},
)
assert response.status_code == 422
class TestRunEndpoint:
"""Tests für POST /alerts/run."""
def setup_method(self):
"""Setup für jeden Test."""
_alerts_store.clear()
_profile_store.clear()
self.client = TestClient(app)
def test_run_empty(self):
"""Test Scoring ohne Alerts."""
response = self.client.post(
"/api/alerts/run",
json={"limit": 10},
)
assert response.status_code == 200
data = response.json()
assert data["processed"] == 0
assert data["keep"] == 0
assert data["drop"] == 0
def test_run_scores_alerts(self):
"""Test Scoring bewertet Alerts."""
# Alerts importieren
self.client.post("/api/alerts/ingest", json={
"title": "Inklusion in Schulen",
"url": "https://example.com/1",
})
self.client.post("/api/alerts/ingest", json={
"title": "Stellenanzeige Lehrer",
"url": "https://example.com/2",
})
# Scoring starten
response = self.client.post(
"/api/alerts/run",
json={"limit": 10},
)
assert response.status_code == 200
data = response.json()
assert data["processed"] == 2
assert data["keep"] + data["drop"] + data["review"] == 2
def test_run_keyword_scoring_keep(self):
"""Test Keyword-Scoring: Priorität → KEEP."""
# Explizit "Datenschutz Schule" als Snippet für besseren Match
self.client.post("/api/alerts/ingest", json={
"title": "Neue Datenschutz-Regelung für Schulen",
"url": "https://example.com/datenschutz",
"snippet": "Datenschutz Schule DSGVO Regelung",
})
response = self.client.post("/api/alerts/run", json={"limit": 10})
data = response.json()
# Sollte als KEEP oder REVIEW bewertet werden (nicht DROP)
assert data["drop"] == 0
assert data["keep"] + data["review"] == 1
def test_run_keyword_scoring_drop(self):
"""Test Keyword-Scoring: Ausschluss → DROP."""
self.client.post("/api/alerts/ingest", json={
"title": "Stellenanzeige: Schulleiter gesucht",
"url": "https://example.com/job",
})
response = self.client.post("/api/alerts/run", json={"limit": 10})
data = response.json()
assert data["drop"] == 1
assert data["keep"] == 0
def test_run_skip_scored(self):
"""Test bereits bewertete werden übersprungen."""
self.client.post("/api/alerts/ingest", json={
"title": "Test Alert",
"url": "https://example.com/test",
})
# Erstes Scoring
self.client.post("/api/alerts/run", json={"limit": 10})
# Zweites Scoring mit skip_scored=true
response = self.client.post(
"/api/alerts/run",
json={"limit": 10, "skip_scored": True},
)
data = response.json()
assert data["processed"] == 0
def test_run_rescore(self):
"""Test Re-Scoring mit skip_scored=false."""
self.client.post("/api/alerts/ingest", json={
"title": "Test Alert",
"url": "https://example.com/test",
})
# Erstes Scoring
self.client.post("/api/alerts/run", json={"limit": 10})
# Zweites Scoring mit skip_scored=false
response = self.client.post(
"/api/alerts/run",
json={"limit": 10, "skip_scored": False},
)
data = response.json()
assert data["processed"] == 1
def test_run_limit(self):
"""Test Limit Parameter."""
# 5 Alerts importieren
for i in range(5):
self.client.post("/api/alerts/ingest", json={
"title": f"Alert {i}",
"url": f"https://example.com/{i}",
})
# Nur 2 scoren
response = self.client.post(
"/api/alerts/run",
json={"limit": 2},
)
data = response.json()
assert data["processed"] == 2
def test_run_returns_duration(self):
"""Test Duration wird zurückgegeben."""
response = self.client.post("/api/alerts/run", json={"limit": 10})
data = response.json()
assert "duration_ms" in data
assert isinstance(data["duration_ms"], int)
class TestInboxEndpoint:
"""Tests für GET /alerts/inbox."""
def setup_method(self):
"""Setup für jeden Test."""
_alerts_store.clear()
_profile_store.clear()
self.client = TestClient(app)
def _create_and_score_alerts(self):
"""Helfer: Erstelle und score Test-Alerts."""
# KEEP Alert
self.client.post("/api/alerts/ingest", json={
"title": "Inklusion Regelung",
"url": "https://example.com/keep",
})
# DROP Alert
self.client.post("/api/alerts/ingest", json={
"title": "Stellenanzeige",
"url": "https://example.com/drop",
})
# Scoring
self.client.post("/api/alerts/run", json={"limit": 10})
def test_inbox_empty(self):
"""Test leere Inbox."""
response = self.client.get("/api/alerts/inbox")
assert response.status_code == 200
data = response.json()
assert data["items"] == []
assert data["total"] == 0
def test_inbox_shows_keep_and_review(self):
"""Test Inbox zeigt KEEP und REVIEW."""
self._create_and_score_alerts()
response = self.client.get("/api/alerts/inbox")
data = response.json()
# Nur KEEP sollte angezeigt werden (Stellenanzeige ist DROP)
assert data["total"] == 1
assert data["items"][0]["relevance_decision"] == "KEEP"
def test_inbox_filter_by_decision(self):
"""Test Inbox Filter nach Decision."""
self._create_and_score_alerts()
# Nur DROP
response = self.client.get("/api/alerts/inbox?decision=DROP")
data = response.json()
assert data["total"] == 1
assert data["items"][0]["relevance_decision"] == "DROP"
def test_inbox_pagination(self):
"""Test Inbox Pagination."""
# 5 KEEP Alerts
for i in range(5):
self.client.post("/api/alerts/ingest", json={
"title": f"Inklusion Alert {i}",
"url": f"https://example.com/{i}",
})
self.client.post("/api/alerts/run", json={"limit": 10})
# Erste Seite
response = self.client.get("/api/alerts/inbox?page=1&page_size=2")
data = response.json()
assert data["total"] == 5
assert len(data["items"]) == 2
assert data["page"] == 1
assert data["page_size"] == 2
# Zweite Seite
response = self.client.get("/api/alerts/inbox?page=2&page_size=2")
data = response.json()
assert len(data["items"]) == 2
def test_inbox_item_fields(self):
"""Test Inbox Items haben alle Felder."""
self.client.post("/api/alerts/ingest", json={
"title": "Test Alert",
"url": "https://example.com/test",
"snippet": "Test snippet",
"topic_label": "Test Topic",
})
self.client.post("/api/alerts/run", json={"limit": 10})
response = self.client.get("/api/alerts/inbox?decision=REVIEW")
data = response.json()
if data["items"]:
item = data["items"][0]
assert "id" in item
assert "title" in item
assert "url" in item
assert "snippet" in item
assert "topic_label" in item
assert "relevance_score" in item
assert "relevance_decision" in item
assert "status" in item
class TestFeedbackEndpoint:
"""Tests für POST /alerts/feedback."""
def setup_method(self):
"""Setup für jeden Test."""
_alerts_store.clear()
_profile_store.clear()
self.client = TestClient(app)
def _create_alert(self):
"""Helfer: Erstelle Test-Alert."""
response = self.client.post("/api/alerts/ingest", json={
"title": "Test Alert",
"url": "https://example.com/test",
})
return response.json()["id"]
def test_feedback_positive(self):
"""Test positives Feedback."""
alert_id = self._create_alert()
response = self.client.post(
"/api/alerts/feedback",
json={
"alert_id": alert_id,
"is_relevant": True,
"reason": "Sehr relevant",
},
)
assert response.status_code == 200
data = response.json()
assert data["success"] is True
assert data["profile_updated"] is True
def test_feedback_negative(self):
"""Test negatives Feedback."""
alert_id = self._create_alert()
response = self.client.post(
"/api/alerts/feedback",
json={
"alert_id": alert_id,
"is_relevant": False,
"reason": "Werbung",
},
)
assert response.status_code == 200
assert response.json()["success"] is True
def test_feedback_updates_alert_status(self):
"""Test Feedback aktualisiert Alert-Status."""
alert_id = self._create_alert()
self.client.post("/api/alerts/feedback", json={
"alert_id": alert_id,
"is_relevant": True,
})
assert _alerts_store[alert_id].status == AlertStatus.REVIEWED
def test_feedback_updates_profile(self):
"""Test Feedback aktualisiert Profil."""
alert_id = self._create_alert()
# Positives Feedback
self.client.post("/api/alerts/feedback", json={
"alert_id": alert_id,
"is_relevant": True,
"reason": "Wichtig",
})
profile = _profile_store.get("default")
assert profile is not None
assert profile.total_kept == 1
assert len(profile.positive_examples) == 1
def test_feedback_not_found(self):
"""Test Feedback für nicht existierenden Alert."""
response = self.client.post(
"/api/alerts/feedback",
json={
"alert_id": "non-existent-id",
"is_relevant": True,
},
)
assert response.status_code == 404
def test_feedback_with_tags(self):
"""Test Feedback mit Tags."""
alert_id = self._create_alert()
response = self.client.post(
"/api/alerts/feedback",
json={
"alert_id": alert_id,
"is_relevant": True,
"tags": ["wichtig", "inklusion"],
},
)
assert response.status_code == 200
class TestProfileEndpoint:
"""Tests für GET/PUT /alerts/profile."""
def setup_method(self):
"""Setup für jeden Test."""
_alerts_store.clear()
_profile_store.clear()
self.client = TestClient(app)
def test_get_profile_default(self):
"""Test Default-Profil abrufen."""
response = self.client.get("/api/alerts/profile")
assert response.status_code == 200
data = response.json()
assert "id" in data
assert "priorities" in data
assert "exclusions" in data
assert len(data["priorities"]) > 0 # Default hat Prioritäten
def test_get_profile_creates_default(self):
"""Test Profil wird automatisch erstellt."""
assert "default" not in _profile_store
self.client.get("/api/alerts/profile")
assert "default" in _profile_store
def test_update_profile_priorities(self):
"""Test Prioritäten aktualisieren."""
response = self.client.put(
"/api/alerts/profile",
json={
"priorities": [
{"label": "Neue Priorität", "weight": 0.9},
{"label": "Zweite Priorität", "weight": 0.7},
],
},
)
assert response.status_code == 200
data = response.json()
assert len(data["priorities"]) == 2
assert data["priorities"][0]["label"] == "Neue Priorität"
def test_update_profile_exclusions(self):
"""Test Ausschlüsse aktualisieren."""
response = self.client.put(
"/api/alerts/profile",
json={
"exclusions": ["Spam", "Werbung", "Newsletter"],
},
)
assert response.status_code == 200
data = response.json()
assert "Spam" in data["exclusions"]
assert len(data["exclusions"]) == 3
def test_update_profile_policies(self):
"""Test Policies aktualisieren."""
response = self.client.put(
"/api/alerts/profile",
json={
"policies": {
"max_age_days": 14,
"prefer_german_sources": True,
},
},
)
assert response.status_code == 200
data = response.json()
assert data["policies"]["max_age_days"] == 14
def test_profile_stats(self):
"""Test Profil enthält Statistiken."""
response = self.client.get("/api/alerts/profile")
data = response.json()
assert "total_scored" in data
assert "total_kept" in data
assert "total_dropped" in data
class TestStatsEndpoint:
"""Tests für GET /alerts/stats."""
def setup_method(self):
"""Setup für jeden Test."""
_alerts_store.clear()
_profile_store.clear()
self.client = TestClient(app)
def test_stats_empty(self):
"""Test Stats ohne Alerts."""
response = self.client.get("/api/alerts/stats")
assert response.status_code == 200
data = response.json()
assert data["total_alerts"] == 0
def test_stats_with_alerts(self):
"""Test Stats mit Alerts."""
# Alerts erstellen und scoren
self.client.post("/api/alerts/ingest", json={
"title": "Inklusion",
"url": "https://example.com/1",
})
self.client.post("/api/alerts/ingest", json={
"title": "Stellenanzeige",
"url": "https://example.com/2",
})
self.client.post("/api/alerts/run", json={"limit": 10})
response = self.client.get("/api/alerts/stats")
data = response.json()
assert data["total_alerts"] == 2
assert "by_status" in data
assert "by_decision" in data
assert "scored" in data["by_status"]
def test_stats_avg_score(self):
"""Test Durchschnittlicher Score."""
self.client.post("/api/alerts/ingest", json={
"title": "Test",
"url": "https://example.com/1",
})
self.client.post("/api/alerts/run", json={"limit": 10})
response = self.client.get("/api/alerts/stats")
data = response.json()
assert "avg_score" in data
assert data["avg_score"] is not None