n8n/packages/nodes-base/nodes/Linear/test/LinearTriggerHelpers.test.ts
Eugene 3b248eedc2
feat(Linear Trigger Node): Add signing secret validation (#28522)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
2026-04-17 12:33:01 +00:00

146 lines
4.4 KiB
TypeScript

import { createHmac } from 'crypto';
import { verifySignature } from '../LinearTriggerHelpers';
describe('LinearTriggerHelpers', () => {
let mockWebhookFunctions: any;
const testSigningSecret = 'test-linear-signing-secret';
const testBody = '{"action":"create","type":"Issue","data":{"id":"123"}}';
const testSignature = createHmac('sha256', testSigningSecret).update(testBody).digest('hex');
beforeEach(() => {
jest.clearAllMocks();
// Mock Date.now() to a fixed timestamp
jest.spyOn(Date, 'now').mockImplementation(() => 1700000000000);
mockWebhookFunctions = {
getCredentials: jest.fn(),
getRequestObject: jest.fn(),
getNodeParameter: jest.fn(),
getBodyData: jest.fn().mockReturnValue({ webhookTimestamp: 1700000000000 }),
getNode: jest.fn().mockReturnValue({ name: 'Linear Trigger' }),
};
mockWebhookFunctions.getNodeParameter.mockReturnValue('apiToken');
mockWebhookFunctions.getRequestObject.mockReturnValue({
header: jest.fn().mockImplementation((header: string) => {
if (header === 'linear-signature') return testSignature;
return null;
}),
rawBody: testBody,
});
});
describe('verifySignature', () => {
it('should return true when no signing secret is configured', async () => {
mockWebhookFunctions.getCredentials.mockResolvedValue({
apiKey: 'test-key',
});
const result = await verifySignature.call(mockWebhookFunctions);
expect(result).toBe(true);
expect(mockWebhookFunctions.getCredentials).toHaveBeenCalledWith('linearApi');
});
it('should use linearOAuth2Api credentials when authentication is oAuth2', async () => {
mockWebhookFunctions.getNodeParameter.mockReturnValue('oAuth2');
mockWebhookFunctions.getCredentials.mockResolvedValue({});
const result = await verifySignature.call(mockWebhookFunctions);
expect(result).toBe(true);
expect(mockWebhookFunctions.getCredentials).toHaveBeenCalledWith('linearOAuth2Api');
});
it('should return false when Linear-Signature header is missing', async () => {
mockWebhookFunctions.getCredentials.mockResolvedValue({
signingSecret: testSigningSecret,
});
mockWebhookFunctions.getRequestObject.mockReturnValue({
header: jest.fn().mockReturnValue(null),
rawBody: testBody,
});
const result = await verifySignature.call(mockWebhookFunctions);
expect(result).toBe(false);
});
it('should return true when empty signing secret and no header', async () => {
mockWebhookFunctions.getCredentials.mockResolvedValue({
signingSecret: '',
});
mockWebhookFunctions.getRequestObject.mockReturnValue({
header: jest.fn().mockReturnValue(null),
rawBody: testBody,
});
const result = await verifySignature.call(mockWebhookFunctions);
expect(result).toBe(true);
});
it('should return true when signature is valid', async () => {
mockWebhookFunctions.getCredentials.mockResolvedValue({
signingSecret: testSigningSecret,
});
const result = await verifySignature.call(mockWebhookFunctions);
expect(result).toBe(true);
});
it('should return false when signature is invalid', async () => {
mockWebhookFunctions.getCredentials.mockResolvedValue({
signingSecret: testSigningSecret,
});
mockWebhookFunctions.getRequestObject.mockReturnValue({
header: jest.fn().mockImplementation((header: string) => {
if (header === 'linear-signature') return 'invalidsignature';
return null;
}),
rawBody: testBody,
});
const result = await verifySignature.call(mockWebhookFunctions);
expect(result).toBe(false);
});
it('should return false when webhookTimestamp is too old', async () => {
mockWebhookFunctions.getCredentials.mockResolvedValue({
signingSecret: testSigningSecret,
});
// Timestamp 120 seconds old (Should be within 60s window)
mockWebhookFunctions.getBodyData.mockReturnValue({
webhookTimestamp: 1700000000000 - 120_000,
});
const result = await verifySignature.call(mockWebhookFunctions);
expect(result).toBe(false);
});
it('should return true when webhookTimestamp difference is within acceptable window', async () => {
mockWebhookFunctions.getCredentials.mockResolvedValue({
signingSecret: testSigningSecret,
});
// Timestamp 30 seconds old
mockWebhookFunctions.getBodyData.mockReturnValue({
webhookTimestamp: 1700000000000 - 30_000,
});
const result = await verifySignature.call(mockWebhookFunctions);
expect(result).toBe(true);
});
});
});