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