refactor: Overhaul nodes-testing setup - Part 1 (no-changelog) (#14303)

This commit is contained in:
कारतोफ्फेलस्क्रिप्ट™
2025-04-01 10:15:13 +02:00
committed by GitHub
parent f85b851851
commit 73e8d76e13
165 changed files with 3397 additions and 6453 deletions

View File

@@ -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.',
);
}
});
}
});

View File

@@ -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
}
]
]
}
}
}

View File

@@ -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 = {

View File

@@ -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;

View File

@@ -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));
}
};

View File

@@ -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 *',
);
});
});

View File

@@ -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([]);
});
}
});

View File

@@ -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);
});
});

View 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 [];
}
}

View 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 {};
}
}

View 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);
}
}

View 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;
}
}

View File

@@ -1,3 +0,0 @@
import type { WorkflowTestData } from 'n8n-workflow';
//TODO: remove, update import in tests
export { WorkflowTestData };

View File

@@ -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']);
});
});