Files
n8n/packages/nodes-base/nodes/Netlify/test/NetlifyTrigger.node.test.ts

230 lines
7.2 KiB
TypeScript

import { randomBytes } from 'crypto';
import type { IHookFunctions, IWebhookFunctions } from 'n8n-workflow';
import { netlifyApiRequest } from '../GenericFunctions';
import { NetlifyTrigger } from '../NetlifyTrigger.node';
import { verifySignature } from '../NetlifyTriggerHelpers';
import type { Mock, Mocked } from 'vitest';
import type * as _importType0 from 'crypto';
vi.mock('../GenericFunctions');
vi.mock('../NetlifyTriggerHelpers');
vi.mock('crypto', async () => ({
...(await vi.importActual<typeof _importType0>('crypto')),
randomBytes: vi.fn(),
}));
describe('NetlifyTrigger', () => {
let trigger: NetlifyTrigger;
let mockHookFunctions: Pick<
Mocked<IHookFunctions>,
'getNodeWebhookUrl' | 'getNodeParameter' | 'getWorkflowStaticData'
>;
let mockWebhookFunctions: Pick<
Mocked<IWebhookFunctions>,
| 'getNodeParameter'
| 'getRequestObject'
| 'getResponseObject'
| 'getWorkflowStaticData'
| 'helpers'
>;
beforeEach(() => {
vi.clearAllMocks();
trigger = new NetlifyTrigger();
mockHookFunctions = {
getNodeWebhookUrl: vi.fn(),
getNodeParameter: vi.fn(),
getWorkflowStaticData: vi.fn(),
};
mockWebhookFunctions = {
getNodeParameter: vi.fn(),
getRequestObject: vi.fn(),
getResponseObject: vi.fn(),
getWorkflowStaticData: vi.fn(),
helpers: {
returnJsonArray: vi.fn((data) => data),
} as any,
};
});
describe('webhookMethods.default.create', () => {
it('should generate a signing secret and pass it to Netlify', async () => {
const webhookUrl = 'https://example.com/webhook';
const siteId = 'site-123';
const webhookSecret = 'a'.repeat(64);
const webhookId = 'hook-1';
mockHookFunctions.getNodeWebhookUrl.mockReturnValue(webhookUrl);
mockHookFunctions.getNodeParameter.mockImplementation((name) => {
if (name === 'event') return 'deployCreated';
if (name === 'siteId') return siteId;
if (name === 'formId') return '*';
return undefined;
});
const webhookData: any = {};
mockHookFunctions.getWorkflowStaticData.mockReturnValue(webhookData);
(randomBytes as Mock).mockReturnValue({
toString: vi.fn().mockReturnValue(webhookSecret),
});
(netlifyApiRequest as Mock).mockResolvedValue({ id: webhookId });
const result = await trigger.webhookMethods!.default.create.call(
mockHookFunctions as unknown as IHookFunctions,
);
expect(result).toBe(true);
expect(randomBytes).toHaveBeenCalledWith(32);
expect(netlifyApiRequest).toHaveBeenCalledWith('POST', '/hooks', {
event: 'deploy_created',
data: {
url: webhookUrl,
signature_secret: webhookSecret,
},
site_id: siteId,
});
expect(webhookData.webhookId).toBe(webhookId);
expect(webhookData.webhookSecret).toBe(webhookSecret);
});
it('should include form_id when event is submissionCreated and a form is selected', async () => {
const webhookUrl = 'https://example.com/webhook';
const siteId = 'site-123';
const formId = 'form-1';
const webhookSecret = 'b'.repeat(64);
mockHookFunctions.getNodeWebhookUrl.mockReturnValue(webhookUrl);
mockHookFunctions.getNodeParameter.mockImplementation((name) => {
if (name === 'event') return 'submissionCreated';
if (name === 'siteId') return siteId;
if (name === 'formId') return formId;
return undefined;
});
mockHookFunctions.getWorkflowStaticData.mockReturnValue({});
(randomBytes as Mock).mockReturnValue({
toString: vi.fn().mockReturnValue(webhookSecret),
});
(netlifyApiRequest as Mock).mockResolvedValue({ id: 'hook-2' });
await trigger.webhookMethods!.default.create.call(
mockHookFunctions as unknown as IHookFunctions,
);
expect(netlifyApiRequest).toHaveBeenCalledWith('POST', '/hooks', {
event: 'submission_created',
data: {
url: webhookUrl,
signature_secret: webhookSecret,
},
site_id: siteId,
form_id: formId,
});
});
});
describe('webhookMethods.default.delete', () => {
it('should remove the webhook and clear the stored secret', async () => {
const webhookId = 'hook-1';
const webhookData: any = {
webhookId,
webhookSecret: 'stored-secret',
};
mockHookFunctions.getWorkflowStaticData.mockReturnValue(webhookData);
(netlifyApiRequest as Mock).mockResolvedValue({});
const result = await trigger.webhookMethods!.default.delete.call(
mockHookFunctions as unknown as IHookFunctions,
);
expect(result).toBe(true);
expect(netlifyApiRequest).toHaveBeenCalledWith('DELETE', `/hooks/${webhookId}`);
expect(webhookData.webhookId).toBeUndefined();
expect(webhookData.webhookSecret).toBeUndefined();
});
});
describe('webhook', () => {
it('should respond 401 when signature verification fails', async () => {
const mockResponse = {
status: vi.fn().mockReturnThis(),
send: vi.fn().mockReturnThis(),
end: vi.fn(),
};
(verifySignature as Mock).mockReturnValue(false);
mockWebhookFunctions.getResponseObject.mockReturnValue(mockResponse as any);
const result = await trigger.webhook.call(
mockWebhookFunctions as unknown as IWebhookFunctions,
);
expect(verifySignature).toHaveBeenCalled();
expect(mockResponse.status).toHaveBeenCalledWith(401);
expect(mockResponse.send).toHaveBeenCalledWith('Unauthorized');
expect(result).toEqual({
noWebhookResponse: true,
});
});
it('should return workflow data when verification passes', async () => {
const body = { id: 'deploy-1', state: 'ready' };
(verifySignature as Mock).mockReturnValue(true);
mockWebhookFunctions.getNodeParameter.mockImplementation((name) => {
if (name === 'simple') return false;
if (name === 'event') return 'deployCreated';
return undefined;
});
mockWebhookFunctions.getRequestObject.mockReturnValue({ body } as any);
const result = await trigger.webhook.call(
mockWebhookFunctions as unknown as IWebhookFunctions,
);
expect(verifySignature).toHaveBeenCalled();
expect(result.workflowData).toBeDefined();
});
it('should return workflow data when no secret is configured (backward compat)', async () => {
const body = { id: 'deploy-1', state: 'ready' };
(verifySignature as Mock).mockReturnValue(true);
mockWebhookFunctions.getNodeParameter.mockImplementation((name) => {
if (name === 'simple') return false;
if (name === 'event') return 'deployCreated';
return undefined;
});
mockWebhookFunctions.getRequestObject.mockReturnValue({ body } as any);
mockWebhookFunctions.getWorkflowStaticData.mockReturnValue({});
const result = await trigger.webhook.call(
mockWebhookFunctions as unknown as IWebhookFunctions,
);
expect(result.workflowData).toBeDefined();
});
it('should unwrap data field for simplified submissionCreated payloads', async () => {
const body = { data: { name: 'Alice', email: 'alice@example.com' } };
(verifySignature as Mock).mockReturnValue(true);
mockWebhookFunctions.getNodeParameter.mockImplementation((name) => {
if (name === 'simple') return true;
if (name === 'event') return 'submissionCreated';
return undefined;
});
mockWebhookFunctions.getRequestObject.mockReturnValue({ body } as any);
await trigger.webhook.call(mockWebhookFunctions as unknown as IWebhookFunctions);
expect(mockWebhookFunctions.helpers.returnJsonArray).toHaveBeenCalledWith(body.data);
});
});
});