Files
railwaysystem/build_ui/tests/unit/roleNavigationVisibility.spec.mjs
T

153 lines
8.5 KiB
JavaScript
Raw Normal View History

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 ===');