import { describe, expect, it, vi } from 'vitest'; import { StaffService } from './staff.service.js'; import type { PrismaService } from '../prisma/prisma.service.js'; import { ErrorCodes } from '../common/error-codes.js'; import { encrypt, hash } from '../common/id-card-cipher.js'; import { runWithRequestContext } from '../common/context/request-context.js'; import type { RequestContext } from '../common/context/request-context.js'; const ctx: RequestContext = { tenantCode: 'T001', userId: 'u1', username: 'admin', super: false, dataScope: 'self', orgCode: null, orgCodes: [], }; const baseDto = { name: '张三', idCardNo: '530102199001011234', phone: '13800000000', jobType: 'nurse', workType: 'full', employStatus: 'on', }; function makeService( overrides: { staffFindFirst?: ReturnType; staffFindUnique?: ReturnType; userFindFirst?: ReturnType; create?: ReturnType; update?: ReturnType; findMany?: ReturnType; count?: ReturnType; } = {}, ) { const staffFindFirst = overrides.staffFindFirst ?? vi.fn().mockResolvedValue(null); const staffFindUnique = overrides.staffFindUnique ?? vi.fn().mockResolvedValue(null); const userFindFirst = overrides.userFindFirst ?? vi.fn().mockResolvedValue({ id: 'u9', tenantCode: 'T001' }); const create = overrides.create ?? vi.fn().mockResolvedValue({ id: 's1', ...baseDto, tenantCode: 'T001' }); const update = overrides.update ?? vi.fn().mockResolvedValue({ id: 's1' }); const findMany = overrides.findMany ?? vi.fn().mockResolvedValue([]); const count = overrides.count ?? vi.fn().mockResolvedValue(0); const prisma = { client: { staff: { findFirst: staffFindFirst, findUnique: staffFindUnique, create, update, findMany, count }, user: { findFirst: userFindFirst }, }, }; return { service: new StaffService(prisma as unknown as PrismaService), staffFindFirst, staffFindUnique, userFindFirst, create, update, findMany, count, }; } describe('StaffService', () => { it('create 自动注入 tenantCode/createdBy,身份证加密存储不落明文', async () => { const { service, create } = makeService(); await runWithRequestContext(ctx, () => service.createStaff(baseDto)); const data = create.mock.calls[0][0].data as Record; expect(data).toMatchObject({ name: '张三', phone: '13800000000', jobType: 'nurse', workType: 'full', employStatus: 'on', tenantCode: 'T001', createdBy: 'u1', }); expect(data.idCardNoHash).toBe(hash(baseDto.idCardNo)); expect(data.idCardNoEnc).toBeTruthy(); expect(data.idCardNo).toBeUndefined(); }); it('create 存 idCardNoHash 为 SHA-256、idCardNoEnc 可解密回原文', async () => { const { service, create } = makeService(); await runWithRequestContext(ctx, () => service.createStaff(baseDto)); const data = create.mock.calls[0][0].data as Record; expect(data.idCardNoHash).toBe(hash(baseDto.idCardNo)); expect(data.idCardNoEnc).not.toContain(baseDto.idCardNo); }); it('update 改身份证时重算 idCardNoHash/idCardNoEnc', async () => { const staffFindUnique = vi.fn().mockResolvedValue({ id: 's1', name: '张三' }); const { service, update } = makeService({ staffFindUnique }); await runWithRequestContext(ctx, () => service.updateStaff('s1', { idCardNo: '530102199001011234' })); const data = update.mock.calls[0][0].data as Record; expect(data.idCardNoHash).toBe(hash('530102199001011234')); expect(data.idCardNoEnc).toBeTruthy(); expect(data.idCardNo).toBeUndefined(); }); it('list 返回掩码 id_card_no,且不泄露密文/哈希列', async () => { const findMany = vi.fn().mockResolvedValue([ { id: 's1', name: '张三', idCardNoEnc: encrypt(baseDto.idCardNo), idCardNoHash: hash(baseDto.idCardNo) }, ]); const count = vi.fn().mockResolvedValue(1); const { service } = makeService({ findMany, count }); const res = await runWithRequestContext(ctx, () => service.listStaff({ page: 1, pageSize: 20 })); expect(res.list[0].idCardNo).toBe('5301**********1234'); expect(res.list[0].idCardNoEnc).toBeUndefined(); expect(res.list[0].idCardNoHash).toBeUndefined(); }); it('userId 指向的账号不存在时 create 抛 USER_NOT_FOUND', async () => { const userFindFirst = vi.fn().mockResolvedValue(null); const staffFindFirst = vi.fn(); const create = vi.fn(); const { service } = makeService({ userFindFirst, staffFindFirst, create }); await expect( runWithRequestContext(ctx, () => service.createStaff({ ...baseDto, userId: 'u9' })), ).rejects.toMatchObject({ response: { code: ErrorCodes.USER_NOT_FOUND } }); expect(userFindFirst).toHaveBeenCalledWith({ where: { id: 'u9', tenantCode: 'T001' } }); expect(staffFindFirst).not.toHaveBeenCalled(); expect(create).not.toHaveBeenCalled(); }); it('userId 已被占用时 create 抛 STAFF_USER_LINKED', async () => { const staffFindFirst = vi.fn().mockResolvedValue({ id: 's-other', userId: 'u9' }); const create = vi.fn(); const { service } = makeService({ staffFindFirst, create }); await expect( runWithRequestContext(ctx, () => service.createStaff({ ...baseDto, userId: 'u9' })), ).rejects.toMatchObject({ response: { code: ErrorCodes.STAFF_USER_LINKED } }); expect(staffFindFirst).toHaveBeenCalledWith({ where: { userId: 'u9' } }); expect(create).not.toHaveBeenCalled(); }); it('userId 为空时不校验、直接创建', async () => { const userFindFirst = vi.fn(); const staffFindFirst = vi.fn(); const { service, create } = makeService({ userFindFirst, staffFindFirst }); await runWithRequestContext(ctx, () => service.createStaff(baseDto)); expect(userFindFirst).not.toHaveBeenCalled(); expect(staffFindFirst).not.toHaveBeenCalled(); expect(create).toHaveBeenCalled(); }); });