113 lines
4.2 KiB
TypeScript
113 lines
4.2 KiB
TypeScript
import { BadRequestException } from '@nestjs/common';
|
|
import { UsersService } from './users.service';
|
|
|
|
function createPrismaMock() {
|
|
const roles = new Map<string, { id: string; code: string; name: string; scope: string }>();
|
|
return {
|
|
user: {
|
|
create: jest.fn().mockImplementation(({ data }) => Promise.resolve({ id: 'user-1', ...data, tenant: null, roles: [{ role: roles.get('platform_admin') ?? roles.get('enterprise_admin') }] })),
|
|
findMany: jest.fn().mockResolvedValue([]),
|
|
findFirst: jest.fn(),
|
|
findUnique: jest.fn(),
|
|
update: jest.fn(),
|
|
},
|
|
role: {
|
|
upsert: jest.fn().mockImplementation(({ where, create, update }) => {
|
|
const role = { id: `role-${where.code}`, ...create, ...update };
|
|
roles.set(where.code, role);
|
|
return Promise.resolve(role);
|
|
}),
|
|
findMany: jest.fn(),
|
|
create: jest.fn(),
|
|
},
|
|
userRole: {
|
|
upsert: jest.fn(),
|
|
deleteMany: jest.fn(),
|
|
create: jest.fn(),
|
|
},
|
|
permission: { findMany: jest.fn(), create: jest.fn() },
|
|
rolePermission: { upsert: jest.fn() },
|
|
operationLog: { create: jest.fn() },
|
|
$transaction: jest.fn((callback) => callback({
|
|
user: {
|
|
update: jest.fn().mockResolvedValue({ id: 'user-1', username: 'u', roles: [] }),
|
|
},
|
|
userRole: {
|
|
deleteMany: jest.fn(),
|
|
create: jest.fn(),
|
|
},
|
|
})),
|
|
};
|
|
}
|
|
|
|
describe('UsersService', () => {
|
|
it('requires enterprise admins to be linked to a tenant', async () => {
|
|
const service = new UsersService(createPrismaMock() as never);
|
|
await expect(service.create({
|
|
displayName: '企业管理员',
|
|
email: 'tenant@example.com',
|
|
password: 'secret1',
|
|
roleCode: 'enterprise_admin',
|
|
})).rejects.toBeInstanceOf(BadRequestException);
|
|
});
|
|
|
|
it('rejects platform admins linked to tenants', async () => {
|
|
const service = new UsersService(createPrismaMock() as never);
|
|
await expect(service.create({
|
|
tenantId: 'tenant-1',
|
|
displayName: '平台管理员',
|
|
email: 'admin@example.com',
|
|
password: 'secret1',
|
|
roleCode: 'platform_admin',
|
|
})).rejects.toThrow('platform_admin must not be linked to a tenant');
|
|
});
|
|
|
|
it('creates platform admins and writes operation logs', async () => {
|
|
const prisma = createPrismaMock();
|
|
const service = new UsersService(prisma as never);
|
|
await service.create({
|
|
displayName: '平台管理员',
|
|
email: 'admin@example.com',
|
|
phone: '13800000000',
|
|
password: 'secret1',
|
|
roleCode: 'platform_admin',
|
|
operatorId: 'operator-1',
|
|
});
|
|
expect(prisma.user.create).toHaveBeenCalledWith(expect.objectContaining({
|
|
data: expect.objectContaining({
|
|
email: 'admin@example.com',
|
|
phone: '13800000000',
|
|
status: 'active',
|
|
}),
|
|
}));
|
|
expect(prisma.operationLog.create).toHaveBeenCalledWith(expect.objectContaining({
|
|
data: expect.objectContaining({ action: 'user.created', resource: 'user' }),
|
|
}));
|
|
});
|
|
|
|
it('revokes the affected user session when a role is assigned', async () => {
|
|
const tx = {
|
|
userRole: { upsert: jest.fn().mockResolvedValue({ userId: 'user-1', roleId: 'role-1' }) },
|
|
user: { update: jest.fn().mockResolvedValue({ id: 'user-1' }) },
|
|
};
|
|
const prisma = { $transaction: jest.fn((callback) => callback(tx)) };
|
|
const service = new UsersService(prisma as never);
|
|
await service.assignRole({ userId: 'user-1', roleId: 'role-1' });
|
|
expect(tx.user.update).toHaveBeenCalledWith({ where: { id: 'user-1' }, data: { sessionVersion: { increment: 1 } } });
|
|
});
|
|
|
|
it('revokes every affected user session when role permissions change', async () => {
|
|
const tx = {
|
|
rolePermission: { upsert: jest.fn().mockResolvedValue({ roleId: 'role-1', permissionId: 'permission-1' }) },
|
|
user: { updateMany: jest.fn().mockResolvedValue({ count: 2 }) },
|
|
};
|
|
const prisma = { $transaction: jest.fn((callback) => callback(tx)) };
|
|
const service = new UsersService(prisma as never);
|
|
await service.assignPermission({ roleId: 'role-1', permissionId: 'permission-1' });
|
|
expect(tx.user.updateMany).toHaveBeenCalledWith({
|
|
where: { roles: { some: { roleId: 'role-1' } } },
|
|
data: { sessionVersion: { increment: 1 } },
|
|
});
|
|
});
|
|
});
|