Some checks failed
Deploy Development / deploy (push) Successful in 34s
Test Suite / pytest-backend (push) Successful in 6s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 6s
Test Suite / playwright-tests (push) Failing after 34s
- Bumped application version to 0.8.35 in both backend and frontend files. - Updated profile retrieval and deletion endpoints to restrict access to the profile owner or platform admins, returning a 403 status for unauthorized access. - Added integration tests to verify access control for profile retrieval. - Enhanced changelog to reflect the new version and changes made in this release.
132 lines
4.6 KiB
Python
132 lines
4.6 KiB
Python
"""
|
|
Zugriffsrechte Profile: GET /api/profiles/{pid} und DELETE /api/profiles/{pid}.
|
|
|
|
TestClient + Dependency-Override für Sessions; GET mockt profile_document, DELETE mockt 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
|
|
)
|