import { describe, it, expect, vi, beforeEach } from "vitest"; import { mount } from "@vue/test-utils"; import { createPinia, setActivePinia } from "pinia"; import { readFileSync } from "node:fs"; import { resolve } from "node:path"; import ApiEndpointPermissions from "@/components/ApiEndpointPermissions.vue"; import type { ApiEndpointPermissionsResponse } from "@/types/api"; vi.mock("@/api/projectPermissions", () => ({ fetchApiOperations: vi.fn().mockResolvedValue({ data: { operations: [] } }), fetchPermissionTemplates: vi.fn().mockResolvedValue({ data: [ { category: "PV", name: "PV", description: "负责药物警戒相关工作,跟踪安全性事件并支持风险评估。" }, { category: "QA", name: "QA", description: "负责医学审核与稽查,关注质量风险、合规性和医学一致性。" }, { category: "CTA", name: "CTA", description: "负责合同、药品及相关项目事务管理,保障执行支持与物资协同。" }, { category: "CRA", name: "CRA", description: "负责各中心临床监查执行,跟进现场质量、数据和问题闭环。" }, { category: "PM", name: "PM", description: "项目负责人,统筹项目全局,协调进度、资源与关键决策。" }, ], }), })); describe("ApiEndpointPermissions.vue", () => { beforeEach(() => { Object.defineProperty(window, "localStorage", { value: { getItem: vi.fn(() => null), setItem: vi.fn(), removeItem: vi.fn(), clear: vi.fn(), }, configurable: true, }); Object.defineProperty(globalThis, "localStorage", { value: window.localStorage, configurable: true, }); setActivePinia(createPinia()); }); const mockMatrix: ApiEndpointPermissionsResponse = { PM: { "subjects:create": true, "subjects:list": true, "subjects:read": true, "subjects:update": true, "subjects:delete": true, }, CRA: { "subjects:create": true, "subjects:list": true, "subjects:read": true, "subjects:update": true, "subjects:delete": false, }, }; it("renders permission matrix table", () => { const wrapper = mount(ApiEndpointPermissions, { props: { project: { id: "test-id", name: "Test Project" }, matrix: mockMatrix, }, global: { stubs: { ElTable: false, ElTableColumn: false, }, }, }); expect(wrapper.find(".api-permissions").exists()).toBe(true); }); it("keeps matrix read-only and renders authorization states", async () => { const source = readFileSync(resolve(__dirname, "./ApiEndpointPermissions.vue"), "utf8"); expect(source).not.toContain("只读查看各角色权限覆盖"); expect(source).toContain("permission-state--allowed"); expect(source).toContain("permission-state--disabled"); expect(source).toContain("CircleCloseFilled"); expect(source).not.toContain('? "✓" : "—"'); expect(source).not.toContain("defineEmits"); expect(source).not.toContain("el-checkbox"); }); it("renders role headers from template display names while keeping matrix role keys", () => { const source = readFileSync(resolve(__dirname, "./ApiEndpointPermissions.vue"), "utf8"); expect(source).toContain("useRoleTemplateMeta"); expect(source).toContain("compareRolesByTemplateOrder"); expect(source).toContain(':label="roleLabel(role)"'); expect(source).toContain(':key="role"'); expect(source).toContain("isOperationAllowed(row.operation_key, role)"); expect(source).toContain("permission-state"); expect(source).toContain('label="权限类型"'); expect(source).not.toContain(':label="role"'); }); it("pins PM first in matrix role columns even when templates return PM last", async () => { const wrapper = mount(ApiEndpointPermissions, { props: { project: { id: "test-id", name: "Test Project" }, matrix: { CRA: {}, CTA: {}, QA: {}, PM: {}, PV: {}, }, }, global: { stubs: { ElTable: false, ElTableColumn: false, }, }, }); await vi.dynamicImportSettled(); expect((wrapper.vm as any).roles).toEqual(["PM", "PV", "QA", "CTA", "CRA"]); }); it("sorts project permission modules by sidebar menu order", async () => { const wrapper = mount(ApiEndpointPermissions, { props: { project: { id: "test-id", name: "Test Project" }, matrix: mockMatrix, }, global: { stubs: { ElTable: false, ElTableColumn: false, }, }, }); (wrapper.vm as any).operations = [ { operation_key: "subjects:read", module: "subjects", action: "read", description: "查询参与者详情", default_roles: [] }, { operation_key: "sites:read", module: "sites", action: "read", description: "查询中心详情", default_roles: [] }, { operation_key: "audit_logs:read", module: "audit_export", action: "read", description: "查询审计日志", default_roles: [] }, { operation_key: "project_overview:read", module: "project_overview", action: "read", description: "查询项目总览", default_roles: [] }, { operation_key: "startup_auth:read", module: "startup_auth", action: "read", description: "查询启动授权", default_roles: [] }, { operation_key: "fees_contracts:update", module: "fees", action: "write", description: "更新合同费用", default_roles: [] }, { operation_key: "fees_contracts:read", module: "fees", action: "read", description: "查询合同费用", default_roles: [] }, { operation_key: "monitoring_issues:read", module: "risk_issues", action: "read", description: "查询监查访视问题详情", default_roles: [] }, ]; expect((wrapper.vm as any).filteredOperations.map((operation: any) => operation.module)).toEqual([ "sites", "audit_export", "project_overview", "fees", "fees", "startup_auth", "subjects", "risk_issues", ]); expect((wrapper.vm as any).filteredOperations.map((operation: any) => operation.operation_key)).toEqual([ "sites:read", "audit_logs:read", "project_overview:read", "fees_contracts:update", "fees_contracts:read", "startup_auth:read", "subjects:read", "monitoring_issues:read", ]); expect((wrapper.vm as any).uniqueModules).toEqual([ "sites", "audit_export", "project_overview", "fees", "startup_auth", "subjects", "risk_issues", ]); expect((wrapper.vm as any).getPermissionCategoryLabel({ module: "sites" })).toBe("通用权限"); expect((wrapper.vm as any).getPermissionCategoryLabel({ module: "fees" })).toBe("业务权限"); }); it("groups participant permissions by section labels", async () => { const wrapper = mount(ApiEndpointPermissions, { props: { project: { id: "test-id", name: "Test Project" }, matrix: mockMatrix, }, global: { stubs: { ElTable: false, ElTableColumn: false, }, }, }); expect((wrapper.vm as any).getOperationSection({ operation_key: "subject_aes:create", module: "subjects", action: "write", description: "创建参与者AE", default_roles: [], })).toBe("AE"); expect((wrapper.vm as any).getOperationSection({ operation_key: "subject_pds:create", module: "subjects", action: "write", description: "创建参与者PD", default_roles: [], })).toBe("PD"); expect((wrapper.vm as any).getOperationSection({ operation_key: "visits:list", module: "subjects", action: "read", description: "查询访视列表", default_roles: [], })).toBe("访视"); expect((wrapper.vm as any).getOperationSection({ operation_key: "monitoring_issues:create", module: "risk_issues", action: "write", description: "创建监查访视问题", default_roles: [], })).toBe("监查访视问题"); }); it("projects AE/SAE and PD permissions into risk issues while reusing participant operation keys", async () => { const wrapper = mount(ApiEndpointPermissions, { props: { project: { id: "test-id", name: "Test Project" }, matrix: mockMatrix, }, global: { stubs: { ElTable: false, ElTableColumn: false, }, }, }); (wrapper.vm as any).operations = [ { operation_key: "subject_aes:read", module: "subjects", action: "read", description: "查询参与者AE列表", default_roles: [] }, { operation_key: "subject_pds:list", module: "subjects", action: "read", description: "查询参与者PD列表", default_roles: [] }, { operation_key: "monitoring_issues:read", module: "risk_issues", action: "read", description: "查询监查访视问题列表", default_roles: [] }, ]; const riskRows = (wrapper.vm as any).filteredOperations.filter((operation: any) => operation.module === "risk_issues"); expect(riskRows.map((operation: any) => operation.operation_key)).toEqual([ "subject_aes:read", "subject_pds:list", "monitoring_issues:read", ]); expect(riskRows.map((operation: any) => (wrapper.vm as any).getOperationSection(operation))).toEqual([ "AE/SAE", "PD", "监查访视问题", ]); expect((wrapper.vm as any).filteredOperations.filter((operation: any) => operation.operation_key === "subject_aes:read")).toHaveLength(2); expect((wrapper.vm as any).filteredOperations.filter((operation: any) => operation.operation_key === "subject_pds:list")).toHaveLength(2); }); it("keeps contract fee permissions as one module section", () => { const wrapper = mount(ApiEndpointPermissions, { props: { project: { id: "test-id", name: "Test Project" }, matrix: mockMatrix, }, global: { stubs: { ElTable: false, ElTableColumn: false, }, }, }); expect((wrapper.vm as any).getOperationSection({ operation_key: "fees_contracts:create", module: "fees", action: "write", description: "创建合同费用", default_roles: [], })).toBe("合同费用"); const source = readFileSync(resolve(__dirname, "./ApiEndpointPermissions.vue"), "utf8"); expect(source).not.toContain("finance_contracts"); expect(source).not.toContain("fees_payments"); expect(source).not.toContain("fees_attachments"); expect(source).not.toContain('contract_fees: "合同费用"'); expect(source).not.toContain("FEE_SECTION_LABELS"); }); it("groups materials permissions into drug flow and equipment sections", () => { const wrapper = mount(ApiEndpointPermissions, { props: { project: { id: "test-id", name: "Test Project" }, matrix: mockMatrix, }, global: { stubs: { ElTable: false, ElTableColumn: false, }, }, }); expect((wrapper.vm as any).getOperationSection({ operation_key: "drug_shipments:read", module: "materials", action: "read", description: "查询药品流向详情", default_roles: [], })).toBe("药品流向管理"); expect((wrapper.vm as any).getOperationSection({ operation_key: "drug_shipments_attachments:delete", module: "materials", action: "write", description: "删除药物发货附件", default_roles: [], })).toBe("药品流向管理"); expect((wrapper.vm as any).getOperationSection({ operation_key: "material_equipments:read", module: "materials", action: "read", description: "查询设备详情", default_roles: [], })).toBe("设备管理"); expect((wrapper.vm as any).getOperationSection({ operation_key: "material_equipments_attachments:delete", module: "materials", action: "write", description: "删除物资设备附件", default_roles: [], })).toBe("设备管理"); }); it("groups startup ethics permissions into initiation and ethics sections", () => { const wrapper = mount(ApiEndpointPermissions, { props: { project: { id: "test-id", name: "Test Project" }, matrix: mockMatrix, }, global: { stubs: { ElTable: false, ElTableColumn: false, }, }, }); expect((wrapper.vm as any).getOperationSection({ operation_key: "startup_initiation:read", module: "startup_ethics", action: "read", description: "查询立项记录详情", default_roles: [], })).toBe("立项"); expect((wrapper.vm as any).getOperationSection({ operation_key: "startup_initiation_attachments:delete", module: "startup_ethics", action: "write", description: "删除立项记录附件", default_roles: [], })).toBe("立项"); expect((wrapper.vm as any).getOperationSection({ operation_key: "startup_ethics:read", module: "startup_ethics", action: "read", description: "查询伦理记录详情", default_roles: [], })).toBe("伦理"); expect((wrapper.vm as any).getOperationSection({ operation_key: "startup_ethics_attachments:delete", module: "startup_ethics", action: "write", description: "删除伦理记录附件", default_roles: [], })).toBe("伦理"); }); it("filters endpoints by search text", async () => { const wrapper = mount(ApiEndpointPermissions, { props: { project: { id: "test-id", name: "Test Project" }, matrix: mockMatrix, }, global: { stubs: { ElTable: false, ElTableColumn: false, ElInput: false, ElSelect: false, }, }, }); // 获取搜索输入框 const searchInput = wrapper.find("input[placeholder*='搜索']"); if (searchInput.exists()) { await searchInput.setValue("subjects"); // 验证过滤逻辑 expect((wrapper.vm as any).searchText).toBe("subjects"); } }); });