n8n/packages/nodes-base/nodes/Github/__tests__/GithubTriggerHelpers.test.ts

160 lines
5.0 KiB
TypeScript

import { createHmac, timingSafeEqual } from 'crypto';
import { verifySignature } from '../GithubTriggerHelpers';
jest.mock('crypto', () => ({
...jest.requireActual('crypto'),
createHmac: jest.fn().mockReturnValue({
update: jest.fn().mockReturnThis(),
digest: jest
.fn()
.mockReturnValue('757107ea0eb2509fc211221cce984b8a37570b6d7586c22c46f4379c8b043e17'),
}),
timingSafeEqual: jest.fn(),
}));
describe('GithubTriggerHelpers', () => {
let mockWebhookFunctions: {
getWorkflowStaticData: jest.Mock;
getRequestObject: jest.Mock;
};
const testWebhookSecret = 'a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2';
const testBody =
'{"action":"opened","pull_request":{"id":123},"repository":{"full_name":"owner/repo"}}';
const testSignature = 'sha256=757107ea0eb2509fc211221cce984b8a37570b6d7586c22c46f4379c8b043e17';
beforeEach(() => {
jest.clearAllMocks();
mockWebhookFunctions = {
getWorkflowStaticData: jest.fn(),
getRequestObject: jest.fn(),
};
// Default mock return values
mockWebhookFunctions.getWorkflowStaticData.mockReturnValue({
webhookSecret: testWebhookSecret,
});
mockWebhookFunctions.getRequestObject.mockReturnValue({
header: jest.fn().mockImplementation((header) => {
if (header === 'x-hub-signature-256') return testSignature;
return null;
}),
rawBody: testBody,
});
});
describe('verifySignature', () => {
it('should return true when no webhook secret is stored (backwards compatibility)', () => {
mockWebhookFunctions.getWorkflowStaticData.mockReturnValue({});
const result = verifySignature.call(mockWebhookFunctions as never);
expect(result).toBe(true);
expect(mockWebhookFunctions.getWorkflowStaticData).toHaveBeenCalledWith('node');
});
it('should return false when signature header is missing', () => {
mockWebhookFunctions.getRequestObject.mockReturnValue({
header: jest.fn().mockReturnValue(null),
rawBody: testBody,
});
const result = verifySignature.call(mockWebhookFunctions as never);
expect(result).toBe(false);
});
it('should return false when signature does not start with sha256=', () => {
mockWebhookFunctions.getRequestObject.mockReturnValue({
header: jest.fn().mockImplementation((header) => {
if (header === 'x-hub-signature-256') return 'invalid-format-signature';
return null;
}),
rawBody: testBody,
});
const result = verifySignature.call(mockWebhookFunctions as never);
expect(result).toBe(false);
});
it('should return false when rawBody is missing', () => {
mockWebhookFunctions.getRequestObject.mockReturnValue({
header: jest.fn().mockImplementation((header) => {
if (header === 'x-hub-signature-256') return testSignature;
return null;
}),
rawBody: undefined,
});
const result = verifySignature.call(mockWebhookFunctions as never);
expect(result).toBe(false);
});
it('should return true when signature is valid', () => {
(timingSafeEqual as jest.Mock).mockReturnValue(true);
const result = verifySignature.call(mockWebhookFunctions as never);
expect(result).toBe(true);
expect(createHmac).toHaveBeenCalledWith('sha256', testWebhookSecret);
expect(timingSafeEqual).toHaveBeenCalled();
});
it('should return false when signature is invalid', () => {
(timingSafeEqual as jest.Mock).mockReturnValue(false);
const result = verifySignature.call(mockWebhookFunctions as never);
expect(result).toBe(false);
expect(createHmac).toHaveBeenCalledWith('sha256', testWebhookSecret);
expect(timingSafeEqual).toHaveBeenCalled();
});
it('should handle Buffer rawBody correctly', () => {
const bufferBody = Buffer.from(testBody);
mockWebhookFunctions.getRequestObject.mockReturnValue({
header: jest.fn().mockImplementation((header) => {
if (header === 'x-hub-signature-256') return testSignature;
return null;
}),
rawBody: bufferBody,
});
(timingSafeEqual as jest.Mock).mockReturnValue(true);
const result = verifySignature.call(mockWebhookFunctions as never);
expect(result).toBe(true);
const mockHmac = createHmac('sha256', testWebhookSecret);
expect(mockHmac.update).toHaveBeenCalledWith(bufferBody);
});
it('should return false when computed and provided signatures have different lengths', () => {
// Mock a different length signature
const mockHmacInstance = {
update: jest.fn().mockReturnThis(),
digest: jest.fn().mockReturnValue('short'),
};
(createHmac as jest.Mock).mockReturnValue(mockHmacInstance);
const result = verifySignature.call(mockWebhookFunctions as never);
expect(result).toBe(false);
// timingSafeEqual should not be called if lengths don't match
expect(timingSafeEqual).not.toHaveBeenCalled();
});
it('should return false when an error occurs during verification', () => {
(createHmac as jest.Mock).mockImplementation(() => {
throw new Error('Crypto error');
});
const result = verifySignature.call(mockWebhookFunctions as never);
expect(result).toBe(false);
});
});
});