文件
OmniCard/backend/test/modules/crm/webhook-dispatcher.test.ts

98 行
3.3 KiB
TypeScript

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);
});
});
});