ec-nest/src/staff/staff.service.spec.ts
liuyun 394d7a9731 feat(security): id_card_no 存储层 AES-256-GCM 加密 + SHA-256 哈希唯一
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-10-21 12:03:43 +08:00

161 lines
6.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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