task-32(会话/菜单/权限): 普通用户菜单 key 查找下沉为共享纯函数
hasMenuKey 深度优先查找(空 key/空树返回 false,不改输入)抽到 helpers 供 守卫复用,去掉路由内私有 hasMenu 副本。8 用例全过。
This commit is contained in:
@@ -12,3 +12,23 @@ export function firstVisiblePath(nodes: AdminMenuNode[] | null | undefined): str
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
/** 菜单树中是否含指定 key(深度优先递归;空 key/空树一律 false)。 */
|
||||
export function hasMenuKey(nodes: AdminMenuNode[] | null | undefined, key: string): boolean {
|
||||
if (!key) return false
|
||||
return (nodes || []).some((node) => node.key === key || hasMenuKey(node.children || [], key))
|
||||
}
|
||||
|
||||
/**
|
||||
* 页面级路由准入:无 menuKey 的开放页放行;超级管理员可进全部;其余须在
|
||||
* 本人菜单树中命中 key。后端已过滤菜单,此处只做前端路由级兜底。
|
||||
*/
|
||||
export function isRouteAllowed(
|
||||
isSuperAdmin: boolean,
|
||||
menuKey: string | undefined,
|
||||
menuTree: AdminMenuNode[] | null | undefined,
|
||||
): boolean {
|
||||
if (!menuKey) return true
|
||||
if (isSuperAdmin) return true
|
||||
return hasMenuKey(menuTree, menuKey)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { readSource } from './helpers.ts'
|
||||
import { hasMenuKey } from '../src/router/helpers.ts'
|
||||
|
||||
test('test_task_032_has_menu_key_lookup_normal_primary_path', () => {
|
||||
// 正常主路径:树内顶层菜单 key 可被命中。
|
||||
const tree = [{ key: 'admin_users', name: '用户管理', route: '/account/users' }]
|
||||
assert.equal(hasMenuKey(tree, 'admin_users'), true)
|
||||
})
|
||||
|
||||
test('test_task_032_has_menu_key_lookup_normal_variant_input', () => {
|
||||
// 正常变体:叶子在分组深层时也能命中;未授权 key 不命中。
|
||||
const tree = [{ key: 'g', name: '组', children: [{ key: 'leaf', name: '叶', route: '/leaf' }] }]
|
||||
assert.equal(hasMenuKey(tree, 'leaf'), true)
|
||||
assert.equal(hasMenuKey(tree, 'missing'), false)
|
||||
})
|
||||
|
||||
test('test_task_032_has_menu_key_lookup_normal_repeated_operation_is_idempotent', () => {
|
||||
// 正常重复:查找不修改树、结果稳定。
|
||||
const tree = [{ key: 'a', name: 'A', children: [{ key: 'b', name: 'B', route: '/b' }] }]
|
||||
const snapshot = JSON.stringify(tree)
|
||||
assert.equal(hasMenuKey(tree, 'b'), hasMenuKey(tree, 'b'))
|
||||
assert.deepEqual(JSON.stringify(tree), snapshot)
|
||||
})
|
||||
|
||||
test('test_task_032_has_menu_key_lookup_boundary_empty_input', () => {
|
||||
// 边界空值:空/缺省树查找返回 false,不抛错。
|
||||
assert.equal(hasMenuKey([], 'x'), false)
|
||||
assert.equal(hasMenuKey(null as unknown as { key: string; name: string }[], 'x'), false)
|
||||
assert.equal(hasMenuKey(undefined as unknown as { key: string; name: string }[], 'x'), false)
|
||||
})
|
||||
|
||||
test('test_task_032_has_menu_key_lookup_boundary_single_item', () => {
|
||||
// 边界单元素:单节点树命中与未命中。
|
||||
const tree = [{ key: 'solo', name: '单页', route: '/solo' }]
|
||||
assert.equal(hasMenuKey(tree, 'solo'), true)
|
||||
assert.equal(hasMenuKey(tree, 'nope'), false)
|
||||
})
|
||||
|
||||
test('test_task_032_has_menu_key_lookup_boundary_limit_or_missing_field', () => {
|
||||
// 边界上限/缺字段:缺 children 的节点不崩溃;深树正确递归。
|
||||
const tree = [{ key: 'a', name: 'A' }, { key: 'b', name: 'B', children: [{ key: 'c', name: 'C', route: '/c' }] }]
|
||||
assert.equal(hasMenuKey(tree, 'c'), true)
|
||||
})
|
||||
|
||||
test('test_task_032_has_menu_key_lookup_invalid_input_rejected', () => {
|
||||
// 异常输入:空 key 查找返回 false,绝不误放行。
|
||||
assert.equal(hasMenuKey([{ key: '', name: '无key' }], ''), false)
|
||||
assert.equal(hasMenuKey([], ''), false)
|
||||
})
|
||||
|
||||
test('test_task_032_has_menu_key_lookup_dependency_failure_returns_actionable_message', () => {
|
||||
// 依赖失败:key 查找收敛到 helpers,路由守卫经共享权限判定复用,不留私有副本。
|
||||
const helpers = readSource('src/router/helpers.ts')
|
||||
assert.match(helpers, /export function hasMenuKey/)
|
||||
const router = readSource('src/router/index.ts')
|
||||
assert.equal(router.includes('function hasMenu'), false, '不再保留模块内私有 hasMenu')
|
||||
assert.match(router, /isRouteAllowed/, '守卫委托共享判定(内含 key 查找)')
|
||||
})
|
||||
Reference in New Issue
Block a user