diff --git a/tests/test_api_meta.py b/tests/test_api_meta.py new file mode 100644 index 0000000..7f6eb24 --- /dev/null +++ b/tests/test_api_meta.py @@ -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"} diff --git a/tests/test_api_smoke.py b/tests/test_api_smoke.py new file mode 100644 index 0000000..68ab6e7 --- /dev/null +++ b/tests/test_api_smoke.py @@ -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