统一项目权限读取模型
1、合并列表与详情读取权限,统一使用 subjects:read、project_members:read、monitoring_issues:read 等业务读取 key。 2、增加历史读取权限 key 的兼容映射,确保存量项目权限覆盖值不会丢失。 3、同步前端权限工具、路由权限与权限管理页面,避免继续引用已合并的旧权限 key。 4、补充项目角色启停保护与抽屉未保存变更守卫,防止停用 PM 或仍有关联成员的角色。
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
import uuid
|
||||
from dataclasses import dataclass
|
||||
|
||||
import pytest
|
||||
from fastapi import HTTPException
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.api.v1.study_active_roles import _normalize_active_roles, update_active_roles
|
||||
|
||||
|
||||
@dataclass
|
||||
class UserStub:
|
||||
id: uuid.UUID
|
||||
is_admin: bool = False
|
||||
|
||||
|
||||
def test_normalize_active_roles_allows_renamed_qa_project_role():
|
||||
assert _normalize_active_roles(["PM", "QA", "CTA", "QA", "", None]) == ["PM", "QA", "CTA"]
|
||||
|
||||
|
||||
def test_normalize_active_roles_still_rejects_system_admin_role():
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
_normalize_active_roles(["ADMIN"])
|
||||
|
||||
assert exc_info.value.status_code == 422
|
||||
assert exc_info.value.detail == "ADMIN 不能作为项目角色"
|
||||
|
||||
|
||||
def test_project_pm_update_keeps_pm_active_role():
|
||||
assert _normalize_active_roles(["PV", "CRA"], preserve_pm=True) == ["PM", "PV", "CRA"]
|
||||
|
||||
|
||||
def test_system_admin_update_can_remove_pm_active_role():
|
||||
assert _normalize_active_roles(["PV", "CRA"], preserve_pm=False) == ["PV", "CRA"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_project_pm_cannot_remove_pm_from_active_roles(db_session: AsyncSession):
|
||||
user_id = uuid.uuid4()
|
||||
study_id = uuid.uuid4()
|
||||
await db_session.execute(
|
||||
text(
|
||||
"""
|
||||
INSERT INTO studies (id, code, name, status, is_locked, visit_schedule, active_roles)
|
||||
VALUES (:id, :code, :name, :status, :is_locked, :visit_schedule, :active_roles)
|
||||
"""
|
||||
),
|
||||
{
|
||||
"id": str(study_id),
|
||||
"code": f"PROJECT-PM-ACTIVE-ROLE-{study_id.hex[:8]}",
|
||||
"name": "PROJECT-PM-ACTIVE-ROLE",
|
||||
"status": "ACTIVE",
|
||||
"is_locked": False,
|
||||
"visit_schedule": "[]",
|
||||
"active_roles": '["PM","PV","CRA"]',
|
||||
},
|
||||
)
|
||||
await db_session.commit()
|
||||
|
||||
result = await update_active_roles(
|
||||
study_id=study_id,
|
||||
payload={"active_roles": ["PV", "CRA"]},
|
||||
current_user=UserStub(id=user_id),
|
||||
db=db_session,
|
||||
)
|
||||
|
||||
assert result["active_roles"] == ["PM", "PV", "CRA"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cannot_remove_active_role_assigned_to_active_member(db_session: AsyncSession):
|
||||
user_id = uuid.uuid4()
|
||||
member_id = uuid.uuid4()
|
||||
study_id = uuid.uuid4()
|
||||
await db_session.execute(
|
||||
text(
|
||||
"""
|
||||
INSERT INTO studies (id, code, name, status, is_locked, visit_schedule, active_roles)
|
||||
VALUES (:id, :code, :name, :status, :is_locked, :visit_schedule, :active_roles)
|
||||
"""
|
||||
),
|
||||
{
|
||||
"id": str(study_id),
|
||||
"code": f"PROJECT-ROLE-IN-USE-{study_id.hex[:8]}",
|
||||
"name": "PROJECT-ROLE-IN-USE",
|
||||
"status": "ACTIVE",
|
||||
"is_locked": False,
|
||||
"visit_schedule": "[]",
|
||||
"active_roles": '["PM","PV","CRA"]',
|
||||
},
|
||||
)
|
||||
await db_session.execute(
|
||||
text(
|
||||
"""
|
||||
INSERT INTO users (id, email, password_hash, full_name, clinical_department, is_admin, status)
|
||||
VALUES (:id, :email, :password_hash, :full_name, :clinical_department, :is_admin, :status)
|
||||
"""
|
||||
),
|
||||
{
|
||||
"id": str(user_id),
|
||||
"email": f"{user_id.hex}@example.com",
|
||||
"password_hash": "hash",
|
||||
"full_name": "PV User",
|
||||
"clinical_department": "Clinical",
|
||||
"is_admin": False,
|
||||
"status": "ACTIVE",
|
||||
},
|
||||
)
|
||||
await db_session.execute(
|
||||
text(
|
||||
"""
|
||||
INSERT INTO study_members (id, study_id, user_id, role_in_study, is_active)
|
||||
VALUES (:id, :study_id, :user_id, :role, :active)
|
||||
"""
|
||||
),
|
||||
{
|
||||
"id": str(member_id),
|
||||
"study_id": str(study_id),
|
||||
"user_id": str(user_id),
|
||||
"role": "PV",
|
||||
"active": True,
|
||||
},
|
||||
)
|
||||
await db_session.commit()
|
||||
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
await update_active_roles(
|
||||
study_id=study_id,
|
||||
payload={"active_roles": ["CRA"]},
|
||||
current_user=UserStub(id=uuid.uuid4(), is_admin=True),
|
||||
db=db_session,
|
||||
)
|
||||
|
||||
assert exc_info.value.status_code == 409
|
||||
assert exc_info.value.detail == "角色 PV 仍有成员使用,不能停用"
|
||||
Reference in New Issue
Block a user