124 行
4.2 KiB
TypeScript
124 行
4.2 KiB
TypeScript
import * as crypto from 'crypto';
|
|
import { CrmSyncService } from '../../../src/modules/crm/service/sync';
|
|
import { CrmRadarPushService } from '../../../src/modules/crm/service/push';
|
|
import { AdminCrmLeadController } from '../../../src/modules/crm/controller/admin/lead';
|
|
import { CrmLeadEntity } from '../../../src/modules/crm/entity/lead';
|
|
import { RadarTrack } from '../../../src/comm/types/omni';
|
|
|
|
describe('【BTP-20】线索流转自动化与外部 Webhook / 实时推送集成测试', () => {
|
|
let syncService: CrmSyncService;
|
|
let pushService: CrmRadarPushService;
|
|
let mockLead: CrmLeadEntity;
|
|
|
|
beforeEach(() => {
|
|
syncService = new CrmSyncService();
|
|
pushService = new CrmRadarPushService();
|
|
mockLead = new CrmLeadEntity();
|
|
mockLead.id = 101;
|
|
mockLead.name = 'John Doe';
|
|
mockLead.company = 'Acme Global Corp';
|
|
mockLead.email = 'john.doe@acme.com';
|
|
mockLead.phone = '+1-555-0199';
|
|
mockLead.status = 'hot';
|
|
mockLead.intentScore = 88;
|
|
mockLead.source = 'nfc';
|
|
});
|
|
|
|
it('1. Webhook HMAC-SHA256 签名生成与防伪防篡改验证', () => {
|
|
const secret = 'whsec_omnicard_lead_flow_test_2026';
|
|
const payload = {
|
|
event: 'crm.lead.status_changed',
|
|
timestamp: 1789924567,
|
|
data: {
|
|
leadId: mockLead.id,
|
|
previousStatus: 'new',
|
|
currentStatus: 'hot',
|
|
lead: mockLead,
|
|
},
|
|
};
|
|
|
|
const signature = syncService.generateSignature(payload, secret);
|
|
expect(signature).toBeDefined();
|
|
expect(signature).toHaveLength(64);
|
|
expect(/^[a-f0-9]{64}$/.test(signature)).toBe(true);
|
|
|
|
// 篡改测试
|
|
const tamperedPayload = { ...payload, timestamp: 1789924568 };
|
|
const tamperedSignature = syncService.generateSignature(tamperedPayload, secret);
|
|
expect(tamperedSignature).not.toBe(signature);
|
|
});
|
|
|
|
it('2. 国际 CRM 契约格式转换 (HubSpot & Salesforce)', () => {
|
|
const hubspot = syncService.formatToHubSpot(mockLead);
|
|
expect(hubspot.properties.firstname).toBe('John');
|
|
expect(hubspot.properties.lastname).toBe('Doe');
|
|
expect(hubspot.properties.company).toBe('Acme Global Corp');
|
|
expect(hubspot.properties.lifecyclestage).toBe('lead');
|
|
|
|
const sf = syncService.formatToSalesforce(mockLead);
|
|
expect(sf.FirstName).toBe('John');
|
|
expect(sf.LastName).toBe('Doe');
|
|
expect(sf.Company).toBe('Acme Global Corp');
|
|
expect(sf.Status).toBe('Open - Not Contacted');
|
|
});
|
|
|
|
it('3. 雷达实时 Push 渠道广播与定向名片监听机制 (CrmRadarPushService)', () => {
|
|
const received: any[] = [];
|
|
pushService.subscribe(101, (p) => {
|
|
received.push(p);
|
|
});
|
|
|
|
const track: RadarTrack = {
|
|
id: 1,
|
|
tenantId: 1,
|
|
cardId: 101,
|
|
visitorId: 'vis_dubai_101',
|
|
ip: '185.220.101.5',
|
|
country: 'AE',
|
|
city: 'Dubai',
|
|
touchType: 'nfc',
|
|
stayDurationMs: 15400,
|
|
scrollDepth: 85,
|
|
viewedBlocks: ['hero', 'form', 'contact'],
|
|
createdAt: new Date().toISOString(),
|
|
};
|
|
|
|
const pushResult = pushService.publish(track);
|
|
expect(pushResult.event).toBe('radar.visitor');
|
|
expect(pushResult.cardId).toBe(101);
|
|
expect(received).toHaveLength(1);
|
|
expect(received[0].visitorId).toBe('vis_dubai_101');
|
|
expect(received[0].country).toBe('AE');
|
|
expect(received[0].city).toBe('Dubai');
|
|
});
|
|
|
|
it('4. 控制器同步接口 syncExternal 校验与 Webhook 调用', async () => {
|
|
const mockRepo: any = {
|
|
findOne: jest.fn().mockResolvedValue(mockLead),
|
|
};
|
|
const controller = new AdminCrmLeadController();
|
|
controller.crmLeadEntity = mockRepo;
|
|
controller.crmSyncService = syncService;
|
|
|
|
// 模拟目标 Webhook URL 发送
|
|
jest.spyOn(syncService, 'sendWebhook').mockResolvedValueOnce({
|
|
success: true,
|
|
attempts: 1,
|
|
data: { received: true },
|
|
});
|
|
|
|
const res: any = await controller.syncExternal({
|
|
id: 101,
|
|
target: 'webhook',
|
|
webhookUrl: 'https://api.omnicard.ai/webhook/lead-flow',
|
|
webhookSecret: 'secret_lead_flow_2026',
|
|
});
|
|
|
|
expect(res.code).toBe(1000);
|
|
expect(res.data.target).toBe('webhook');
|
|
expect(res.data.leadId).toBe(101);
|
|
expect(res.data.result.success).toBe(true);
|
|
expect(res.data.result.attempts).toBe(1);
|
|
});
|
|
});
|