import test from 'node:test' import assert from 'node:assert/strict' import { readSource } from './helpers.ts' import { buildMenuOptionTree, compactDirectGrantIds, parseMenuOptionList, parsePermissionMenuItem, } from '../src/pages/account/user-menu-auth.ts' // 2026-09-16 线上事故:非超管(普通管理员)在授权树里勾到自己无权授予的菜单后, // 后端 ensureGrantable 抛 403 并回滚整笔事务——创建用户与保存权限双双失败, // 前端只显示「普通管理员只能分配自己已有的菜单权限」。 // 修复:菜单列表按操作者标记 grantable,前端把不可授予的节点置灰不可勾。 // // 注意:这里是「置灰」而非「隐藏」。授权保存是整树替换,隐藏会让超管早先授予、 // 而操作者自己没有的菜单在提交时被当作取消勾选删掉(与 09-13「权限自己没掉」同类)。 test('align_user_menu_grantable_false_maps_to_disabled_node', () => { const locked = parsePermissionMenuItem( { id: 5, name: '查询ASIN', parent_id: null, sort_order: 1, grantable: false }, 'admin', ) assert.equal(locked?.disabled, true, 'grantable=false → 节点置灰') const allowed = parsePermissionMenuItem( { id: 6, name: '店铺管理', parent_id: null, sort_order: 2, grantable: true }, 'admin', ) assert.equal(allowed?.disabled, false, 'grantable=true → 可勾选') // 菜单管理页等未标记 grantable 的接口必须保持旧行为(全部可勾选) const unmarked = parsePermissionMenuItem({ id: 7, name: '菜单权限配置', parent_id: null, sort_order: 3 }, 'admin') assert.equal(unmarked?.disabled, false, '缺省 grantable 视为可授予') }) test('align_user_menu_grantable_survives_tree_build', () => { const nodes = parseMenuOptionList( [ { id: 98, name: '账号与权限', parent_id: null, sort_order: 1, grantable: false }, { id: 7, name: '用户管理', parent_id: 98, sort_order: 1, grantable: false }, { id: 100, name: '店铺管理', parent_id: null, sort_order: 2, grantable: true }, ], 'admin', ) const tree = buildMenuOptionTree(nodes) const account = tree.find((node) => node.id === 98) assert.equal(account?.disabled, true, '分组节点置灰') assert.equal(account?.children?.[0]?.disabled, true, '子节点置灰随树保留') assert.equal(tree.find((node) => node.id === 100)?.disabled, false, '可授予节点不受影响') }) test('align_user_menu_grantable_disabled_node_still_compactable', () => { // 已持有但无权授予的节点会保持勾选并原样提交,压缩逻辑不能因 disabled 漏掉它 const tree = buildMenuOptionTree( parseMenuOptionList( [ { id: 98, name: '账号与权限', parent_id: null, sort_order: 1, grantable: false }, { id: 7, name: '用户管理', parent_id: 98, sort_order: 1, grantable: false }, ], 'admin', ), ) assert.deepEqual(compactDirectGrantIds([98, 7], tree), [98], '父级已勾选时仍压缩掉后代') }) test('align_user_menu_grantable_wired_end_to_end', () => { const tree = readSource('src/pages/account/UserMenuAuthTree.vue') assert.match(tree, /disabled: 'disabled'/, 'el-tree 按 disabled 键置灰节点') const vo = readSource( '../backend-java/src/main/java/com/nanri/aiimage/modules/permission/model/vo/PermissionMenuItemVo.java', ) assert.match(vo, /private Boolean grantable;/, 'VO 暴露 grantable') const controller = readSource( '../backend-java/src/main/java/com/nanri/aiimage/modules/permission/controller/PermissionMenuController.java', ) assert.match(controller, /permissionMenuService\.list\(requireAdmin\(request\), menuType\)/, '列表接口传入操作者') const service = readSource( '../backend-java/src/main/java/com/nanri/aiimage/modules/permission/service/PermissionMenuService.java', ) assert.match(service, /resolveGrantableMenuIds/, '按操作者计算可授予集') assert.match(service, /ensureGrantable\(operator, grantIds, userId\)/, '保存校验传入目标用户以放行既有授权') })