"""Tests for AuthentikClient and the directory endpoint.""" from __future__ import annotations from pathlib import Path from unittest.mock import MagicMock, patch import pytest from cryptography.fernet import Fernet from fastapi.testclient import TestClient from media_library_viewer_api.clients.authentik import AuthentikClient from media_library_viewer_api.dependencies import get_settings_store from media_library_viewer_api.main import app from media_library_viewer_api.services.secrets import reset_encryption_key_cache from media_library_viewer_api.services.settings_store import SettingsStore TEST_KEY = Fernet.generate_key().decode() @pytest.fixture(autouse=True) def _encryption_key(monkeypatch: pytest.MonkeyPatch) -> None: """Provide a stable MANAGE_ENCRYPTION_KEY for every test.""" monkeypatch.setenv("MANAGE_ENCRYPTION_KEY", TEST_KEY) reset_encryption_key_cache() yield reset_encryption_key_cache() @pytest.fixture() def store(tmp_path: Path) -> SettingsStore: s = SettingsStore(tmp_path / "settings.sqlite") s.ensure_defaults() app.dependency_overrides[get_settings_store] = lambda: s yield s app.dependency_overrides.clear() # --------------------------------------------------------------------------- # Client unit tests # --------------------------------------------------------------------------- class TestAuthentikClient: def test_base_url_normalizes_trailing_slash(self) -> None: c = AuthentikClient(base_url="https://auth.example.com/", api_token="t") assert c.base_url == "https://auth.example.com" def test_base_url_strips_api_v3_suffix(self) -> None: c = AuthentikClient(base_url="https://auth.example.com/api/v3", api_token="t") assert c.base_url == "https://auth.example.com" def test_bearer_header_is_set(self) -> None: c = AuthentikClient(base_url="https://auth.example.com", api_token="tok") assert c.session.headers["Authorization"] == "Bearer tok" def test_empty_base_url_raises(self) -> None: with pytest.raises(ValueError): AuthentikClient(base_url="", api_token="t") def test_empty_api_token_raises(self) -> None: with pytest.raises(ValueError): AuthentikClient(base_url="https://auth.example.com", api_token="") @patch.object(AuthentikClient, "get") def test_users_normalizes_pagination(self, mock_get: MagicMock) -> None: mock_get.return_value = { "pagination": {"count": 42, "next": 2, "previous": 0, "current": 1}, "results": [ {"pk": 1, "username": "alice", "email": "alice@example.com"}, {"pk": 2, "username": "bob", "email": "bob@example.com"}, ], } client = AuthentikClient(base_url="https://auth.example.com", api_token="t") result = client.users(search="ali", page=1, page_size=2) assert result["total"] == 42 assert result["page"] == 1 assert result["page_size"] == 2 assert len(result["items"]) == 2 assert result["items"][0]["username"] == "alice" @patch.object(AuthentikClient, "get") def test_users_handles_empty_results(self, mock_get: MagicMock) -> None: mock_get.return_value = {"pagination": {"count": 0}, "results": []} client = AuthentikClient(base_url="https://auth.example.com", api_token="t") result = client.users() assert result["items"] == [] assert result["total"] == 0 @patch.object(AuthentikClient, "get") def test_users_handles_non_dict_payload(self, mock_get: MagicMock) -> None: mock_get.return_value = [] client = AuthentikClient(base_url="https://auth.example.com", api_token="t") result = client.users() assert result["items"] == [] assert result["total"] == 0 @patch("media_library_viewer_api.clients.authentik.requests.Session") def test_get_sends_correct_url_and_params(self, mock_session_cls: MagicMock) -> None: mock_session = MagicMock() mock_session_cls.return_value = mock_session mock_response = MagicMock() mock_response.json.return_value = {"results": []} mock_response.raise_for_status.return_value = None mock_session.get.return_value = mock_response c = AuthentikClient(base_url="https://auth.example.com", api_token="t") c.get("/core/users/", search="x", page=2) call_args = mock_session.get.call_args assert call_args.kwargs["params"] == {"search": "x", "page": 2} assert call_args.args[0] == "https://auth.example.com/api/v3/core/users/" # --------------------------------------------------------------------------- # Endpoint integration tests # --------------------------------------------------------------------------- class TestAuthentikUsersEndpoint: def test_not_configured_returns_empty_with_error(self, store: SettingsStore) -> None: client = TestClient(app) response = client.get("/api/services/authentik/nonexistent/users") assert response.status_code == 200 data = response.json() assert data["items"] == [] assert data["total"] == 0 assert "error" in data @patch("media_library_viewer_api.routers.authentik_users.AuthentikClient") def test_success_returns_users(self, mock_client_cls: MagicMock, store: SettingsStore) -> None: mock_client = MagicMock() mock_client.users.return_value = { "items": [{"pk": 1, "username": "alice"}], "total": 1, "page": 1, "page_size": 50, } mock_client_cls.return_value = mock_client created = store.upsert_service( { "service_type": "authentik", "name": "Main", "config": {"base_url": "https://auth.example.com"}, "enabled": True, }, secret_values={"api_token": "secret-token"}, ) service_id = created["id"] client = TestClient(app) response = client.get(f"/api/services/authentik/{service_id}/users?search=ali") assert response.status_code == 200 data = response.json() assert len(data["items"]) == 1 assert data["items"][0]["username"] == "alice" assert data["total"] == 1 assert "error" not in data @patch("media_library_viewer_api.routers.authentik_users.AuthentikClient") def test_unreachable_returns_error(self, mock_client_cls: MagicMock, store: SettingsStore) -> None: mock_client = MagicMock() mock_client.users.side_effect = ConnectionError("refused") mock_client_cls.return_value = mock_client created = store.upsert_service( { "service_type": "authentik", "name": "Main", "config": {"base_url": "https://auth.example.com"}, "enabled": True, }, secret_values={"api_token": "secret-token"}, ) service_id = created["id"] client = TestClient(app) response = client.get(f"/api/services/authentik/{service_id}/users") assert response.status_code == 200 data = response.json() assert data["items"] == [] assert "error" in data