import assert from 'node:assert'; import { isSelfServiceRouteAllowed, filterSelfServiceRoutes } from '../../src/utils/selfServiceRoute.js'; console.log('=== Running Role Navigation Visibility & Self-Service Isolation Tests ==='); // FIX-05:自服务可见性事实源已由 role_key 切换为后端下发的真实业务身份标志 selfService。 // 下列 *Identity 常量即各类账号登录后 getInfo 返回的 selfService 取值。 const ADMIN_IDENTITY = { teacherWorkbench: false, studentCenter: false }; const TRAIN_ADMIN_IDENTITY = { teacherWorkbench: false, studentCenter: false }; const TEACHER_IDENTITY = { teacherWorkbench: true, studentCenter: false }; const STUDENT_IDENTITY = { teacherWorkbench: false, studentCenter: true }; const DUAL_IDENTITY = { teacherWorkbench: true, studentCenter: true }; // 1. 测试 isSelfServiceRouteAllowed 基础路由拦截 { const teacherWorkbench = { path: 'teacher', component: 'train/me/teacher/index', meta: { title: '教师工作台' } }; const studentCenter = { path: 'student', component: 'train/me/student/index', meta: { title: '学员中心' } }; const teacherInfo = { path: 'teacher', component: 'train/teacher/index', meta: { title: '教师信息' } }; const studentInfo = { path: 'student', component: 'train/student/index', meta: { title: '学员信息' } }; const portrait = { path: 'portrait', component: 'train/portrait/index', meta: { title: '学员画像' } }; const system = { path: 'system', component: 'Layout', meta: { title: '系统管理' } }; // A. 超级管理员 (admin) const adminRoles = ADMIN_IDENTITY; assert.strictEqual(isSelfServiceRouteAllowed(teacherWorkbench, adminRoles), false, 'admin 不能看到教师工作台'); assert.strictEqual(isSelfServiceRouteAllowed(studentCenter, adminRoles), false, 'admin 不能看到学员中心'); assert.strictEqual(isSelfServiceRouteAllowed(teacherInfo, adminRoles), true, 'admin 必须保留人员管理下的教师信息'); assert.strictEqual(isSelfServiceRouteAllowed(studentInfo, adminRoles), true, 'admin 必须保留人员管理下的学员信息'); assert.strictEqual(isSelfServiceRouteAllowed(portrait, adminRoles), true, 'admin 必须保留学员画像'); assert.strictEqual(isSelfServiceRouteAllowed(system, adminRoles), true, 'admin 必须保留系统管理'); console.log('✔ PASS: admin correctly blocks self-service routes while preserving management routes'); // B. 培训管理员 (train_admin) const trainAdminRoles = TRAIN_ADMIN_IDENTITY; assert.strictEqual(isSelfServiceRouteAllowed(teacherWorkbench, trainAdminRoles), false, 'train_admin 不能看到教师工作台'); assert.strictEqual(isSelfServiceRouteAllowed(studentCenter, trainAdminRoles), false, 'train_admin 不能看到学员中心'); assert.strictEqual(isSelfServiceRouteAllowed(teacherInfo, trainAdminRoles), true, 'train_admin 必须保留人员管理下的教师信息'); assert.strictEqual(isSelfServiceRouteAllowed(studentInfo, trainAdminRoles), true, 'train_admin 必须保留人员管理下的学员信息'); console.log('✔ PASS: train_admin correctly blocks self-service routes while preserving business routes'); // C. 教师 (train_teacher 角色 + 有效教师档案绑定) const teacherRoles = TEACHER_IDENTITY; assert.strictEqual(isSelfServiceRouteAllowed(teacherWorkbench, teacherRoles), true, 'teacher 可以看到教师工作台'); assert.strictEqual(isSelfServiceRouteAllowed(studentCenter, teacherRoles), false, 'teacher 不能看到学员中心'); console.log('✔ PASS: teacher sees teacher workbench only'); // D. 学员 (train_student 角色 + 有效学员档案绑定) const studentRoles = STUDENT_IDENTITY; assert.strictEqual(isSelfServiceRouteAllowed(teacherWorkbench, studentRoles), false, 'student 不能看到教师工作台'); assert.strictEqual(isSelfServiceRouteAllowed(studentCenter, studentRoles), true, 'student 可以看到学员中心'); console.log('✔ PASS: student sees student center only'); // E. 双重身份 (同时绑定有效教师与学员档案) const dualRoles = DUAL_IDENTITY; assert.strictEqual(isSelfServiceRouteAllowed(teacherWorkbench, dualRoles), true, '具备教师身份时教师工作台可见'); assert.strictEqual(isSelfServiceRouteAllowed(studentCenter, dualRoles), true, '具备学员身份时学员中心可见'); console.log('✔ PASS: dual roles allow both self-service routes based on respective identities'); } // 2. 测试完整的菜单树过滤与 Wrapper 清理 { const mockMenuTree = [ { path: '/train', component: 'Layout', meta: { title: '人员管理' }, children: [ { path: 'teacher', component: 'train/teacher/index', meta: { title: '教师信息' } }, { path: 'student', component: 'train/student/index', meta: { title: '学员信息' } } ] }, { path: '/', component: 'Layout', meta: null, children: [ { path: 'teacher', component: 'train/me/teacher/index', meta: { title: '教师工作台' } } ] }, { path: '/', component: 'Layout', meta: null, children: [ { path: 'student', component: 'train/me/student/index', meta: { title: '学员中心' } } ] }, { path: '/system', component: 'Layout', meta: { title: '系统管理' }, children: [ { path: 'user', component: 'system/user/index', meta: { title: '用户管理' } } ] } ]; // 管理员视角下的路由过滤 const adminFiltered = filterSelfServiceRoutes(mockMenuTree, ADMIN_IDENTITY); assert.strictEqual(adminFiltered.length, 2, '管理员过滤后仅保留人员管理和系统管理两个根节点'); assert.strictEqual(adminFiltered[0].meta.title, '人员管理'); assert.strictEqual(adminFiltered[0].children.length, 2, '人员管理下的教师信息与学员信息完好'); assert.strictEqual(adminFiltered[1].meta.title, '系统管理'); console.log('✔ PASS: admin menu tree completely eliminates empty wrappers for self-service routes'); // 教师视角下的路由过滤 const teacherFiltered = filterSelfServiceRoutes(mockMenuTree, TEACHER_IDENTITY); const teacherTitles = teacherFiltered.map(r => (r.meta ? r.meta.title : (r.children && r.children[0] ? r.children[0].meta.title : ''))); assert.strictEqual(teacherTitles.includes('教师工作台'), true, '教师视角保留教师工作台'); assert.strictEqual(teacherTitles.includes('学员中心'), false, '教师视角不包含学员中心'); console.log('✔ PASS: teacher menu tree retains teacher workbench and excludes student center'); // 学员视角下的路由过滤 const studentFiltered = filterSelfServiceRoutes(mockMenuTree, STUDENT_IDENTITY); const studentTitles = studentFiltered.map(r => (r.meta ? r.meta.title : (r.children && r.children[0] ? r.children[0].meta.title : ''))); assert.strictEqual(studentTitles.includes('教师工作台'), false, '学员视角不包含教师工作台'); assert.strictEqual(studentTitles.includes('学员中心'), true, '学员视角保留学员中心'); console.log('✔ PASS: student menu tree retains student center and excludes teacher workbench'); } // 3. 测试 403 权限异常防抖逻辑 { const FORBIDDEN_DEBOUNCE_MS = 2500; let lastNotificationTime = 0; let notificationCount = 0; function simulate403(currentTime) { if (currentTime - lastNotificationTime > FORBIDDEN_DEBOUNCE_MS) { lastNotificationTime = currentTime; notificationCount++; return true; // 弹窗触发 } return false; // 防抖拦截 } // 模拟并发调用 6 个接口(如页面初始化时的多 Tab 数据并发) const t0 = 100000; assert.strictEqual(simulate403(t0), true, '首个 403 必须正常弹窗通知'); assert.strictEqual(simulate403(t0 + 10), false, '并发第2个请求防抖拦截'); assert.strictEqual(simulate403(t0 + 50), false, '并发第3个请求防抖拦截'); assert.strictEqual(simulate403(t0 + 100), false, '并发第4个请求防抖拦截'); assert.strictEqual(simulate403(t0 + 500), false, '500ms后仍处于防抖窗口,拦截'); assert.strictEqual(simulate403(t0 + 2400), false, '2400ms后仍处于防抖窗口,拦截'); assert.strictEqual(notificationCount, 1, '风暴期间只弹窗一次'); // 超过 2500ms 后的新操作 const t1 = t0 + 2600; assert.strictEqual(simulate403(t1), true, '超过 2500ms 后用户新操作被拒应再次弹窗'); assert.strictEqual(notificationCount, 2); console.log('✔ PASS: 403 notification debouncing prevents toast storm while preserving subsequent alerts'); } console.log('=== All Role Navigation & Debounce Unit Tests Passed ===');