ec-nest/src/document/document.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

222 lines
8.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 { DocumentService, EXPORT_NOTICE } from './document.service.js';
import type { PrismaService } from '../prisma/prisma.service.js';
import { ErrorCodes } from '../common/error-codes.js';
import { encrypt } 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: [],
};
function makeService(
overrides: {
elderFindUnique?: ReturnType<typeof vi.fn>;
agencyFindUnique?: ReturnType<typeof vi.fn>;
planFindMany?: ReturnType<typeof vi.fn>;
planItemFindMany?: ReturnType<typeof vi.fn>;
recordFindMany?: ReturnType<typeof vi.fn>;
recordItemFindMany?: ReturnType<typeof vi.fn>;
recordAbnormalFindMany?: ReturnType<typeof vi.fn>;
} = {},
) {
const elderFindUnique = overrides.elderFindUnique ?? vi.fn().mockResolvedValue(null);
const agencyFindUnique = overrides.agencyFindUnique ?? vi.fn().mockResolvedValue({ code: 'T001', orgName: '测试机构' });
const planFindMany = overrides.planFindMany ?? vi.fn().mockResolvedValue([]);
const planItemFindMany = overrides.planItemFindMany ?? vi.fn().mockResolvedValue([]);
const recordFindMany = overrides.recordFindMany ?? vi.fn().mockResolvedValue([]);
const recordItemFindMany = overrides.recordItemFindMany ?? vi.fn().mockResolvedValue([]);
const recordAbnormalFindMany = overrides.recordAbnormalFindMany ?? vi.fn().mockResolvedValue([]);
const prisma = {
client: {
elder: { findUnique: elderFindUnique },
agency: { findUnique: agencyFindUnique },
plan: { findMany: planFindMany },
planItem: { findMany: planItemFindMany },
record: { findMany: recordFindMany },
recordItem: { findMany: recordItemFindMany },
recordAbnormal: { findMany: recordAbnormalFindMany },
},
};
const service = new DocumentService(prisma as unknown as PrismaService);
return {
service,
elderFindUnique,
agencyFindUnique,
planFindMany,
planItemFindMany,
recordFindMany,
recordItemFindMany,
recordAbnormalFindMany,
};
}
describe('DocumentService.export', () => {
it('老人不存在 → ELDER_NOT_FOUND', async () => {
const { service, recordFindMany } = makeService();
await expect(
runWithRequestContext(ctx, () => service.export('missing')),
).rejects.toMatchObject({ response: { code: ErrorCodes.ELDER_NOT_FOUND } });
expect(recordFindMany).not.toHaveBeenCalled();
});
it('组装一人一档:掩码身份证 + 日期转 YYYY-MM-DD + 计划/记录/异常 + 水印', async () => {
const elder = {
id: 'e1',
name: '张三',
gender: '1',
birthDate: new Date('1950-01-01T00:00:00Z'),
idCardNoEnc: encrypt('530102195001010011'),
phone: '13800000000',
address: '昆明市',
insurNo: 'I001',
careLevel: '重度',
treatStatus: 'active',
benefitStart: new Date('2026-01-01T00:00:00Z'),
benefitEnd: null,
};
const plans = [
{
id: 'p1',
elderId: 'e1',
planPeriodStart: new Date('2026-10-01T00:00:00Z'),
planPeriodEnd: new Date('2026-10-31T00:00:00Z'),
careLevel: '重度',
careMode: 'home_org',
planStatus: 'active',
},
];
const planItems = [
{ id: 'pi1', planId: 'p1', svcId: 'svc1', svcNameSnapshot: '协助翻身', freqType: 'weekly', freqValue: 3, durationMin: 30, photoTier: 'must' },
];
const records = [
{
id: 'r1',
elderId: 'e1',
checkinTime: new Date('2026-10-15T02:00:00Z'),
checkoutTime: new Date('2026-10-15T03:00:00Z'),
durationMin: 60,
recordStatus: 'submitted',
},
];
const recordItems = [
{ id: 'ri1', recordId: 'r1', svcId: 'svc1', svcNameSnapshot: '协助翻身', execResult: 'done', undoReason: null, extraFlag: false },
];
const abnormals = [
{ id: 'a1', elderId: 'e1', abnormalType: '跌倒', abnormalDesc: '轻微', handleStatus: 'open' },
];
const {
service,
elderFindUnique,
agencyFindUnique,
planFindMany,
planItemFindMany,
recordFindMany,
recordItemFindMany,
recordAbnormalFindMany,
} = makeService({
elderFindUnique: vi.fn().mockResolvedValue(elder),
planFindMany: vi.fn().mockResolvedValue(plans),
planItemFindMany: vi.fn().mockResolvedValue(planItems),
recordFindMany: vi.fn().mockResolvedValue(records),
recordItemFindMany: vi.fn().mockResolvedValue(recordItems),
recordAbnormalFindMany: vi.fn().mockResolvedValue(abnormals),
});
const res = await runWithRequestContext(ctx, () => service.export('e1', '2026-10-01', '2026-10-31'));
// 定位与范围
expect(elderFindUnique).toHaveBeenCalledWith({ where: { id: 'e1' } });
expect(agencyFindUnique).toHaveBeenCalledWith({ where: { code: 'T001' } });
expect(planFindMany).toHaveBeenCalledWith({ where: { elderId: 'e1' }, orderBy: { createdAt: 'desc' } });
expect(planItemFindMany).toHaveBeenCalledWith({ where: { planId: { in: ['p1'] } } });
expect(recordFindMany).toHaveBeenCalledWith({
where: {
elderId: 'e1',
checkinTime: {
gte: new Date('2026-10-01T00:00:00+08:00'),
lte: new Date('2026-10-31T23:59:59.999+08:00'),
},
},
orderBy: { checkinTime: 'desc' },
});
expect(recordItemFindMany).toHaveBeenCalledWith({ where: { recordId: { in: ['r1'] } } });
expect(recordAbnormalFindMany).toHaveBeenCalledWith({ where: { elderId: 'e1' } });
// 水印:机构名 + 时间 + 操作人(username)+ R4 文案
expect(res.watermark).toMatchObject({
orgName: '测试机构',
operator: 'admin',
notice: EXPORT_NOTICE,
});
expect(res.watermark.exportTime).toEqual(expect.any(String));
// 老人信息:身份证掩码 + 日期转 YYYY-MM-DD
expect(res.elder).toEqual({
name: '张三',
gender: '1',
birthDate: '1950-01-01',
idCardNo: '5301**********0011',
phone: '13800000000',
address: '昆明市',
insurNo: 'I001',
careLevel: '重度',
treatStatus: 'active',
benefitStart: '2026-01-01',
benefitEnd: null,
});
// 计划 + 计划项
expect(res.plans).toEqual([
{
id: 'p1',
planPeriodStart: '2026-10-01',
planPeriodEnd: '2026-10-31',
careLevel: '重度',
careMode: 'home_org',
planStatus: 'active',
items: [{ svcId: 'svc1', svcName: '协助翻身', freqType: 'weekly', freqValue: 3, durationMin: 30, photoTier: 'must' }],
},
]);
// 记录 + 记录项(checkin/checkout 转 ISO)
expect(res.records).toEqual([
{
id: 'r1',
checkinTime: new Date('2026-10-15T02:00:00Z').toISOString(),
checkoutTime: new Date('2026-10-15T03:00:00Z').toISOString(),
durationMin: 60,
recordStatus: 'submitted',
items: [{ svcId: 'svc1', svcName: '协助翻身', execResult: 'done', undoReason: null, extraFlag: false }],
},
]);
// 异常
expect(res.abnormals).toEqual([{ id: 'a1', type: '跌倒', desc: '轻微', handleStatus: 'open' }]);
});
it('无日期范围时不加 checkinTime 条件', async () => {
const elder = { id: 'e1', name: '张三', gender: '1', birthDate: new Date('1950-01-01T00:00:00Z'), idCardNoEnc: encrypt('530102195001010011'), phone: null, address: '昆明市', insurNo: 'I001', careLevel: '重度', treatStatus: 'active', benefitStart: null, benefitEnd: null };
const recordFindMany = vi.fn().mockResolvedValue([]);
const { service } = makeService({
elderFindUnique: vi.fn().mockResolvedValue(elder),
recordFindMany,
});
await runWithRequestContext(ctx, () => service.export('e1'));
expect(recordFindMany).toHaveBeenCalledWith({ where: { elderId: 'e1' }, orderBy: { checkinTime: 'desc' } });
});
});