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

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