菜单层与 API 层已按真实业务身份限制,但首页只看 role_key,导致有 train_teacher 角色却无教师档案绑定的账号先进入教师工作台再被 API 拒绝。 - 身份服务新增 hasActiveTeacherIdentity / hasActiveStudentIdentity, 语义与原菜单过滤完全一致,异常吞掉返回 false - SysLoginController 抽出 resolveSelfServiceIdentity 作为唯一事实源, 自服务菜单过滤与 getInfo 新增的 selfService 标志共用该方法,不新增第三套规则 - 前端 store 新增 selfService 标志,缺失时默认全 false 并严格布尔化,登出时重置 - 自服务路由过滤与首页渲染改为按身份标志判断,且与角色授权取交集,与菜单层口径一致 - 有角色无业务档案的账号落到普通首页,Dashboard 组件根本不创建, 不会发起任何自服务 API 请求,页面给出明确的未绑定提示 Co-authored-by: Cursor <cursoragent@cursor.com>
154 lines
8.5 KiB
JavaScript
154 lines
8.5 KiB
JavaScript
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 ===');
|