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"); }); });