mirror of
https://github.com/n8n-io/n8n.git
synced 2026-06-01 17:27:14 +02:00
160 lines
5.0 KiB
TypeScript
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);
|
|
});
|
|
});
|
|
});
|