Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
194 lines
6.2 KiB
TypeScript
194 lines
6.2 KiB
TypeScript
import { createHmac } from 'crypto';
|
|
import type { IWebhookFunctions } from 'n8n-workflow';
|
|
|
|
import { verifySignature } from '../ZendeskTriggerHelpers';
|
|
|
|
describe('ZendeskTriggerHelpers', () => {
|
|
describe('verifySignature', () => {
|
|
let mockWebhookFunctions: IWebhookFunctions;
|
|
const testWebhookSecret = 'dGhpc19zZWNyZXRfaXNfZm9yX3Rlc3Rpbmdfb25seQ==';
|
|
const testBody = '{"ticket":{"id":"123","subject":"Test ticket","status":"open"}}';
|
|
const testTimestamp = '2024-01-15T10:30:00Z';
|
|
|
|
function generateValidSignature(timestamp: string, body: string, secret: string): string {
|
|
// Zendesk signature: base64(HMACSHA256(TIMESTAMP + BODY))
|
|
return createHmac('sha256', secret)
|
|
.update(timestamp + body)
|
|
.digest('base64');
|
|
}
|
|
|
|
beforeEach(() => {
|
|
mockWebhookFunctions = {
|
|
getWorkflowStaticData: jest.fn().mockReturnValue({
|
|
webhookSecret: testWebhookSecret,
|
|
}),
|
|
getRequestObject: jest.fn().mockReturnValue({
|
|
rawBody: Buffer.from(testBody),
|
|
}),
|
|
getHeaderData: jest.fn().mockReturnValue({
|
|
'x-zendesk-webhook-signature': generateValidSignature(
|
|
testTimestamp,
|
|
testBody,
|
|
testWebhookSecret,
|
|
),
|
|
'x-zendesk-webhook-signature-timestamp': testTimestamp,
|
|
}),
|
|
} as unknown as IWebhookFunctions;
|
|
});
|
|
|
|
it('should return true when no webhook secret is stored (backwards compatibility)', () => {
|
|
(mockWebhookFunctions.getWorkflowStaticData as jest.Mock).mockReturnValue({});
|
|
|
|
const result = verifySignature.call(mockWebhookFunctions);
|
|
|
|
expect(result).toBe(true);
|
|
expect(mockWebhookFunctions.getWorkflowStaticData).toHaveBeenCalledWith('node');
|
|
});
|
|
|
|
it('should return false when signature header is missing', () => {
|
|
(mockWebhookFunctions.getHeaderData as jest.Mock).mockReturnValue({
|
|
'x-zendesk-webhook-signature-timestamp': testTimestamp,
|
|
});
|
|
|
|
const result = verifySignature.call(mockWebhookFunctions);
|
|
|
|
expect(result).toBe(false);
|
|
});
|
|
|
|
it('should return false when timestamp header is missing', () => {
|
|
(mockWebhookFunctions.getHeaderData as jest.Mock).mockReturnValue({
|
|
'x-zendesk-webhook-signature': generateValidSignature(
|
|
testTimestamp,
|
|
testBody,
|
|
testWebhookSecret,
|
|
),
|
|
});
|
|
|
|
const result = verifySignature.call(mockWebhookFunctions);
|
|
|
|
expect(result).toBe(false);
|
|
});
|
|
|
|
it('should return false when rawBody is missing', () => {
|
|
(mockWebhookFunctions.getRequestObject as jest.Mock).mockReturnValue({
|
|
rawBody: undefined,
|
|
});
|
|
|
|
const result = verifySignature.call(mockWebhookFunctions);
|
|
|
|
expect(result).toBe(false);
|
|
});
|
|
|
|
it('should return true when signature is valid', () => {
|
|
const result = verifySignature.call(mockWebhookFunctions);
|
|
|
|
expect(result).toBe(true);
|
|
});
|
|
|
|
it('should return false when signature is invalid', () => {
|
|
(mockWebhookFunctions.getHeaderData as jest.Mock).mockReturnValue({
|
|
'x-zendesk-webhook-signature': 'invalid-signature',
|
|
'x-zendesk-webhook-signature-timestamp': testTimestamp,
|
|
});
|
|
|
|
const result = verifySignature.call(mockWebhookFunctions);
|
|
|
|
expect(result).toBe(false);
|
|
});
|
|
|
|
it('should return false when signature is computed with wrong secret', () => {
|
|
const wrongSecret = 'wrong_secret_key';
|
|
(mockWebhookFunctions.getHeaderData as jest.Mock).mockReturnValue({
|
|
'x-zendesk-webhook-signature': generateValidSignature(testTimestamp, testBody, wrongSecret),
|
|
'x-zendesk-webhook-signature-timestamp': testTimestamp,
|
|
});
|
|
|
|
const result = verifySignature.call(mockWebhookFunctions);
|
|
|
|
expect(result).toBe(false);
|
|
});
|
|
|
|
it('should handle Buffer rawBody correctly', () => {
|
|
const bufferBody = Buffer.from(testBody);
|
|
(mockWebhookFunctions.getRequestObject as jest.Mock).mockReturnValue({
|
|
rawBody: bufferBody,
|
|
});
|
|
|
|
const result = verifySignature.call(mockWebhookFunctions);
|
|
|
|
expect(result).toBe(true);
|
|
});
|
|
|
|
it('should handle string rawBody correctly', () => {
|
|
(mockWebhookFunctions.getRequestObject as jest.Mock).mockReturnValue({
|
|
rawBody: testBody, // String instead of Buffer
|
|
});
|
|
|
|
const result = verifySignature.call(mockWebhookFunctions);
|
|
|
|
expect(result).toBe(true);
|
|
});
|
|
|
|
it('should return false when computed and provided signatures have different lengths', () => {
|
|
(mockWebhookFunctions.getHeaderData as jest.Mock).mockReturnValue({
|
|
'x-zendesk-webhook-signature': 'short',
|
|
'x-zendesk-webhook-signature-timestamp': testTimestamp,
|
|
});
|
|
|
|
const result = verifySignature.call(mockWebhookFunctions);
|
|
|
|
expect(result).toBe(false);
|
|
});
|
|
|
|
it('should handle different timestamp formats', () => {
|
|
const differentTimestamp = '1705315800';
|
|
const validSignature = generateValidSignature(
|
|
differentTimestamp,
|
|
testBody,
|
|
testWebhookSecret,
|
|
);
|
|
(mockWebhookFunctions.getHeaderData as jest.Mock).mockReturnValue({
|
|
'x-zendesk-webhook-signature': validSignature,
|
|
'x-zendesk-webhook-signature-timestamp': differentTimestamp,
|
|
});
|
|
|
|
const result = verifySignature.call(mockWebhookFunctions);
|
|
|
|
expect(result).toBe(true);
|
|
});
|
|
|
|
it('should handle empty body', () => {
|
|
const emptyBody = '';
|
|
const validSignature = generateValidSignature(testTimestamp, emptyBody, testWebhookSecret);
|
|
(mockWebhookFunctions.getRequestObject as jest.Mock).mockReturnValue({
|
|
rawBody: Buffer.from(emptyBody),
|
|
});
|
|
(mockWebhookFunctions.getHeaderData as jest.Mock).mockReturnValue({
|
|
'x-zendesk-webhook-signature': validSignature,
|
|
'x-zendesk-webhook-signature-timestamp': testTimestamp,
|
|
});
|
|
|
|
const result = verifySignature.call(mockWebhookFunctions);
|
|
|
|
expect(result).toBe(true);
|
|
});
|
|
|
|
it('should handle complex JSON body with special characters', () => {
|
|
const complexBody =
|
|
'{"ticket":{"id":"123","subject":"Test with émojis 🎉 and spëcial chars"}}';
|
|
const validSignature = generateValidSignature(testTimestamp, complexBody, testWebhookSecret);
|
|
(mockWebhookFunctions.getRequestObject as jest.Mock).mockReturnValue({
|
|
rawBody: Buffer.from(complexBody, 'utf8'),
|
|
});
|
|
(mockWebhookFunctions.getHeaderData as jest.Mock).mockReturnValue({
|
|
'x-zendesk-webhook-signature': validSignature,
|
|
'x-zendesk-webhook-signature-timestamp': testTimestamp,
|
|
});
|
|
|
|
const result = verifySignature.call(mockWebhookFunctions);
|
|
|
|
expect(result).toBe(true);
|
|
});
|
|
});
|
|
});
|