mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +00:00
refactor: Overhaul nodes-testing setup - Part 1 (no-changelog) (#14303)
This commit is contained in:
committed by
GitHub
parent
f85b851851
commit
73e8d76e13
@@ -1,54 +0,0 @@
|
||||
import nock from 'nock';
|
||||
|
||||
import { executeWorkflow } from '../ExecuteWorkflow';
|
||||
import * as Helpers from '../Helpers';
|
||||
import type { WorkflowTestData } from '../types';
|
||||
|
||||
const records = [
|
||||
{
|
||||
id: 'rec2BWBoyS5QsS7pT',
|
||||
createdTime: '2022-08-25T08:22:34.000Z',
|
||||
fields: {
|
||||
name: 'Tim',
|
||||
email: 'tim@email.com',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
describe('Execute Airtable Node', () => {
|
||||
beforeEach(() => {
|
||||
nock('https://api.airtable.com/v0')
|
||||
.get('/appIaXXdDqS5ORr4V/tbljyBEdYzCPF0NDh?pageSize=100')
|
||||
.reply(200, { records });
|
||||
});
|
||||
|
||||
const tests: WorkflowTestData[] = [
|
||||
{
|
||||
description: 'List Airtable Records',
|
||||
input: {
|
||||
workflowData: Helpers.readJsonFileSync('test/nodes/Airtable/workflow.json'),
|
||||
},
|
||||
output: {
|
||||
nodeData: {
|
||||
Airtable: [[...records.map((r) => ({ json: r }))]],
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const nodeTypes = Helpers.setup(tests);
|
||||
|
||||
for (const testData of tests) {
|
||||
test(testData.description, async () => {
|
||||
try {
|
||||
// execute workflow
|
||||
await executeWorkflow(testData, nodeTypes);
|
||||
} catch (error) {
|
||||
expect(error).toBeUndefined();
|
||||
expect(error.message).toEqual(
|
||||
'The API Key connection was deprecated by Airtable, please use Access Token or OAuth2 instead.',
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -1,57 +0,0 @@
|
||||
{
|
||||
"meta": {
|
||||
"instanceId": "104a4d08d8897b8bdeb38aaca515021075e0bd8544c983c2bb8c86e6a8e6081c"
|
||||
},
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"id": "f857c37f-36c1-4c9c-9b5f-f6ef49db67e3",
|
||||
"name": "On clicking 'execute'",
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [820, 380]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"operation": "list",
|
||||
"application": {
|
||||
"__rl": true,
|
||||
"value": "https://airtable.com/appIaXXdDqS5ORr4V/tbljyBEdYzCPF0NDh/viwInsMdsxffad0aU",
|
||||
"mode": "url",
|
||||
"__regex": "https://airtable.com/([a-zA-Z0-9]{2,})"
|
||||
},
|
||||
"table": {
|
||||
"__rl": true,
|
||||
"value": "https://airtable.com/appIaXXdDqS5ORr4V/tbljyBEdYzCPF0NDh/viwInsMdsxffad0aU",
|
||||
"mode": "url",
|
||||
"__regex": "https://airtable.com/[a-zA-Z0-9]{2,}/([a-zA-Z0-9]{2,})"
|
||||
},
|
||||
"additionalOptions": {}
|
||||
},
|
||||
"id": "5654d3b3-fe83-4988-889b-94f107d41807",
|
||||
"name": "Airtable",
|
||||
"type": "n8n-nodes-base.airtable",
|
||||
"typeVersion": 1,
|
||||
"position": [1020, 380],
|
||||
"credentials": {
|
||||
"airtableApi": {
|
||||
"id": "20",
|
||||
"name": "Airtable account"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"On clicking 'execute'": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Airtable",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,24 @@
|
||||
import { WorkflowExecute } from 'n8n-core';
|
||||
import type { INodeTypes, IRun, IRunExecutionData } from 'n8n-workflow';
|
||||
import { Container } from '@n8n/di';
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import { ExecutionLifecycleHooks, WorkflowExecute } from 'n8n-core';
|
||||
import type {
|
||||
IRun,
|
||||
IRunExecutionData,
|
||||
IWorkflowExecuteAdditionalData,
|
||||
WorkflowTestData,
|
||||
} from 'n8n-workflow';
|
||||
import { createDeferredPromise, Workflow } from 'n8n-workflow';
|
||||
import nock from 'nock';
|
||||
|
||||
import * as Helpers from './Helpers';
|
||||
import type { WorkflowTestData } from './types';
|
||||
import { CredentialsHelper } from './credentials-helper';
|
||||
import { NodeTypes } from './node-types';
|
||||
|
||||
// This is (temporarily) needed to setup LoadNodesAndCredentials
|
||||
import './Helpers';
|
||||
|
||||
export async function executeWorkflow(testData: WorkflowTestData) {
|
||||
const nodeTypes = Container.get(NodeTypes);
|
||||
|
||||
export async function executeWorkflow(testData: WorkflowTestData, nodeTypes: INodeTypes) {
|
||||
if (testData.nock) {
|
||||
const { baseUrl, mocks } = testData.nock;
|
||||
const agent = nock(baseUrl);
|
||||
@@ -46,7 +58,18 @@ export async function executeWorkflow(testData: WorkflowTestData, nodeTypes: INo
|
||||
});
|
||||
const waitPromise = createDeferredPromise<IRun>();
|
||||
const nodeExecutionOrder: string[] = [];
|
||||
const additionalData = Helpers.WorkflowExecuteAdditionalData(waitPromise, nodeExecutionOrder);
|
||||
|
||||
const hooks = new ExecutionLifecycleHooks('trigger', '1', mock());
|
||||
hooks.addHandler('nodeExecuteAfter', (nodeName) => {
|
||||
nodeExecutionOrder.push(nodeName);
|
||||
});
|
||||
hooks.addHandler('workflowExecuteAfter', (fullRunData) => waitPromise.resolve(fullRunData));
|
||||
const additionalData = mock<IWorkflowExecuteAdditionalData>({
|
||||
credentialsHelper: Container.get(CredentialsHelper),
|
||||
hooks,
|
||||
// Get from node.parameters
|
||||
currentNodeParameters: undefined,
|
||||
});
|
||||
|
||||
let executionData: IRun;
|
||||
const runExecutionData: IRunExecutionData = {
|
||||
|
||||
@@ -131,6 +131,18 @@ BQIDAQAB
|
||||
},
|
||||
},
|
||||
},
|
||||
microsoftExcelOAuth2Api: {
|
||||
scope: 'openid',
|
||||
oauthTokenData: {
|
||||
access_token: 'token',
|
||||
},
|
||||
},
|
||||
microsoftTeamsOAuth2Api: {
|
||||
scope: 'openid',
|
||||
oauthTokenData: {
|
||||
access_token: 'token',
|
||||
},
|
||||
},
|
||||
n8nApi: {
|
||||
apiKey: 'key123',
|
||||
baseUrl: 'https://test.app.n8n.cloud/api/v1',
|
||||
@@ -239,4 +251,7 @@ BQIDAQAB
|
||||
username: 'nathan@n8n.io',
|
||||
password: 'fake-password',
|
||||
},
|
||||
discordWebhookApi: {
|
||||
webhookUri: 'https://discord.com/webhook',
|
||||
},
|
||||
} as const;
|
||||
|
||||
@@ -1,256 +1,50 @@
|
||||
import { Container } from '@n8n/di';
|
||||
import { readFileSync, readdirSync, mkdtempSync } from 'fs';
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import { get } from 'lodash';
|
||||
import { isEmpty } from 'lodash';
|
||||
import {
|
||||
BinaryDataService,
|
||||
Credentials,
|
||||
UnrecognizedNodeTypeError,
|
||||
constructExecutionMetaData,
|
||||
ExecutionLifecycleHooks,
|
||||
} from 'n8n-core';
|
||||
import { BinaryDataService, constructExecutionMetaData } from 'n8n-core';
|
||||
import type {
|
||||
CredentialLoadingDetails,
|
||||
ICredentialDataDecryptedObject,
|
||||
ICredentialType,
|
||||
ICredentialTypeData,
|
||||
ICredentialTypes,
|
||||
IDataObject,
|
||||
IDeferredPromise,
|
||||
IExecuteFunctions,
|
||||
IGetNodeParameterOptions,
|
||||
IHttpRequestHelper,
|
||||
IHttpRequestOptions,
|
||||
INode,
|
||||
INodeCredentials,
|
||||
INodeCredentialsDetails,
|
||||
INodeType,
|
||||
INodeTypeData,
|
||||
INodeTypes,
|
||||
IRun,
|
||||
IVersionedNodeType,
|
||||
IWorkflowBase,
|
||||
IWorkflowExecuteAdditionalData,
|
||||
NodeLoadingDetails,
|
||||
WorkflowTestData,
|
||||
} from 'n8n-workflow';
|
||||
import { ApplicationError, ICredentialsHelper, NodeHelpers } from 'n8n-workflow';
|
||||
import { ApplicationError } from 'n8n-workflow';
|
||||
import nock from 'nock';
|
||||
import { tmpdir } from 'os';
|
||||
import path from 'path';
|
||||
|
||||
import { executeWorkflow } from './ExecuteWorkflow';
|
||||
import { FAKE_CREDENTIALS_DATA } from './FakeCredentialsMap';
|
||||
import { LoadNodesAndCredentials } from './load-nodes-and-credentials';
|
||||
|
||||
const baseDir = path.resolve(__dirname, '../..');
|
||||
|
||||
const getFakeDecryptedCredentials = (
|
||||
nodeCredentials: INodeCredentialsDetails,
|
||||
type: string,
|
||||
fakeCredentialsMap: IDataObject,
|
||||
) => {
|
||||
if (nodeCredentials && fakeCredentialsMap[JSON.stringify(nodeCredentials)]) {
|
||||
return fakeCredentialsMap[JSON.stringify(nodeCredentials)] as ICredentialDataDecryptedObject;
|
||||
}
|
||||
|
||||
if (type && fakeCredentialsMap[type]) {
|
||||
return fakeCredentialsMap[type] as ICredentialDataDecryptedObject;
|
||||
}
|
||||
|
||||
return {};
|
||||
};
|
||||
|
||||
export const readJsonFileSync = <T = any>(filePath: string) =>
|
||||
JSON.parse(readFileSync(path.join(baseDir, filePath), 'utf-8')) as T;
|
||||
|
||||
const knownCredentials = readJsonFileSync<Record<string, CredentialLoadingDetails>>(
|
||||
'dist/known/credentials.json',
|
||||
);
|
||||
const loadNodesAndCredentials = new LoadNodesAndCredentials(baseDir);
|
||||
Container.set(LoadNodesAndCredentials, loadNodesAndCredentials);
|
||||
|
||||
const knownNodes = readJsonFileSync<Record<string, NodeLoadingDetails>>('dist/known/nodes.json');
|
||||
|
||||
class CredentialType implements ICredentialTypes {
|
||||
credentialTypes: ICredentialTypeData = {};
|
||||
|
||||
addCredential(credentialTypeName: string, credentialType: ICredentialType) {
|
||||
this.credentialTypes[credentialTypeName] = {
|
||||
sourcePath: '',
|
||||
type: credentialType,
|
||||
};
|
||||
}
|
||||
|
||||
recognizes(credentialType: string): boolean {
|
||||
return credentialType in this.credentialTypes;
|
||||
}
|
||||
|
||||
getByName(credentialType: string): ICredentialType {
|
||||
return this.credentialTypes[credentialType].type;
|
||||
}
|
||||
|
||||
getSupportedNodes(type: string): string[] {
|
||||
return knownCredentials[type]?.supportedNodes ?? [];
|
||||
}
|
||||
|
||||
getParentTypes(_typeName: string): string[] {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
const credentialTypes = new CredentialType();
|
||||
|
||||
export class CredentialsHelper extends ICredentialsHelper {
|
||||
getCredentialsProperties() {
|
||||
return [];
|
||||
}
|
||||
|
||||
async authenticate(
|
||||
credentials: ICredentialDataDecryptedObject,
|
||||
typeName: string,
|
||||
requestParams: IHttpRequestOptions,
|
||||
): Promise<IHttpRequestOptions> {
|
||||
const credentialType = credentialTypes.getByName(typeName);
|
||||
if (typeof credentialType.authenticate === 'function') {
|
||||
return await credentialType.authenticate(credentials, requestParams);
|
||||
}
|
||||
return requestParams;
|
||||
}
|
||||
|
||||
async preAuthentication(
|
||||
_helpers: IHttpRequestHelper,
|
||||
_credentials: ICredentialDataDecryptedObject,
|
||||
_typeName: string,
|
||||
_node: INode,
|
||||
_credentialsExpired: boolean,
|
||||
): Promise<ICredentialDataDecryptedObject | undefined> {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
getParentTypes(_name: string): string[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
async getDecrypted(
|
||||
_additionalData: IWorkflowExecuteAdditionalData,
|
||||
nodeCredentials: INodeCredentialsDetails,
|
||||
type: string,
|
||||
): Promise<ICredentialDataDecryptedObject> {
|
||||
return getFakeDecryptedCredentials(nodeCredentials, type, FAKE_CREDENTIALS_DATA);
|
||||
}
|
||||
|
||||
async getCredentials(
|
||||
_nodeCredentials: INodeCredentialsDetails,
|
||||
_type: string,
|
||||
): Promise<Credentials> {
|
||||
return new Credentials({ id: null, name: '' }, '', '');
|
||||
}
|
||||
|
||||
async updateCredentials(
|
||||
_nodeCredentials: INodeCredentialsDetails,
|
||||
_type: string,
|
||||
_data: ICredentialDataDecryptedObject,
|
||||
): Promise<void> {}
|
||||
}
|
||||
|
||||
export function WorkflowExecuteAdditionalData(
|
||||
waitPromise: IDeferredPromise<IRun>,
|
||||
nodeExecutionOrder: string[],
|
||||
): IWorkflowExecuteAdditionalData {
|
||||
const hooks = new ExecutionLifecycleHooks('trigger', '1', mock());
|
||||
hooks.addHandler('nodeExecuteAfter', (nodeName) => {
|
||||
nodeExecutionOrder.push(nodeName);
|
||||
});
|
||||
hooks.addHandler('workflowExecuteAfter', (fullRunData) => waitPromise.resolve(fullRunData));
|
||||
|
||||
return mock<IWorkflowExecuteAdditionalData>({
|
||||
credentialsHelper: new CredentialsHelper(),
|
||||
hooks,
|
||||
// Get from node.parameters
|
||||
currentNodeParameters: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
class NodeTypes implements INodeTypes {
|
||||
nodeTypes: INodeTypeData = {};
|
||||
|
||||
getByName(nodeType: string): INodeType | IVersionedNodeType {
|
||||
return this.nodeTypes[nodeType].type;
|
||||
}
|
||||
|
||||
addNode(nodeTypeName: string, nodeType: INodeType | IVersionedNodeType) {
|
||||
const loadedNode = {
|
||||
[nodeTypeName]: {
|
||||
sourcePath: '',
|
||||
type: nodeType,
|
||||
},
|
||||
};
|
||||
this.nodeTypes = {
|
||||
...this.nodeTypes,
|
||||
...loadedNode,
|
||||
};
|
||||
}
|
||||
|
||||
getByNameAndVersion(nodeType: string, version?: number): INodeType {
|
||||
return NodeHelpers.getVersionedNodeType(this.nodeTypes[nodeType].type, version);
|
||||
}
|
||||
|
||||
getKnownTypes(): IDataObject {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
}
|
||||
beforeAll(async () => await loadNodesAndCredentials.init());
|
||||
beforeEach(() => nock.disableNetConnect());
|
||||
|
||||
export function createTemporaryDir(prefix = 'n8n') {
|
||||
return mkdtempSync(path.join(tmpdir(), prefix));
|
||||
}
|
||||
|
||||
export async function initBinaryDataService(mode: 'default' | 'filesystem' = 'default') {
|
||||
export async function initBinaryDataService() {
|
||||
const binaryDataService = new BinaryDataService();
|
||||
await binaryDataService.init({
|
||||
mode: 'default',
|
||||
availableModes: [mode],
|
||||
availableModes: ['default'],
|
||||
localStoragePath: createTemporaryDir(),
|
||||
});
|
||||
Container.set(BinaryDataService, binaryDataService);
|
||||
}
|
||||
|
||||
export function setup(testData: WorkflowTestData[] | WorkflowTestData) {
|
||||
if (!Array.isArray(testData)) {
|
||||
testData = [testData];
|
||||
}
|
||||
|
||||
const nodeTypes = new NodeTypes();
|
||||
|
||||
const nodes = [...new Set(testData.flatMap((data) => data.input.workflowData.nodes))];
|
||||
const credentialNames = nodes
|
||||
.filter((n) => n.credentials)
|
||||
.flatMap(({ credentials }) => Object.keys(credentials as INodeCredentials));
|
||||
for (const credentialName of credentialNames) {
|
||||
const loadInfo = knownCredentials[credentialName];
|
||||
if (!loadInfo) {
|
||||
throw new ApplicationError(`Unknown credential type: ${credentialName}`, {
|
||||
level: 'warning',
|
||||
});
|
||||
}
|
||||
const sourcePath = loadInfo.sourcePath.replace(/^dist\//, './').replace(/\.js$/, '.ts');
|
||||
const nodeSourcePath = path.join(baseDir, sourcePath);
|
||||
const credential = new (require(nodeSourcePath)[loadInfo.className])() as ICredentialType;
|
||||
credentialTypes.addCredential(credentialName, credential);
|
||||
}
|
||||
|
||||
const nodeNames = nodes.map((n) => n.type);
|
||||
for (const nodeName of nodeNames) {
|
||||
const loadInfo = knownNodes[nodeName.replace('n8n-nodes-base.', '')];
|
||||
if (!loadInfo) {
|
||||
throw new UnrecognizedNodeTypeError('n8n-nodes-base', nodeName);
|
||||
}
|
||||
const sourcePath = loadInfo.sourcePath.replace(/^dist\//, './').replace(/\.js$/, '.ts');
|
||||
const nodeSourcePath = path.join(baseDir, sourcePath);
|
||||
const node = new (require(nodeSourcePath)[loadInfo.className])() as INodeType;
|
||||
nodeTypes.addNode(nodeName, node);
|
||||
}
|
||||
|
||||
return nodeTypes;
|
||||
}
|
||||
|
||||
export function getResultNodeData(result: IRun, testData: WorkflowTestData) {
|
||||
return Object.keys(testData.output.nodeData).map((nodeName) => {
|
||||
const error = result.data.resultData.error;
|
||||
@@ -287,9 +81,9 @@ export function getResultNodeData(result: IRun, testData: WorkflowTestData) {
|
||||
});
|
||||
}
|
||||
|
||||
export const equalityTest = async (testData: WorkflowTestData, types: INodeTypes) => {
|
||||
export const equalityTest = async (testData: WorkflowTestData) => {
|
||||
// execute workflow
|
||||
const { result } = await executeWorkflow(testData, types);
|
||||
const { result } = await executeWorkflow(testData);
|
||||
|
||||
// check if result node data matches expected test data
|
||||
const resultNodeData = getResultNodeData(result, testData);
|
||||
@@ -330,6 +124,7 @@ const preparePinData = (pinData: IDataObject) => {
|
||||
);
|
||||
return returnData;
|
||||
};
|
||||
|
||||
export const workflowToTests = (workflowFiles: string[]) => {
|
||||
const testCases: WorkflowTestData[] = [];
|
||||
for (const filePath of workflowFiles) {
|
||||
@@ -365,10 +160,9 @@ export const workflowToTests = (workflowFiles: string[]) => {
|
||||
|
||||
export const testWorkflows = (workflows: string[]) => {
|
||||
const tests = workflowToTests(workflows);
|
||||
const nodeTypes = setup(tests);
|
||||
|
||||
for (const testData of tests) {
|
||||
test(testData.description, async () => await equalityTest(testData, nodeTypes));
|
||||
test(testData.description, async () => await equalityTest(testData));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,175 +0,0 @@
|
||||
const pgPromise = require('pg-promise');
|
||||
|
||||
const PostgresFun = require('../../../nodes/Postgres/v1/genericFunctions');
|
||||
|
||||
type NodeParams = Record<string, string | {}>;
|
||||
|
||||
describe('pgUpdate', () => {
|
||||
it('runs query to update db', async () => {
|
||||
const updateItem = { id: 1234, name: 'test' };
|
||||
const nodeParams: NodeParams = {
|
||||
table: 'mytable',
|
||||
schema: 'myschema',
|
||||
updateKey: 'id',
|
||||
columns: 'id,name',
|
||||
additionalFields: {},
|
||||
returnFields: '*',
|
||||
};
|
||||
const getNodeParam = (key: string) => nodeParams[key];
|
||||
const pgp = pgPromise();
|
||||
const any = jest.fn();
|
||||
const db = { any };
|
||||
|
||||
const items = [
|
||||
{
|
||||
json: updateItem,
|
||||
},
|
||||
];
|
||||
|
||||
await PostgresFun.pgUpdate(getNodeParam, pgp, db, items);
|
||||
|
||||
expect(db.any).toHaveBeenCalledWith(
|
||||
'update "myschema"."mytable" as t set "id"=v."id","name"=v."name" from (values(1234,\'test\')) as v("id","name") WHERE v."id" = t."id" RETURNING *',
|
||||
);
|
||||
});
|
||||
|
||||
it('runs query to update db if updateKey is not in columns', async () => {
|
||||
const updateItem = { id: 1234, name: 'test' };
|
||||
const nodeParams: NodeParams = {
|
||||
table: 'mytable',
|
||||
schema: 'myschema',
|
||||
updateKey: 'id',
|
||||
columns: 'name',
|
||||
additionalFields: {},
|
||||
returnFields: '*',
|
||||
};
|
||||
const getNodeParam = (key: string) => nodeParams[key];
|
||||
const pgp = pgPromise();
|
||||
const any = jest.fn();
|
||||
const db = { any };
|
||||
|
||||
const items = [
|
||||
{
|
||||
json: updateItem,
|
||||
},
|
||||
];
|
||||
|
||||
await PostgresFun.pgUpdate(getNodeParam, pgp, db, items);
|
||||
|
||||
expect(db.any).toHaveBeenCalledWith(
|
||||
'update "myschema"."mytable" as t set "id"=v."id","name"=v."name" from (values(1234,\'test\')) as v("id","name") WHERE v."id" = t."id" RETURNING *',
|
||||
);
|
||||
});
|
||||
|
||||
it('runs query to update db with cast as updateKey', async () => {
|
||||
const updateItem = { id: '1234', name: 'test' };
|
||||
const nodeParams: NodeParams = {
|
||||
table: 'mytable',
|
||||
schema: 'myschema',
|
||||
updateKey: 'id:uuid',
|
||||
columns: 'name',
|
||||
additionalFields: {},
|
||||
returnFields: '*',
|
||||
};
|
||||
const getNodeParam = (key: string) => nodeParams[key];
|
||||
const pgp = pgPromise();
|
||||
const any = jest.fn();
|
||||
const db = { any };
|
||||
|
||||
const items = [
|
||||
{
|
||||
json: updateItem,
|
||||
},
|
||||
];
|
||||
|
||||
await PostgresFun.pgUpdate(getNodeParam, pgp, db, items);
|
||||
|
||||
expect(db.any).toHaveBeenCalledWith(
|
||||
'update "myschema"."mytable" as t set "id"=v."id","name"=v."name" from (values(\'1234\'::uuid,\'test\')) as v("id","name") WHERE v."id" = t."id" RETURNING *',
|
||||
);
|
||||
});
|
||||
|
||||
it('runs query to update db with cast in target columns', async () => {
|
||||
const updateItem = { id: '1234', name: 'test' };
|
||||
const nodeParams: NodeParams = {
|
||||
table: 'mytable',
|
||||
schema: 'myschema',
|
||||
updateKey: 'id',
|
||||
columns: 'id:uuid,name',
|
||||
additionalFields: {},
|
||||
returnFields: '*',
|
||||
};
|
||||
const getNodeParam = (key: string) => nodeParams[key];
|
||||
const pgp = pgPromise();
|
||||
const any = jest.fn();
|
||||
const db = { any };
|
||||
|
||||
const items = [
|
||||
{
|
||||
json: updateItem,
|
||||
},
|
||||
];
|
||||
|
||||
await PostgresFun.pgUpdate(getNodeParam, pgp, db, items);
|
||||
|
||||
expect(db.any).toHaveBeenCalledWith(
|
||||
'update "myschema"."mytable" as t set "id"=v."id","name"=v."name" from (values(\'1234\'::uuid,\'test\')) as v("id","name") WHERE v."id" = t."id" RETURNING *',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('pgInsert', () => {
|
||||
it('runs query to insert', async () => {
|
||||
const insertItem = { id: 1234, name: 'test', age: 34 };
|
||||
const nodeParams: NodeParams = {
|
||||
table: 'mytable',
|
||||
schema: 'myschema',
|
||||
columns: 'id,name,age',
|
||||
returnFields: '*',
|
||||
additionalFields: {},
|
||||
};
|
||||
const getNodeParam = (key: string) => nodeParams[key];
|
||||
const pgp = pgPromise();
|
||||
const any = jest.fn();
|
||||
const db = { any };
|
||||
|
||||
const items = [
|
||||
{
|
||||
json: insertItem,
|
||||
},
|
||||
];
|
||||
|
||||
await PostgresFun.pgInsert(getNodeParam, pgp, db, items);
|
||||
|
||||
expect(db.any).toHaveBeenCalledWith(
|
||||
'insert into "myschema"."mytable"("id","name","age") values(1234,\'test\',34) RETURNING *',
|
||||
);
|
||||
});
|
||||
|
||||
it('runs query to insert with type casting', async () => {
|
||||
const insertItem = { id: 1234, name: 'test', age: 34 };
|
||||
const nodeParams: NodeParams = {
|
||||
table: 'mytable',
|
||||
schema: 'myschema',
|
||||
columns: 'id:int,name:text,age',
|
||||
returnFields: '*',
|
||||
additionalFields: {},
|
||||
};
|
||||
const getNodeParam = (key: string) => nodeParams[key];
|
||||
const pgp = pgPromise();
|
||||
const any = jest.fn();
|
||||
const db = { any };
|
||||
|
||||
const items = [
|
||||
{
|
||||
json: insertItem,
|
||||
},
|
||||
];
|
||||
|
||||
await PostgresFun.pgInsert(getNodeParam, pgp, db, items);
|
||||
|
||||
expect(db.any).toHaveBeenCalledWith(
|
||||
'insert into "myschema"."mytable"("id","name","age") values(1234::int,\'test\'::text,34) RETURNING *',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,45 +0,0 @@
|
||||
import { executeWorkflow } from '../ExecuteWorkflow';
|
||||
import * as Helpers from '../Helpers';
|
||||
import type { WorkflowTestData } from '../types';
|
||||
|
||||
describe('Execute Start Node', () => {
|
||||
const tests: WorkflowTestData[] = [
|
||||
{
|
||||
description: 'should run start node',
|
||||
input: {
|
||||
workflowData: {
|
||||
nodes: [
|
||||
{
|
||||
id: 'uuid-1',
|
||||
parameters: {},
|
||||
name: 'Start',
|
||||
type: 'n8n-nodes-base.start',
|
||||
typeVersion: 1,
|
||||
position: [100, 300],
|
||||
},
|
||||
],
|
||||
connections: {},
|
||||
},
|
||||
},
|
||||
output: {
|
||||
nodeExecutionOrder: ['Start'],
|
||||
nodeData: {},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const nodeTypes = Helpers.setup(tests);
|
||||
|
||||
for (const testData of tests) {
|
||||
test(testData.description, async () => {
|
||||
// execute workflow
|
||||
const { result, nodeExecutionOrder } = await executeWorkflow(testData, nodeTypes);
|
||||
// Check if the nodes did execute in the correct order
|
||||
expect(nodeExecutionOrder).toEqual(testData.output.nodeExecutionOrder);
|
||||
// Check if other data has correct value
|
||||
expect(result.finished).toEqual(true);
|
||||
expect(result.data.executionData!.contextData).toEqual({});
|
||||
expect(result.data.executionData!.nodeExecutionStack).toEqual([]);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -1,30 +0,0 @@
|
||||
const helpers = require('../../../nodes/Stripe/helpers');
|
||||
|
||||
describe('adjustMetadata', () => {
|
||||
it('it should adjust multiple metadata values', async () => {
|
||||
const additionalFieldsValues = {
|
||||
metadata: {
|
||||
metadataProperties: [
|
||||
{
|
||||
key: 'keyA',
|
||||
value: 'valueA',
|
||||
},
|
||||
{
|
||||
key: 'keyB',
|
||||
value: 'valueB',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const adjustedMetadata = helpers.adjustMetadata(additionalFieldsValues);
|
||||
|
||||
const expectedAdjustedMetadata = {
|
||||
metadata: {
|
||||
keyA: 'valueA',
|
||||
keyB: 'valueB',
|
||||
},
|
||||
};
|
||||
expect(adjustedMetadata).toStrictEqual(expectedAdjustedMetadata);
|
||||
});
|
||||
});
|
||||
25
packages/nodes-base/test/nodes/credential-types.ts
Normal file
25
packages/nodes-base/test/nodes/credential-types.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Service } from '@n8n/di';
|
||||
import type { ICredentialType, ICredentialTypes } from 'n8n-workflow';
|
||||
|
||||
import { LoadNodesAndCredentials } from './load-nodes-and-credentials';
|
||||
|
||||
@Service()
|
||||
export class CredentialTypes implements ICredentialTypes {
|
||||
constructor(private readonly loadNodesAndCredentials: LoadNodesAndCredentials) {}
|
||||
|
||||
recognizes(type: string): boolean {
|
||||
return this.loadNodesAndCredentials.recognizesCredential(type);
|
||||
}
|
||||
|
||||
getByName(type: string): ICredentialType {
|
||||
return this.loadNodesAndCredentials.getCredential(type).type;
|
||||
}
|
||||
|
||||
getSupportedNodes(type: string): string[] {
|
||||
return this.loadNodesAndCredentials.known.credentials[type]?.supportedNodes ?? [];
|
||||
}
|
||||
|
||||
getParentTypes(_type: string): string[] {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
82
packages/nodes-base/test/nodes/credentials-helper.ts
Normal file
82
packages/nodes-base/test/nodes/credentials-helper.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { Container, Service } from '@n8n/di';
|
||||
import { Credentials } from 'n8n-core';
|
||||
import { ICredentialsHelper } from 'n8n-workflow';
|
||||
import type {
|
||||
ICredentialDataDecryptedObject,
|
||||
IDataObject,
|
||||
IHttpRequestHelper,
|
||||
IHttpRequestOptions,
|
||||
INode,
|
||||
INodeCredentialsDetails,
|
||||
IWorkflowExecuteAdditionalData,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { CredentialTypes } from './credential-types';
|
||||
import { FAKE_CREDENTIALS_DATA } from './FakeCredentialsMap';
|
||||
|
||||
@Service()
|
||||
export class CredentialsHelper extends ICredentialsHelper {
|
||||
getCredentialsProperties() {
|
||||
return [];
|
||||
}
|
||||
|
||||
async authenticate(
|
||||
credentials: ICredentialDataDecryptedObject,
|
||||
typeName: string,
|
||||
requestParams: IHttpRequestOptions,
|
||||
): Promise<IHttpRequestOptions> {
|
||||
const credentialType = Container.get(CredentialTypes).getByName(typeName);
|
||||
if (typeof credentialType.authenticate === 'function') {
|
||||
return await credentialType.authenticate(credentials, requestParams);
|
||||
}
|
||||
return requestParams;
|
||||
}
|
||||
|
||||
async preAuthentication(
|
||||
_helpers: IHttpRequestHelper,
|
||||
_credentials: ICredentialDataDecryptedObject,
|
||||
_typeName: string,
|
||||
_node: INode,
|
||||
_credentialsExpired: boolean,
|
||||
): Promise<ICredentialDataDecryptedObject | undefined> {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
getParentTypes(_name: string): string[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
async getDecrypted(
|
||||
_additionalData: IWorkflowExecuteAdditionalData,
|
||||
nodeCredentials: INodeCredentialsDetails,
|
||||
type: string,
|
||||
): Promise<ICredentialDataDecryptedObject> {
|
||||
return this.getFakeDecryptedCredentials(nodeCredentials, type);
|
||||
}
|
||||
|
||||
async getCredentials(
|
||||
_nodeCredentials: INodeCredentialsDetails,
|
||||
_type: string,
|
||||
): Promise<Credentials> {
|
||||
return new Credentials({ id: null, name: '' }, '', '');
|
||||
}
|
||||
|
||||
async updateCredentials(
|
||||
_nodeCredentials: INodeCredentialsDetails,
|
||||
_type: string,
|
||||
_data: ICredentialDataDecryptedObject,
|
||||
): Promise<void> {}
|
||||
|
||||
private getFakeDecryptedCredentials(nodeCredentials: INodeCredentialsDetails, type: string) {
|
||||
const credentialsMap = FAKE_CREDENTIALS_DATA as IDataObject;
|
||||
if (nodeCredentials && credentialsMap[JSON.stringify(nodeCredentials)]) {
|
||||
return credentialsMap[JSON.stringify(nodeCredentials)] as ICredentialDataDecryptedObject;
|
||||
}
|
||||
|
||||
if (type && credentialsMap[type]) {
|
||||
return credentialsMap[type] as ICredentialDataDecryptedObject;
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
}
|
||||
39
packages/nodes-base/test/nodes/load-nodes-and-credentials.ts
Normal file
39
packages/nodes-base/test/nodes/load-nodes-and-credentials.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { Service } from '@n8n/di';
|
||||
import { LazyPackageDirectoryLoader } from 'n8n-core';
|
||||
import type {
|
||||
ICredentialType,
|
||||
INodeType,
|
||||
IVersionedNodeType,
|
||||
KnownNodesAndCredentials,
|
||||
LoadedClass,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
@Service()
|
||||
export class LoadNodesAndCredentials {
|
||||
private loader: LazyPackageDirectoryLoader;
|
||||
|
||||
readonly known: KnownNodesAndCredentials = { nodes: {}, credentials: {} };
|
||||
|
||||
constructor(baseDir: string) {
|
||||
this.loader = new LazyPackageDirectoryLoader(baseDir);
|
||||
}
|
||||
|
||||
async init() {
|
||||
await this.loader.loadAll();
|
||||
this.known.credentials = this.loader.known.credentials;
|
||||
this.known.nodes = this.loader.known.nodes;
|
||||
}
|
||||
|
||||
recognizesCredential(credentialType: string): boolean {
|
||||
return credentialType in this.known.credentials;
|
||||
}
|
||||
|
||||
getCredential(credentialType: string): LoadedClass<ICredentialType> {
|
||||
return this.loader.getCredential(credentialType);
|
||||
}
|
||||
|
||||
getNode(fullNodeType: string): LoadedClass<INodeType | IVersionedNodeType> {
|
||||
const nodeType = fullNodeType.split('.')[1];
|
||||
return this.loader.getNode(nodeType);
|
||||
}
|
||||
}
|
||||
23
packages/nodes-base/test/nodes/node-types.ts
Normal file
23
packages/nodes-base/test/nodes/node-types.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { Service } from '@n8n/di';
|
||||
import { NodeHelpers } from 'n8n-workflow';
|
||||
import type { INodeType, INodeTypes, IVersionedNodeType } from 'n8n-workflow';
|
||||
|
||||
import { LoadNodesAndCredentials } from './load-nodes-and-credentials';
|
||||
|
||||
@Service()
|
||||
export class NodeTypes implements INodeTypes {
|
||||
constructor(private readonly loadNodesAndCredentials: LoadNodesAndCredentials) {}
|
||||
|
||||
getByName(type: string): INodeType | IVersionedNodeType {
|
||||
return this.loadNodesAndCredentials.getNode(type).type;
|
||||
}
|
||||
|
||||
getByNameAndVersion(type: string, version?: number): INodeType {
|
||||
const node = this.loadNodesAndCredentials.getNode(type);
|
||||
return NodeHelpers.getVersionedNodeType(node.type, version);
|
||||
}
|
||||
|
||||
getKnownTypes() {
|
||||
return this.loadNodesAndCredentials.known.nodes;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
import type { WorkflowTestData } from 'n8n-workflow';
|
||||
//TODO: remove, update import in tests
|
||||
export { WorkflowTestData };
|
||||
@@ -1,314 +0,0 @@
|
||||
import type { INodeExecutionData } from 'n8n-workflow';
|
||||
|
||||
import {
|
||||
compareItems,
|
||||
flattenKeys,
|
||||
fuzzyCompare,
|
||||
getResolvables,
|
||||
keysToLowercase,
|
||||
shuffleArray,
|
||||
sortItemKeysByPriorityList,
|
||||
wrapData,
|
||||
} from '@utils/utilities';
|
||||
|
||||
//most test cases for fuzzyCompare are done in Compare Datasets node tests
|
||||
describe('Test fuzzyCompare', () => {
|
||||
it('should do strict comparison', () => {
|
||||
const compareFunction = fuzzyCompare(false);
|
||||
|
||||
expect(compareFunction(1, '1')).toEqual(false);
|
||||
});
|
||||
|
||||
it('should do fuzzy comparison', () => {
|
||||
const compareFunction = fuzzyCompare(true);
|
||||
|
||||
expect(compareFunction(1, '1')).toEqual(true);
|
||||
});
|
||||
|
||||
it('should treat null, 0 and "0" as equal', () => {
|
||||
const compareFunction = fuzzyCompare(true, 2);
|
||||
|
||||
expect(compareFunction(null, null)).toEqual(true);
|
||||
expect(compareFunction(null, 0)).toEqual(true);
|
||||
expect(compareFunction(null, '0')).toEqual(true);
|
||||
});
|
||||
|
||||
it('should not treat null, 0 and "0" as equal', () => {
|
||||
const compareFunction = fuzzyCompare(true);
|
||||
|
||||
expect(compareFunction(null, 0)).toEqual(false);
|
||||
expect(compareFunction(null, '0')).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test wrapData', () => {
|
||||
it('should wrap object in json', () => {
|
||||
const data = {
|
||||
id: 1,
|
||||
name: 'Name',
|
||||
};
|
||||
const wrappedData = wrapData(data);
|
||||
expect(wrappedData).toBeDefined();
|
||||
expect(wrappedData).toEqual([{ json: data }]);
|
||||
});
|
||||
it('should wrap each object in array in json', () => {
|
||||
const data = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Name',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Name 2',
|
||||
},
|
||||
];
|
||||
const wrappedData = wrapData(data);
|
||||
expect(wrappedData).toBeDefined();
|
||||
expect(wrappedData).toEqual([{ json: data[0] }, { json: data[1] }]);
|
||||
});
|
||||
it('json key from source should be inside json', () => {
|
||||
const data = {
|
||||
json: {
|
||||
id: 1,
|
||||
name: 'Name',
|
||||
},
|
||||
};
|
||||
const wrappedData = wrapData(data);
|
||||
expect(wrappedData).toBeDefined();
|
||||
expect(wrappedData).toEqual([{ json: data }]);
|
||||
expect(Object.keys(wrappedData[0].json)).toContain('json');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test keysToLowercase', () => {
|
||||
it('should convert keys to lowercase', () => {
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Test-Header': 'Test',
|
||||
Accept: 'application/json',
|
||||
};
|
||||
|
||||
const newHeaders = keysToLowercase(headers);
|
||||
|
||||
expect(newHeaders).toEqual({
|
||||
'content-type': 'application/json',
|
||||
'x-test-header': 'Test',
|
||||
accept: 'application/json',
|
||||
});
|
||||
});
|
||||
it('should return original value if it is not an object', () => {
|
||||
const test1 = keysToLowercase(['hello']);
|
||||
const test2 = keysToLowercase('test');
|
||||
const test3 = keysToLowercase(1);
|
||||
const test4 = keysToLowercase(true);
|
||||
const test5 = keysToLowercase(null);
|
||||
const test6 = keysToLowercase(undefined);
|
||||
|
||||
expect(test1).toEqual(['hello']);
|
||||
expect(test2).toEqual('test');
|
||||
expect(test3).toEqual(1);
|
||||
expect(test4).toEqual(true);
|
||||
expect(test5).toEqual(null);
|
||||
expect(test6).toEqual(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test getResolvables', () => {
|
||||
it('should return empty array when there are no resolvables', () => {
|
||||
expect(getResolvables('Plain String, no resolvables here.')).toEqual([]);
|
||||
});
|
||||
it('should properly handle resovables in SQL query', () => {
|
||||
expect(getResolvables('SELECT * FROM {{ $json.db }}.{{ $json.table }};')).toEqual([
|
||||
'{{ $json.db }}',
|
||||
'{{ $json.table }}',
|
||||
]);
|
||||
});
|
||||
it('should properly handle resovables in HTML string', () => {
|
||||
expect(
|
||||
getResolvables(
|
||||
`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head><title>{{ $json.pageTitle }}</title></head>
|
||||
<body><h1>{{ $json.heading }}</h1></body>
|
||||
<html>
|
||||
<style>
|
||||
body { height: {{ $json.pageHeight }}; }
|
||||
</style>
|
||||
<script>
|
||||
console.log('{{ $json.welcomeMessage }}');
|
||||
</script>
|
||||
`,
|
||||
),
|
||||
).toEqual([
|
||||
'{{ $json.pageTitle }}',
|
||||
'{{ $json.heading }}',
|
||||
'{{ $json.pageHeight }}',
|
||||
'{{ $json.welcomeMessage }}',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('shuffleArray', () => {
|
||||
it('should shuffle array', () => {
|
||||
const array = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
|
||||
const toShuffle = [...array];
|
||||
shuffleArray(toShuffle);
|
||||
expect(toShuffle).not.toEqual(array);
|
||||
expect(toShuffle).toHaveLength(array.length);
|
||||
expect(toShuffle).toEqual(expect.arrayContaining(array));
|
||||
});
|
||||
});
|
||||
|
||||
describe('flattenKeys', () => {
|
||||
const name = 'Lisa';
|
||||
const city1 = 'Berlin';
|
||||
const city2 = 'Schoenwald';
|
||||
const withNestedObject = {
|
||||
name,
|
||||
address: { city: city1 },
|
||||
};
|
||||
|
||||
const withNestedArrays = {
|
||||
name,
|
||||
addresses: [{ city: city1 }, { city: city2 }],
|
||||
};
|
||||
|
||||
it('should handle empty object', () => {
|
||||
const flattenedObj = flattenKeys({});
|
||||
expect(flattenedObj).toEqual({});
|
||||
});
|
||||
|
||||
it('should flatten object with nested object', () => {
|
||||
const flattenedObj = flattenKeys(withNestedObject);
|
||||
expect(flattenedObj).toEqual({
|
||||
name,
|
||||
'address.city': city1,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle object with nested arrays', () => {
|
||||
const flattenedObj = flattenKeys(withNestedArrays);
|
||||
expect(flattenedObj).toEqual({
|
||||
name,
|
||||
'addresses.0.city': city1,
|
||||
'addresses.1.city': city2,
|
||||
});
|
||||
});
|
||||
|
||||
it('should flatten object with nested object and specified prefix', () => {
|
||||
const flattenedObj = flattenKeys(withNestedObject, ['test']);
|
||||
expect(flattenedObj).toEqual({
|
||||
'test.name': name,
|
||||
'test.address.city': city1,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle object with nested arrays and specified prefix', () => {
|
||||
const flattenedObj = flattenKeys(withNestedArrays, ['test']);
|
||||
expect(flattenedObj).toEqual({
|
||||
'test.name': name,
|
||||
'test.addresses.0.city': city1,
|
||||
'test.addresses.1.city': city2,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('compareItems', () => {
|
||||
it('should return true if all values of specified keys are equal', () => {
|
||||
const obj1 = { json: { a: 1, b: 2, c: 3 } };
|
||||
const obj2 = { json: { a: 1, b: 2, c: 3 } };
|
||||
const keys = ['a', 'b', 'c'];
|
||||
const result = compareItems(obj1, obj2, keys);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if any values of specified keys are not equal', () => {
|
||||
const obj1 = { json: { a: 1, b: 2, c: 3 } };
|
||||
const obj2 = { json: { a: 1, b: 2, c: 4 } };
|
||||
const keys = ['a', 'b', 'c'];
|
||||
const result = compareItems(obj1, obj2, keys);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true if all values of specified keys are equal using dot notation', () => {
|
||||
const obj1 = { json: { a: { b: { c: 1 } } } };
|
||||
const obj2 = { json: { a: { b: { c: 1 } } } };
|
||||
const keys = ['a.b.c'];
|
||||
const result = compareItems(obj1, obj2, keys);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if any values of specified keys are not equal using dot notation', () => {
|
||||
const obj1 = { json: { a: { b: { c: 1 } } } };
|
||||
const obj2 = { json: { a: { b: { c: 2 } } } };
|
||||
const keys = ['a.b.c'];
|
||||
const result = compareItems(obj1, obj2, keys);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true if all values of specified keys are equal using bracket notation', () => {
|
||||
const obj1 = { json: { 'a.b': { 'c.d': 1 } } };
|
||||
const obj2 = { json: { 'a.b': { 'c.d': 1 } } };
|
||||
const keys = ['a.b.c.d'];
|
||||
const result = compareItems(obj1, obj2, keys, true);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sortItemKeysByPriorityList', () => {
|
||||
it('should reorder keys based on priority list', () => {
|
||||
const data: INodeExecutionData[] = [{ json: { c: 3, a: 1, b: 2 } }];
|
||||
const priorityList = ['b', 'a'];
|
||||
|
||||
const result = sortItemKeysByPriorityList(data, priorityList);
|
||||
|
||||
expect(Object.keys(result[0].json)).toEqual(['b', 'a', 'c']);
|
||||
});
|
||||
|
||||
it('should sort keys not in the priority list alphabetically', () => {
|
||||
const data: INodeExecutionData[] = [{ json: { c: 3, a: 1, b: 2, d: 4 } }];
|
||||
const priorityList = ['b', 'a'];
|
||||
|
||||
const result = sortItemKeysByPriorityList(data, priorityList);
|
||||
|
||||
expect(Object.keys(result[0].json)).toEqual(['b', 'a', 'c', 'd']);
|
||||
});
|
||||
|
||||
it('should sort all keys alphabetically when priority list is empty', () => {
|
||||
const data: INodeExecutionData[] = [{ json: { c: 3, a: 1, b: 2 } }];
|
||||
const priorityList: string[] = [];
|
||||
|
||||
const result = sortItemKeysByPriorityList(data, priorityList);
|
||||
|
||||
expect(Object.keys(result[0].json)).toEqual(['a', 'b', 'c']);
|
||||
});
|
||||
|
||||
it('should handle an empty data array', () => {
|
||||
const data: INodeExecutionData[] = [];
|
||||
const priorityList = ['b', 'a'];
|
||||
|
||||
const result = sortItemKeysByPriorityList(data, priorityList);
|
||||
|
||||
// Expect an empty array since there is no data
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle a single object in the data array', () => {
|
||||
const data: INodeExecutionData[] = [{ json: { d: 4, b: 2, a: 1 } }];
|
||||
const priorityList = ['a', 'b', 'c'];
|
||||
|
||||
const result = sortItemKeysByPriorityList(data, priorityList);
|
||||
|
||||
expect(Object.keys(result[0].json)).toEqual(['a', 'b', 'd']);
|
||||
});
|
||||
|
||||
it('should handle duplicate keys in the priority list gracefully', () => {
|
||||
const data: INodeExecutionData[] = [{ json: { d: 4, b: 2, a: 1 } }];
|
||||
const priorityList = ['a', 'b', 'a'];
|
||||
|
||||
const result = sortItemKeysByPriorityList(data, priorityList);
|
||||
|
||||
expect(Object.keys(result[0].json)).toEqual(['a', 'b', 'd']);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user