mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-29 06:29:42 -08:00
233 lines
7.2 KiB
TypeScript
233 lines
7.2 KiB
TypeScript
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,
|
|
};
|
|
}
|