Some checks failed
Tests / Go Tests (push) Has been cancelled
Tests / Python Tests (push) Has been cancelled
Tests / Integration Tests (push) Has been cancelled
Tests / Go Lint (push) Has been cancelled
Tests / Python Lint (push) Has been cancelled
Tests / Security Scan (push) Has been cancelled
Tests / All Checks Passed (push) Has been cancelled
Security Scanning / Secret Scanning (push) Has been cancelled
Security Scanning / Dependency Vulnerability Scan (push) Has been cancelled
Security Scanning / Go Security Scan (push) Has been cancelled
Security Scanning / Python Security Scan (push) Has been cancelled
Security Scanning / Node.js Security Scan (push) Has been cancelled
Security Scanning / Docker Image Security (push) Has been cancelled
Security Scanning / Security Summary (push) Has been cancelled
CI/CD Pipeline / Go Tests (push) Has been cancelled
CI/CD Pipeline / Python Tests (push) Has been cancelled
CI/CD Pipeline / Website Tests (push) Has been cancelled
CI/CD Pipeline / Linting (push) Has been cancelled
CI/CD Pipeline / Security Scan (push) Has been cancelled
CI/CD Pipeline / Docker Build & Push (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Deploy to Staging (push) Has been cancelled
CI/CD Pipeline / Deploy to Production (push) Has been cancelled
CI/CD Pipeline / CI Summary (push) Has been cancelled
ci/woodpecker/manual/build-ci-image Pipeline was successful
ci/woodpecker/manual/main Pipeline failed
All services: admin-v2, studio-v2, website, ai-compliance-sdk, consent-service, klausur-service, voice-service, and infrastructure. Large PDFs and compiled binaries excluded via .gitignore.
595 lines
18 KiB
Python
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
|