Files
n8n/packages/nodes-base/nodes/Currents/test/CurrentsTriggerHelpers.test.ts
alighasami 3d5eaf9445
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
first commit
2026-03-17 16:22:57 +03:30

470 lines
14 KiB
TypeScript

import type { IDataObject, IHookFunctions, IWebhookFunctions } from 'n8n-workflow';
import {
createWebhook,
deleteWebhook,
findWebhookByUrl,
generateWebhookSecret,
listWebhooks,
updateWebhook,
verifyWebhook,
} from '../CurrentsTriggerHelpers';
describe('CurrentsTriggerHelpers', () => {
describe('generateWebhookSecret', () => {
it('should generate a 64-character hex string', () => {
const secret = generateWebhookSecret();
expect(secret).toHaveLength(64);
expect(/^[0-9a-f]+$/.test(secret)).toBe(true);
});
it('should generate unique secrets', () => {
const secret1 = generateWebhookSecret();
const secret2 = generateWebhookSecret();
expect(secret1).not.toBe(secret2);
});
});
describe('verifyWebhook', () => {
let mockWebhookFunctions: Partial<IWebhookFunctions>;
beforeEach(() => {
mockWebhookFunctions = {
getRequestObject: jest.fn(),
getHeaderData: jest.fn(),
getWorkflowStaticData: jest.fn(),
};
});
it('should return true when no secret in static data (no verification)', () => {
const nowMs = Date.now();
(mockWebhookFunctions.getRequestObject as jest.Mock).mockReturnValue({
headers: { 'x-timestamp': String(nowMs) },
});
(mockWebhookFunctions.getHeaderData as jest.Mock).mockReturnValue({});
(mockWebhookFunctions.getWorkflowStaticData as jest.Mock).mockReturnValue({});
const result = verifyWebhook.call(mockWebhookFunctions as IWebhookFunctions);
expect(result).toBe(true);
});
it('should return false when timestamp is stale', () => {
const tenMinutesAgoMs = Date.now() - 600 * 1000;
(mockWebhookFunctions.getRequestObject as jest.Mock).mockReturnValue({
headers: { 'x-timestamp': String(tenMinutesAgoMs) },
});
(mockWebhookFunctions.getHeaderData as jest.Mock).mockReturnValue({});
(mockWebhookFunctions.getWorkflowStaticData as jest.Mock).mockReturnValue({});
const result = verifyWebhook.call(mockWebhookFunctions as IWebhookFunctions);
expect(result).toBe(false);
});
it('should return false when timestamp is invalid/non-numeric', () => {
(mockWebhookFunctions.getRequestObject as jest.Mock).mockReturnValue({
headers: { 'x-timestamp': 'not-a-number' },
});
(mockWebhookFunctions.getHeaderData as jest.Mock).mockReturnValue({});
(mockWebhookFunctions.getWorkflowStaticData as jest.Mock).mockReturnValue({});
const result = verifyWebhook.call(mockWebhookFunctions as IWebhookFunctions);
expect(result).toBe(false);
});
it('should return true when secret matches from static data', () => {
const nowMs = Date.now();
const secret = 'auto-generated-secret';
(mockWebhookFunctions.getRequestObject as jest.Mock).mockReturnValue({
headers: { 'x-timestamp': String(nowMs) },
});
(mockWebhookFunctions.getHeaderData as jest.Mock).mockReturnValue({
'x-webhook-secret': secret,
});
(mockWebhookFunctions.getWorkflowStaticData as jest.Mock).mockReturnValue({
webhookSecret: secret,
} as IDataObject);
const result = verifyWebhook.call(mockWebhookFunctions as IWebhookFunctions);
expect(result).toBe(true);
});
it('should return false when secret does not match (different length)', () => {
const nowMs = Date.now();
(mockWebhookFunctions.getRequestObject as jest.Mock).mockReturnValue({
headers: { 'x-timestamp': String(nowMs) },
});
(mockWebhookFunctions.getHeaderData as jest.Mock).mockReturnValue({
'x-webhook-secret': 'wrong-secret',
});
(mockWebhookFunctions.getWorkflowStaticData as jest.Mock).mockReturnValue({
webhookSecret: 'correct-secret',
} as IDataObject);
const result = verifyWebhook.call(mockWebhookFunctions as IWebhookFunctions);
expect(result).toBe(false);
});
it('should return false when secret does not match (same length)', () => {
const nowMs = Date.now();
(mockWebhookFunctions.getRequestObject as jest.Mock).mockReturnValue({
headers: { 'x-timestamp': String(nowMs) },
});
(mockWebhookFunctions.getHeaderData as jest.Mock).mockReturnValue({
'x-webhook-secret': 'wrong-secret-aa',
});
(mockWebhookFunctions.getWorkflowStaticData as jest.Mock).mockReturnValue({
webhookSecret: 'correct-secret',
} as IDataObject);
const result = verifyWebhook.call(mockWebhookFunctions as IWebhookFunctions);
expect(result).toBe(false);
});
it('should return false when secret header is missing but expected', () => {
const nowMs = Date.now();
(mockWebhookFunctions.getRequestObject as jest.Mock).mockReturnValue({
headers: { 'x-timestamp': String(nowMs) },
});
(mockWebhookFunctions.getHeaderData as jest.Mock).mockReturnValue({});
(mockWebhookFunctions.getWorkflowStaticData as jest.Mock).mockReturnValue({
webhookSecret: 'expected-secret',
} as IDataObject);
const result = verifyWebhook.call(mockWebhookFunctions as IWebhookFunctions);
expect(result).toBe(false);
});
it('should handle missing timestamp header gracefully', () => {
(mockWebhookFunctions.getRequestObject as jest.Mock).mockReturnValue({
headers: {},
});
(mockWebhookFunctions.getHeaderData as jest.Mock).mockReturnValue({});
(mockWebhookFunctions.getWorkflowStaticData as jest.Mock).mockReturnValue({});
const result = verifyWebhook.call(mockWebhookFunctions as IWebhookFunctions);
expect(result).toBe(true);
});
});
describe('listWebhooks', () => {
let mockHookFunctions: Partial<IHookFunctions>;
beforeEach(() => {
mockHookFunctions = {
helpers: {
httpRequestWithAuthentication: jest.fn(),
} as unknown as IHookFunctions['helpers'],
};
});
it('should return webhooks from API response', async () => {
const mockWebhooks = [
{
hookId: 'hook-1',
projectId: 'project-123',
url: 'https://example.com/webhook1',
hookEvents: ['RUN_FINISH'],
},
{
hookId: 'hook-2',
projectId: 'project-123',
url: 'https://example.com/webhook2',
hookEvents: ['RUN_START', 'RUN_FINISH'],
},
];
(mockHookFunctions.helpers!.httpRequestWithAuthentication as jest.Mock).mockResolvedValue({
data: mockWebhooks,
});
const result = await listWebhooks.call(mockHookFunctions as IHookFunctions, 'project-123');
expect(mockHookFunctions.helpers!.httpRequestWithAuthentication).toHaveBeenCalledWith(
'currentsApi',
{
method: 'GET',
url: 'https://api.currents.dev/v1/webhooks',
qs: { projectId: 'project-123' },
},
);
expect(result).toEqual(mockWebhooks);
});
it('should return empty array when no webhooks exist', async () => {
(mockHookFunctions.helpers!.httpRequestWithAuthentication as jest.Mock).mockResolvedValue({
data: null,
});
const result = await listWebhooks.call(mockHookFunctions as IHookFunctions, 'project-123');
expect(result).toEqual([]);
});
});
describe('findWebhookByUrl', () => {
let mockHookFunctions: Partial<IHookFunctions>;
beforeEach(() => {
mockHookFunctions = {
helpers: {
httpRequestWithAuthentication: jest.fn(),
} as unknown as IHookFunctions['helpers'],
};
});
it('should find webhook matching URL', async () => {
const targetUrl = 'https://example.com/webhook2';
const mockWebhooks = [
{
hookId: 'hook-1',
projectId: 'project-123',
url: 'https://example.com/webhook1',
hookEvents: ['RUN_FINISH'],
},
{
hookId: 'hook-2',
projectId: 'project-123',
url: targetUrl,
hookEvents: ['RUN_START'],
},
];
(mockHookFunctions.helpers!.httpRequestWithAuthentication as jest.Mock).mockResolvedValue({
data: mockWebhooks,
});
const result = await findWebhookByUrl.call(
mockHookFunctions as IHookFunctions,
'project-123',
targetUrl,
);
expect(result).toEqual(mockWebhooks[1]);
});
it('should return undefined when no webhook matches URL', async () => {
const mockWebhooks = [
{
hookId: 'hook-1',
projectId: 'project-123',
url: 'https://example.com/webhook1',
hookEvents: ['RUN_FINISH'],
},
];
(mockHookFunctions.helpers!.httpRequestWithAuthentication as jest.Mock).mockResolvedValue({
data: mockWebhooks,
});
const result = await findWebhookByUrl.call(
mockHookFunctions as IHookFunctions,
'project-123',
'https://example.com/nonexistent',
);
expect(result).toBeUndefined();
});
});
describe('createWebhook', () => {
let mockHookFunctions: Partial<IHookFunctions>;
beforeEach(() => {
mockHookFunctions = {
helpers: {
httpRequestWithAuthentication: jest.fn(),
} as unknown as IHookFunctions['helpers'],
};
});
it('should create webhook with all options', async () => {
const createdWebhook = {
hookId: 'new-hook-id',
projectId: 'project-123',
url: 'https://example.com/webhook',
hookEvents: ['RUN_FINISH', 'RUN_START'],
headers: '{"x-webhook-secret":"secret123"}',
label: 'n8n workflow 456',
};
(mockHookFunctions.helpers!.httpRequestWithAuthentication as jest.Mock).mockResolvedValue({
data: createdWebhook,
});
const result = await createWebhook.call(mockHookFunctions as IHookFunctions, 'project-123', {
url: 'https://example.com/webhook',
hookEvents: ['RUN_FINISH', 'RUN_START'],
headers: '{"x-webhook-secret":"secret123"}',
label: 'n8n workflow 456',
});
expect(mockHookFunctions.helpers!.httpRequestWithAuthentication).toHaveBeenCalledWith(
'currentsApi',
{
method: 'POST',
url: 'https://api.currents.dev/v1/webhooks',
qs: { projectId: 'project-123' },
body: {
url: 'https://example.com/webhook',
hookEvents: ['RUN_FINISH', 'RUN_START'],
headers: '{"x-webhook-secret":"secret123"}',
label: 'n8n workflow 456',
},
},
);
expect(result).toEqual(createdWebhook);
});
it('should create webhook with minimal options', async () => {
const createdWebhook = {
hookId: 'new-hook-id',
projectId: 'project-123',
url: 'https://example.com/webhook',
hookEvents: [],
};
(mockHookFunctions.helpers!.httpRequestWithAuthentication as jest.Mock).mockResolvedValue({
data: createdWebhook,
});
const result = await createWebhook.call(mockHookFunctions as IHookFunctions, 'project-123', {
url: 'https://example.com/webhook',
});
expect(mockHookFunctions.helpers!.httpRequestWithAuthentication).toHaveBeenCalledWith(
'currentsApi',
{
method: 'POST',
url: 'https://api.currents.dev/v1/webhooks',
qs: { projectId: 'project-123' },
body: {
url: 'https://example.com/webhook',
hookEvents: [],
headers: undefined,
label: undefined,
},
},
);
expect(result).toEqual(createdWebhook);
});
});
describe('updateWebhook', () => {
let mockHookFunctions: Partial<IHookFunctions>;
beforeEach(() => {
mockHookFunctions = {
helpers: {
httpRequestWithAuthentication: jest.fn(),
} as unknown as IHookFunctions['helpers'],
};
});
it('should update webhook with new events', async () => {
const updatedWebhook = {
hookId: 'hook-123',
projectId: 'project-123',
url: 'https://example.com/webhook',
hookEvents: ['RUN_FINISH', 'RUN_TIMEOUT'],
};
(mockHookFunctions.helpers!.httpRequestWithAuthentication as jest.Mock).mockResolvedValue({
data: updatedWebhook,
});
const result = await updateWebhook.call(mockHookFunctions as IHookFunctions, 'hook-123', {
hookEvents: ['RUN_FINISH', 'RUN_TIMEOUT'],
});
expect(mockHookFunctions.helpers!.httpRequestWithAuthentication).toHaveBeenCalledWith(
'currentsApi',
{
method: 'PUT',
url: 'https://api.currents.dev/v1/webhooks/hook-123',
body: {
hookEvents: ['RUN_FINISH', 'RUN_TIMEOUT'],
},
},
);
expect(result).toEqual(updatedWebhook);
});
it('should update webhook with multiple fields', async () => {
const updatedWebhook = {
hookId: 'hook-123',
projectId: 'project-123',
url: 'https://example.com/new-webhook',
hookEvents: ['RUN_START'],
headers: '{"x-webhook-secret":"newsecret"}',
label: 'updated label',
};
(mockHookFunctions.helpers!.httpRequestWithAuthentication as jest.Mock).mockResolvedValue({
data: updatedWebhook,
});
const result = await updateWebhook.call(mockHookFunctions as IHookFunctions, 'hook-123', {
url: 'https://example.com/new-webhook',
hookEvents: ['RUN_START'],
headers: '{"x-webhook-secret":"newsecret"}',
label: 'updated label',
});
expect(mockHookFunctions.helpers!.httpRequestWithAuthentication).toHaveBeenCalledWith(
'currentsApi',
{
method: 'PUT',
url: 'https://api.currents.dev/v1/webhooks/hook-123',
body: {
url: 'https://example.com/new-webhook',
hookEvents: ['RUN_START'],
headers: '{"x-webhook-secret":"newsecret"}',
label: 'updated label',
},
},
);
expect(result).toEqual(updatedWebhook);
});
});
describe('deleteWebhook', () => {
let mockHookFunctions: Partial<IHookFunctions>;
beforeEach(() => {
mockHookFunctions = {
helpers: {
httpRequestWithAuthentication: jest.fn(),
} as unknown as IHookFunctions['helpers'],
};
});
it('should delete webhook by hookId', async () => {
(mockHookFunctions.helpers!.httpRequestWithAuthentication as jest.Mock).mockResolvedValue({});
await deleteWebhook.call(mockHookFunctions as IHookFunctions, 'hook-123');
expect(mockHookFunctions.helpers!.httpRequestWithAuthentication).toHaveBeenCalledWith(
'currentsApi',
{
method: 'DELETE',
url: 'https://api.currents.dev/v1/webhooks/hook-123',
},
);
});
it('should not throw on successful deletion', async () => {
(mockHookFunctions.helpers!.httpRequestWithAuthentication as jest.Mock).mockResolvedValue({});
await expect(
deleteWebhook.call(mockHookFunctions as IHookFunctions, 'hook-123'),
).resolves.toBeUndefined();
});
});
});