import { LeadWebhookDispatcher, computeWebhookSignature, verifyWebhookSignature, LeadWebhookPayload, } from '../../../src/modules/crm/service/webhook-dispatcher'; describe('Point 51: 商机实时 Webhook 触发器与 HMAC-SHA256 签名分发', () => { let dispatcher: LeadWebhookDispatcher; const mockSecret = 'whsec_8f9a2b4c6e1d7g5h3j8k9l0m'; const mockPayload: LeadWebhookPayload = { event: 'lead.created', timestamp: Date.now(), tenantId: 'tenant-uae-101', data: { leadId: 501, name: 'Sheikh Khalid', company: 'Dubai Financial Group', score: 95, channel: 'nfc_tap', }, }; beforeEach(() => { mockPayload.timestamp = Date.now(); dispatcher = new LeadWebhookDispatcher(); }); describe('1. computeWebhookSignature & verifyWebhookSignature (HMAC-SHA256 签名验签)', () => { it('应计算规范的 sha256 签名串', () => { const payloadStr = JSON.stringify(mockPayload); const sig = computeWebhookSignature(payloadStr, mockSecret, mockPayload.timestamp); expect(sig).toHaveLength(64); expect(/^[0-9a-f]{64}$/i.test(sig)).toBe(true); // 验签通过 const isValid = verifyWebhookSignature( payloadStr, sig, mockSecret, mockPayload.timestamp ); expect(isValid).toBe(true); }); it('篡改 Payload 任何内容时验签应失败', () => { const payloadStr = JSON.stringify(mockPayload); const sig = computeWebhookSignature(payloadStr, mockSecret, mockPayload.timestamp); const tamperedStr = JSON.stringify({ ...mockPayload, tenantId: 'tampered-tenant' }); expect(verifyWebhookSignature(tamperedStr, sig, mockSecret, mockPayload.timestamp)).toBe(false); }); it('超出 5 分钟 (300,000ms) 时钟窗口的请求应被拒绝', () => { const oldTs = Date.now() - 6 * 60 * 1000; const payloadStr = JSON.stringify({ ...mockPayload, timestamp: oldTs }); const sig = computeWebhookSignature(payloadStr, mockSecret, oldTs); expect(verifyWebhookSignature(payloadStr, sig, mockSecret, oldTs)).toBe(false); }); }); describe('2. LeadWebhookDispatcher 异步分发与重试逻辑', () => { it('目标端成功接收 (200 OK) 时标记成功', async () => { const mockHttpPoster = jest.fn(async () => ({ statusCode: 200 })); dispatcher.setHttpPoster(mockHttpPoster); const result = await dispatcher.dispatch({ targetUrl: 'https://api.partner.com/omnicard/webhook', secret: mockSecret, payload: mockPayload, }); expect(result.success).toBe(true); expect(result.attempts).toBe(1); expect(mockHttpPoster).toHaveBeenCalledTimes(1); }); it('遇到 500 异常时应自动重试,全部失败后记入死信', async () => { const mockHttpPoster = jest.fn(async () => ({ statusCode: 500 })); dispatcher.setHttpPoster(mockHttpPoster); const result = await dispatcher.dispatch({ targetUrl: 'https://api.partner.com/failing/webhook', secret: mockSecret, payload: mockPayload, maxRetries: 2, initialBackoffMs: 10, }); expect(result.success).toBe(false); expect(result.attempts).toBe(2); expect(result.isDeadLetter).toBe(true); }); }); });