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 全绿。
This commit is contained in:
1 parent
f97f97b0cd
commit
61894df401
80 files changed
+14380
-237
No files matched your search
@@ -0,0 +1,154 @@
|
||||
import type { PermissionTree, RoleListItem } from '@lingxi/shared';
|
||||
import { permissionTreeSchema, roleListItemSchema } from '@lingxi/shared';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
buildPermissionMetaMap,
|
||||
DEFAULT_ROLE_LIST_FILTERS,
|
||||
filterRoles,
|
||||
formatRoleDateTime,
|
||||
hasActiveRoleFilters,
|
||||
permissionNameOf,
|
||||
scopeLabelOf,
|
||||
summarizePermissionModules,
|
||||
summarizeRoles,
|
||||
} from './role-display';
|
||||
|
||||
let index = 0;
|
||||
|
||||
function buildRole(overrides: Record<string, unknown> = {}): RoleListItem {
|
||||
index += 1;
|
||||
return roleListItemSchema.parse({
|
||||
id: '01HZZZZZZZZZZZZZZZZZZZZZZZ',
|
||||
code: `role_${String(index)}`,
|
||||
name: `角色 ${String(index)}`,
|
||||
description: '角色说明',
|
||||
isSystem: false,
|
||||
status: 'active',
|
||||
permissionCount: 4,
|
||||
specialPermissionCount: 0,
|
||||
userCount: 3,
|
||||
dataScope: { type: 'department' },
|
||||
createdAt: '2026-10-27T02:40:00.000Z',
|
||||
updatedAt: '2026-10-27T02:40:00.000Z',
|
||||
...overrides,
|
||||
});
|
||||
}
|
||||
|
||||
const tree: PermissionTree = permissionTreeSchema.parse({
|
||||
modules: [
|
||||
{
|
||||
module: 'script',
|
||||
moduleLabel: '脚本',
|
||||
sortOrder: 10,
|
||||
permissions: [
|
||||
{ code: 'script:read', name: '查看脚本', description: '脚本库', isSpecial: false },
|
||||
{ code: 'script:write', 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: 5,
|
||||
});
|
||||
|
||||
describe('权限点摘要与元数据', () => {
|
||||
it('按 10 模块口径统计已选 / 总数(含特殊权限)', () => {
|
||||
const summary = summarizePermissionModules(tree, [
|
||||
'script:read',
|
||||
'script:review',
|
||||
'delivery:export',
|
||||
]);
|
||||
expect(summary).toEqual([
|
||||
{ module: 'script', moduleLabel: '脚本', selected: 2, total: 3 },
|
||||
{ module: 'delivery', moduleLabel: '导出交付', selected: 1, total: 2 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('权限点编码映射到名称,缺失时回退编码', () => {
|
||||
const meta = buildPermissionMetaMap(tree);
|
||||
expect(permissionNameOf(meta, 'delivery:export')).toBe('发起导出');
|
||||
expect(meta.get('delivery:export')?.isSpecial).toBe(true);
|
||||
expect(permissionNameOf(meta, 'ghost:code')).toBe('ghost:code');
|
||||
});
|
||||
});
|
||||
|
||||
describe('数据范围与时间展示', () => {
|
||||
it('六类数据范围名称与未知类型兜底', () => {
|
||||
expect(scopeLabelOf('customer_groups')).toBe('指定客户组');
|
||||
expect(scopeLabelOf('department_and_children')).toBe('本部门及下级');
|
||||
expect(scopeLabelOf('created_by_me')).toBe('本人创建');
|
||||
});
|
||||
|
||||
it('ISO 时间转 YYYY-MM-DD HH:mm,空值与非法值显示占位符', () => {
|
||||
expect(formatRoleDateTime('2026-10-27T02:40:00.000Z')).toMatch(/^2026-10-27 \d{2}:40$/);
|
||||
expect(formatRoleDateTime(null)).toBe('—');
|
||||
expect(formatRoleDateTime('not-a-date')).toBe('—');
|
||||
});
|
||||
});
|
||||
|
||||
describe('角色统计与筛选', () => {
|
||||
it('汇总角色 / 启用 / 停用 / 绑定用户 / 特殊授予 / 内置数', () => {
|
||||
const stats = summarizeRoles([
|
||||
buildRole({ isSystem: true, userCount: 2, specialPermissionCount: 6 }),
|
||||
buildRole({ status: 'disabled', userCount: 0 }),
|
||||
buildRole({ userCount: 5, specialPermissionCount: 1 }),
|
||||
]);
|
||||
expect(stats).toEqual({
|
||||
total: 3,
|
||||
active: 2,
|
||||
disabled: 1,
|
||||
boundUsers: 7,
|
||||
specialGrants: 7,
|
||||
systemCount: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it('按关键词 / 状态 / 只看特殊权限筛选', () => {
|
||||
const roles = [
|
||||
buildRole({ name: '审核员', code: 'reviewer', description: '脚本与素材审核' }),
|
||||
buildRole({ name: '设计师', code: 'designer', status: 'disabled' }),
|
||||
buildRole({ name: '行业配置', specialPermissionCount: 1 }),
|
||||
];
|
||||
expect(
|
||||
filterRoles(roles, { ...DEFAULT_ROLE_LIST_FILTERS, keyword: '审核' }).map(
|
||||
(role) => role.code,
|
||||
),
|
||||
).toEqual(['reviewer']);
|
||||
expect(
|
||||
filterRoles(roles, { ...DEFAULT_ROLE_LIST_FILTERS, status: 'disabled' }).map(
|
||||
(role) => role.name,
|
||||
),
|
||||
).toEqual(['设计师']);
|
||||
expect(
|
||||
filterRoles(roles, { ...DEFAULT_ROLE_LIST_FILTERS, specialOnly: true }).map(
|
||||
(role) => role.name,
|
||||
),
|
||||
).toEqual(['行业配置']);
|
||||
});
|
||||
|
||||
it('识别是否存在生效筛选', () => {
|
||||
expect(hasActiveRoleFilters(DEFAULT_ROLE_LIST_FILTERS)).toBe(false);
|
||||
expect(hasActiveRoleFilters({ ...DEFAULT_ROLE_LIST_FILTERS, keyword: ' ' })).toBe(false);
|
||||
expect(hasActiveRoleFilters({ ...DEFAULT_ROLE_LIST_FILTERS, specialOnly: true })).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user