import type * as express from 'express'; import { mock } from 'jest-mock-extended'; import get from 'lodash/get'; import merge from 'lodash/merge'; import set from 'lodash/set'; import { PollContext, returnJsonArray, type InstanceSettings } from 'n8n-core'; import { ScheduledTaskManager } from 'n8n-core/dist/ScheduledTaskManager'; import type { IBinaryData, ICredentialDataDecryptedObject, IDataObject, IHttpRequestOptions, INode, INodeType, INodeTypes, ITriggerFunctions, IWebhookFunctions, IWorkflowExecuteAdditionalData, NodeTypeAndVersion, VersionedNodeType, Workflow, WorkflowHooks, } from 'n8n-workflow'; type MockDeepPartial = Parameters>[0]; type TestTriggerNodeOptions = { mode?: 'manual' | 'trigger'; node?: MockDeepPartial; timezone?: string; workflowStaticData?: IDataObject; credential?: ICredentialDataDecryptedObject; }; type TestWebhookTriggerNodeOptions = TestTriggerNodeOptions & { webhookName?: string; request?: MockDeepPartial; bodyData?: IDataObject; childNodes?: NodeTypeAndVersion[]; }; type TestPollingTriggerNodeOptions = TestTriggerNodeOptions & {}; function getNodeVersion(Trigger: new () => VersionedNodeType, version?: number) { const instance = new Trigger(); return instance.nodeVersions[version ?? instance.currentVersion]; } export async function testVersionedTriggerNode( Trigger: new () => VersionedNodeType, version?: number, options: TestTriggerNodeOptions = {}, ) { return await testTriggerNode(getNodeVersion(Trigger, version), options); } export async function testTriggerNode( Trigger: (new () => INodeType) | INodeType, options: TestTriggerNodeOptions = {}, ) { const trigger = 'description' in Trigger ? Trigger : new Trigger(); const emit: jest.MockedFunction = jest.fn(); const timezone = options.timezone ?? 'Europe/Berlin'; const version = trigger.description.version; const node = merge( { type: trigger.description.name, name: trigger.description.defaults.name ?? `Test Node (${trigger.description.name})`, typeVersion: typeof version === 'number' ? version : version.at(-1), } satisfies Partial, options.node, ) as INode; const workflow = mock({ timezone: options.timezone ?? 'Europe/Berlin' }); const scheduledTaskManager = new ScheduledTaskManager(mock()); const helpers = mock({ returnJsonArray, registerCron: (cronExpression, onTick) => scheduledTaskManager.registerCron(workflow, cronExpression, onTick), }); const triggerFunctions = mock({ helpers, emit, getTimezone: () => timezone, getNode: () => node, getMode: () => options.mode ?? 'trigger', getWorkflowStaticData: () => options.workflowStaticData ?? {}, getNodeParameter: (parameterName, fallback) => get(node.parameters, parameterName) ?? fallback, }); const response = await trigger.trigger?.call(triggerFunctions); if (options.mode === 'manual') { expect(response?.manualTriggerFunction).toBeInstanceOf(Function); await response?.manualTriggerFunction?.(); } else { expect(response?.manualTriggerFunction).toBeUndefined(); } return { close: jest.fn(response?.closeFunction), emit, }; } export async function testVersionedWebhookTriggerNode( Trigger: new () => VersionedNodeType, version?: number, options: TestWebhookTriggerNodeOptions = {}, ) { return await testWebhookTriggerNode(getNodeVersion(Trigger, version), options); } export async function testWebhookTriggerNode( Trigger: (new () => INodeType) | INodeType, options: TestWebhookTriggerNodeOptions = {}, ) { const trigger = 'description' in Trigger ? Trigger : new Trigger(); const timezone = options.timezone ?? 'Europe/Berlin'; const version = trigger.description.version; const node = merge( { type: trigger.description.name, name: trigger.description.defaults.name ?? `Test Node (${trigger.description.name})`, typeVersion: typeof version === 'number' ? version : version.at(-1), } satisfies Partial, options.node, ) as INode; const workflow = mock({ timezone: options.timezone ?? 'Europe/Berlin' }); const scheduledTaskManager = new ScheduledTaskManager(mock()); const helpers = mock({ returnJsonArray, registerCron: (cronExpression, onTick) => scheduledTaskManager.registerCron(workflow, cronExpression, onTick), }); const request = mock({ method: 'GET', ...options.request, }); const response = mock({ status: jest.fn(() => mock()) }); const webhookFunctions = mock({ helpers, nodeHelpers: { copyBinaryFile: jest.fn(async () => mock()), }, getTimezone: () => timezone, getNode: () => node, getMode: () => options.mode ?? 'trigger', getInstanceId: () => 'instanceId', getBodyData: () => options.bodyData ?? {}, getHeaderData: () => ({}), getInputConnectionData: async () => ({}), getNodeWebhookUrl: (name) => `/test-webhook-url/${name}`, getParamsData: () => ({}), getQueryData: () => ({}), getRequestObject: () => request, getResponseObject: () => response, getWebhookName: () => options.webhookName ?? 'default', getWorkflowStaticData: () => options.workflowStaticData ?? {}, getNodeParameter: (parameterName, fallback) => get(node.parameters, parameterName) ?? fallback, getChildNodes: () => options.childNodes ?? [], }); const responseData = await trigger.webhook?.call(webhookFunctions); return { responseData, response: webhookFunctions.getResponseObject(), }; } export async function testPollingTriggerNode( Trigger: (new () => INodeType) | INodeType, options: TestPollingTriggerNodeOptions = {}, ) { const trigger = 'description' in Trigger ? Trigger : new Trigger(); const timezone = options.timezone ?? 'Europe/Berlin'; const version = trigger.description.version; const node = merge( { type: trigger.description.name, name: trigger.description.defaults.name ?? `Test Node (${trigger.description.name})`, typeVersion: typeof version === 'number' ? version : version.at(-1), credentials: {}, } satisfies Partial, options.node, ) as INode; const workflow = mock({ timezone, nodeTypes: mock({ getByNameAndVersion: () => mock({ description: trigger.description }), }), getStaticData: () => options.workflowStaticData ?? {}, }); const mode = options.mode ?? 'trigger'; const pollContext = new PollContext( workflow, node, mock({ currentNodeParameters: node.parameters, credentialsHelper: mock({ getParentTypes: () => [], authenticate: async (_creds, _type, options) => { set(options, 'headers.authorization', 'mockAuth'); return options as IHttpRequestOptions; }, }), hooks: mock(), }), mode, 'init', ); pollContext.getNode = () => node; pollContext.getCredentials = async () => (options.credential ?? {}) as T; pollContext.getNodeParameter = (parameterName, fallback) => get(node.parameters, parameterName) ?? fallback; const response = await trigger.poll?.call(pollContext); return { response, }; }