import assert from 'node:assert'; import { isSelfServiceRouteAllowed, filterSelfServiceRoutes } from '../../src/utils/selfServiceRoute.js'; console.log('=== Running Self-Service Identity Flag Tests (FIX-05) ==='); 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: '系统管理' } }; // 1. 新签名:依据真实业务身份标志判定,而不是 role_key { const noIdentity = { teacherWorkbench: false, studentCenter: false }; assert.strictEqual(isSelfServiceRouteAllowed(teacherWorkbench, noIdentity), false, '无教师业务身份时教师工作台必须隐藏'); assert.strictEqual(isSelfServiceRouteAllowed(studentCenter, noIdentity), false, '无学员业务身份时学员中心必须隐藏'); assert.strictEqual(isSelfServiceRouteAllowed(teacherInfo, noIdentity), true, '人员管理下的教师信息不受自服务过滤影响'); assert.strictEqual(isSelfServiceRouteAllowed(studentInfo, noIdentity), true, '人员管理下的学员信息不受自服务过滤影响'); assert.strictEqual(isSelfServiceRouteAllowed(portrait, noIdentity), true, '学员画像不受自服务过滤影响'); assert.strictEqual(isSelfServiceRouteAllowed(system, noIdentity), true, '系统管理不受自服务过滤影响'); console.log('✔ PASS: no business identity hides both self-service entries while keeping management routes'); const teacherIdentity = { teacherWorkbench: true, studentCenter: false }; assert.strictEqual(isSelfServiceRouteAllowed(teacherWorkbench, teacherIdentity), true, '有教师业务身份时教师工作台可见'); assert.strictEqual(isSelfServiceRouteAllowed(studentCenter, teacherIdentity), false, '教师身份不得看到学员中心'); console.log('✔ PASS: teacher identity sees teacher workbench only'); const studentIdentity = { teacherWorkbench: false, studentCenter: true }; assert.strictEqual(isSelfServiceRouteAllowed(teacherWorkbench, studentIdentity), false, '学员身份不得看到教师工作台'); assert.strictEqual(isSelfServiceRouteAllowed(studentCenter, studentIdentity), true, '有学员业务身份时学员中心可见'); console.log('✔ PASS: student identity sees student center only'); } // 2. Fail closed:标志缺失 / 非布尔真值一律按无身份处理 { assert.strictEqual(isSelfServiceRouteAllowed(teacherWorkbench), false, '未传身份标志时必须 fail closed'); assert.strictEqual(isSelfServiceRouteAllowed(studentCenter), false, '未传身份标志时必须 fail closed'); assert.strictEqual(isSelfServiceRouteAllowed(teacherWorkbench, {}), false, '空标志对象必须 fail closed'); assert.strictEqual(isSelfServiceRouteAllowed(teacherWorkbench, null), false, 'null 标志必须 fail closed'); assert.strictEqual(isSelfServiceRouteAllowed(teacherWorkbench, { teacherWorkbench: undefined }), false, 'undefined 标志必须 fail closed'); assert.strictEqual(isSelfServiceRouteAllowed(teacherWorkbench, { teacherWorkbench: 'true' }), false, '非布尔真值不得放行'); assert.strictEqual(isSelfServiceRouteAllowed(teacherWorkbench, { teacherWorkbench: 1 }), false, '数字 1 不得放行'); assert.strictEqual(isSelfServiceRouteAllowed(null, { teacherWorkbench: true }), false, '空路由必须返回 false'); console.log('✔ PASS: missing or non-boolean identity flags fail closed'); } // 3. 关键回归:只有 role_key 没有业务档案绑定的账号(qa_role_nobody 场景)必须被拦截 { // 旧实现传入 ['train_teacher'] 角色数组即可放行,新实现必须拒绝 assert.strictEqual(isSelfServiceRouteAllowed(teacherWorkbench, ['train_teacher']), false, '角色数组不得再被当作身份凭据'); assert.strictEqual(isSelfServiceRouteAllowed(studentCenter, ['train_student']), false, '角色数组不得再被当作身份凭据'); console.log('✔ PASS: role_key alone can no longer unlock self-service routes (qa_role_nobody regression guard)'); } // 4. 菜单树深度过滤与空 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 titlesOf = (routes) => routes.map(r => (r.meta ? r.meta.title : (r.children && r.children[0] ? r.children[0].meta.title : ''))); // A. 管理员 / 有角色无绑定账号:两个自服务 wrapper 都被清理 const noIdentityFiltered = filterSelfServiceRoutes(mockMenuTree, { teacherWorkbench: false, studentCenter: false }); assert.strictEqual(noIdentityFiltered.length, 2, '无业务身份时仅保留人员管理与系统管理'); assert.strictEqual(noIdentityFiltered[0].meta.title, '人员管理'); assert.strictEqual(noIdentityFiltered[0].children.length, 2, '人员管理下的教师信息与学员信息完好'); assert.strictEqual(noIdentityFiltered[1].meta.title, '系统管理'); console.log('✔ PASS: identity-less user gets both self-service wrappers pruned'); // B. 教师身份 const teacherTitles = titlesOf(filterSelfServiceRoutes(mockMenuTree, { teacherWorkbench: true, studentCenter: false })); assert.strictEqual(teacherTitles.includes('教师工作台'), true, '教师身份保留教师工作台'); assert.strictEqual(teacherTitles.includes('学员中心'), false, '教师身份不含学员中心'); // C. 学员身份 const studentTitles = titlesOf(filterSelfServiceRoutes(mockMenuTree, { teacherWorkbench: false, studentCenter: true })); assert.strictEqual(studentTitles.includes('教师工作台'), false, '学员身份不含教师工作台'); assert.strictEqual(studentTitles.includes('学员中心'), true, '学员身份保留学员中心'); // D. 未传标志时整棵树的自服务入口都被清理 const defaultTitles = titlesOf(filterSelfServiceRoutes(mockMenuTree)); assert.strictEqual(defaultTitles.includes('教师工作台'), false, '默认参数下教师工作台必须隐藏'); assert.strictEqual(defaultTitles.includes('学员中心'), false, '默认参数下学员中心必须隐藏'); assert.strictEqual(filterSelfServiceRoutes(null).length, 0, '非数组输入返回空数组'); console.log('✔ PASS: menu tree filtering follows identity flags in all branches'); } // 5. 首页面板渲染判定:角色授权 ∩ 真实业务身份,并在缺失绑定时给出提示 { function resolveHomePanel(roles, selfService) { const flags = selfService || {}; const isAdmin = roles.includes('admin') || roles.includes('train_admin'); const hasTeacherRole = roles.includes('train_teacher'); const hasStudentRole = roles.includes('train_student'); const hasTeacherIdentity = flags.teacherWorkbench === true; const hasStudentIdentity = flags.studentCenter === true; const hasRoleConflict = !isAdmin && hasTeacherRole && hasStudentRole; if (isAdmin) return { panel: 'admin', missingBinding: false }; if (hasRoleConflict) return { panel: 'conflict', missingBinding: false }; if (hasTeacherRole && !hasStudentRole && hasTeacherIdentity) return { panel: 'teacher', missingBinding: false }; if (hasStudentRole && !hasTeacherRole && hasStudentIdentity) return { panel: 'student', missingBinding: false }; return { panel: 'fallback', missingBinding: (hasTeacherRole && !hasTeacherIdentity) || (hasStudentRole && !hasStudentIdentity) }; } const bound = { teacherWorkbench: true, studentCenter: false }; const boundStudent = { teacherWorkbench: false, studentCenter: true }; const none = { teacherWorkbench: false, studentCenter: false }; // 不得回归:admin / train_admin 行为不变 assert.deepStrictEqual(resolveHomePanel(['admin'], none), { panel: 'admin', missingBinding: false }, 'admin 仍进管理驾驶舱'); assert.deepStrictEqual(resolveHomePanel(['train_admin'], none), { panel: 'admin', missingBinding: false }, 'train_admin 仍进管理驾驶舱'); // 不得回归:正常绑定的教师 / 学员行为不变 assert.strictEqual(resolveHomePanel(['train_teacher'], bound).panel, 'teacher', '绑定教师仍进教师工作台'); assert.strictEqual(resolveHomePanel(['train_student'], boundStudent).panel, 'student', '绑定学员仍进学员中心'); // 核心缺陷修复:qa_role_nobody(有 train_teacher 角色、无教师档案) const roleNobody = resolveHomePanel(['train_teacher'], none); assert.strictEqual(roleNobody.panel, 'fallback', '有角色无绑定不得渲染教师工作台'); assert.strictEqual(roleNobody.missingBinding, true, '有角色无绑定必须提示未关联业务档案'); const studentRoleNobody = resolveHomePanel(['train_student'], none); assert.strictEqual(studentRoleNobody.panel, 'fallback', '有角色无绑定不得渲染学员中心'); assert.strictEqual(studentRoleNobody.missingBinding, true, '有角色无绑定必须提示未关联业务档案'); // 无培训业务角色的普通账号不应看到绑定缺失提示 const plainUser = resolveHomePanel(['common'], none); assert.strictEqual(plainUser.panel, 'fallback', '普通账号进入兜底首页'); assert.strictEqual(plainUser.missingBinding, false, '无培训角色账号不显示绑定缺失提示'); // 角色互斥冲突分支保持原有行为,且不显示绑定缺失提示 const conflict = resolveHomePanel(['train_teacher', 'train_student'], none); assert.strictEqual(conflict.panel, 'conflict', '角色互斥冲突分支保持原有告警'); assert.strictEqual(conflict.missingBinding, false, '冲突告警场景不叠加绑定缺失提示'); console.log('✔ PASS: home panel selection = role grant ∩ business identity, with missing-binding hint'); } console.log('=== All Self-Service Identity Flag Unit Tests Passed ===');