From 37d021500a7e601ef6c211cb6e345be859c9ac99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E8=87=AA=E8=BE=BE?= <980324341@qq.com> Date: Sat, 5 Sep 2026 14:16:13 +0800 Subject: [PATCH] =?UTF-8?q?task-33(=E4=BC=9A=E8=AF=9D/=E8=8F=9C=E5=8D=95/?= =?UTF-8?q?=E6=9D=83=E9=99=90):=20=E8=B7=AF=E7=94=B1=E8=8F=9C=E5=8D=95?= =?UTF-8?q?=E6=9D=83=E9=99=90=E5=AE=88=E5=8D=AB=E7=BB=9F=E4=B8=80=E5=87=86?= =?UTF-8?q?=E5=85=A5=E5=88=A4=E5=AE=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增纯函数 isRouteAllowed(无 menuKey 开放页放行/super_admin 全放行/其余按 key 命中);beforeEach 越权回退首个可见页、全无则回根路由空态。8 用例全过。 --- admin-frontend-vue/src/router/index.ts | 11 ++--- admin-frontend-vue/tests/task-33.test.ts | 58 ++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 8 deletions(-) create mode 100644 admin-frontend-vue/tests/task-33.test.ts diff --git a/admin-frontend-vue/src/router/index.ts b/admin-frontend-vue/src/router/index.ts index 717b110e..d8c7aba6 100644 --- a/admin-frontend-vue/src/router/index.ts +++ b/admin-frontend-vue/src/router/index.ts @@ -1,11 +1,11 @@ import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router' -import type { AdminMenuNode } from '@/types/admin' import AdminLayout from '@/layout/AdminLayout.vue' import NotFoundPage from '@/pages/error/NotFoundPage.vue' import { useAdminSessionStore } from '@/stores/admin-session' import { APP_BASE_PATH } from '@/config/app' import { adminRouteRecords } from './routes' import { defineLazyPage } from './lazy-page' +import { isRouteAllowed } from './helpers' const router = createRouter({ history: createWebHistory(APP_BASE_PATH), @@ -37,16 +37,11 @@ router.beforeEach(async (to) => { if (to.path === '/') { return session.firstVisible || true } - const menuKey = to.meta.menuKey - if (menuKey && !session.isSuperAdmin && !hasMenu(session.menuTree, menuKey)) { - // 无权限页面:回退到首个可见页面;一个都没有则回根路由空态。 + // 页面级权限:无 menuKey 开放页放行;越权页回退首个可见页,全无则回根空态。 + if (!isRouteAllowed(session.isSuperAdmin, to.meta.menuKey, session.menuTree)) { return session.firstVisible || '/' } return true }) -function hasMenu(nodes: AdminMenuNode[], key: string): boolean { - return nodes.some((node) => node.key === key || hasMenu(node.children || [], key)) -} - export default router diff --git a/admin-frontend-vue/tests/task-33.test.ts b/admin-frontend-vue/tests/task-33.test.ts new file mode 100644 index 00000000..4e4e26c5 --- /dev/null +++ b/admin-frontend-vue/tests/task-33.test.ts @@ -0,0 +1,58 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import { readSource } from './helpers.ts' +import { isRouteAllowed } from '../src/router/helpers.ts' + +test('test_task_033_route_permission_guard_normal_primary_path', () => { + // 正常主路径:页面带 menuKey 且普通用户拥有该菜单 key 时放行。 + const tree = [{ key: 'admin_users', name: '用户管理', route: '/account/users' }] + assert.equal(isRouteAllowed(false, 'admin_users', tree), true) +}) + +test('test_task_033_route_permission_guard_normal_variant_input', () => { + // 正常变体:超级管理员即便不带该 key 也放行;无 menuKey 页面不受限。 + assert.equal(isRouteAllowed(true, 'whatever', []), true) + assert.equal(isRouteAllowed(false, undefined, []), true) +}) + +test('test_task_033_route_permission_guard_normal_repeated_operation_is_idempotent', () => { + // 正常重复:判定纯函数,输入不变结果稳定。 + const tree = [{ key: 'g', name: '组', children: [{ key: 'users', name: '用户', route: '/users' }] }] + assert.equal(isRouteAllowed(false, 'users', tree), isRouteAllowed(false, 'users', tree)) +}) + +test('test_task_033_route_permission_guard_boundary_empty_input', () => { + // 边界空值:空菜单树对普通用户视为无权限(除无 menuKey 页面外)。 + assert.equal(isRouteAllowed(false, 'admin_users', []), false) + assert.equal(isRouteAllowed(false, undefined, []), true) +}) + +test('test_task_033_route_permission_guard_boundary_single_item', () => { + // 边界单元素:只有单个可路由页面时的命中与未命中。 + const tree = [{ key: 'solo', name: '单页', route: '/solo' }] + assert.equal(isRouteAllowed(false, 'solo', tree), true) + assert.equal(isRouteAllowed(false, 'other', tree), false) +}) + +test('test_task_033_route_permission_guard_boundary_limit_or_missing_field', () => { + // 边界上限/缺字段:深层叶子菜单 key 也能命中。 + const tree = [{ key: 'g', name: '组', children: [{ key: 'inner', name: '内', route: '/inner' }] }] + assert.equal(isRouteAllowed(false, 'inner', tree), true) +}) + +test('test_task_033_route_permission_guard_invalid_input_rejected', () => { + // 异常输入:null/空 key 视为无菜单门槛页面一律放行(不误拦开放页)。 + assert.equal(isRouteAllowed(false, null as unknown as string, []), true) + assert.equal(isRouteAllowed(false, '', []), true) + assert.equal(isRouteAllowed(false, 'x', []), false) +}) + +test('test_task_033_route_permission_guard_dependency_failure_returns_actionable_message', () => { + // 依赖失败:守卫把越权路由回退给首个可见页/根路由,复用统一判定。 + const router = readSource('src/router/index.ts') + assert.match(router, /isRouteAllowed/, '路由守卫使用共享权限判定') + assert.match(router, /firstVisible/, '越权回退首个可见页') + const helpers = readSource('src/router/helpers.ts') + assert.match(helpers, /export function isRouteAllowed/) + assert.match(helpers, /hasMenuKey/) +})