102 lines
3.3 KiB
TypeScript
102 lines
3.3 KiB
TypeScript
import { ValidationPipe } from '@nestjs/common';
|
|
import { RbacService } from './rbac.service';
|
|
import { CreateRoleDto, CreateUserDto, UpdateUserDto } from './dto/rbac.dto';
|
|
|
|
function makeService(overrides?: {
|
|
permRepo?: Record<string, jest.Mock>;
|
|
roleRepo?: Record<string, jest.Mock>;
|
|
userRepo?: Record<string, jest.Mock>;
|
|
}) {
|
|
const permRepo = {
|
|
findByIds: jest.fn().mockResolvedValue([]),
|
|
...(overrides?.permRepo ?? {}),
|
|
};
|
|
const roleRepo = {
|
|
create: jest.fn((value) => ({ ...value })),
|
|
save: jest.fn(async (value) => value),
|
|
findByIds: jest.fn().mockResolvedValue([]),
|
|
findOneOrFail: jest.fn(),
|
|
...(overrides?.roleRepo ?? {}),
|
|
};
|
|
const userRepo = {
|
|
create: jest.fn((value) => ({ ...value })),
|
|
save: jest.fn(async (value) => value),
|
|
findOne: jest.fn().mockResolvedValue(null),
|
|
...(overrides?.userRepo ?? {}),
|
|
};
|
|
return {
|
|
service: new RbacService(
|
|
permRepo as never,
|
|
roleRepo as never,
|
|
userRepo as never,
|
|
{} as never,
|
|
{} as never,
|
|
{} as never,
|
|
{} as never,
|
|
{} as never,
|
|
),
|
|
permRepo,
|
|
roleRepo,
|
|
userRepo,
|
|
};
|
|
}
|
|
|
|
describe('RBAC mutation boundaries', () => {
|
|
it('rejects a role when any requested permission id does not exist', async () => {
|
|
const { service, roleRepo } = makeService({
|
|
permRepo: { findByIds: jest.fn().mockResolvedValue([{ id: 1, code: 'student:view' }]) },
|
|
});
|
|
|
|
await expect(service.createRole({ name: 'partial', permissionIds: [1, 999] })).rejects.toThrow(
|
|
'权限不存在: 999',
|
|
);
|
|
expect(roleRepo.save).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('rejects a user when any requested role id does not exist', async () => {
|
|
const { service, userRepo } = makeService({
|
|
roleRepo: { findByIds: jest.fn().mockResolvedValue([{ id: 2, name: '老师' }]) },
|
|
});
|
|
|
|
await expect(
|
|
service.createUser({
|
|
username: 'alice',
|
|
password: 'secret',
|
|
name: 'Alice',
|
|
roleIds: [2, 404],
|
|
}),
|
|
).rejects.toThrow('角色不存在: 404');
|
|
expect(userRepo.save).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('allows explicitly clearing all roles from an existing user', async () => {
|
|
const user = { id: 7, username: 'alice', name: 'Alice', roles: [{ id: 2 }] };
|
|
const { service, userRepo } = makeService({
|
|
userRepo: { findOne: jest.fn().mockResolvedValue(user) },
|
|
});
|
|
|
|
await expect(service.updateUser(7, { roleIds: [] })).resolves.toEqual({ message: '更新成功' });
|
|
expect(user.roles).toEqual([]);
|
|
expect(userRepo.save).toHaveBeenCalledWith(user);
|
|
});
|
|
});
|
|
|
|
describe('RBAC DTO id arrays', () => {
|
|
const pipe = new ValidationPipe({ transform: true, whitelist: true });
|
|
|
|
it.each([
|
|
[CreateRoleDto, { name: 'role', permissionIds: [1, '2'] }],
|
|
[CreateRoleDto, { name: 'role', permissionIds: [1, 1] }],
|
|
[CreateUserDto, { username: 'alice', password: 'secret', name: 'Alice', roleIds: [0] }],
|
|
[UpdateUserDto, { roleIds: [1.5] }],
|
|
])('rejects invalid, duplicate, or non-positive ids for %p', async (metatype, value) => {
|
|
await expect(pipe.transform(value, { type: 'body', metatype })).rejects.toBeDefined();
|
|
});
|
|
|
|
it('strips the retired isActive field from account updates', async () => {
|
|
await expect(
|
|
pipe.transform({ name: 'Alice', isActive: false }, { type: 'body', metatype: UpdateUserDto }),
|
|
).resolves.toEqual({ name: 'Alice' });
|
|
});
|
|
});
|