Phase 14a: Integration-Tests (FastAPI TestClient, ohne DB)

tests/test_api_smoke.py:
  - 43 parametrisierte Auth-Coverage-Tests: jeder geschuetzte Endpoint
    muss ohne Authorization-Header 401 oder 403 liefern (nicht 200, nicht 500).
    Verhindert, dass jemand versehentlich einen Endpoint ohne
    get_current_admin schreibt.
  - 2 Tests fuer oeffentliche Auth-Endpoints (/magic-link, /verify):
    pruefen nur, dass NICHT 401/403 zurueckkommt.
  - 2 Static-Route-Tests (/, /dashboard) muessen 200 liefern.
  - TestClient(raise_server_exceptions=False) damit DB-Probleme nicht zu
    Test-Aborts werden.

tests/test_api_meta.py:
  - Integration-Tests fuer /api/sources/meta mit dependency_overrides
    (Mock get_current_admin). DB-frei, deshalb echte Endpoint-Logik
    vollstaendig durchgetestet.
  - 5 Tests: Schema vorhanden, Pflichtfelder, spezielle Lagen-Themen,
    alle 5 source-types.

Insgesamt: 80 Tests, 0.63s. Aufruf:
  PYTHONPATH=src ./venv/bin/python -m pytest tests/ -v

Phase 14b (echtes DB-Schema-Setup mit aiosqlite-In-Memory) folgt separat,
braucht Schema-Bootstrap - viel groesserer Aufwand fuer CRUD-Tests.
Dieser Commit ist enthalten in:
claude-dev
2026-05-09 04:25:39 +00:00
Ursprung 9d16aba5f9
Commit ff83f64aa6
2 geänderte Dateien mit 162 neuen und 0 gelöschten Zeilen

65
tests/test_api_meta.py Normale Datei
Datei anzeigen

@@ -0,0 +1,65 @@
"""Integration-Tests fuer DB-freie Endpoints mit Mock-Auth.
GET /api/sources/meta liefert die Single-Source-of-Truth Kategorien/Typen
und braucht keine DB. Mit override des get_current_admin Dependency
testen wir den Endpoint richtig durch (echtes JSON, echtes Schema).
"""
import pytest
from fastapi.testclient import TestClient
@pytest.fixture
def authed_client():
from main import app
from auth import get_current_admin
def fake_admin():
return {"id": 1, "email": "test@aegis-sight.de", "username": "test"}
app.dependency_overrides[get_current_admin] = fake_admin
yield TestClient(app)
app.dependency_overrides = {}
def test_meta_returns_schema(authed_client):
r = authed_client.get("/api/sources/meta")
assert r.status_code == 200
data = r.json()
assert "categories" in data
assert "types" in data
assert isinstance(data["categories"], list)
assert isinstance(data["types"], list)
def test_meta_categories_have_required_fields(authed_client):
r = authed_client.get("/api/sources/meta")
data = r.json()
for cat in data["categories"]:
assert "key" in cat
assert "label" in cat
assert isinstance(cat["key"], str) and cat["key"]
assert isinstance(cat["label"], str) and cat["label"]
def test_meta_types_have_required_fields(authed_client):
r = authed_client.get("/api/sources/meta")
data = r.json()
for t in data["types"]:
assert "key" in t
assert "label" in t
def test_meta_includes_specialized_categories(authed_client):
"""Phase 3b - die spezielleren Lagen-Themen muessen als Kategorien existieren."""
r = authed_client.get("/api/sources/meta")
keys = {c["key"] for c in r.json()["categories"]}
assert "cybercrime" in keys
assert "ukraine-russland-krieg" in keys
assert "russische-staatspropaganda" in keys
def test_meta_includes_all_source_types(authed_client):
"""Alle 5 Source-Types muessen rauskommen."""
r = authed_client.get("/api/sources/meta")
keys = {t["key"] for t in r.json()["types"]}
assert keys == {"rss_feed", "web_source", "telegram_channel", "podcast_feed", "excluded"}

97
tests/test_api_smoke.py Normale Datei
Datei anzeigen

@@ -0,0 +1,97 @@
"""Smoke-Tests fuer alle API-Endpoints: Auth-Coverage.
Pruef, dass jeder geschuetzte Endpoint ohne Auth-Header 401/403 liefert -
verhindert, dass jemand versehentlich einen Endpoint ohne `get_current_admin`
schreibt und ihn oeffentlich macht.
"""
import pytest
from fastapi.testclient import TestClient
@pytest.fixture(scope="module")
def client():
from main import app
# raise_server_exceptions=False -> Exceptions werden als 500 ausgeliefert.
# Wir testen nur, dass Auth korrekt vor DB-Aufrufen greift.
return TestClient(app, raise_server_exceptions=False)
# (method, path, expected_status)
# Auth-geschuetzte Endpoints -> 401 (HTTPBearer ohne credentials wirft 403,
# aber FastAPI HTTPBearer auto_error=True liefert 403; wir akzeptieren beides).
AUTH_PROTECTED = [
("GET", "/api/orgs"),
("POST", "/api/orgs"),
("GET", "/api/orgs/1"),
("PUT", "/api/orgs/1"),
("DELETE", "/api/orgs/1"),
("GET", "/api/licenses"),
("POST", "/api/licenses"),
("PUT", "/api/licenses/1/revoke"),
("PUT", "/api/licenses/1/extend"),
("GET", "/api/licenses/expiring"),
("GET", "/api/users"),
("POST", "/api/users"),
("PUT", "/api/users/1/deactivate"),
("PUT", "/api/users/1/activate"),
("PUT", "/api/users/1/globe-access"),
("PUT", "/api/users/1/network-access"),
("PUT", "/api/users/1/role"),
("DELETE", "/api/users/1"),
("GET", "/api/dashboard/stats"),
("GET", "/api/sources/meta"),
("GET", "/api/sources/global"),
("POST", "/api/sources/global"),
("PUT", "/api/sources/global/1"),
("DELETE", "/api/sources/global/1"),
("GET", "/api/sources/global/stats"),
("GET", "/api/sources/tenant"),
("POST", "/api/sources/tenant/1/promote"),
("POST", "/api/sources/tenant/bulk-promote"),
("POST", "/api/sources/discover"),
("POST", "/api/sources/discover/add"),
("GET", "/api/sources/health"),
("GET", "/api/sources/suggestions"),
("PUT", "/api/sources/suggestions/1"),
("POST", "/api/sources/health/run"),
("POST", "/api/sources/health/run-stream"),
("POST", "/api/sources/health/search-fix/1"),
("GET", "/api/token-usage/overview"),
("GET", "/api/token-usage/1"),
("GET", "/api/token-usage/1/current"),
("PUT", "/api/token-usage/budget/1"),
("GET", "/api/audit-log"),
("GET", "/api/audit-log/distinct"),
]
@pytest.mark.parametrize("method,path", AUTH_PROTECTED)
def test_endpoint_requires_auth(client, method, path):
"""Ohne Authorization-Header muss jeder Endpoint 401 oder 403 liefern."""
r = client.request(method, path, json={})
assert r.status_code in (401, 403), (
f"{method} {path} sollte 401/403 sein, war {r.status_code}: {r.text[:200]}"
)
def test_magic_link_endpoint_is_public(client):
"""/api/auth/magic-link ist absichtlich oeffentlich (sonst kann sich keiner einloggen)."""
r = client.post("/api/auth/magic-link", json={"email": "stranger@example.com"})
# Mit gueltigem JSON -> 200 generische Antwort, ohne Auth-Header.
# 200 erwartet (Anti-Enumeration), aber DB-Aufruf koennte mit /tmp/x.db failen ->
# akzeptieren wir auch 500. Wir wollen nur sicherstellen, dass NICHT 401/403 kommt.
assert r.status_code != 401 and r.status_code != 403
def test_verify_endpoint_is_public(client):
"""/api/auth/verify ist auch oeffentlich (ohne Token koennen wir keinen JWT haben)."""
r = client.post("/api/auth/verify", json={"token": "x" * 20})
assert r.status_code != 401 and r.status_code != 403
def test_static_routes_public(client):
"""/ und /dashboard liefern HTML ohne Auth (Frontend regelt Login-Redirect)."""
r = client.get("/")
assert r.status_code == 200
r = client.get("/dashboard")
assert r.status_code == 200