Files
ctms/backend/tests/test_project_management_permissions_static.py
T
Cheng Zhou a17f4cc522 完善项目级权限与项目管理入口
- 新增项目成员候选用户接口,避免 PM 读取全局用户列表
- 按项目角色和权限矩阵控制项目管理、成员、中心与审计入口
- 合并侧边栏管理后台入口,统一从项目管理进入成员等模块
- 加强成员管理安全约束,禁止 PM 修改自己、系统管理员或更高权限角色
- 修复多项目角色缓存串用问题,并补充前后端回归测试
2026-05-13 14:16:15 +08:00

167 lines
7.2 KiB
Python

from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
def test_project_setup_routes_keep_project_admin_and_pm_defaults():
source = (ROOT / "app/api/v1/studies.py").read_text(encoding="utf-8")
assert 'require_study_roles(["PM"])' not in source
assert 'require_study_roles(["ADMIN", "PM"])' not in source
assert 'require_roles(["ADMIN"])' in source
def test_project_permission_routes_are_registered():
source = (ROOT / "app/api/v1/router.py").read_text(encoding="utf-8")
assert "project_permissions" in source
assert 'prefix="/studies/{study_id}/permissions"' in source
def test_project_permission_updates_use_project_member_write_permission():
source = (ROOT / "app/api/v1/project_permissions.py").read_text(encoding="utf-8")
assert 'require_study_permission("project_members", "write")' in source
assert 'require_roles(["ADMIN"])' not in source
def test_project_member_writes_use_saved_permission_matrix():
source = (ROOT / "app/api/v1/members.py").read_text(encoding="utf-8")
assert 'require_study_permission("project_members", "write")' in source
assert 'require_study_roles(["ADMIN", "PM"])' not in source
def test_project_member_candidates_use_project_write_permission():
source = (ROOT / "app/api/v1/members.py").read_text(encoding="utf-8")
user_crud_source = (ROOT / "app/crud/user.py").read_text(encoding="utf-8")
assert '"/candidates"' in source
assert 'response_model=list[UserResponse]' in source
assert 'dependencies=[Depends(require_study_permission("project_members", "write"))]' in source
assert "list_active_member_candidates_for_study" in source
assert "User.status == UserStatus.ACTIVE" in user_crud_source
assert ".where(~existing_member)" in user_crud_source
def test_project_member_mutation_blocks_self_and_higher_role_changes():
source = (ROOT / "app/api/v1/members.py").read_text(encoding="utf-8")
assert "PROJECT_ROLE_RANK" in source
assert 'if target_member and target_member.user_id == current_user.id:' in source
assert "不能修改自己的项目成员权限" in source
assert "不能修改系统管理员账号的项目权限" in source
assert 'if _role_value(target_user) == "ADMIN":' in source
assert "不能修改权限高于自己的项目成员" in source
assert "不能授予高于自己的项目角色" in source
assert "_ensure_member_mutation_allowed(" in source
def test_business_modules_use_saved_permission_matrix():
expected = {
"overview.py": [
'require_study_permission("project_overview", "read")',
],
"project_milestones.py": [
'require_study_permission("project_milestones", "read")',
'require_study_permission("project_milestones", "write")',
],
"subjects.py": [
'require_study_permission("subjects", "read")',
'require_study_permission("subjects", "write")',
],
"material_equipments.py": [
'require_study_permission("materials", "read")',
'require_study_permission("materials", "write")',
],
"startup.py": [
'require_study_permission("startup_ethics", "read")',
'require_study_permission("startup_ethics", "write")',
'require_study_permission("startup_auth", "read")',
'require_study_permission("startup_auth", "write")',
],
"aes.py": [
'require_study_permission("risk_issues", "read")',
'require_study_permission("risk_issues", "write")',
],
"monitoring_visit_issues.py": [
'require_study_permission("monitoring_audit", "read")',
'require_study_permission("monitoring_audit", "write")',
],
}
for filename, checks in expected.items():
source = (ROOT / f"app/api/v1/{filename}").read_text(encoding="utf-8")
for check in checks:
assert check in source
ae_source = (ROOT / "app/api/v1/aes.py").read_text(encoding="utf-8")
assert "ALLOWED_CREATE_ROLES" not in ae_source
assert "ALLOWED_UPDATE_ROLES" not in ae_source
assert 'member_role not in {"PM", "PV"}' not in ae_source
def test_fee_contracts_use_saved_permission_matrix():
source = (ROOT / "app/api/v1/fees_contracts.py").read_text(encoding="utf-8")
assert 'role_has_project_permission(db, project_id, membership.role_in_study, "fees", action)' in source
assert 'if write and membership.role_in_study not in {"ADMIN", "PM"}' not in source
def test_attachment_deletes_use_saved_permission_matrix():
source = (ROOT / "app/api/v1/attachments.py").read_text(encoding="utf-8")
fees_source = (ROOT / "app/api/v1/fees_attachments.py").read_text(encoding="utf-8")
assert "_permission_module_for_entity(entity_type)" in source
assert '"knowledge_note": "shared_library"' in source
assert "_permission_module_for_entity(attachment.entity_type)" in source
assert 'getattr(membership, "role_in_study", None) == "PM"' not in source
assert 'role_has_project_permission(db, project_id, membership.role_in_study, "fees", action)' in fees_source
assert 'getattr(membership, "role_in_study", None) == "PM"' not in fees_source
def test_faq_uses_dedicated_permission_matrix():
source = (ROOT / "app/api/v1/faqs.py").read_text(encoding="utf-8")
category_source = (ROOT / "app/api/v1/faq_categories.py").read_text(encoding="utf-8")
assert 'role_has_project_permission(db, study_id, member.role_in_study, "faq", action)' in source
assert 'role_has_project_permission(db, study_id, member.role_in_study, "faq", action)' in category_source
assert '"etmf"' not in source
assert '"etmf"' not in category_source
assert 'member_role != "PM"' not in source
assert 'member_role != "PM"' not in category_source
def test_audit_logs_use_saved_permission_matrix():
source = (ROOT / "app/api/v1/audit_logs.py").read_text(encoding="utf-8")
get_block = source[source.index("@router.get("):source.index("@router.delete(")]
assert 'require_study_permission("audit_export", "read")' in get_block
assert 'require_study_member()' not in get_block
def test_document_service_uses_saved_permission_matrix():
source = (ROOT / "app/services/document_service.py").read_text(encoding="utf-8")
assert 'role_has_project_permission' in source
assert '"file_versions"' in source
assert 'rbac.is_allowed' not in source
def test_remaining_business_reads_use_saved_permission_matrix():
expected = {
"finance_contracts.py": ['require_study_permission("fees", "read")'],
"drug_shipments.py": ['require_study_permission("materials", "read")'],
"knowledge_notes.py": ['require_study_permission("shared_library", "read")'],
"subject_histories.py": ['require_study_permission("subjects", "read")'],
"subject_pds.py": ['require_study_permission("risk_issues", "read")'],
"visits.py": ['require_study_permission("subjects", "read")'],
"attachments.py": ['_permission_module_for_entity(entity_type)'],
}
for filename, checks in expected.items():
source = (ROOT / f"app/api/v1/{filename}").read_text(encoding="utf-8")
for check in checks:
assert check in source