Files
lingxi/packages/shared/src/schemas/roles.spec.ts
T
Stev_Wang 61894df401 feat(identity): 完成 A2 角色权限与数据范围全栈落地
背景:A2 开卡前完成 H1 权限门,确认权限点目录、特殊权限、SoD 三对、六类数据范围与超管隐式全集口径(ADR-001)。

改动:
- shared:30 权限点 / 10 模块目录与元数据、特殊权限 6 编码、SoD 三对、角色与权限树契约;/me 改 dataScopes 多角色并集。
- prisma:expand 迁移新增 permissions / data_scopes;种子按原型口径重写角色权限、数据范围与 8 项特殊授予。
- api:角色 CRUD / 复制 / 停用 / 删除、权限点启动同步、SoD 40304、系统角色保护、六类范围 ScopeContext 与 CustomerDirectory 端口、变更审计与 outbox。
- web:方案 B 主从分栏三角色页面(列表 / 编辑器 / 新建),权限树、范围选择、特殊权限二次确认、保存影响、SoD 冲突、只读态与模板预填。
- 测试:角色集成 13 条 + A2 e2e 1 条;A1 全量回归。

验证:format:check / lint / typecheck / test(306)/ test:int(27)/ A2 e2e(1,复跑通过)/ openapi:export 全绿。
2026-11-03 12:26:51 +08:00

224 lines
7.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, expect, it } from 'vitest';
import { ulid } from '../ulid.js';
import {
createRoleInputSchema,
permissionCodeSchema,
permissionTreeSchema,
roleDataScopeInputSchema,
roleDetailSchema,
roleListItemSchema,
roleListQuerySchema,
roleSaveResultSchema,
updateRoleInputSchema,
} from './roles.js';
const ISO = '2026-11-03T08:00:00.000Z';
function makeListItem(): Record<string, unknown> {
return {
id: ulid(),
code: 'material_operator',
name: '素材运营',
description: '素材生产、快筛、导出与归档',
isSystem: false,
status: 'active',
permissionCount: 9,
specialPermissionCount: 0,
userCount: 2,
dataScope: { type: 'owned_customers' },
createdAt: ISO,
updatedAt: ISO,
};
}
describe('roleListItemSchema / roleDetailSchema', () => {
it('列表项包含目录统计与数据范围', () => {
const parsed = roleListItemSchema.parse(makeListItem());
expect(parsed.permissionCount).toBe(9);
expect(parsed.userCount).toBe(2);
expect(parsed.dataScope.type).toBe('owned_customers');
});
it('详情在列表项基础上含普通权限、特殊权限与绑定用户', () => {
const detail = roleDetailSchema.parse({
...makeListItem(),
permissions: ['script:read', 'script:write'],
specialPermissions: ['delivery:export'],
boundUsers: [
{
id: ulid(),
name: '唐嘉',
account: 'tang.jia',
phoneMasked: '135****6641',
departmentName: '素材一组',
status: 'disabled',
},
],
});
expect(detail.permissions).toHaveLength(2);
expect(detail.specialPermissions).toEqual(['delivery:export']);
expect(detail.boundUsers[0]?.phoneMasked).toBe('135****6641');
});
it('拒绝负统计值 / 未登记权限点 / 非法状态', () => {
expect(() => roleListItemSchema.parse({ ...makeListItem(), userCount: -1 })).toThrow();
expect(() =>
roleDetailSchema.parse({
...makeListItem(),
permissions: ['script:delete_all'],
specialPermissions: [],
boundUsers: [],
}),
).toThrow('未登记的权限点编码');
expect(() => roleListItemSchema.parse({ ...makeListItem(), status: 'enabled' })).toThrow();
});
});
describe('角色输入 schema', () => {
it('新建角色:权限与特殊权限默认空,但数据范围必填(六类单选)', () => {
expect(
createRoleInputSchema.parse({ name: '运营', dataScope: { type: 'created_by_me' } }),
).toEqual({ name: '运营', dataScope: { type: 'created_by_me' } });
expect(() => createRoleInputSchema.parse({ name: '运营' })).toThrow();
});
it('权限点必须已登记,且特殊权限单独字段提交', () => {
expect(() =>
createRoleInputSchema.parse({
name: '运营',
dataScope: { type: 'all' },
permissions: ['script:delete_all'],
}),
).toThrow('未登记的权限点编码');
expect(() =>
createRoleInputSchema.parse({
name: '运营',
dataScope: { type: 'all' },
permissions: ['delivery:export'],
}),
).toThrow('必须放入 specialPermissions');
expect(() =>
createRoleInputSchema.parse({
name: '运营',
dataScope: { type: 'all' },
specialPermissions: ['script:read'],
}),
).toThrow('不能放入 specialPermissions');
const parsed = createRoleInputSchema.parse({
name: '运营',
dataScope: { type: 'customer_groups', targetIds: [] },
permissions: ['script:read'],
specialPermissions: ['delivery:export'],
});
expect(parsed.permissions).toEqual(['script:read']);
expect(parsed.specialPermissions).toEqual(['delivery:export']);
});
it('未知字段被拒绝(strict)', () => {
expect(() =>
createRoleInputSchema.parse({ name: '运营', dataScope: { type: 'all' }, unknown: 1 }),
).toThrow();
expect(() => updateRoleInputSchema.parse({ copyFromRoleId: ulid() })).toThrow();
});
it('更新支持 status enabled / disabled 与停用原因', () => {
expect(updateRoleInputSchema.parse({ status: 'disabled', reason: '岗位合并' })).toEqual({
status: 'disabled',
reason: '岗位合并',
});
expect(() => updateRoleInputSchema.parse({ status: 'active' })).toThrow();
});
it('权限点编码 schema 与目录同源', () => {
expect(permissionCodeSchema.parse('role:view')).toBe('role:view');
expect(() => permissionCodeSchema.parse('role:delete')).toThrow();
});
});
describe('roleDataScopeInputSchema', () => {
it('六类范围可用;指定客户组可带 targetIds', () => {
expect(roleDataScopeInputSchema.parse({ type: 'all' })).toEqual({ type: 'all' });
const groups = roleDataScopeInputSchema.parse({
type: 'customer_groups',
targetIds: [ulid()],
});
expect(groups.targetIds).toHaveLength(1);
expect(roleDataScopeInputSchema.parse({ type: 'customer_groups', targetIds: [] })).toEqual({
type: 'customer_groups',
targetIds: [],
});
});
it('非「指定客户组」范围不允许 targetIds', () => {
expect(() => roleDataScopeInputSchema.parse({ type: 'all', targetIds: [ulid()] })).toThrow(
'只有「指定客户组」范围允许填写 targetIds',
);
});
});
describe('roleListQuerySchema', () => {
it('默认 status=active(兼容 A1 角色多选)、sort=created_at、order=desc、limit=20', () => {
expect(roleListQuerySchema.parse({})).toEqual({
status: 'active',
sort: 'created_at',
order: 'desc',
limit: 20,
});
});
it('白名单校验排序与状态', () => {
expect(roleListQuerySchema.parse({ sort: 'name', order: 'asc', status: 'disabled' }).sort).toBe(
'name',
);
expect(() => roleListQuerySchema.parse({ sort: 'permission_count' })).toThrow();
expect(() => roleListQuerySchema.parse({ status: 'enabled' })).toThrow();
});
});
describe('permissionTreeSchema / roleSaveResultSchema', () => {
it('权限树含模块、特殊权限与 SoD 对', () => {
const parsed = permissionTreeSchema.parse({
modules: [
{
module: 'script',
moduleLabel: '脚本',
sortOrder: 50,
permissions: [
{ code: 'script:read', name: '脚本查看', description: '', isSpecial: false },
],
},
],
specialPermissions: [
{ code: 'delivery:export', name: '交付导出', description: '', isSpecial: true },
],
sodPairs: [
{
id: 'script-write-review',
label: '脚本生成 × 脚本审核',
description: '',
permissions: ['script:write', 'script:review'],
},
],
totalPermissions: 30,
});
expect(parsed.modules).toHaveLength(1);
expect(parsed.totalPermissions).toBe(30);
});
it('保存结果含角色详情、影响用户数与软提示', () => {
const result = roleSaveResultSchema.parse({
role: {
...makeListItem(),
permissions: [],
specialPermissions: [],
boundUsers: [],
},
affectedUserCount: 3,
warnings: ['角色数量已达建议上限 20'],
});
expect(result.affectedUserCount).toBe(3);
expect(result.warnings).toHaveLength(1);
});
});