20260920220520
This commit is contained in:
1 parent
6f03d92425
commit
b3364fe6dc
44 files changed
+2313
-2737
No files matched your search
@@ -1,7 +1,14 @@
|
||||
/**
|
||||
* permissions store(新表口径,设计文档 §14)。
|
||||
*
|
||||
* 数据来源:s_module(启用模块)+ s_power(启用权限点)+ s_user_power(用户授权)。
|
||||
* 判断口径:白名单(无授权即无权限);标准操作由 module.* 统一表达,
|
||||
* 业务动作走 action.{模块}.{动作};菜单入口走 menu.*。
|
||||
*/
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
||||
import { setActivePinia, createPinia } from 'pinia'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { usePermissionStore } from '@/stores/permissions'
|
||||
import { createTestPinia } from './helpers/app.js'
|
||||
|
||||
// mock 后端数据接口,避免真实网络请求
|
||||
vi.mock('@/services/api', () => ({
|
||||
@@ -11,58 +18,69 @@ vi.mock('@/services/api', () => ({
|
||||
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: '1',
|
||||
b_code: 'm1',
|
||||
b_name: 'M1',
|
||||
b_canuse: 1,
|
||||
b_canmenu: 1,
|
||||
b_module_type: 'directory',
|
||||
b_parent_id: null,
|
||||
b_icon: '',
|
||||
b_route: '',
|
||||
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',
|
||||
},
|
||||
]
|
||||
const mockUserModules = [{ b_id: '1', b_module_id: '1', b_user_id: '123' }]
|
||||
const mockUserPowers = []
|
||||
const mockPowers = []
|
||||
|
||||
function mockApiResponses() {
|
||||
loadDataApi.mockImplementation((view) => {
|
||||
const data =
|
||||
view === 's_module'
|
||||
? mockModules
|
||||
: view === 'b_user_module'
|
||||
? mockUserModules
|
||||
: view === 'b_user_power'
|
||||
? mockUserPowers
|
||||
: view === 's_module_power'
|
||||
? mockPowers
|
||||
: []
|
||||
return Promise.resolve({ data, code: 0, success: true })
|
||||
})
|
||||
/** 用户 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() {
|
||||
function setLoggedInUser(account = 'u1', id = 123) {
|
||||
const auth = useAuthStore()
|
||||
auth.setSession({ token: 't', user: { id: 123, account: 'u1', name: 'U' } }, 'ORG')
|
||||
auth.setSession({ token: 't', user: { id, account, name: 'U' } }, 'ORG')
|
||||
}
|
||||
|
||||
// 冲刷微任务:让 setSession 触发的 watch(clear) 先执行,隔离“手动 clear”的断言
|
||||
const flush = () => Promise.resolve()
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
localStorage.clear()
|
||||
sessionStorage.clear()
|
||||
createTestPinia()
|
||||
loadDataApi.mockReset()
|
||||
})
|
||||
|
||||
describe('permissions store', () => {
|
||||
it('load 成功后写入 modules 且记录 loadedPrincipalKey', async () => {
|
||||
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 () => {
|
||||
@@ -71,8 +89,8 @@ describe('permissions store', () => {
|
||||
const perm = usePermissionStore()
|
||||
await perm.load()
|
||||
await perm.load()
|
||||
// 首次发 4 个接口调用;第二次因主体已加载直接 return
|
||||
expect(loadDataApi).toHaveBeenCalledTimes(4)
|
||||
// 首次发 3 个接口调用(s_module / s_power / s_user_power);第二次因主体已加载直接 return
|
||||
expect(loadDataApi).toHaveBeenCalledTimes(3)
|
||||
})
|
||||
|
||||
it('登录场景:load 进行中触发 clear(主体相同)不作废结果', async () => {
|
||||
@@ -95,27 +113,64 @@ describe('permissions store', () => {
|
||||
const perm = usePermissionStore()
|
||||
const p = perm.load() // 主体 ORG:123 进行中
|
||||
// 模拟切换账号:authStore 变为另一主体,watch 触发 clear
|
||||
const auth = useAuthStore()
|
||||
auth.setSession({ token: 't2', user: { id: 999, account: 'u2', name: 'U2' } }, 'ORG2')
|
||||
setLoggedInUser('u2', 999)
|
||||
perm.clear()
|
||||
await p
|
||||
// 旧请求被作废:结果丢弃,modules 仍为空(且未触发新主体的 load)
|
||||
expect(perm.modules.length).toBe(0)
|
||||
})
|
||||
|
||||
it('g3soft 提权:仅加载全部启用模块', async () => {
|
||||
loadDataApi.mockImplementation((view) =>
|
||||
Promise.resolve({
|
||||
data: view === 's_module' ? mockModules : [],
|
||||
code: 0,
|
||||
success: true,
|
||||
}),
|
||||
)
|
||||
const auth = useAuthStore()
|
||||
auth.setSession({ token: 't', user: { id: 1, account: 'g3soft', name: 'G' } }, 'ORG')
|
||||
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)
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user