test: harden business boundary conditions
This commit is contained in:
95
apps/server/src/rbac/rbac.boundary.spec.ts
Normal file
95
apps/server/src/rbac/rbac.boundary.spec.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user