177 lines
11 KiB
JavaScript
177 lines
11 KiB
JavaScript
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 ===');
|