Files
n8n-enterprise-unlocked/packages/workflow/test/RoutingNode.test.ts
T
0da398b0e4 ✨ Nodes as JSON and authentication redesign (#2401)
* ✨ change FE to handle new object type

* 🚸 improve UX of handling invalid credentials

* 🚧 WIP

* 🎨 fix typescript issues

* 🐘 add migrations for all supported dbs

* ✏️ add description to migrations

* ⚡ add credential update on import

* ⚡ resolve after merge issues

* 👕 fix lint issues

* ⚡ check credentials on workflow create/update

* update interface

* 👕 fix ts issues

* ⚡ adaption to new credentials UI

* 🐛 intialize cache on BE for credentials check

* 🐛 fix undefined oldCredentials

* 🐛 fix deleting credential

* 🐛 fix check for undefined keys

* 🐛 fix disabling edit in execution

* 🎨 just show credential name on execution view

* ✏️  remove TODO

* ⚡ implement review suggestions

* ⚡ add cache to getCredentialsByType

* ⏪ use getter instead of cache

* ✏️ fix variable name typo

* 🐘 include waiting nodes to migrations

* 🐛 fix reverting migrations command

* ⚡ update typeorm command

* ✨ create db:revert command

* 👕 fix lint error

* ✨ Add optional authenticate method to credentials

* ⚡ Simplify code and add authentication support to MattermostApi

* 👕 Fix lint issue

* ⚡ Add support to own-mode

* 👕 Fix lint issue

* ✨ Add support for predefined auth types bearer and headerAuth

* ⚡ Make sure that DateTime Node always returns strings

* ⚡ Add support for moment types to If Node

* ⚡ Make it possible for HTTP Request Node to use all credential types

* ✨ Add basicAuth support

* Add a new dropcontact node

* ✨ First basic implementation of mainly JSON based nodes

* ✨ Add fixedCollection support, added value parameter and
expression support for value and property

* Improvements to #2389

* ⚡ Add credentials verification

* ⚡ Small improvement

* ⚡ set default time to 45 seconds

* ✨ Add support for preSend and postReceive methods

* ➕ Add lodash merge and set depedency to workflow

* 👕 Fix lint issue

* ⚡ Improvements

* ⚡ Improvements

* ⚡ Improvements

* ⚡ Improvements

* ⚡ Improvements

* 🐛 Set siren and language correctly

* ⚡ Add support for requestDefaults

* ⚡ Add support for baseURL to httpRequest

* ⚡ Move baseURL to correct location

* ✨ Add support for options loading

* 🐛 Fix error with fullAccess nodes

* ✨ Add credential test functionality

* 🐛 Fix issue with OAuth autentication and lint issue

* ⚡ Fix build issue

* 🐛 Fix issue that url got always overwritten to empty

* ✨ Add pagination support

* ⚡ Code fix required after merge

* ⚡ Remove not needed imports

* ⚡ Fix credential test

* ✨ Add expression support for request properties and $self
support on properties

* ⚡ Rename $self to $value

* 👕 Fix lint issue

* ⚡ Add example how to send data in path

* ✨ Make it possible to not sent in dot notation

* ✨ Add support for postReceive:rootProperty

* ⚡ Fix typo

* ✨ Add support for postReceive:set

* ⚡ Some fixes

* ⚡ Small improvement

* ;zap: Separate RoutingNode code

* ⚡ Simplify code and fix bug

* ⚡ Remove unused code

* ✨ Make it possible to define "request" and "requestProperty" on
options

* 👕 Fix lint issue

* ⚡ Change $credentials variables name

* ✨ Enable expressions and access to credentials in requestDefaults

* ⚡ Make parameter option loading use RoutingNode.makeRoutingRequest

* ✨ Allow requestOperations overwrite on LoadOptions

* ✨ Make it possible to access current node parameters in loadOptions

* ⚡ Rename parameters variable to make future proof

* ⚡ Make it possible to use offset-pagination with body

* ✨ Add support for queryAuth

* ⚡ Never return more items than requested

* ✨ Make it possible to overwrite requestOperations on parameter
and option level

* 👕 Fix lint issue

* ✨ Allow simplified auth also with regular nodes

* ✨ Add support for receiving binary data

* 🐛 Fix example node

* ⚡ Rename property "name" to "displayName" in loadOptions

* ⚡ Send data by default as "query" if nothing is set

* ⚡ Rename $self to $parent

* ⚡ Change to work with INodeExecutionData instead of IDataObject

* ⚡ Improve binaryData handling

* ⚡ Property design improvements

* ⚡ Fix property name

* 🚨 Add some tests

* ⚡ Add also test for request

* ⚡ Improve test and fix issues

* ⚡ Improvements to loadOptions

* ⚡ Normalize loadOptions with rest of code

* ⚡ Add info text

* ✨ Add support for $value in postReceive

* 🚨 Add tests for RoutingNode.runNode

* ⚡ Remove TODOs and make url property optional

* ⚡ Fix bug and lint issue

* 🐛 Fix bug that not the correct property got used

* 🚨 Add tests for CredentialsHelper.authenticate

* ⚡ Improve code and resolve expressions also everywhere for
loadOptions and credential test requests

* ✨ Make it possible to define multiple preSend and postReceive
actions

* ✨ Allow to define tests on credentials

* ⚡ Remove test data

* ⬆️ Update package-lock.json file

* ⚡ Remove old not longer used code

Co-authored-by: Ben Hesseldieck <b.hesseldieck@gmail.com>
Co-authored-by: Mutasem <mutdmour@gmail.com>
Co-authored-by: PaulineDropcontact <pauline@dropcontact.io>
Co-authored-by: ricardo <ricardoespinoza105@gmail.com>
2022-02-05 22:55:43 +01:00

1681 lines
36 KiB
TypeScript

import {
INode,
INodeExecutionData,
INodeParameters,
IRequestOptionsFromParameters,
IRunExecutionData,
RoutingNode,
Workflow,
INodeProperties,
IDataObject,
IExecuteSingleFunctions,
IHttpRequestOptions,
IN8nHttpFullResponse,
ITaskDataConnections,
INodeExecuteFunctions,
IN8nRequestOperations,
INodeCredentialDescription,
} from '../src';
import * as Helpers from './Helpers';
const postReceiveFunction1 = async function (
this: IExecuteSingleFunctions,
items: INodeExecutionData[],
response: IN8nHttpFullResponse,
): Promise<INodeExecutionData[]> {
items.forEach((item) => (item.json1 = { success: true }));
return items;
};
const preSendFunction1 = async function (
this: IExecuteSingleFunctions,
requestOptions: IHttpRequestOptions,
): Promise<IHttpRequestOptions> {
requestOptions.headers = (requestOptions.headers || {}) as IDataObject;
requestOptions.headers.addedIn = 'preSendFunction1';
return requestOptions;
};
describe('RoutingNode', () => {
describe('getRequestOptionsFromParameters', () => {
const tests: Array<{
description: string;
input: {
nodeParameters: INodeParameters;
nodeTypeProperties: INodeProperties;
};
output: IRequestOptionsFromParameters | undefined;
}> = [
{
description: 'single parameter, only send defined, fixed value',
input: {
nodeParameters: {},
nodeTypeProperties: {
displayName: 'Email',
name: 'email',
type: 'string',
routing: {
send: {
property: 'toEmail',
type: 'body',
value: 'fixedValue',
},
},
default: '',
},
},
output: {
options: {
qs: {},
body: {
toEmail: 'fixedValue',
},
},
preSend: [],
postReceive: [],
requestOperations: {},
},
},
{
description: 'single parameter, only send defined, using expression',
input: {
nodeParameters: {
email: 'test@test.com',
},
nodeTypeProperties: {
displayName: 'Email',
name: 'email',
type: 'string',
routing: {
send: {
property: 'toEmail',
type: 'body',
value: '={{$value.toUpperCase()}}',
},
},
default: '',
},
},
output: {
options: {
qs: {},
body: {
toEmail: 'TEST@TEST.COM',
},
},
preSend: [],
postReceive: [],
requestOperations: {},
},
},
{
description: 'single parameter, send and operations defined, fixed value',
input: {
nodeParameters: {},
nodeTypeProperties: {
displayName: 'Email',
name: 'email',
type: 'string',
routing: {
send: {
property: 'toEmail',
type: 'body',
value: 'fixedValue',
},
operations: {
pagination: {
type: 'offset',
properties: {
limitParameter: 'limit',
offsetParameter: 'offset',
pageSize: 10,
rootProperty: 'data',
type: 'body',
},
},
},
},
default: '',
},
},
output: {
options: {
qs: {},
body: {
toEmail: 'fixedValue',
},
},
preSend: [],
postReceive: [],
requestOperations: {
pagination: {
type: 'offset',
properties: {
limitParameter: 'limit',
offsetParameter: 'offset',
pageSize: 10,
rootProperty: 'data',
type: 'body',
},
},
},
},
},
{
description: 'mutliple parameters, complex example with everything',
input: {
nodeParameters: {
multipleFields: {
value1: 'v1',
value2: 'v2',
value3: 'v3',
value4: 4,
lowerLevel: {
lowLevelValue1: 1,
lowLevelValue2: 'llv2',
},
customPropertiesSingle1: {
property: {
name: 'cSName1',
value: 'cSValue1',
},
},
customPropertiesMulti: {
property0: [
{
name: 'cM0Name1',
value: 'cM0Value1',
},
{
name: 'cM0Name2',
value: 'cM0Value2',
},
],
property1: [
{
name: 'cM1Name2',
value: 'cM1Value2',
},
{
name: 'cM1Name2',
value: 'cM1Value2',
},
],
},
},
},
nodeTypeProperties: {
displayName: 'Multiple Fields',
name: 'multipleFields',
type: 'collection',
placeholder: 'Add Field',
routing: {
request: {
method: 'GET',
url: '/destination1',
},
operations: {
pagination: {
type: 'offset',
properties: {
limitParameter: 'limit1',
offsetParameter: 'offset1',
pageSize: 1,
rootProperty: 'data1',
type: 'body',
},
},
},
output: {
maxResults: 10,
postReceive: [postReceiveFunction1],
},
},
default: {},
options: [
{
displayName: 'Value 1',
name: 'value1',
type: 'string',
routing: {
send: {
property: 'value1',
type: 'body',
},
},
default: '',
},
{
displayName: 'Value 2',
name: 'value2',
type: 'string',
routing: {
send: {
property: 'topLevel.value2',
propertyInDotNotation: false,
type: 'body',
preSend: [preSendFunction1],
},
},
default: '',
},
{
displayName: 'Value 3',
name: 'value3',
type: 'string',
routing: {
send: {
property: 'lowerLevel.value3',
type: 'body',
},
},
default: '',
},
{
displayName: 'Value 4',
name: 'value4',
type: 'number',
default: 0,
routing: {
send: {
property: 'value4',
type: 'query',
},
output: {
maxResults: '={{$value}}',
},
operations: {
pagination: {
type: 'offset',
properties: {
limitParameter: 'limit100',
offsetParameter: 'offset100',
pageSize: 100,
rootProperty: 'data100',
type: 'query',
},
},
},
},
},
// This one should not be included
{
displayName: 'Value 5',
name: 'value5',
type: 'number',
displayOptions: {
show: {
value4: [1],
},
},
default: 5,
routing: {
send: {
property: 'value5',
type: 'query',
},
operations: {
pagination: {
type: 'offset',
properties: {
limitParameter: 'limit10',
offsetParameter: 'offset10',
pageSize: 10,
rootProperty: 'data10',
type: 'body',
},
},
},
},
},
{
displayName: 'Lower Level',
name: 'lowerLevel',
type: 'collection',
placeholder: 'Add Field',
default: {},
options: [
{
displayName: 'Low Level Value1',
name: 'lowLevelValue1',
type: 'number',
default: 0,
routing: {
send: {
property: 'llvalue1',
type: 'query',
},
},
},
{
displayName: 'Low Level Value2',
name: 'lowLevelValue2',
type: 'string',
default: '',
routing: {
send: {
property: 'llvalue2',
type: 'query',
preSend: [preSendFunction1],
},
output: {
postReceive: [
{
type: 'rootProperty',
properties: {
property: 'data',
},
},
],
},
},
},
],
},
// Test fixed collection1: multipleValues=false
{
displayName: 'Custom Properties1 (single)',
name: 'customPropertiesSingle1',
placeholder: 'Add Custom Property',
type: 'fixedCollection',
default: {},
options: [
{
name: 'property',
displayName: 'Property',
values: [
// To set: { single-customValues: { name: 'name', value: 'value'} }
{
displayName: 'Property Name',
name: 'name',
type: 'string',
default: '',
routing: {
request: {
method: 'POST',
url: '=/{{$value}}',
},
send: {
property: 'single-customValues.name',
},
},
},
{
displayName: 'Property Value',
name: 'value',
type: 'string',
default: '',
routing: {
send: {
property: 'single-customValues.value',
},
},
},
],
},
],
},
// Test fixed collection: multipleValues=true
{
displayName: 'Custom Properties (multi)',
name: 'customPropertiesMulti',
placeholder: 'Add Custom Property',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
default: {},
options: [
{
name: 'property0',
displayName: 'Property0',
values: [
// To set: { name0: 'value0', name1: 'value1' }
{
displayName: 'Property Name',
name: 'name',
type: 'string',
default: '',
description: 'Name of the property to set.',
},
{
displayName: 'Property Value',
name: 'value',
type: 'string',
default: '',
routing: {
send: {
property: '=customMulti0.{{$parent.name}}',
type: 'body',
},
},
description: 'Value of the property to set.',
},
],
},
{
name: 'property1',
displayName: 'Property1',
values: [
// To set: { customValues: [ { name: 'name0', value: 'value0'}, { name: 'name1', value: 'value1'} ]}
{
displayName: 'Property Name',
name: 'name',
type: 'string',
default: '',
routing: {
send: {
property: '=customMulti1[{{$index}}].name',
type: 'body',
},
},
},
{
displayName: 'Property Value',
name: 'value',
type: 'string',
default: '',
routing: {
send: {
property: '=customMulti1[{{$index}}].value',
type: 'body',
},
},
},
],
},
],
},
],
},
},
output: {
maxResults: 4,
options: {
method: 'POST',
url: '/cSName1',
qs: {
value4: 4,
llvalue1: 1,
llvalue2: 'llv2',
'single-customValues': {
name: 'cSName1',
value: 'cSValue1',
},
},
body: {
value1: 'v1',
'topLevel.value2': 'v2',
lowerLevel: {
value3: 'v3',
},
customMulti0: {
cM0Name1: 'cM0Value1',
cM0Name2: 'cM0Value2',
},
customMulti1: [
{
name: 'cM1Name2',
value: 'cM1Value2',
},
{
name: 'cM1Name2',
value: 'cM1Value2',
},
],
},
},
preSend: [preSendFunction1, preSendFunction1],
postReceive: [
{
actions: [postReceiveFunction1],
data: {
parameterValue: {
value1: 'v1',
value2: 'v2',
value3: 'v3',
value4: 4,
lowerLevel: {
lowLevelValue1: 1,
lowLevelValue2: 'llv2',
},
customPropertiesSingle1: {
property: {
name: 'cSName1',
value: 'cSValue1',
},
},
customPropertiesMulti: {
property0: [
{
name: 'cM0Name1',
value: 'cM0Value1',
},
{
name: 'cM0Name2',
value: 'cM0Value2',
},
],
property1: [
{
name: 'cM1Name2',
value: 'cM1Value2',
},
{
name: 'cM1Name2',
value: 'cM1Value2',
},
],
},
},
},
},
{
actions: [
{
type: 'rootProperty',
properties: {
property: 'data',
},
},
],
data: {
parameterValue: 'llv2',
},
},
],
requestOperations: {
pagination: {
type: 'offset',
properties: {
limitParameter: 'limit100',
offsetParameter: 'offset100',
pageSize: 100,
rootProperty: 'data100',
type: 'query',
},
},
},
},
},
];
const nodeTypes = Helpers.NodeTypes();
const node: INode = {
parameters: {},
name: 'test',
type: 'test.set',
typeVersion: 1,
position: [0, 0],
};
const mode = 'internal';
const runIndex = 0;
const itemIndex = 0;
const connectionInputData: INodeExecutionData[] = [];
const runExecutionData: IRunExecutionData = { resultData: { runData: {} } };
const additionalData = Helpers.WorkflowExecuteAdditionalData();
const path = '';
const nodeType = nodeTypes.getByName(node.type);
const workflowData = {
nodes: [node],
connections: {},
};
for (const testData of tests) {
test(testData.description, () => {
node.parameters = testData.input.nodeParameters;
// @ts-ignore
nodeType.description.properties = [testData.input.nodeTypeProperties];
const workflow = new Workflow({
nodes: workflowData.nodes,
connections: workflowData.connections,
active: false,
nodeTypes,
});
const routingNode = new RoutingNode(
workflow,
node,
connectionInputData,
runExecutionData ?? null,
additionalData,
mode,
);
const executeSingleFunctions = Helpers.getExecuteSingleFunctions(
workflow,
runExecutionData,
runIndex,
connectionInputData,
{},
node,
itemIndex,
additionalData,
mode,
);
const result = routingNode.getRequestOptionsFromParameters(
executeSingleFunctions,
testData.input.nodeTypeProperties,
itemIndex,
runIndex,
path,
{},
);
expect(result).toEqual(testData.output);
});
}
});
describe('runNode', () => {
const tests: Array<{
description: string;
input: {
nodeType: {
properties?: INodeProperties[];
credentials?: INodeCredentialDescription[];
requestDefaults?: IHttpRequestOptions;
requestOperations?: IN8nRequestOperations;
};
node: {
parameters: INodeParameters;
};
};
output: INodeExecutionData[][] | undefined;
}> = [
{
description: 'single parameter, only send defined, fixed value, using requestDefaults',
input: {
nodeType: {
requestDefaults: {
baseURL: 'http://127.0.0.1:5678',
url: '/test-url',
},
properties: [
{
displayName: 'Email',
name: 'email',
type: 'string',
routing: {
send: {
property: 'toEmail',
type: 'body',
value: 'fixedValue',
},
},
default: '',
},
],
},
node: {
parameters: {},
},
},
output: [
[
{
json: {
headers: {},
statusCode: 200,
requestOptions: {
url: '/test-url',
qs: {},
body: {
toEmail: 'fixedValue',
},
baseURL: 'http://127.0.0.1:5678',
returnFullResponse: true,
},
},
},
],
],
},
{
description: 'single parameter, only send defined, fixed value, using requestDefaults',
input: {
nodeType: {
requestDefaults: {
baseURL: 'http://127.0.0.1:5678',
url: '/test-url',
},
properties: [
{
displayName: 'Email',
name: 'email',
type: 'string',
routing: {
send: {
property: 'toEmail',
type: 'body',
value: 'fixedValue',
},
},
default: '',
},
],
},
node: {
parameters: {},
},
},
output: [
[
{
json: {
headers: {},
statusCode: 200,
requestOptions: {
url: '/test-url',
qs: {},
body: {
toEmail: 'fixedValue',
},
baseURL: 'http://127.0.0.1:5678',
returnFullResponse: true,
},
},
},
],
],
},
{
description:
'single parameter, only send defined, using expression, using requestDefaults with overwrite',
input: {
node: {
parameters: {
email: 'test@test.com',
},
},
nodeType: {
requestDefaults: {
baseURL: 'http://127.0.0.1:5678',
url: '/test-url',
},
properties: [
{
displayName: 'Email',
name: 'email',
type: 'string',
routing: {
send: {
property: 'toEmail',
type: 'body',
value: '={{$value.toUpperCase()}}',
},
request: {
url: '/overwritten',
},
},
default: '',
},
],
},
},
output: [
[
{
json: {
headers: {},
statusCode: 200,
requestOptions: {
url: '/overwritten',
qs: {},
body: {
toEmail: 'TEST@TEST.COM',
},
baseURL: 'http://127.0.0.1:5678',
returnFullResponse: true,
},
},
},
],
],
},
{
description:
'single parameter, only send defined, using expression, using requestDefaults with overwrite and expressions',
input: {
node: {
parameters: {
endpoint: 'custom-overwritten',
},
},
nodeType: {
requestDefaults: {
baseURL: 'http://127.0.0.1:5678',
url: '/test-url',
},
properties: [
{
displayName: 'Endpoint',
name: 'endpoint',
type: 'string',
routing: {
send: {
property: '={{"theProperty"}}',
type: 'body',
value: '={{$value}}',
},
request: {
url: '=/{{$value}}',
},
},
default: '',
},
],
},
},
output: [
[
{
json: {
headers: {},
statusCode: 200,
requestOptions: {
url: '/custom-overwritten',
qs: {},
body: {
theProperty: 'custom-overwritten',
},
baseURL: 'http://127.0.0.1:5678',
returnFullResponse: true,
},
},
},
],
],
},
{
description: 'single parameter, send and operations defined, fixed value with pagination',
input: {
node: {
parameters: {},
},
nodeType: {
properties: [
{
displayName: 'Email',
name: 'email',
type: 'string',
routing: {
send: {
property: 'toEmail',
type: 'body',
value: 'fixedValue',
paginate: true,
},
operations: {
pagination: {
type: 'offset',
properties: {
limitParameter: 'limit',
offsetParameter: 'offset',
pageSize: 10,
type: 'body',
},
},
},
},
default: '',
},
],
},
},
output: [
[
{
json: {
headers: {},
statusCode: 200,
requestOptions: {
qs: {},
body: {
toEmail: 'fixedValue',
limit: 10,
offset: 10,
},
returnFullResponse: true,
},
},
},
],
],
},
{
description: 'mutliple parameters, complex example with everything',
input: {
node: {
parameters: {
multipleFields: {
value1: 'v1',
value2: 'v2',
value3: 'v3',
value4: 4,
lowerLevel: {
lowLevelValue1: 1,
lowLevelValue2: 'llv2',
},
customPropertiesSingle1: {
property: {
name: 'cSName1',
value: 'cSValue1',
},
},
customPropertiesMulti: {
property0: [
{
name: 'cM0Name1',
value: 'cM0Value1',
},
{
name: 'cM0Name2',
value: 'cM0Value2',
},
],
property1: [
{
name: 'cM1Name2',
value: 'cM1Value2',
},
{
name: 'cM1Name2',
value: 'cM1Value2',
},
],
},
},
},
},
nodeType: {
properties: [
{
displayName: 'Multiple Fields',
name: 'multipleFields',
type: 'collection',
placeholder: 'Add Field',
routing: {
request: {
method: 'GET',
url: '/destination1',
},
operations: {
pagination: {
type: 'offset',
properties: {
limitParameter: 'limit1',
offsetParameter: 'offset1',
pageSize: 1,
rootProperty: 'data1',
type: 'body',
},
},
},
output: {
maxResults: 10,
postReceive: [postReceiveFunction1],
},
},
default: {},
options: [
{
displayName: 'Value 1',
name: 'value1',
type: 'string',
routing: {
send: {
property: 'value1',
type: 'body',
},
},
default: '',
},
{
displayName: 'Value 2',
name: 'value2',
type: 'string',
routing: {
send: {
property: 'topLevel.value2',
propertyInDotNotation: false,
type: 'body',
preSend: [preSendFunction1],
},
},
default: '',
},
{
displayName: 'Value 3',
name: 'value3',
type: 'string',
routing: {
send: {
property: 'lowerLevel.value3',
type: 'body',
},
},
default: '',
},
{
displayName: 'Value 4',
name: 'value4',
type: 'number',
default: 0,
routing: {
send: {
property: 'value4',
type: 'query',
},
output: {
maxResults: '={{$value}}',
},
operations: {
pagination: {
type: 'offset',
properties: {
limitParameter: 'limit100',
offsetParameter: 'offset100',
pageSize: 100,
rootProperty: 'data100',
type: 'query',
},
},
},
},
},
// This one should not be included
{
displayName: 'Value 5',
name: 'value5',
type: 'number',
displayOptions: {
show: {
value4: [1],
},
},
default: 5,
routing: {
send: {
property: 'value5',
type: 'query',
},
operations: {
pagination: {
type: 'offset',
properties: {
limitParameter: 'limit10',
offsetParameter: 'offset10',
pageSize: 10,
rootProperty: 'data10',
type: 'body',
},
},
},
},
},
{
displayName: 'Lower Level',
name: 'lowerLevel',
type: 'collection',
placeholder: 'Add Field',
default: {},
options: [
{
displayName: 'Low Level Value1',
name: 'lowLevelValue1',
type: 'number',
default: 0,
routing: {
send: {
property: 'llvalue1',
type: 'query',
},
},
},
{
displayName: 'Low Level Value2',
name: 'lowLevelValue2',
type: 'string',
default: '',
routing: {
send: {
property: 'llvalue2',
type: 'query',
preSend: [preSendFunction1],
},
output: {
postReceive: [
{
type: 'rootProperty',
properties: {
property: 'requestOptions',
},
},
],
},
},
},
],
},
// Test fixed collection1: multipleValues=false
{
displayName: 'Custom Properties1 (single)',
name: 'customPropertiesSingle1',
placeholder: 'Add Custom Property',
type: 'fixedCollection',
default: {},
options: [
{
name: 'property',
displayName: 'Property',
values: [
// To set: { single-customValues: { name: 'name', value: 'value'} }
{
displayName: 'Property Name',
name: 'name',
type: 'string',
default: '',
routing: {
request: {
method: 'POST',
url: '=/{{$value}}',
},
send: {
property: 'single-customValues.name',
},
},
},
{
displayName: 'Property Value',
name: 'value',
type: 'string',
default: '',
routing: {
send: {
property: 'single-customValues.value',
},
},
},
],
},
],
},
// Test fixed collection: multipleValues=true
{
displayName: 'Custom Properties (multi)',
name: 'customPropertiesMulti',
placeholder: 'Add Custom Property',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
default: {},
options: [
{
name: 'property0',
displayName: 'Property0',
values: [
// To set: { name0: 'value0', name1: 'value1' }
{
displayName: 'Property Name',
name: 'name',
type: 'string',
default: '',
description: 'Name of the property to set.',
},
{
displayName: 'Property Value',
name: 'value',
type: 'string',
default: '',
routing: {
send: {
property: '=customMulti0.{{$parent.name}}',
type: 'body',
},
},
description: 'Value of the property to set.',
},
],
},
{
name: 'property1',
displayName: 'Property1',
values: [
// To set: { customValues: [ { name: 'name0', value: 'value0'}, { name: 'name1', value: 'value1'} ]}
{
displayName: 'Property Name',
name: 'name',
type: 'string',
default: '',
routing: {
send: {
property: '=customMulti1[{{$index}}].name',
type: 'body',
},
},
},
{
displayName: 'Property Value',
name: 'value',
type: 'string',
default: '',
routing: {
send: {
property: '=customMulti1[{{$index}}].value',
type: 'body',
},
},
},
],
},
],
},
],
},
],
},
},
output: [
[
{
json: {
url: '/cSName1',
qs: {
value4: 4,
llvalue1: 1,
llvalue2: 'llv2',
'single-customValues': {
name: 'cSName1',
value: 'cSValue1',
},
},
body: {
value1: 'v1',
'topLevel.value2': 'v2',
lowerLevel: {
value3: 'v3',
},
customMulti0: {
cM0Name1: 'cM0Value1',
cM0Name2: 'cM0Value2',
},
customMulti1: [
{
name: 'cM1Name2',
value: 'cM1Value2',
},
{
name: 'cM1Name2',
value: 'cM1Value2',
},
],
},
method: 'POST',
headers: {
addedIn: 'preSendFunction1',
},
returnFullResponse: true,
},
},
],
],
},
{
description: 'single parameter, postReceive: set',
input: {
nodeType: {
requestDefaults: {
baseURL: 'http://127.0.0.1:5678',
url: '/test-url',
},
properties: [
{
displayName: 'JSON Data',
name: 'jsonData',
type: 'string',
routing: {
send: {
property: 'jsonData',
type: 'body',
},
output: {
postReceive: [
{
type: 'set',
properties: {
value: '={{ { "value": $value, "response": $response } }}',
},
},
],
},
},
default: '',
},
],
},
node: {
parameters: {
jsonData: {
root: [
{
name: 'Jim',
age: 34,
},
{
name: 'James',
age: 44,
},
],
},
},
},
},
output: [
[
{
json: {
value: {
root: [
{
name: 'Jim',
age: 34,
},
{
name: 'James',
age: 44,
},
],
},
response: {
body: {
headers: {},
statusCode: 200,
requestOptions: {
qs: {},
body: {
jsonData: {
root: [
{
name: 'Jim',
age: 34,
},
{
name: 'James',
age: 44,
},
],
},
},
baseURL: 'http://127.0.0.1:5678',
url: '/test-url',
returnFullResponse: true,
},
},
},
},
},
],
],
},
{
description: 'single parameter, postReceive: rootProperty',
input: {
nodeType: {
requestDefaults: {
baseURL: 'http://127.0.0.1:5678',
url: '/test-url',
},
properties: [
{
displayName: 'JSON Data',
name: 'jsonData',
type: 'string',
routing: {
send: {
property: 'jsonData',
type: 'body',
},
output: {
postReceive: [
{
type: 'rootProperty',
properties: {
property: 'requestOptions',
},
},
{
type: 'rootProperty',
properties: {
property: 'body.jsonData.root',
},
},
],
},
},
default: '',
},
],
},
node: {
parameters: {
jsonData: {
root: [
{
name: 'Jim',
age: 34,
},
{
name: 'James',
age: 44,
},
],
},
},
},
},
output: [
[
{
json: {
name: 'Jim',
age: 34,
},
},
{
json: {
name: 'James',
age: 44,
},
},
],
],
},
{
description: 'single parameter, mutliple postReceive: rootProperty, setKeyValue, sort',
input: {
nodeType: {
requestDefaults: {
baseURL: 'http://127.0.0.1:5678',
url: '/test-url',
},
properties: [
{
displayName: 'JSON Data',
name: 'jsonData',
type: 'string',
routing: {
send: {
property: 'jsonData',
type: 'body',
},
output: {
postReceive: [
{
type: 'rootProperty',
properties: {
property: 'requestOptions.body.jsonData.root',
},
},
{
type: 'setKeyValue',
properties: {
display1: '={{$responseItem.name}} ({{$responseItem.age}})',
display2: '={{$responseItem.name}} is {{$responseItem.age}}',
},
},
{
type: 'sort',
properties: {
key: 'display1',
},
},
],
},
},
default: '',
},
],
},
node: {
parameters: {
jsonData: {
root: [
{
name: 'Jim',
age: 34,
},
{
name: 'James',
age: 44,
},
],
},
},
},
},
output: [
[
{
json: {
display1: 'James (44)',
display2: 'James is 44',
},
},
{
json: {
display1: 'Jim (34)',
display2: 'Jim is 34',
},
},
],
],
},
];
const nodeTypes = Helpers.NodeTypes();
const baseNode: INode = {
parameters: {},
name: 'test',
type: 'test.set',
typeVersion: 1,
position: [0, 0],
};
const mode = 'internal';
const runIndex = 0;
const itemIndex = 0;
const connectionInputData: INodeExecutionData[] = [];
const runExecutionData: IRunExecutionData = { resultData: { runData: {} } };
const additionalData = Helpers.WorkflowExecuteAdditionalData();
const nodeType = nodeTypes.getByName(baseNode.type);
const inputData: ITaskDataConnections = {
main: [
[
{
json: {},
},
],
],
};
for (const testData of tests) {
test(testData.description, async () => {
const node: INode = { ...baseNode, ...testData.input.node };
const workflowData = {
nodes: [node],
connections: {},
};
// @ts-ignore
nodeType.description = { ...testData.input.nodeType };
const workflow = new Workflow({
nodes: workflowData.nodes,
connections: workflowData.connections,
active: false,
nodeTypes,
});
const routingNode = new RoutingNode(
workflow,
node,
connectionInputData,
runExecutionData ?? null,
additionalData,
mode,
);
// @ts-ignore
const nodeExecuteFunctions: INodeExecuteFunctions = {
getExecuteFunctions: () => {
return Helpers.getExecuteFunctions(
workflow,
runExecutionData,
runIndex,
connectionInputData,
{},
node,
itemIndex,
additionalData,
mode,
);
},
getExecuteSingleFunctions: () => {
return Helpers.getExecuteSingleFunctions(
workflow,
runExecutionData,
runIndex,
connectionInputData,
{},
node,
itemIndex,
additionalData,
mode,
);
},
};
const result = await routingNode.runNode(
inputData,
runIndex,
nodeType,
nodeExecuteFunctions,
);
expect(result).toEqual(testData.output);
});
}
});
});