import { beforeEach, describe, expect, it, vi } from 'vitest'; import { useAuthStore } from '@/stores/auth-store'; import { createRole, deleteRole, fetchPermissionTree, fetchRoleDetail, fetchRoleList, toRoleListSearchParams, updateRole, } from './roles'; const ROLE_ID = '01HZZZZZZZZZZZZZZZZZZZZZZ1'; const USER_ID = '01HZZZZZZZZZZZZZZZZZZZZZZ2'; const roleListItem = { id: ROLE_ID, code: 'reviewer', name: '审核员', description: '脚本与素材审核', isSystem: false, status: 'active', permissionCount: 4, specialPermissionCount: 0, userCount: 3, dataScope: { type: 'department_and_children' }, createdAt: '2026-10-27T02:40:00.000Z', updatedAt: '2026-10-27T02:40:00.000Z', }; const roleDetail = { ...roleListItem, permissions: ['script:read', 'script:review', 'material:read', 'material:review'], specialPermissions: [], boundUsers: [ { id: USER_ID, name: '赵一诺', account: 'zhao.yinuo', phoneMasked: '136****3188', departmentName: '内容生产中心 / 素材二组', status: 'active', }, ], }; const permissionTree = { modules: [ { module: 'script', moduleLabel: '脚本', sortOrder: 10, permissions: [ { code: 'script:read', name: '查看脚本', description: '脚本库', isSpecial: false }, { code: 'script:review', name: '审核脚本', description: '入库审核', isSpecial: false }, ], }, { module: 'delivery', moduleLabel: '导出交付', sortOrder: 40, permissions: [ { code: 'delivery:read', name: '查看交付记录', description: '交付清单', isSpecial: false }, { code: 'delivery:export', name: '发起导出', description: '数据出口', isSpecial: true }, ], }, ], specialPermissions: [ { code: 'delivery:export', name: '发起导出', description: '数据出口', isSpecial: true }, ], sodPairs: [ { id: 'script-write-review', label: '脚本生成 × 脚本审核', description: '同一角色不得同时拥有', permissions: ['script:write', 'script:review'], }, ], totalPermissions: 4, }; const saveResult = { role: roleDetail, affectedUserCount: 3, warnings: [], }; const jsonResponse = (body: unknown, status = 200): Response => new Response(JSON.stringify(body), { status, headers: { 'Content-Type': 'application/json' }, }); function toUrl(input: string | URL | Request | undefined): string { if (typeof input === 'string') { return input; } if (input instanceof URL) { return input.href; } return input?.url ?? ''; } function bodyOf(init: RequestInit | undefined): unknown { return typeof init?.body === 'string' ? JSON.parse(init.body) : null; } describe('角色列表请求参数', () => { it('按后端 query 约定拼装 snake_case 参数,空关键词不传', () => { const params = toRoleListSearchParams({ limit: 100, status: 'all', sort: 'created_at', order: 'desc', keyword: ' 审核 ', }); expect(params.toString()).toBe( 'limit=100&keyword=%E5%AE%A1%E6%A0%B8&status=all&sort=created_at&order=desc', ); const empty = toRoleListSearchParams({ limit: 20, status: 'all', keyword: '' }); expect(empty.has('keyword')).toBe(false); expect(empty.has('cursor')).toBe(false); }); it('带游标时完整输出', () => { const params = toRoleListSearchParams({ limit: 20, status: 'disabled', cursor: 'cursor-2' }); expect(params.get('cursor')).toBe('cursor-2'); expect(params.get('status')).toBe('disabled'); }); }); describe('角色 API 客户端', () => { beforeEach(() => { window.localStorage.clear(); useAuthStore.getState().setSession({ accessToken: 'token', user: { id: ROLE_ID, name: '灵犀管理员', account: 'admin' }, permissions: ['role:view'], }); vi.restoreAllMocks(); }); it('解析角色列表信封与详情', async () => { const fetchMock = vi .spyOn(globalThis, 'fetch') .mockResolvedValueOnce(jsonResponse({ items: [roleListItem], next_cursor: 'cursor-2' })) .mockResolvedValueOnce(jsonResponse(roleDetail)); const list = await fetchRoleList({ limit: 100, status: 'all' }); expect(list.items).toHaveLength(1); expect(list.next_cursor).toBe('cursor-2'); expect(toUrl(fetchMock.mock.calls[0]?.[0])).toContain('/roles?limit=100&status=all'); const detail = await fetchRoleDetail(ROLE_ID); expect(detail.permissions).toContain('script:review'); expect(detail.boundUsers[0]?.name).toBe('赵一诺'); expect(toUrl(fetchMock.mock.calls[1]?.[0])).toContain(`/roles/${ROLE_ID}`); }); it('解析权限点目录', async () => { vi.spyOn(globalThis, 'fetch').mockResolvedValue(jsonResponse(permissionTree)); const tree = await fetchPermissionTree(); expect(tree.totalPermissions).toBe(4); expect(tree.specialPermissions[0]?.code).toBe('delivery:export'); }); it('新建角色以 camelCase body 提交并解析保存结果', async () => { const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue(jsonResponse(saveResult)); const result = await createRole({ name: '审核员', description: '脚本与素材审核', permissions: ['script:read', 'script:review'], dataScope: { type: 'department_and_children' }, }); expect(result.affectedUserCount).toBe(3); const init = fetchMock.mock.calls[0]?.[1]; expect(init?.method).toBe('POST'); expect(bodyOf(init)).toEqual({ name: '审核员', description: '脚本与素材审核', permissions: ['script:read', 'script:review'], dataScope: { type: 'department_and_children' }, }); }); it('更新角色状态与停用原因', async () => { const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue(jsonResponse(saveResult)); await updateRole(ROLE_ID, { status: 'disabled', reason: '岗位调整' }); const init = fetchMock.mock.calls[0]?.[1]; expect(init?.method).toBe('PATCH'); expect(bodyOf(init)).toEqual({ status: 'disabled', reason: '岗位调整' }); }); it('删除角色返回 204', async () => { const fetchMock = vi .spyOn(globalThis, 'fetch') .mockResolvedValue(new Response(null, { status: 204 })); await expect(deleteRole(ROLE_ID)).resolves.toBeUndefined(); expect(fetchMock.mock.calls[0]?.[1]?.method).toBe('DELETE'); }); it('响应用共享 schema 校验:非法权限点编码直接报错', async () => { vi.spyOn(globalThis, 'fetch').mockResolvedValue( jsonResponse({ ...roleDetail, permissions: ['ghost:permission'] }), ); await expect(fetchRoleDetail(ROLE_ID)).rejects.toThrow(); }); });