import { BadRequestException } from '@nestjs/common'; import { UsersService } from './users.service'; function createPrismaMock() { const roles = new Map(); 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 } }, }); }); });