Files
lislgosms/api/src/users/users.service.spec.ts
T

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