""" 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