/** * permissions store(新表口径,设计文档 §14)。 * * 数据来源:s_module(启用模块)+ s_power(启用权限点)+ s_user_power(用户授权)。 * 判断口径:白名单(无授权即无权限);标准操作由 module.* 统一表达, * 业务动作走 action.{模块}.{动作};菜单入口走 menu.*。 */ import { describe, it, expect, beforeEach, vi } from 'vitest' import { useAuthStore } from '@/stores/auth' import { usePermissionStore } from '@/stores/permissions' import { createTestPinia } from './helpers/app.js' // mock 后端数据接口,避免真实网络请求 vi.mock('@/services/api', () => ({ loadDataApi: vi.fn(), })) import { loadDataApi } from '@/services/api' const mockModules = [ { b_id: 'v_b_contact', b_name: '联系人', b_module_type: 'data', b_parent_id: 'base' }, ] const mockPowers = [ { b_id: 'menu.contact', b_type: 'menu', b_object_type: 'menu', b_object_id: 'contact' }, { b_id: 'module.v_b_contact', b_type: 'module', b_object_type: 'module', b_object_id: 'v_b_contact', }, { b_id: 'action.v_b_contact.audit', b_type: 'action', b_object_type: 'module', b_object_id: 'v_b_contact', }, ] /** 用户 123:能进联系人菜单、能用联系人模块,但没有审核动作权限 */ const mockUserPowers = [ { b_user_id: '123', b_power_id: 'menu.contact', b_canuse: 1 }, { b_user_id: '123', b_power_id: 'module.v_b_contact', b_canuse: 1 }, // s_power 中不存在的权限点不算数(停用/已删除) { b_user_id: '123', b_power_id: 'module.b_money', b_canuse: 1 }, ] function mockApiResponses(overrides = {}) { const dataByTable = { s_module: mockModules, s_power: mockPowers, s_user_power: mockUserPowers, ...overrides, } loadDataApi.mockImplementation((table) => Promise.resolve({ data: dataByTable[table] || [], code: 0, success: true }), ) } function setLoggedInUser(account = 'u1', id = 123) { const auth = useAuthStore() auth.setSession({ token: 't', user: { id, account, name: 'U' } }, 'ORG') } // 冲刷微任务:让 setSession 触发的 watch(clear) 先执行,隔离“手动 clear”的断言 const flush = () => Promise.resolve() beforeEach(() => { localStorage.clear() sessionStorage.clear() createTestPinia() loadDataApi.mockReset() }) describe('permissions store:加载', () => { it('load 成功后写入 modules 与授权码,并记录 loadedPrincipalKey', async () => { mockApiResponses() setLoggedInUser() const perm = usePermissionStore() await perm.load() expect(perm.modules.length).toBe(1) // s_power 中不存在的授权被过滤 expect(perm.grantedPowerCodes).toEqual(['menu.contact', 'module.v_b_contact']) }) it('同一主体重复 load 不重复发请求(按主体去重)', async () => { mockApiResponses() setLoggedInUser() const perm = usePermissionStore() await perm.load() await perm.load() // 首次发 3 个接口调用(s_module / s_power / s_user_power);第二次因主体已加载直接 return expect(loadDataApi).toHaveBeenCalledTimes(3) }) it('登录场景:load 进行中触发 clear(主体相同)不作废结果', async () => { mockApiResponses() setLoggedInUser() await flush() // 先让 watch(clear) 跑完(清空空状态,不作废) const perm = usePermissionStore() const p = perm.load() // loadingPrincipalKey = 'ORG:123' // 模拟登录后 setSession 触发的 watch(clear):当前主体与进行中请求主体相同 perm.clear() await p // 关键修复:clear 未作废进行中请求,modules 正常写入 expect(perm.modules.length).toBe(1) }) it('切换账号:load 进行中触发 clear(主体不同)作废旧请求', async () => { mockApiResponses() setLoggedInUser() await flush() const perm = usePermissionStore() const p = perm.load() // 主体 ORG:123 进行中 // 模拟切换账号:authStore 变为另一主体,watch 触发 clear setLoggedInUser('u2', 999) perm.clear() await p // 旧请求被作废:结果丢弃,modules 仍为空(且未触发新主体的 load) expect(perm.modules.length).toBe(0) }) it('g3soft 提权:仅加载全部启用模块,不查授权表', async () => { mockApiResponses() setLoggedInUser('g3soft', 1) const perm = usePermissionStore() await perm.load() expect(perm.isG3soft).toBe(true) expect(perm.modules.length).toBe(1) expect(perm.grantedPowerCodes).toEqual([]) expect(loadDataApi).toHaveBeenCalledTimes(1) expect(loadDataApi).toHaveBeenCalledWith('s_module', 'b_canuse = 1', 'b_id ASC') }) }) describe('permissions store:判断口径', () => { async function loadedStore() { mockApiResponses() setLoggedInUser() const perm = usePermissionStore() await perm.load() return perm } it('canEnterMenu:有 menu.* 授权才能进入该菜单', async () => { const perm = await loadedStore() expect(perm.canEnterMenu('contact')).toBe(true) expect(perm.canEnterMenu('user')).toBe(false) }) it('canAccess:有 module.* 授权才能使用该模块', async () => { const perm = await loadedStore() expect(perm.canAccess('v_b_contact')).toBe(true) // 未授权模块 / 不存在的模块一律拒绝 expect(perm.canAccess('b_money')).toBe(false) expect(perm.canAccess('not-exist')).toBe(false) }) it('canPower:标准操作由模块使用权限统一表达,业务动作需单独授权', async () => { const perm = await loadedStore() expect(perm.canPower('v_b_contact', 'create')).toBe(true) expect(perm.canPower('v_b_contact', 'delete')).toBe(true) expect(perm.canPower('v_b_contact', 'audit')).toBe(false) expect(perm.canPower('b_money', 'create')).toBe(false) }) it('g3soft 一切放行,且不需要任何授权数据', async () => { mockApiResponses() setLoggedInUser('g3soft', 1) const perm = usePermissionStore() await perm.load() expect(perm.canAccess('b_money')).toBe(true) expect(perm.canPower('b_money', 'audit')).toBe(true) expect(perm.canEnterMenu('user')).toBe(true) }) })