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>
436 lines
14 KiB
Python
436 lines
14 KiB
Python
"""
|
|
Tests für Alerts Agent Topics API.
|
|
|
|
Testet CRUD-Operationen für Topics über die REST-API.
|
|
"""
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
from unittest.mock import patch, AsyncMock
|
|
|
|
from sqlalchemy import create_engine
|
|
from sqlalchemy.orm import sessionmaker
|
|
from sqlalchemy.pool import StaticPool
|
|
|
|
# WICHTIG: Models ZUERST importieren damit sie bei Base registriert werden
|
|
from alerts_agent.db.models import (
|
|
AlertTopicDB, AlertItemDB, AlertRuleDB, AlertProfileDB,
|
|
)
|
|
# Dann Base importieren (hat jetzt die Models in metadata)
|
|
from classroom_engine.database import Base
|
|
from alerts_agent.db import get_db
|
|
from alerts_agent.api.topics import router
|
|
|
|
|
|
# Test-Client Setup
|
|
from fastapi import FastAPI
|
|
|
|
app = FastAPI()
|
|
app.include_router(router, prefix="/api/alerts")
|
|
|
|
|
|
@pytest.fixture(scope="function")
|
|
def db_engine():
|
|
"""Erstellt eine In-Memory SQLite-Engine mit Threading-Support."""
|
|
# StaticPool stellt sicher, dass alle Connections die gleiche DB nutzen
|
|
# check_same_thread=False erlaubt Cross-Thread-Zugriff (für TestClient)
|
|
engine = create_engine(
|
|
"sqlite:///:memory:",
|
|
echo=False,
|
|
connect_args={"check_same_thread": False},
|
|
poolclass=StaticPool,
|
|
)
|
|
# Alle Tables erstellen
|
|
Base.metadata.create_all(engine)
|
|
yield engine
|
|
engine.dispose()
|
|
|
|
|
|
@pytest.fixture(scope="function")
|
|
def db_session(db_engine):
|
|
"""Erstellt eine Session für Tests."""
|
|
SessionLocal = sessionmaker(bind=db_engine)
|
|
session = SessionLocal()
|
|
yield session
|
|
session.rollback()
|
|
session.close()
|
|
|
|
|
|
@pytest.fixture(scope="function")
|
|
def client(db_session):
|
|
"""Erstellt einen Test-Client mit überschriebener DB-Dependency."""
|
|
def override_get_db():
|
|
try:
|
|
yield db_session
|
|
finally:
|
|
db_session.rollback()
|
|
|
|
app.dependency_overrides[get_db] = override_get_db
|
|
with TestClient(app) as test_client:
|
|
yield test_client
|
|
app.dependency_overrides.clear()
|
|
|
|
|
|
# =============================================================================
|
|
# CREATE TESTS
|
|
# =============================================================================
|
|
|
|
class TestCreateTopic:
|
|
"""Tests für POST /api/alerts/topics"""
|
|
|
|
def test_create_topic_minimal(self, client):
|
|
"""Test: Topic mit minimalen Daten erstellen."""
|
|
response = client.post(
|
|
"/api/alerts/topics",
|
|
json={"name": "Test Topic"},
|
|
)
|
|
|
|
assert response.status_code == 201
|
|
data = response.json()
|
|
assert data["name"] == "Test Topic"
|
|
assert data["is_active"] is True
|
|
assert data["feed_type"] == "rss"
|
|
assert data["fetch_interval_minutes"] == 60
|
|
|
|
def test_create_topic_full(self, client):
|
|
"""Test: Topic mit allen Feldern erstellen."""
|
|
response = client.post(
|
|
"/api/alerts/topics",
|
|
json={
|
|
"name": "Vollständiges Topic",
|
|
"description": "Eine Beschreibung",
|
|
"feed_url": "https://example.com/feed.rss",
|
|
"feed_type": "rss",
|
|
"fetch_interval_minutes": 30,
|
|
"is_active": False,
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 201
|
|
data = response.json()
|
|
assert data["name"] == "Vollständiges Topic"
|
|
assert data["description"] == "Eine Beschreibung"
|
|
assert data["feed_url"] == "https://example.com/feed.rss"
|
|
assert data["fetch_interval_minutes"] == 30
|
|
assert data["is_active"] is False
|
|
|
|
def test_create_topic_empty_name_fails(self, client):
|
|
"""Test: Leerer Name führt zu Fehler."""
|
|
response = client.post(
|
|
"/api/alerts/topics",
|
|
json={"name": ""},
|
|
)
|
|
|
|
assert response.status_code == 422 # Validation Error
|
|
|
|
def test_create_topic_invalid_interval(self, client):
|
|
"""Test: Ungültiges Fetch-Intervall führt zu Fehler."""
|
|
response = client.post(
|
|
"/api/alerts/topics",
|
|
json={
|
|
"name": "Test",
|
|
"fetch_interval_minutes": 1, # < 5 ist ungültig
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 422
|
|
|
|
|
|
# =============================================================================
|
|
# READ TESTS
|
|
# =============================================================================
|
|
|
|
class TestReadTopics:
|
|
"""Tests für GET /api/alerts/topics"""
|
|
|
|
def test_list_topics_empty(self, client):
|
|
"""Test: Leere Topic-Liste."""
|
|
response = client.get("/api/alerts/topics")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["topics"] == []
|
|
assert data["total"] == 0
|
|
|
|
def test_list_topics(self, client):
|
|
"""Test: Topics auflisten."""
|
|
# Erstelle Topics
|
|
client.post("/api/alerts/topics", json={"name": "Topic 1"})
|
|
client.post("/api/alerts/topics", json={"name": "Topic 2"})
|
|
client.post("/api/alerts/topics", json={"name": "Topic 3"})
|
|
|
|
response = client.get("/api/alerts/topics")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["total"] == 3
|
|
assert len(data["topics"]) == 3
|
|
|
|
def test_list_topics_filter_active(self, client):
|
|
"""Test: Nur aktive Topics auflisten."""
|
|
# Erstelle aktives und inaktives Topic
|
|
client.post("/api/alerts/topics", json={"name": "Aktiv", "is_active": True})
|
|
client.post("/api/alerts/topics", json={"name": "Inaktiv", "is_active": False})
|
|
|
|
response = client.get("/api/alerts/topics?is_active=true")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["total"] == 1
|
|
assert data["topics"][0]["name"] == "Aktiv"
|
|
|
|
def test_get_topic_by_id(self, client):
|
|
"""Test: Topic nach ID abrufen."""
|
|
# Erstelle Topic
|
|
create_response = client.post(
|
|
"/api/alerts/topics",
|
|
json={"name": "Find Me"},
|
|
)
|
|
topic_id = create_response.json()["id"]
|
|
|
|
# Abrufen
|
|
response = client.get(f"/api/alerts/topics/{topic_id}")
|
|
|
|
assert response.status_code == 200
|
|
assert response.json()["name"] == "Find Me"
|
|
|
|
def test_get_topic_not_found(self, client):
|
|
"""Test: Topic nicht gefunden."""
|
|
response = client.get("/api/alerts/topics/nonexistent-id")
|
|
|
|
assert response.status_code == 404
|
|
|
|
|
|
# =============================================================================
|
|
# UPDATE TESTS
|
|
# =============================================================================
|
|
|
|
class TestUpdateTopic:
|
|
"""Tests für PUT /api/alerts/topics/{id}"""
|
|
|
|
def test_update_topic_name(self, client):
|
|
"""Test: Topic-Namen aktualisieren."""
|
|
# Erstelle Topic
|
|
create_response = client.post(
|
|
"/api/alerts/topics",
|
|
json={"name": "Original"},
|
|
)
|
|
topic_id = create_response.json()["id"]
|
|
|
|
# Update
|
|
response = client.put(
|
|
f"/api/alerts/topics/{topic_id}",
|
|
json={"name": "Updated"},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
assert response.json()["name"] == "Updated"
|
|
|
|
def test_update_topic_partial(self, client):
|
|
"""Test: Partielles Update."""
|
|
# Erstelle Topic
|
|
create_response = client.post(
|
|
"/api/alerts/topics",
|
|
json={
|
|
"name": "Original",
|
|
"description": "Desc",
|
|
"is_active": True,
|
|
},
|
|
)
|
|
topic_id = create_response.json()["id"]
|
|
|
|
# Nur is_active ändern
|
|
response = client.put(
|
|
f"/api/alerts/topics/{topic_id}",
|
|
json={"is_active": False},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["name"] == "Original" # Unverändert
|
|
assert data["description"] == "Desc" # Unverändert
|
|
assert data["is_active"] is False # Geändert
|
|
|
|
def test_update_topic_not_found(self, client):
|
|
"""Test: Update für nicht existierendes Topic."""
|
|
response = client.put(
|
|
"/api/alerts/topics/nonexistent-id",
|
|
json={"name": "New Name"},
|
|
)
|
|
|
|
assert response.status_code == 404
|
|
|
|
def test_update_topic_empty_fails(self, client):
|
|
"""Test: Update ohne Änderungen führt zu Fehler."""
|
|
# Erstelle Topic
|
|
create_response = client.post(
|
|
"/api/alerts/topics",
|
|
json={"name": "Test"},
|
|
)
|
|
topic_id = create_response.json()["id"]
|
|
|
|
# Leeres Update
|
|
response = client.put(
|
|
f"/api/alerts/topics/{topic_id}",
|
|
json={},
|
|
)
|
|
|
|
assert response.status_code == 400
|
|
|
|
|
|
# =============================================================================
|
|
# DELETE TESTS
|
|
# =============================================================================
|
|
|
|
class TestDeleteTopic:
|
|
"""Tests für DELETE /api/alerts/topics/{id}"""
|
|
|
|
def test_delete_topic(self, client):
|
|
"""Test: Topic löschen."""
|
|
# Erstelle Topic
|
|
create_response = client.post(
|
|
"/api/alerts/topics",
|
|
json={"name": "To Delete"},
|
|
)
|
|
topic_id = create_response.json()["id"]
|
|
|
|
# Löschen
|
|
response = client.delete(f"/api/alerts/topics/{topic_id}")
|
|
assert response.status_code == 204
|
|
|
|
# Prüfen, dass es weg ist
|
|
get_response = client.get(f"/api/alerts/topics/{topic_id}")
|
|
assert get_response.status_code == 404
|
|
|
|
def test_delete_topic_not_found(self, client):
|
|
"""Test: Löschen eines nicht existierenden Topics."""
|
|
response = client.delete("/api/alerts/topics/nonexistent-id")
|
|
|
|
assert response.status_code == 404
|
|
|
|
|
|
# =============================================================================
|
|
# STATS TESTS
|
|
# =============================================================================
|
|
|
|
class TestTopicStats:
|
|
"""Tests für GET /api/alerts/topics/{id}/stats"""
|
|
|
|
def test_get_topic_stats(self, client):
|
|
"""Test: Topic-Statistiken abrufen."""
|
|
# Erstelle Topic
|
|
create_response = client.post(
|
|
"/api/alerts/topics",
|
|
json={"name": "Stats Test"},
|
|
)
|
|
topic_id = create_response.json()["id"]
|
|
|
|
# Stats abrufen
|
|
response = client.get(f"/api/alerts/topics/{topic_id}/stats")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["topic_id"] == topic_id
|
|
assert data["name"] == "Stats Test"
|
|
assert data["total_alerts"] == 0
|
|
|
|
def test_get_stats_not_found(self, client):
|
|
"""Test: Stats für nicht existierendes Topic."""
|
|
response = client.get("/api/alerts/topics/nonexistent-id/stats")
|
|
|
|
assert response.status_code == 404
|
|
|
|
|
|
# =============================================================================
|
|
# ACTIVATION TESTS
|
|
# =============================================================================
|
|
|
|
class TestTopicActivation:
|
|
"""Tests für Topic-Aktivierung/-Deaktivierung"""
|
|
|
|
def test_activate_topic(self, client):
|
|
"""Test: Topic aktivieren."""
|
|
# Erstelle inaktives Topic
|
|
create_response = client.post(
|
|
"/api/alerts/topics",
|
|
json={"name": "Inactive", "is_active": False},
|
|
)
|
|
topic_id = create_response.json()["id"]
|
|
|
|
# Aktivieren
|
|
response = client.post(f"/api/alerts/topics/{topic_id}/activate")
|
|
|
|
assert response.status_code == 200
|
|
assert response.json()["is_active"] is True
|
|
|
|
def test_deactivate_topic(self, client):
|
|
"""Test: Topic deaktivieren."""
|
|
# Erstelle aktives Topic
|
|
create_response = client.post(
|
|
"/api/alerts/topics",
|
|
json={"name": "Active", "is_active": True},
|
|
)
|
|
topic_id = create_response.json()["id"]
|
|
|
|
# Deaktivieren
|
|
response = client.post(f"/api/alerts/topics/{topic_id}/deactivate")
|
|
|
|
assert response.status_code == 200
|
|
assert response.json()["is_active"] is False
|
|
|
|
|
|
# =============================================================================
|
|
# FETCH TESTS
|
|
# =============================================================================
|
|
|
|
class TestTopicFetch:
|
|
"""Tests für POST /api/alerts/topics/{id}/fetch"""
|
|
|
|
def test_fetch_topic_no_url(self, client):
|
|
"""Test: Fetch ohne Feed-URL führt zu Fehler."""
|
|
# Erstelle Topic ohne URL
|
|
create_response = client.post(
|
|
"/api/alerts/topics",
|
|
json={"name": "No URL"},
|
|
)
|
|
topic_id = create_response.json()["id"]
|
|
|
|
# Fetch versuchen
|
|
response = client.post(f"/api/alerts/topics/{topic_id}/fetch")
|
|
|
|
assert response.status_code == 400
|
|
assert "Feed-URL" in response.json()["detail"]
|
|
|
|
def test_fetch_topic_not_found(self, client):
|
|
"""Test: Fetch für nicht existierendes Topic."""
|
|
response = client.post("/api/alerts/topics/nonexistent-id/fetch")
|
|
|
|
assert response.status_code == 404
|
|
|
|
@patch("alerts_agent.ingestion.rss_fetcher.fetch_and_store_feed", new_callable=AsyncMock)
|
|
def test_fetch_topic_success(self, mock_fetch, client):
|
|
"""Test: Erfolgreiches Fetchen."""
|
|
# Mock Setup - async function braucht AsyncMock
|
|
mock_fetch.return_value = {
|
|
"new_items": 5,
|
|
"duplicates_skipped": 2,
|
|
}
|
|
|
|
# Erstelle Topic mit URL
|
|
create_response = client.post(
|
|
"/api/alerts/topics",
|
|
json={
|
|
"name": "With URL",
|
|
"feed_url": "https://example.com/feed.rss",
|
|
},
|
|
)
|
|
topic_id = create_response.json()["id"]
|
|
|
|
# Fetch ausführen
|
|
response = client.post(f"/api/alerts/topics/{topic_id}/fetch")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["success"] is True
|
|
assert data["new_items"] == 5
|
|
assert data["duplicates_skipped"] == 2
|