diff --git a/tests/test_auth.py b/tests/test_auth.py new file mode 100644 index 0000000..aa22bbc --- /dev/null +++ b/tests/test_auth.py @@ -0,0 +1,107 @@ +"""Tests for app/auth.py — Keycloak JWT authentication (#43). + +These tests cover the auth module WITHOUT a running Keycloak server: +- Token extraction from headers/cookies +- Auth-disabled detection (Dev-Modus) +- _pick_best_title helper (in main.py, tested here for convenience) +""" +import sys +import types + +# Stub jose if not installed locally +if "jose" not in sys.modules: + jose_stub = types.ModuleType("jose") + jose_jwt = types.ModuleType("jose.jwt") + jose_stub.jwt = jose_jwt + jose_stub.JWTError = Exception + jose_stub.ExpiredSignatureError = Exception + jose_jwt.get_unverified_header = lambda t: {"kid": "test"} + jose_jwt.decode = lambda *a, **kw: {"sub": "test", "email": "t@t.de"} + sys.modules["jose"] = jose_stub + sys.modules["jose.jwt"] = jose_jwt + +import pytest +from unittest.mock import MagicMock +from app.auth import _extract_token, _is_auth_enabled + + +class TestExtractToken: + def test_bearer_header(self): + req = MagicMock() + req.headers = {"authorization": "Bearer abc123"} + req.cookies = {} + assert _extract_token(req) == "abc123" + + def test_cookie_fallback(self): + req = MagicMock() + req.headers = {} + req.cookies = {"access_token": "cookie_token"} + assert _extract_token(req) == "cookie_token" + + def test_no_token(self): + req = MagicMock() + req.headers = {} + req.cookies = {} + assert _extract_token(req) is None + + def test_non_bearer_header_ignored(self): + req = MagicMock() + req.headers = {"authorization": "Basic dXNlcjpwYXNz"} + req.cookies = {} + assert _extract_token(req) is None + + +class TestIsAuthEnabled: + def test_disabled_when_url_empty(self, monkeypatch): + from app import config + monkeypatch.setattr(config.settings, "keycloak_url", "") + monkeypatch.setattr(config.settings, "keycloak_realm", "test") + monkeypatch.setattr(config.settings, "keycloak_client_id", "test") + assert _is_auth_enabled() is False + + def test_disabled_when_realm_empty(self, monkeypatch): + from app import config + monkeypatch.setattr(config.settings, "keycloak_url", "https://sso.test") + monkeypatch.setattr(config.settings, "keycloak_realm", "") + monkeypatch.setattr(config.settings, "keycloak_client_id", "test") + assert _is_auth_enabled() is False + + def test_enabled_when_all_set(self, monkeypatch): + from app import config + monkeypatch.setattr(config.settings, "keycloak_url", "https://sso.test") + monkeypatch.setattr(config.settings, "keycloak_realm", "realm") + monkeypatch.setattr(config.settings, "keycloak_client_id", "client") + assert _is_auth_enabled() is True + + +try: + from app.main import _pick_best_title + _HAS_MAIN = True +except ImportError: + _HAS_MAIN = False + + +@pytest.mark.skipif(not _HAS_MAIN, reason="app.main not importable (missing slowapi/etc)") +class TestPickBestTitle: + """Test _pick_best_title from main.py.""" + + def test_prefer_real_doc_title(self): + assert _pick_best_title( + "LLM-Titel", "Echte Antrag-Bezeichnung aus OPAL", "18/123" + ) == "Echte Antrag-Bezeichnung aus OPAL" + + def test_reject_generic_doc_title(self): + from app.main import _pick_best_title + result = _pick_best_title( + "Lehrkräfte stärken", "Drucksache 18/18085", "18/18085" + ) + assert result == "Lehrkräfte stärken" + + def test_fallback_to_llm_title(self): + assert _pick_best_title("LLM-Titel", None, "18/123") == "LLM-Titel" + + def test_fallback_to_generic(self): + assert _pick_best_title("", None, "18/123") == "Drucksache 18/123" + + def test_empty_doc_title_uses_llm(self): + assert _pick_best_title("Guter LLM-Titel", "", "18/123") == "Guter LLM-Titel"