Files
ctms/frontend/src/router.test.ts
T
chengchengzhou7 6c45cef4b7
Client Quality Gates / Shared client and Web (push) Has been cancelled
Client Quality Gates / macOS Desktop (push) Has been cancelled
Storage Persistence Guard / storage-persistence-audit (push) Has been cancelled
Client Quality Gates / Shared client and Web (pull_request) Has been cancelled
Client Quality Gates / macOS Desktop (pull_request) Has been cancelled
Storage Persistence Guard / storage-persistence-audit (pull_request) Has been cancelled
同步全局协作台账与工作台界面优化
工作台新增与项目平级的全局协作台账,复用共享库在线协作能力。两本台账使用合同台账明细表、临床运营项目编号的空白模板,支持独立账号授权、在线编辑和历史版本保留,禁止删除整个台账及历史版本。

台账管理拆分为信息维护、访问与权限、历史版本三个独立入口;后两者与项目共享组件。统一项目和台账的卡片高度、数量徽标与标题布局,缩小工作台顶部留白,优化信息维护弹窗,并修复空用途说明和版本命名的默认提示。同步现有飞线图白色残影修复,以及在线文档的字体构建支持。

已验证:前端五百五十四项测试、后端台账与在线文档七十四项测试、类型检查、界面约束、运行时边界、桌面发布静态检查、网页构建和本地桌面应用构建均通过。数据库表结构升级已完成离线脚本生成检查,存储持久化检查通过。桌面凭据测试显式隔离构建环境,并覆盖使用默认服务器地址读取凭据的场景;在注入默认服务器地址的环境中完成全部前端测试。

提交包含必需的表结构升级和只有表头的初始模板;不包含本地台账业务记录、数据库导出、账号授权运行数据或上传目录中的历史文件。字体文件沿用部署方单独提供的方式。
2026-09-04 08:42:21 +08:00

238 lines
14 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
const readRouter = () => readFileSync(resolve(__dirname, "./router/index.ts"), "utf8").replace(/\r\n/g, "\n");
describe("admin project route permissions", () => {
it("registers independent full-bleed routes for attachment and version Office previews", () => {
const source = readRouter();
expect(source).toContain('path: "office-preview/attachment/:id"');
expect(source).toContain('name: "OfficeAttachmentPreview"');
expect(source).toContain('path: "office-preview/version/:id"');
expect(source).toContain('name: "OfficeVersionPreview"');
expect(source).toContain('path: "office-preview/collaboration/:fileId/revision/:id"');
expect(source).toContain('name: "OfficeCollaborationRevisionPreview"');
expect(source).toContain('path: "/ledgers/:fileId/revisions/:id/preview"');
expect(source).toContain('name: "LedgerRevisionPreview"');
expect(source).toContain('meta: { title: "台账历史版本预览", ledgerWorkspace: true }');
expect(source.match(/component: OfficePreviewWorkspace/g)).toHaveLength(4);
expect(source.match(/fullBleed: true/g)?.length).toBeGreaterThanOrEqual(2);
expect(source.match(/transientWorkspaceTask: true/g)?.length).toBeGreaterThanOrEqual(2);
});
it("registers the independent collaboration library and transient editor workspace", () => {
const source = readRouter();
expect(source).toContain('path: "knowledge/collaboration"');
expect(source).toContain('name: "KnowledgeCollaboration"');
expect(source).toContain('meta: { title: TEXT.menu.knowledgeCollaboration, requiresStudy: true, fullBleed: true }');
expect(source).toContain('path: "knowledge/collaboration/:fileId"');
expect(source).toContain('name: "KnowledgeCollaborationWorkspace"');
expect(source).toContain("component: CollaborationWorkspace");
expect(source).toContain("immersiveWorkspace: true");
expect(source).toContain("workspaceTaskGroup: TEXT.menu.knowledgeCollaboration");
});
it("registers the collaboration share page outside authenticated project layout", () => {
const source = readRouter();
expect(source).toContain('path: "/collaboration/share"');
expect(source).toContain('name: "CollaborationPublicShare"');
expect(source).toContain("component: CollaborationShareWorkspace");
expect(source).toContain('meta: { public: true, title: "共享协作文件" }');
expect(source).toContain("!to.meta.public && !auth.user");
});
it("lets any authorized project role open the project management detail page", () => {
const source = readRouter();
expect(source).toContain('name: "AdminProjectDetail"');
expect(source).toContain("requiresProjectMember: true");
expect(source).toContain('projectPermission: "setup_config:read"');
expect(source).toContain("to.meta.projectPermission");
expect(source).toContain("ensureProjectPermissionAccess(to.meta.projectPermission as string, isAdmin, studyStore)");
expect(source).not.toContain("meta: { title: TEXT.modules.adminProjects.detailTitle, requiresAdmin: true }");
expect(source).toContain("ensureRouteProjectMember(to, studyStore)");
});
it("allows project admin pages through matrix-authorized PMs instead of all project roles", () => {
const source = readRouter();
expect(source).toContain('adminProjectPermission: { module: "sites", action: "read" }');
expect(source).toContain('adminProjectPermission: { module: "audit_export", action: "read" }');
expect(source).toContain("meta: { title: TEXT.menu.projectManagement }");
expect(source).toContain("ADMIN_PROJECT_OPERATION_KEYS");
expect(source).toContain('if (role !== "PM") return false;');
expect(source).toContain("isApiPermissionAllowed");
expect(source).toContain("studyStore.currentPermissions?.[role]?.[operationKey]");
});
it("checks admin project permissions against query project id when present", () => {
const source = readRouter();
const accessStart = source.indexOf("const ensureAdminProjectAccess");
const accessEnd = source.indexOf("const ensureRouteProjectMember", accessStart);
const accessBlock = source.slice(accessStart, accessEnd);
expect(accessBlock).toContain("const targetProjectId = getTargetProjectId(to);");
expect(accessBlock).toContain("studyStore.currentStudy?.id !== targetProjectId");
expect(accessBlock).toContain("fetchStudyDetail(targetProjectId)");
expect(accessBlock).toContain("fetchApiEndpointPermissions(targetProjectId)");
expect(accessBlock).not.toContain("typeof to.params.projectId");
});
it("guards project permission configuration with the system-level project config permission", () => {
const source = readRouter();
const projectPermissionRouteStart = source.indexOf('name: "AdminPermissionsProject"');
const projectPermissionRouteEnd = source.indexOf('path: "system-monitoring"', projectPermissionRouteStart);
const projectPermissionRoute = source.slice(projectPermissionRouteStart, projectPermissionRouteEnd);
expect(source).toContain('const SYSTEM_PERMISSION_PROJECT_CONFIG = "system:permissions:project_config";');
expect(projectPermissionRoute).toContain("systemPermission: SYSTEM_PERMISSION_PROJECT_CONFIG");
expect(projectPermissionRoute).not.toContain("requiresProjectPm: true");
expect(source).toContain("to.meta.systemPermission");
expect(source).toContain("hasSystemPermissionAccess(to.meta.systemPermission as string, isAdmin, to)");
expect(source).toContain("const targetProjectId = getTargetProjectId(to);");
expect(source).toContain('const matchedProject = items.find((study: any) => study.id === targetProjectId);');
const targetProjectBranchStart = source.indexOf("if (targetProjectId) {");
const targetProjectBranchEnd = source.indexOf("const hasPmProject", targetProjectBranchStart);
const targetProjectBranch = source.slice(targetProjectBranchStart, targetProjectBranchEnd);
expect(targetProjectBranch).not.toContain("ensureDefaultPmStudy");
});
it("registers system monitoring as an admin-only peer admin module with legacy redirects", () => {
const source = readRouter();
const monitoringRouteStart = source.indexOf('name: "AdminSystemMonitoring"');
const monitoringRouteEnd = source.indexOf("],", monitoringRouteStart);
const monitoringRoute = source.slice(monitoringRouteStart, monitoringRouteEnd);
expect(source).toContain('const SystemMonitoringPage = () => import("../views/admin/SystemMonitoringPage.vue");');
expect(source).toContain('path: "system-monitoring"');
expect(monitoringRoute).toContain("component: SystemMonitoringPage");
expect(monitoringRoute).toContain("requiresAdmin: true");
expect(source).toContain('path: "permission-monitoring"');
expect(source).toContain('path: "permissions/monitoring"');
expect(source).toContain('redirect: "/admin/system-monitoring"');
expect(source).not.toContain('const SYSTEM_PERMISSION_MONITORING_METRICS = "system:monitoring:metrics";');
expect(source).not.toContain("[SYSTEM_PERMISSION_MONITORING_METRICS]");
});
it("routes web and desktop login plus missing-project flows through the entry chooser", () => {
const source = readRouter();
expect(source).not.toContain("findPmAdminLandingPath");
expect(source).not.toContain("pmAdminLandingModules");
expect(source).toContain('const WEB_WORKBENCH_ENTRY_PATH = "/workbench";');
expect(source).toContain('const DESKTOP_PROJECT_ENTRY_PATH = "/desktop/project-entry";');
expect(source).toContain("const resolveWorkbenchEntryPath = (isDesktopRuntime: boolean)");
expect(source).toContain("const isWorkbenchEntryPath = (path: string)");
expect(source).toContain('const WebWorkbenchEntry = () => import("../views/WebWorkbenchEntry.vue");');
expect(source).toContain("path: WEB_WORKBENCH_ENTRY_PATH");
expect(source).toContain('name: "WorkbenchEntry"');
expect(source).toContain("component: WebWorkbenchEntry");
expect(source).toContain("path: DESKTOP_PROJECT_ENTRY_PATH");
expect(source).toContain("component: DesktopProjectEntry");
expect(source).toContain("DesktopSessionRestore");
expect(source).toContain("DESKTOP_SESSION_RESTORE_PATH");
expect(source).toContain("if (isDesktopRuntime && to.path === WEB_WORKBENCH_ENTRY_PATH)");
expect(source).toContain("if (!isDesktopRuntime && to.path === DESKTOP_PROJECT_ENTRY_PATH)");
expect(source).toContain("const workbenchEntryPath = resolveWorkbenchEntryPath(isDesktopRuntime)");
expect(source).toContain("next({ path: workbenchEntryPath });");
expect(source).toContain('if (isDesktopRuntime && token && !isAdmin && to.path.startsWith("/admin"))');
expect(source).toContain("const canAccessProjectManagement = studyStore.currentStudyRole === \"PM\"");
expect(source).toContain("if (!canAccessProjectManagement)");
expect(source).toContain('next({ path: DESKTOP_PROJECT_ENTRY_PATH });');
expect(source).toContain("if (token && (isWorkbenchEntryPath(to.path) || to.meta.ledgerWorkspace) && studyStore.currentStudy)");
expect(source).not.toContain("if (token && !studyStore.currentStudy && !isDesktopRuntime)");
expect(source).not.toContain("ensureDefaultActiveStudy();");
expect(source).not.toContain("ensureDefaultStudy();");
});
it("canonicalizes the legacy file version entry before rendering its loading fallback", () => {
const source = readRouter();
const guardIndex = source.indexOf('if (to.name === "FileVersionManagement" && studyStore.currentStudy?.id)');
const requiresStudyIndex = source.indexOf("if (to.meta.requiresStudy && !studyStore.currentStudy)");
const legacyRouteIndex = source.indexOf('name: "FileVersionManagement"');
const legacyRouteEnd = source.indexOf('path: "trial/:trialId/documents"', legacyRouteIndex);
const legacyRoute = source.slice(legacyRouteIndex, legacyRouteEnd);
expect(guardIndex).toBeGreaterThan(-1);
expect(requiresStudyIndex).toBeGreaterThan(guardIndex);
expect(legacyRoute).toContain("component: DocumentList");
expect(source).not.toContain('import FileVersionManagement from "../views/ia/FileVersionManagement.vue";');
expect(source).toContain('next({ name: "DocumentList", params: { trialId: studyStore.currentStudy.id }, replace: true });');
});
it("validates restored session tokens through /me before entering protected routes", () => {
const source = readRouter();
const restoreGuardIndex = source.indexOf("if (!to.meta.public && !auth.user && getToken() && !isDesktopSessionRestoreRoute)");
const fetchMeIndex = source.indexOf("await auth.fetchMe({ disableNetworkRetry: true, suppressErrorMessage: true })", restoreGuardIndex);
const preserveIndex = source.indexOf("shouldPreserveDesktopSessionOnAuthCheckFailure(error)", fetchMeIndex);
const restoreRedirectIndex = source.indexOf("next({ path: DESKTOP_SESSION_RESTORE_PATH", preserveIndex);
const logoutIndex = source.indexOf("await auth.logout()", restoreRedirectIndex);
const loginRedirectIndex = source.indexOf('next({ path: "/login" })', logoutIndex);
const workbenchEntryIndex = source.indexOf("const workbenchEntryPath = resolveWorkbenchEntryPath(isDesktopRuntime)", restoreGuardIndex);
expect(restoreGuardIndex).toBeGreaterThan(-1);
expect(fetchMeIndex).toBeGreaterThan(restoreGuardIndex);
expect(preserveIndex).toBeGreaterThan(fetchMeIndex);
expect(restoreRedirectIndex).toBeGreaterThan(preserveIndex);
expect(logoutIndex).toBeGreaterThan(restoreRedirectIndex);
expect(loginRedirectIndex).toBeGreaterThan(logoutIndex);
expect(workbenchEntryIndex).toBeGreaterThan(fetchMeIndex);
expect(source).toContain("const isDesktopSessionRestoreRoute = to.path === DESKTOP_SESSION_RESTORE_PATH");
expect(source).toContain("!isDesktopSessionRestoreRoute");
expect(source).toContain("disableNetworkRetry: true");
expect(source).toContain("suppressErrorMessage: true");
});
it("does not register the removed non-admin personal dashboard route", () => {
const source = readRouter();
const removedComponent = ["My", "Work", "bench"].join("");
expect(source).not.toContain(removedComponent);
expect(source).not.toContain(`component: ${removedComponent}`);
expect(source).not.toContain('path: "workbench"');
});
it("does not register legacy finance contract routes", () => {
const source = readRouter();
expect(source).not.toContain("FinanceContracts");
expect(source).not.toContain("FinanceContract");
expect(source).not.toContain("finance/contracts");
});
it("does not register standalone contract fee create or edit pages", () => {
const source = readRouter();
expect(source).not.toContain("FeeContractForm");
expect(source).not.toContain('name: "FeeContractNew"');
expect(source).not.toContain('name: "FeeContractEdit"');
expect(source).not.toContain('path: "fees/contracts/new"');
expect(source).not.toContain('path: "fees/contracts/:contractId/edit"');
});
it("does not register standalone startup auth edit pages", () => {
const source = readRouter();
expect(source).not.toContain('name: "StartupKickoffEdit"');
expect(source).not.toContain('name: "StartupTrainingEdit"');
expect(source).not.toContain('name: "StartupTrainingNew"');
expect(source).not.toContain('path: "startup/kickoff/:meetingId/edit"');
expect(source).not.toContain('path: "startup/training/new"');
expect(source).not.toContain('path: "startup/training/:recordId/edit"');
});
it("uses precautions routes instead of knowledge note routes", () => {
const source = readRouter();
expect(source).toContain('path: "knowledge/precautions"');
expect(source).toContain('path: "knowledge/precautions/new"');
expect(source).toContain('path: "knowledge/precautions/:id"');
expect(source).toContain('path: "knowledge/precautions/:id/edit"');
expect(source).not.toContain("KnowledgeNote");
expect(source).not.toContain("/knowledge/notes");
});
});