import unittest from types import SimpleNamespace from fastapi import HTTPException from app.services.project_service import ( normalize_project_role, require_project_read_access, require_project_roles, require_project_write_access, ) class _ScalarResult: def __init__(self, value): self.value = value def scalar_one_or_none(self): return self.value class _RecordingDB: def __init__(self, values): self.values = iter(values) async def execute(self, statement): return _ScalarResult(next(self.values)) class ProjectPermissionsTest(unittest.IsolatedAsyncioTestCase): def setUp(self): self.project = SimpleNamespace(id=25, owner_id=1) self.current_user = SimpleNamespace(id=2) def test_normalize_project_role_handles_legacy_case(self): self.assertEqual(normalize_project_role(" ADMIN "), "admin") self.assertEqual(normalize_project_role("Editor"), "editor") self.assertIsNone(normalize_project_role(None)) async def test_write_access_accepts_legacy_uppercase_admin(self): db = _RecordingDB([ self.project, SimpleNamespace(role="ADMIN"), ]) project, role = await require_project_write_access(db, 25, self.current_user) self.assertEqual(project, self.project) self.assertEqual(role, "admin") async def test_write_access_accepts_legacy_uppercase_editor(self): db = _RecordingDB([ self.project, SimpleNamespace(role="EDITOR"), ]) project, role = await require_project_write_access(db, 25, self.current_user) self.assertEqual(project, self.project) self.assertEqual(role, "editor") async def test_write_access_rejects_viewer(self): db = _RecordingDB([ self.project, SimpleNamespace(role="viewer"), ]) with self.assertRaises(HTTPException) as context: await require_project_write_access(db, 25, self.current_user) self.assertEqual(context.exception.status_code, 403) async def test_write_access_rejects_legacy_uppercase_viewer(self): """历史数据里 role 可能是大写 VIEWER,写权限校验必须先归一化再判断。""" db = _RecordingDB([ self.project, SimpleNamespace(role="VIEWER"), ]) with self.assertRaises(HTTPException) as context: await require_project_write_access(db, 25, self.current_user) self.assertEqual(context.exception.status_code, 403) async def test_read_access_returns_normalized_role(self): db = _RecordingDB([ self.project, SimpleNamespace(role="VIEWER"), ]) project, role = await require_project_read_access(db, 25, self.current_user) self.assertEqual(role, "viewer") async def test_admin_only_permission_rejects_editor(self): db = _RecordingDB([ self.project, SimpleNamespace(role="EDITOR"), ]) with self.assertRaises(HTTPException) as context: await require_project_roles( db, 25, self.current_user, allowed_roles=["admin"], ) self.assertEqual(context.exception.status_code, 403)