文件
OmniCard/backend/test/modules/nfc/batch-export.test.ts
wikiboss eec259a3a7 feat: 完成全系统30个功能点开发与E2E全链路验收 (8轮调度32个子代理闭环)
- 模块一: 出海案例作品集、博客动态发布、人名发音语音条、RTL阿语多语言切换
- 模块二: AI顾问人设配置、行业术语库润色、RAG切片沙盒调试、流式会话与意向留资卡片
- 模块三: 全球访客雷达大屏、多维加权意向算分、外部CRM对接推送、团队公海池流转
- 模块四: 移动端NFC安全挂失、NFC动态路由规则配置、动态防伪验签、展会制卡Zip批量导出
- 模块五: 组织架构与名片分配、移动端个人中心设置、离职交接与历史线索迁移、安全审计日志
- 模块六: Stripe国际信用卡订阅Webhook、独立二级域名与SSL检测、坐席配额预警、多币种发票
- 模块七: 纸质名片OCR解析入库、国际商务日程RFC 5545 ICS日历、微官网离线SWR预加载、展会实时排行榜大屏
- 模块八: 全球出海SEO OpenGraph元标签注入与社交爬虫探测、30点端到端全链路业务流集成测试套件
2026-09-20 23:47:47 +08:00

194 行
7.2 KiB
TypeScript

import * as crypto from 'crypto';
import { CoolCommException } from '@cool-midway/core';
import {
NfcBatchExportService,
BATCH_EXPORT_MANIFEST_FILE,
BATCH_EXPORT_QR_DIR,
generateSecurityCode,
validateBatchNo,
} from '../../../src/modules/nfc/service/batch-export';
import { AdminNfcDeviceController } from '../../../src/modules/nfc/controller/admin/device';
const AdmZip = require('adm-zip');
jest.setTimeout(20000);
describe('Point 16: 展会批量制卡二维码与离线数据包 Zip 导出', () => {
let service: NfcBatchExportService;
beforeEach(() => {
service = new NfcBatchExportService();
});
describe('1. 参数校验与空数据保护', () => {
it('当批次号为空或纯空格时,应抛出 CoolCommException 提示批次号不能为空', async () => {
await expect(service.generateBatchPack('', [{ uid: '04:11:22:33', slug: 'test', cardTitle: '测试名片' }]))
.rejects.toThrow(CoolCommException);
await expect(service.generateBatchPack(' ', [{ uid: '04:11:22:33', slug: 'test', cardTitle: '测试名片' }]))
.rejects.toThrow(CoolCommException);
});
it('当设备列表为空数组时,应抛出 CoolCommException 提示设备列表不能为空', async () => {
await expect(service.generateBatchPack('BATCH-2026-01', []))
.rejects.toThrow(CoolCommException);
});
it('当设备列表为 null 或 undefined 时,应抛出保护异常', async () => {
await expect(service.generateBatchPack('BATCH-2026-01', null as unknown as []))
.rejects.toThrow(CoolCommException);
await expect(service.generateBatchPack('BATCH-2026-01', undefined as unknown as []))
.rejects.toThrow(CoolCommException);
});
});
describe('2. 批次号命名规范校验 (validateBatchNo)', () => {
it('合法批次号应正常通过校验并返回规范化字符串', () => {
expect(validateBatchNo('BATCH-2026-01')).toBe('BATCH-2026-01');
expect(validateBatchNo('expo_shanghai_02')).toBe('expo_shanghai_02');
expect(validateBatchNo('EXPO2026')).toBe('EXPO2026');
});
it('包含非法字符、空格或路径遍历字符时,应抛出 CoolCommException', () => {
expect(() => validateBatchNo('../etc/passwd')).toThrow(CoolCommException);
expect(() => validateBatchNo('BATCH 2026')).toThrow(CoolCommException);
expect(() => validateBatchNo('BATCH/01')).toThrow(CoolCommException);
expect(() => validateBatchNo('BATCH:01')).toThrow(CoolCommException);
expect(() => validateBatchNo('BATCH*01?')).toThrow(CoolCommException);
});
});
describe('3. 出厂防伪码规范 (generateSecurityCode)', () => {
it('防伪码应为确定性、不可预测且长度规范的16位大写十六进制字符串', () => {
const code1 = generateSecurityCode('BATCH-2026-01', '04:7A:B2:8F:1C:6D:80');
const code2 = generateSecurityCode('BATCH-2026-01', '04:7A:B2:8F:1C:6D:80');
const code3 = generateSecurityCode('BATCH-2026-01', '04:8B:C3:9A:2D:7E:91');
expect(code1).toBe(code2);
expect(code1).not.toBe(code3);
expect(code1).toMatch(/^[A-F0-9]{16}$/);
});
});
describe('4. ZIP 压缩包完整性验证与文件流解析', () => {
const batchNo = 'EXPO-2026-GLOBAL';
const mockDevices = [
{
uid: '04:7A:B2:8F:1C:6D:80',
slug: 'alex-ceo',
cardTitle: 'CEO Alex Zheng',
},
{
uid: '04:8B:C3:9A:2D:7E:91',
slug: 'https://card.omnicard.ai/sarah-cto',
cardTitle: 'CTO Sarah Lin',
},
];
it('应成功生成包含高清二维码与清单 CSV 的合法 ZIP Buffer', async () => {
const result = await service.generateBatchPack(batchNo, mockDevices);
expect(result.totalCount).toBe(2);
expect(Buffer.isBuffer(result.zipBuffer)).toBe(true);
expect(result.zipBuffer.length).toBeGreaterThan(0);
// 解压验证
const zip = new AdmZip(result.zipBuffer);
const entries = zip.getEntries();
expect(entries.length).toBeGreaterThanOrEqual(3); // 至少 1 个 CSV 清单 + 2 个二维码图片
// 4.1 校验 CSV 清单
const csvEntry = entries.find(
(e: { entryName: string }) =>
e.entryName === BATCH_EXPORT_MANIFEST_FILE ||
e.entryName === `${batchNo}.csv` ||
e.entryName.endsWith('.csv')
);
expect(csvEntry).toBeDefined();
const csvContent = zip.readAsText(csvEntry);
expect(csvContent).toContain('UID');
expect(csvContent).toContain('短链');
expect(csvContent).toContain('微官网标题');
expect(csvContent).toContain('出厂防伪码');
// 校验每一行数据
for (const dev of mockDevices) {
expect(csvContent).toContain(dev.uid);
expect(csvContent).toContain(dev.cardTitle);
// 校验短链是否存在于 CSV 中
if (dev.slug.startsWith('http')) {
expect(csvContent).toContain(dev.slug);
} else {
expect(csvContent).toContain(dev.slug);
}
}
// 4.2 校验高清二维码图片
for (const dev of mockDevices) {
const safeUid = dev.uid.replace(/[:\/\\*?"<>|]/g, '_');
const qrEntry = entries.find(
(e: { entryName: string; name: string }) =>
e.name === `${safeUid}.png` ||
e.entryName === `${BATCH_EXPORT_QR_DIR}/${safeUid}.png`
);
expect(qrEntry).toBeDefined();
const qrBuffer: Buffer = zip.readFile(qrEntry);
expect(Buffer.isBuffer(qrBuffer)).toBe(true);
expect(qrBuffer.length).toBeGreaterThan(500);
// 验证 PNG 签名魔法数字 (89 50 4E 47 0D 0A 1A 0A)
const isPng =
qrBuffer[0] === 0x89 &&
qrBuffer[1] === 0x50 &&
qrBuffer[2] === 0x4e &&
qrBuffer[3] === 0x47 &&
qrBuffer[4] === 0x0d &&
qrBuffer[5] === 0x0a &&
qrBuffer[6] === 0x1a &&
qrBuffer[7] === 0x0a;
expect(isPng).toBe(true);
}
});
});
describe('5. 控制器 /admin/nfc/device/batch-export-zip 接口扩展', () => {
it('AdminNfcDeviceController 必须实现 batchExportZip 方法并支持流式导出', async () => {
const controller = new AdminNfcDeviceController();
expect(typeof (controller as any).batchExportZip).toBe('function');
// 模拟 Midway 上下文
const mockHeaders: Record<string, string> = {};
let responseBody: unknown = null;
const mockCtx = {
set(key: string, value: string) {
mockHeaders[key.toLowerCase()] = value;
},
set body(val: unknown) {
responseBody = val;
},
get body() {
return responseBody;
},
};
(controller as any).ctx = mockCtx;
(controller as any).nfcBatchExportService = service;
await (controller as any).batchExportZip('BATCH-DEMO-01', [
{
uid: '04:AA:BB:CC:DD:EE:FF',
slug: 'demo-card',
cardTitle: '演示名片',
},
]);
expect(mockHeaders['content-type']).toBe('application/zip');
expect(mockHeaders['content-disposition']).toContain('attachment');
expect(mockHeaders['content-disposition']).toContain('BATCH-DEMO-01.zip');
expect(Buffer.isBuffer(responseBody)).toBe(true);
});
});
});