Files
gongxue-base/apps/server/src/rbac/rbac.boundary.spec.ts

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' });
});
});