- 模块一: 出海案例作品集、博客动态发布、人名发音语音条、RTL阿语多语言切换 - 模块二: AI顾问人设配置、行业术语库润色、RAG切片沙盒调试、流式会话与意向留资卡片 - 模块三: 全球访客雷达大屏、多维加权意向算分、外部CRM对接推送、团队公海池流转 - 模块四: 移动端NFC安全挂失、NFC动态路由规则配置、动态防伪验签、展会制卡Zip批量导出 - 模块五: 组织架构与名片分配、移动端个人中心设置、离职交接与历史线索迁移、安全审计日志 - 模块六: Stripe国际信用卡订阅Webhook、独立二级域名与SSL检测、坐席配额预警、多币种发票 - 模块七: 纸质名片OCR解析入库、国际商务日程RFC 5545 ICS日历、微官网离线SWR预加载、展会实时排行榜大屏 - 模块八: 全球出海SEO OpenGraph元标签注入与社交爬虫探测、30点端到端全链路业务流集成测试套件
194 行
7.2 KiB
TypeScript
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);
|
|
});
|
|
});
|
|
});
|