Files
ctms/frontend/src/router.test.ts
T
2026-07-08 20:46:56 +08:00

154 lines
8.1 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");
describe("admin project route permissions", () => {
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('import SystemMonitoringPage from "../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("keeps login landing independent from PM management backend access", () => {
const source = readRouter();
expect(source).not.toContain("findPmAdminLandingPath");
expect(source).not.toContain("pmAdminLandingModules");
expect(source).toContain(' : studyStore.currentStudy\n ? "/project/overview"\n : "/admin/projects",');
});
it("validates restored session tokens through /me before entering protected routes", () => {
const source = readRouter();
const restoreGuardIndex = source.indexOf("if (!auth.user && getToken())");
const fetchMeIndex = source.indexOf("await auth.fetchMe()", restoreGuardIndex);
const logoutIndex = source.indexOf("await auth.logout()", fetchMeIndex);
const loginRedirectIndex = source.indexOf('next({ path: "/login" })', logoutIndex);
const projectFallbackIndex = source.indexOf("if (token && !studyStore.currentStudy)", restoreGuardIndex);
expect(restoreGuardIndex).toBeGreaterThan(-1);
expect(fetchMeIndex).toBeGreaterThan(restoreGuardIndex);
expect(logoutIndex).toBeGreaterThan(fetchMeIndex);
expect(loginRedirectIndex).toBeGreaterThan(logoutIndex);
expect(fetchMeIndex).toBeLessThan(projectFallbackIndex);
});
it("does not register the removed non-admin personal dashboard route", () => {
const source = readRouter();
const removedComponent = ["My", "Work", "bench"].join("");
const removedPath = `/${["work", "bench"].join("")}`;
expect(source).not.toContain(removedComponent);
expect(source).not.toContain(`path: "${removedPath.slice(1)}"`);
expect(source).not.toContain(`"${removedPath}"`);
});
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");
});
});