Files
lingxi/packages/shared/src/permissions.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

226 lines
7.1 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 {
findSodConflicts,
isPermissionCode,
isSpecialPermission,
parsePermission,
Permission,
PERMISSION_CATALOG,
PERMISSION_CODES,
PERMISSION_MODULES,
permissionMetaOf,
permissionsByModule,
ROLE_SOFT_LIMIT,
SOD_PAIRS,
SPECIAL_PERMISSIONS,
splitPermissions,
SUPER_ADMIN_SOFT_LIMIT,
} from './permissions.js';
describe('权限点目录', () => {
it('H1 口径:共 30 个权限点,编码全局唯一', () => {
expect(PERMISSION_CODES).toHaveLength(30);
expect(PERMISSION_CATALOG).toHaveLength(30);
expect(new Set(PERMISSION_CODES).size).toBe(30);
});
it('全部符合 模块:动作 命名且模块为小写', () => {
for (const code of PERMISSION_CODES) {
expect(code).toMatch(/^[a-z][a-z_]*:[a-z][a-z_]*$/);
expect(parsePermission(code)).not.toBeNull();
}
});
it('保留既有 read / write 编码,并补齐 H1 新增的 5 个权限点', () => {
for (const code of [
'script:read',
'script:write',
'material:read',
'material:write',
'asset:read',
'asset:write',
'delivery:read',
'customer:read',
]) {
expect(PERMISSION_CODES).toContain(code);
}
expect(Permission.ROLE_VIEW).toBe('role:view');
expect(Permission.CUSTOMER_FIELD_APPLY).toBe('customer_field:apply');
expect(Permission.CUSTOMER_TRANSFER).toBe('customer:transfer');
expect(Permission.CUSTOMER_GROUP_MANAGE).toBe('customer_group:manage');
expect(Permission.ASSET_DELETE).toBe('asset:delete');
});
it('权限树为 10 个模块,顺序与名称对齐 design/a2-ui-v1/brief.md', () => {
const groups = permissionsByModule();
expect(groups).toHaveLength(10);
expect(PERMISSION_MODULES).toHaveLength(10);
expect(groups.map((group) => group.module)).toEqual([
'script',
'material',
'asset',
'delivery',
'customer',
'customer_group',
'identity',
'industry',
'platform',
'audit',
]);
expect(groups.map((group) => group.moduleLabel)).toEqual([
'脚本',
'素材',
'资产',
'导出交付',
'客户',
'客户组',
'组织与角色',
'行业包',
'平台配置',
'审计',
]);
expect(groups.map((group) => group.sortOrder)).toEqual([
10, 20, 30, 40, 50, 60, 70, 80, 90, 100,
]);
expect(groups.flatMap((group) => group.permissions)).toHaveLength(30);
});
it('权限点显示名与说明与 UI 原型一致(抽样关键项)', () => {
const nameOf = (code: string): { name: string; description: string } => {
const meta = permissionMetaOf(code);
if (meta === undefined) {
throw new Error(`权限点未登记:${code}`);
}
return { name: meta.name, description: meta.description };
};
expect(nameOf('script:read')).toEqual({ name: '查看脚本', description: '脚本库、模板与版本' });
expect(nameOf('delivery:export')).toEqual({
name: '发起导出',
description: '数据离开平台的唯一出口(特殊)',
});
expect(nameOf('user:manage')).toEqual({ name: '管理用户', description: '新建、停用、转交' });
expect(nameOf('customer_group:manage')).toEqual({
name: '管理客户组',
description: '客户组增改与成员',
});
expect(nameOf('audit:export')).toEqual({
name: '导出审计',
description: '审计导出并留痕(特殊)',
});
});
it('目录元数据完整(编码 / 模块 / 名称 / 说明 / 特殊标记 / 排序)', () => {
for (const item of PERMISSION_CATALOG) {
expect(permissionMetaOf(item.code)).toEqual(item);
expect(item.name.length).toBeGreaterThan(0);
expect(item.description.length).toBeGreaterThan(0);
expect(item.moduleLabel.length).toBeGreaterThan(0);
expect(item.isSpecial).toBe(isSpecialPermission(item.code));
}
const sortOrders = PERMISSION_CATALOG.map((item) => item.sortOrder);
expect(sortOrders).toEqual([...sortOrders].sort((a, b) => a - b));
});
it('特殊权限为 6 个编码(5 类),均已在目录登记', () => {
expect(SPECIAL_PERMISSIONS).toHaveLength(6);
expect([...SPECIAL_PERMISSIONS].sort()).toEqual(
[
'delivery:export',
'customer_field:approve',
'industry:publish',
'ai:configure',
'audit:read',
'audit:export',
].sort(),
);
for (const code of SPECIAL_PERMISSIONS) {
expect(PERMISSION_CODES).toContain(code);
expect(isSpecialPermission(code)).toBe(true);
}
expect(isSpecialPermission('script:write')).toBe(false);
});
it('软上限取自 PRD 建议值', () => {
expect(ROLE_SOFT_LIMIT).toBe(20);
expect(SUPER_ADMIN_SOFT_LIMIT).toBe(2);
});
});
describe('SoD 三对', () => {
it('互斥对为三对,且两侧都是已登记权限点', () => {
expect(SOD_PAIRS).toHaveLength(3);
expect(SOD_PAIRS.map((pair) => pair.id)).toEqual([
'script-write-review',
'industry-manage-publish',
'customer-field-apply-approve',
]);
for (const pair of SOD_PAIRS) {
for (const code of pair.permissions) {
expect(PERMISSION_CODES).toContain(code);
}
}
});
it('同时拥有互斥两侧即命中;只拥有单侧不命中', () => {
expect(findSodConflicts(['script:write', 'script:review'])).toEqual([
{
pairId: 'script-write-review',
label: '脚本生成 × 脚本审核',
permissions: ['script:write', 'script:review'],
},
]);
expect(findSodConflicts(['industry:manage', 'industry:publish']).map((c) => c.pairId)).toEqual([
'industry-manage-publish',
]);
expect(
findSodConflicts(['customer_field:apply', 'customer_field:approve']).map((c) => c.pairId),
).toEqual(['customer-field-apply-approve']);
expect(findSodConflicts(['script:write', 'script:read'])).toEqual([]);
expect(findSodConflicts([])).toEqual([]);
});
});
describe('splitPermissions', () => {
it('拆分普通权限与特殊权限并去重排序', () => {
expect(
splitPermissions([
'script:write',
'delivery:export',
'script:read',
'delivery:export',
'audit:read',
]),
).toEqual({
permissions: ['script:read', 'script:write'],
specialPermissions: ['audit:read', 'delivery:export'],
});
});
it('未登记编码抛错(调用方映射 40001)', () => {
expect(() => splitPermissions(['script:delete_all'])).toThrow(
'INVALID_PERMISSION_CODE:script:delete_all',
);
});
});
describe('parsePermission', () => {
it('拆分模块与动作', () => {
expect(parsePermission('material:render')).toEqual({ module: 'material', action: 'render' });
});
it('非法格式返回 null', () => {
expect(parsePermission('scriptread')).toBeNull();
expect(parsePermission(':read')).toBeNull();
expect(parsePermission('script:')).toBeNull();
expect(parsePermission('a:b:c')).toBeNull();
});
});
describe('isPermissionCode', () => {
it('只认可登记值', () => {
expect(isPermissionCode(Permission.SCRIPT_READ)).toBe(true);
expect(isPermissionCode('script:delete_all')).toBe(false);
});
});