forked from wangziqi/gongxue-base
由 OCR(open-codereview.ai,deepseek-v4-flash)审查驱动修复: - wallets 原子扣款防 double-spend;refund 条件更新幂等;findTransactions 分页 - financial/imports/occupancies/attendance 事务与 advisory lock;重复生成/提交幂等 - 矛盾校验器、日期区间、实体双映射/DECIMAL/nullable、时区统一(china-time) - rbac-seed 防重激活、exam 权限恢复、状态一致性、路由顺序、N+1/IN 分块等性能项 Reviewed-by: OCR (open-codereview.ai)
291 lines
9.8 KiB
TypeScript
291 lines
9.8 KiB
TypeScript
import { RbacService } from './rbac.service';
|
|
|
|
describe('RbacService seedData', () => {
|
|
it('seeds AI chat permission without auto-assigning it through the AI config group', async () => {
|
|
const permissions: any[] = [
|
|
{ id: 1, code: 'ai:config:read', name: '查看 AI 配置', group: 'ai' },
|
|
];
|
|
const systemAdminRole: any = {
|
|
id: 1,
|
|
name: '系统管理员',
|
|
code: 'system_admin',
|
|
description: '',
|
|
isSystem: true,
|
|
status: 1,
|
|
permissions: [permissions[0]],
|
|
users: [],
|
|
};
|
|
const permRepo = {
|
|
findOne: jest.fn(async ({ where }: any) =>
|
|
permissions.find((permission) => permission.code === where.code) ?? null,
|
|
),
|
|
create: jest.fn((value: any) => ({ id: permissions.length + 1, ...value })),
|
|
save: jest.fn(async (value: any) => {
|
|
if (!permissions.some((permission) => permission.code === value.code)) permissions.push(value);
|
|
return value;
|
|
}),
|
|
find: jest.fn(async () => permissions),
|
|
remove: jest.fn(async (value: any) => value),
|
|
};
|
|
const roleRepo = {
|
|
count: jest.fn(async () => 0),
|
|
findOne: jest.fn(async ({ where }: any) =>
|
|
where.code === 'system_admin' || where.name === '系统管理员' ? systemAdminRole : null,
|
|
),
|
|
create: jest.fn((value: any) => ({ ...value, permissions: [] })),
|
|
save: jest.fn(async (value: any) => value),
|
|
find: jest.fn(async () => [systemAdminRole]),
|
|
};
|
|
const userRepo = {
|
|
update: jest.fn(async () => ({ affected: 0 })),
|
|
count: jest.fn(async () => 1),
|
|
create: jest.fn(),
|
|
save: jest.fn(),
|
|
};
|
|
|
|
const service = new RbacService(
|
|
permRepo as never,
|
|
roleRepo as never,
|
|
userRepo as never,
|
|
{} as never,
|
|
{} as never,
|
|
{} as never,
|
|
{} as never,
|
|
{} as never,
|
|
);
|
|
|
|
await service.seedData();
|
|
|
|
expect(permissions.some((permission) => permission.code === 'ai:chat:use')).toBe(true);
|
|
expect(systemAdminRole.permissions.map((permission: any) => permission.code)).not.toContain(
|
|
'ai:chat:use',
|
|
);
|
|
});
|
|
|
|
it('migrates the legacy teacher role and replaces broad permissions with the teaching matrix', async () => {
|
|
const permissions = [
|
|
{ id: 1, code: 'profile:view', name: '查看个人资料', group: 'profile' },
|
|
{ id: 2, code: 'notification:view', name: '查看通知', group: 'notification' },
|
|
{ id: 3, code: 'student:view', name: '查看学生', group: 'student' },
|
|
{ id: 4, code: 'class:view', name: '查看班级', group: 'class' },
|
|
{ id: 5, code: 'schedule:view', name: '查看排课', group: 'schedule' },
|
|
{ id: 6, code: 'attendance:view', name: '查看考勤', group: 'attendance' },
|
|
{ id: 7, code: 'attendance:create', name: '新增考勤', group: 'attendance' },
|
|
{ id: 8, code: 'teacher-workspace:view', name: '教师工作台', group: 'teacher-workspace' },
|
|
{ id: 9, code: 'room:view', name: '查看宿舍', group: 'room' },
|
|
{ id: 10, code: 'schedule:create', name: '新增排课', group: 'schedule' },
|
|
];
|
|
const teacherRole = {
|
|
id: 1,
|
|
name: '老师',
|
|
code: 'teacher',
|
|
description: '旧角色',
|
|
isSystem: true,
|
|
status: 1,
|
|
permissions: [permissions[2], permissions[3], permissions[8]],
|
|
};
|
|
|
|
const permRepo = {
|
|
findOne: jest.fn(
|
|
async ({ where }: any) => permissions.find((permission) => permission.code === where.code) ?? null,
|
|
),
|
|
create: jest.fn((value) => value),
|
|
save: jest.fn(async (value) => value),
|
|
find: jest.fn(async (options?: any) => {
|
|
if (options?.where?.code) {
|
|
return permissions.filter((permission) => permission.code === 'profile:view');
|
|
}
|
|
return permissions;
|
|
}),
|
|
remove: jest.fn(async (value) => value),
|
|
};
|
|
const roleRepo = {
|
|
count: jest.fn(async () => 0),
|
|
findOne: jest.fn(async ({ where }: any) =>
|
|
where.code === 'teacher' || where.name === '老师' ? teacherRole : null,
|
|
),
|
|
create: jest.fn((value) => ({ ...value, permissions: [] })),
|
|
save: jest.fn(async (value) => value),
|
|
find: jest.fn(async () => [teacherRole]),
|
|
};
|
|
const userRepo = {
|
|
update: jest.fn(async () => ({ affected: 0 })),
|
|
count: jest.fn(async () => 1),
|
|
create: jest.fn(),
|
|
save: jest.fn(),
|
|
};
|
|
|
|
const service = new RbacService(
|
|
permRepo as never,
|
|
roleRepo as never,
|
|
userRepo as never,
|
|
{} as never,
|
|
{} as never,
|
|
{} as never,
|
|
{} as never,
|
|
{} as never,
|
|
);
|
|
|
|
await service.seedData();
|
|
|
|
expect(teacherRole.name).toBe('任课老师');
|
|
expect(teacherRole.permissions.map((permission) => permission.code)).toEqual(
|
|
expect.arrayContaining([
|
|
'notification:view',
|
|
'teacher-workspace:view',
|
|
'schedule:view',
|
|
'attendance:create',
|
|
]),
|
|
);
|
|
expect(teacherRole.permissions.map((permission) => permission.code)).not.toContain(
|
|
'schedule:create',
|
|
);
|
|
expect(teacherRole.permissions.map((permission) => permission.code)).not.toContain('profile:view');
|
|
expect(teacherRole.permissions.map((permission) => permission.code)).not.toContain('student:view');
|
|
expect(teacherRole.permissions.map((permission) => permission.code)).not.toContain('class:view');
|
|
expect(teacherRole.permissions.map((permission) => permission.code)).not.toContain('room:view');
|
|
});
|
|
});
|
|
|
|
describe('RbacService legacy role consolidation', () => {
|
|
it('moves users from duplicate accommodation roles before deleting the duplicates', async () => {
|
|
const permissions = [
|
|
{ id: 1, code: 'profile:view', name: '查看个人资料', group: 'profile' },
|
|
{ id: 2, code: 'room:view', name: '查看宿舍', group: 'room' },
|
|
{ id: 3, code: 'expense:view', name: '查看费用', group: 'expense' },
|
|
{ id: 4, code: 'student:basic-view', name: '学生基础信息', group: 'student-scope' },
|
|
];
|
|
const targetRole: any = {
|
|
id: 10,
|
|
name: '住宿运营管理员',
|
|
code: 'accommodation_operations',
|
|
description: '',
|
|
isSystem: true,
|
|
status: 1,
|
|
permissions: [],
|
|
users: [],
|
|
};
|
|
const legacyRole: any = {
|
|
id: 11,
|
|
name: '财务',
|
|
code: 'finance',
|
|
description: '',
|
|
isSystem: true,
|
|
status: 1,
|
|
permissions: [],
|
|
users: [{ id: 21 }],
|
|
};
|
|
const user: any = { id: 21, roles: [legacyRole] };
|
|
const permRepo = {
|
|
findOne: jest.fn(async ({ where }: any) => permissions.find((item) => item.code === where.code) ?? null),
|
|
create: jest.fn((value) => value),
|
|
save: jest.fn(async (value) => value),
|
|
find: jest.fn(async (options?: any) => {
|
|
if (options?.where?.code) {
|
|
return permissions.filter((permission) => permission.code === 'profile:view');
|
|
}
|
|
return permissions;
|
|
}),
|
|
remove: jest.fn(async (value) => value),
|
|
};
|
|
const roleRepo = {
|
|
count: jest.fn(async () => 0),
|
|
findOne: jest.fn(async () => targetRole),
|
|
create: jest.fn((value) => ({ ...value, permissions: [] })),
|
|
save: jest.fn(async (value) => value),
|
|
find: jest.fn(async () => [targetRole, legacyRole]),
|
|
remove: jest.fn(async (value) => value),
|
|
};
|
|
const userRepo = {
|
|
update: jest.fn(async () => ({ affected: 0 })),
|
|
count: jest.fn(async () => 1),
|
|
findOne: jest.fn(async () => user),
|
|
save: jest.fn(async (value) => value),
|
|
};
|
|
const service = new RbacService(
|
|
permRepo as never,
|
|
roleRepo as never,
|
|
userRepo as never,
|
|
{} as never,
|
|
{} as never,
|
|
{} as never,
|
|
{} as never,
|
|
{} as never,
|
|
);
|
|
|
|
await service.seedData();
|
|
|
|
expect(user.roles).toEqual([targetRole]);
|
|
expect(userRepo.save).toHaveBeenCalledWith(user);
|
|
expect(roleRepo.remove).toHaveBeenCalledWith(legacyRole);
|
|
});
|
|
});
|
|
|
|
describe('RbacSeedService startup gating', () => {
|
|
const makeService = (roleCount: number) => {
|
|
const permRepo = {
|
|
findOne: jest.fn(async () => null),
|
|
create: jest.fn(),
|
|
save: jest.fn(),
|
|
find: jest.fn(async () => []),
|
|
remove: jest.fn(),
|
|
};
|
|
const roleRepo = {
|
|
count: jest.fn(async () => roleCount),
|
|
findOne: jest.fn(async () => null),
|
|
create: jest.fn(),
|
|
save: jest.fn(),
|
|
find: jest.fn(async () => []),
|
|
remove: jest.fn(),
|
|
};
|
|
const userRepo = {
|
|
update: jest.fn(async () => ({ affected: 0 })),
|
|
count: jest.fn(),
|
|
create: jest.fn(),
|
|
save: jest.fn(),
|
|
findOne: jest.fn(),
|
|
};
|
|
const service = new RbacService(
|
|
permRepo as never,
|
|
roleRepo as never,
|
|
userRepo as never,
|
|
{} as never,
|
|
{} as never,
|
|
{} as never,
|
|
{} as never,
|
|
{} as never,
|
|
);
|
|
return { service, permRepo, roleRepo, userRepo };
|
|
};
|
|
|
|
it('skips re-seeding (no reactivation) when roles exist and SEED_ROLES is not set', async () => {
|
|
const original = process.env.SEED_ROLES;
|
|
delete process.env.SEED_ROLES;
|
|
try {
|
|
const { service, permRepo, roleRepo, userRepo } = makeService(3);
|
|
await service.seedData();
|
|
expect(roleRepo.count).toHaveBeenCalled();
|
|
expect(permRepo.findOne).not.toHaveBeenCalled();
|
|
expect(roleRepo.save).not.toHaveBeenCalled();
|
|
expect(userRepo.update).not.toHaveBeenCalled();
|
|
} finally {
|
|
if (original === undefined) delete process.env.SEED_ROLES;
|
|
else process.env.SEED_ROLES = original;
|
|
}
|
|
});
|
|
|
|
it('seeds when SEED_ROLES=true even if roles already exist', async () => {
|
|
const original = process.env.SEED_ROLES;
|
|
process.env.SEED_ROLES = 'true';
|
|
try {
|
|
const { service, permRepo, roleRepo } = makeService(3);
|
|
await service.seedData();
|
|
expect(permRepo.findOne).toHaveBeenCalled();
|
|
expect(roleRepo.save).toHaveBeenCalled();
|
|
} finally {
|
|
if (original === undefined) delete process.env.SEED_ROLES;
|
|
else process.env.SEED_ROLES = original;
|
|
}
|
|
});
|
|
});
|