161 lines
6.0 KiB
TypeScript
161 lines
6.0 KiB
TypeScript
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<typeof vi.fn>;
|
||
staffFindUnique?: ReturnType<typeof vi.fn>;
|
||
userFindFirst?: ReturnType<typeof vi.fn>;
|
||
create?: ReturnType<typeof vi.fn>;
|
||
update?: ReturnType<typeof vi.fn>;
|
||
findMany?: ReturnType<typeof vi.fn>;
|
||
count?: ReturnType<typeof vi.fn>;
|
||
} = {},
|
||
) {
|
||
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<string, unknown>;
|
||
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<string, unknown>;
|
||
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<string, unknown>;
|
||
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();
|
||
});
|
||
});
|