shinkan-jinkendo/backend/tests/test_profiles_read_access.py
Lars caab9f2863
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
feat: update application version to 0.8.35 and enhance profile access controls
- 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.
2026-05-05 22:57:42 +02:00

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
)