n8n/packages/nodes-base/test/nodes/TriggerHelpers.ts

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

233 lines
7.2 KiB
TypeScript
Raw Normal View History

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<T> = Parameters<typeof mock<T>>[0];
type TestTriggerNodeOptions = {
mode?: 'manual' | 'trigger';
node?: MockDeepPartial<INode>;
timezone?: string;
workflowStaticData?: IDataObject;
credential?: ICredentialDataDecryptedObject;
};
type TestWebhookTriggerNodeOptions = TestTriggerNodeOptions & {
webhookName?: string;
request?: MockDeepPartial<express.Request>;
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<ITriggerFunctions['emit']> = 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<INode>,
options.node,
) as INode;
const workflow = mock<Workflow>({ timezone: options.timezone ?? 'Europe/Berlin' });
const scheduledTaskManager = new ScheduledTaskManager(mock<InstanceSettings>());
const helpers = mock<ITriggerFunctions['helpers']>({
returnJsonArray,
registerCron: (cronExpression, onTick) =>
scheduledTaskManager.registerCron(workflow, cronExpression, onTick),
});
const triggerFunctions = mock<ITriggerFunctions>({
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<INode>,
options.node,
) as INode;
const workflow = mock<Workflow>({ timezone: options.timezone ?? 'Europe/Berlin' });
const scheduledTaskManager = new ScheduledTaskManager(mock<InstanceSettings>());
const helpers = mock<ITriggerFunctions['helpers']>({
returnJsonArray,
registerCron: (cronExpression, onTick) =>
scheduledTaskManager.registerCron(workflow, cronExpression, onTick),
});
const request = mock<express.Request>({
method: 'GET',
...options.request,
});
const response = mock<express.Response>({ status: jest.fn(() => mock<express.Response>()) });
const webhookFunctions = mock<IWebhookFunctions>({
helpers,
nodeHelpers: {
copyBinaryFile: jest.fn(async () => mock<IBinaryData>()),
},
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<INode>,
options.node,
) as INode;
const workflow = mock<Workflow>({
timezone,
nodeTypes: mock<INodeTypes>({
getByNameAndVersion: () => mock<INodeType>({ description: trigger.description }),
}),
getStaticData: () => options.workflowStaticData ?? {},
});
const mode = options.mode ?? 'trigger';
const pollContext = new PollContext(
workflow,
node,
mock<IWorkflowExecuteAdditionalData>({
currentNodeParameters: node.parameters,
credentialsHelper: mock<IWorkflowExecuteAdditionalData['credentialsHelper']>({
getParentTypes: () => [],
authenticate: async (_creds, _type, options) => {
set(options, 'headers.authorization', 'mockAuth');
return options as IHttpRequestOptions;
},
}),
hooks: mock<WorkflowHooks>(),
}),
mode,
'init',
);
pollContext.getNode = () => node;
pollContext.getCredentials = async <T extends object = ICredentialDataDecryptedObject>() =>
(options.credential ?? {}) as T;
pollContext.getNodeParameter = (parameterName, fallback) =>
get(node.parameters, parameterName) ?? fallback;
const response = await trigger.poll?.call(pollContext);
return {
response,
};
}