""" Zugriffsrechte Profile: POST/GET/DELETE /api/profiles/{pid}. TestClient + Dependency-Override für Sessions; GET mockt profile_document, DELETE/POST mocken DB, damit keine echte Datenbank nötig ist. """ from __future__ import annotations import os import pytest from unittest.mock import MagicMock, patch from fastapi.testclient import TestClient os.environ.setdefault("SKIP_DB_MIGRATE", "1") from auth import require_auth from main import app @pytest.fixture def client() -> TestClient: return TestClient(app) @pytest.fixture(autouse=True) def _clear_auth_override(): yield app.dependency_overrides.pop(require_auth, None) def test_get_profile_forbidden_other_user(client: TestClient) -> None: def auth_viewer() -> dict: return {"profile_id": 42, "role": "trainer"} app.dependency_overrides[require_auth] = auth_viewer with patch("routers.profiles.profile_document") as pd_mock: r = client.get("/api/profiles/99", headers={"X-Auth-Token": "dummy"}) assert r.status_code == 403 assert pd_mock.call_count == 0 def test_get_profile_ok_same_numeric_id(client: TestClient) -> None: payload = {"id": 42, "name": "Self"} def auth_self() -> dict: return {"profile_id": 42, "role": "trainer"} app.dependency_overrides[require_auth] = auth_self with patch("routers.profiles.profile_document", return_value=payload) as pd_mock: r = client.get("/api/profiles/42", headers={"X-Auth-Token": "dummy"}) assert r.status_code == 200 assert r.json() == payload pd_mock.assert_called_once_with("42") def test_get_profile_ok_same_string_session_id(client: TestClient) -> None: """Session liefert profile_id als String (Backward-Compatibility).""" payload = {"id": 7, "name": "S"} def auth_self_str() -> dict: return {"profile_id": "7", "role": "user"} app.dependency_overrides[require_auth] = auth_self_str with patch("routers.profiles.profile_document", return_value=payload): r = client.get("/api/profiles/7", headers={"X-Auth-Token": "dummy"}) assert r.status_code == 200 @pytest.mark.parametrize("admin_role", ["admin", "superadmin"]) def test_get_profile_ok_platform_admin_views_other(client: TestClient, admin_role: str) -> None: payload = {"id": 99, "name": "Other"} def auth_admin() -> dict: return {"profile_id": 1, "role": admin_role} app.dependency_overrides[require_auth] = auth_admin with patch("routers.profiles.profile_document", return_value=payload) as pd_mock: r = client.get("/api/profiles/99", headers={"X-Auth-Token": "dummy"}) assert r.status_code == 200 assert r.json() == payload pd_mock.assert_called_once_with("99") def test_delete_profile_forbidden_non_admin(client: TestClient) -> None: def auth_trainer() -> dict: return {"profile_id": 1, "role": "trainer"} app.dependency_overrides[require_auth] = auth_trainer with patch("routers.profiles.get_db") as mock_get_db: r = client.delete("/api/profiles/1", headers={"X-Auth-Token": "dummy"}) assert r.status_code == 403 mock_get_db.assert_not_called() def test_delete_profile_admin_last_profile_protected(client: TestClient) -> None: mock_conn = MagicMock() mock_cm = MagicMock() mock_cm.__enter__.return_value = mock_conn mock_cm.__exit__.return_value = False mock_cur = MagicMock() mock_cur.fetchone.return_value = {"count": 1} app.dependency_overrides[require_auth] = lambda: {"profile_id": 99, "role": "admin"} with patch("routers.profiles.get_db", return_value=mock_cm), patch( "routers.profiles.get_cursor", return_value=mock_cur ): r = client.delete("/api/profiles/5", headers={"X-Auth-Token": "dummy"}) assert r.status_code == 400 def test_delete_profile_admin_ok_when_multiple_profiles(client: TestClient) -> None: mock_conn = MagicMock() mock_cm = MagicMock() mock_cm.__enter__.return_value = mock_conn mock_cm.__exit__.return_value = False mock_cur = MagicMock() mock_cur.fetchone.return_value = {"count": 4} app.dependency_overrides[require_auth] = lambda: {"profile_id": 99, "role": "superadmin"} with patch("routers.profiles.get_db", return_value=mock_cm), patch( "routers.profiles.get_cursor", return_value=mock_cur ): r = client.delete("/api/profiles/12", headers={"X-Auth-Token": "dummy"}) assert r.status_code == 200 assert r.json() == {"ok": True} calls = mock_cur.execute.call_args_list assert any( args and "DELETE FROM profiles" in str(args[0][0]) for args in calls ) def test_create_profile_forbidden_non_admin(client: TestClient) -> None: app.dependency_overrides[require_auth] = lambda: {"profile_id": 1, "role": "trainer"} with patch("routers.profiles.get_db") as mock_get_db: r = client.post( "/api/profiles", json={"name": "Neu", "email": "neu@example.com"}, headers={"X-Auth-Token": "dummy"}, ) assert r.status_code == 403 mock_get_db.assert_not_called() def test_create_profile_admin_duplicate_email(client: TestClient) -> None: mock_cm = MagicMock() mock_conn = MagicMock() mock_cm.__enter__.return_value = mock_conn mock_cm.__exit__.return_value = False mock_cur = MagicMock() mock_cur.fetchone.return_value = {"id": 99} app.dependency_overrides[require_auth] = lambda: {"profile_id": 1, "role": "admin"} with patch("routers.profiles.get_db", return_value=mock_cm), patch( "routers.profiles.get_cursor", return_value=mock_cur ), patch("routers.profiles.hash_pin", return_value="hashed"): r = client.post( "/api/profiles", json={"name": "Dup", "email": "dup@example.com"}, headers={"X-Auth-Token": "dummy"}, ) assert r.status_code == 409 def test_create_profile_admin_success(client: TestClient) -> None: mock_cm = MagicMock() mock_conn = MagicMock() mock_cm.__enter__.return_value = mock_conn mock_cm.__exit__.return_value = False mock_cur = MagicMock() mock_cur.fetchone.side_effect = [None, {"id": 501}] doc = {"id": 501, "name": "Nu User", "email": "nu@example.com"} app.dependency_overrides[require_auth] = lambda: {"profile_id": 1, "role": "superadmin"} with patch("routers.profiles.get_db", return_value=mock_cm), patch( "routers.profiles.get_cursor", return_value=mock_cur ), patch("routers.profiles.profile_document", return_value=doc), patch( "routers.profiles.hash_pin", return_value="hashed" ): r = client.post( "/api/profiles", json={"name": "Nu User", "email": "nu@example.com"}, headers={"X-Auth-Token": "dummy"}, ) assert r.status_code == 200 assert r.json() == doc insert_calls = [ c.args[0] for c in mock_cur.execute.call_args_list if c.args and "INSERT INTO profiles" in str(c.args[0]) ] assert len(insert_calls) >= 1