2023-06-21 00:38:28 -07:00
|
|
|
import { readdirSync, readFileSync } from 'fs';
|
2024-09-30 06:38:56 -07:00
|
|
|
import { mock } from 'jest-mock-extended';
|
2023-06-21 00:38:28 -07:00
|
|
|
import type {
|
|
|
|
IDataObject,
|
|
|
|
IDeferredPromise,
|
|
|
|
INodeType,
|
|
|
|
INodeTypes,
|
|
|
|
IRun,
|
|
|
|
ITaskData,
|
|
|
|
IVersionedNodeType,
|
|
|
|
IWorkflowBase,
|
|
|
|
IWorkflowExecuteAdditionalData,
|
|
|
|
NodeLoadingDetails,
|
|
|
|
WorkflowTestData,
|
2023-11-27 00:11:52 -08:00
|
|
|
INodeTypeData,
|
2023-06-21 00:38:28 -07:00
|
|
|
} from 'n8n-workflow';
|
2024-05-17 09:43:50 -07:00
|
|
|
import { ApplicationError, NodeHelpers, WorkflowHooks } from 'n8n-workflow';
|
2024-09-30 06:38:56 -07:00
|
|
|
import path from 'path';
|
2023-06-21 00:38:28 -07:00
|
|
|
|
|
|
|
import { predefinedNodesTypes } from './constants';
|
2024-09-30 06:38:56 -07:00
|
|
|
|
|
|
|
const BASE_DIR = path.resolve(__dirname, '../../..');
|
2023-06-21 00:38:28 -07:00
|
|
|
|
|
|
|
class NodeTypesClass implements INodeTypes {
|
2024-05-17 09:43:50 -07:00
|
|
|
constructor(private nodeTypes: INodeTypeData = predefinedNodesTypes) {}
|
2023-06-21 00:38:28 -07:00
|
|
|
|
|
|
|
getByName(nodeType: string): INodeType | IVersionedNodeType {
|
|
|
|
return this.nodeTypes[nodeType].type;
|
|
|
|
}
|
|
|
|
|
|
|
|
getByNameAndVersion(nodeType: string, version?: number): INodeType {
|
|
|
|
return NodeHelpers.getVersionedNodeType(this.nodeTypes[nodeType].type, version);
|
|
|
|
}
|
2024-05-17 09:43:50 -07:00
|
|
|
|
|
|
|
getKnownTypes(): IDataObject {
|
|
|
|
throw new Error('Method not implemented.');
|
|
|
|
}
|
2023-06-21 00:38:28 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
let nodeTypesInstance: NodeTypesClass | undefined;
|
|
|
|
|
2024-05-17 09:43:50 -07:00
|
|
|
export function NodeTypes(nodeTypes?: INodeTypeData): INodeTypes {
|
2023-06-21 00:38:28 -07:00
|
|
|
if (nodeTypesInstance === undefined || nodeTypes !== undefined) {
|
|
|
|
nodeTypesInstance = new NodeTypesClass(nodeTypes);
|
|
|
|
}
|
|
|
|
|
|
|
|
return nodeTypesInstance;
|
|
|
|
}
|
|
|
|
|
|
|
|
export function WorkflowExecuteAdditionalData(
|
|
|
|
waitPromise: IDeferredPromise<IRun>,
|
|
|
|
nodeExecutionOrder: string[],
|
|
|
|
): IWorkflowExecuteAdditionalData {
|
|
|
|
const hookFunctions = {
|
|
|
|
nodeExecuteAfter: [
|
2024-05-17 09:43:50 -07:00
|
|
|
async (nodeName: string, _data: ITaskData): Promise<void> => {
|
2023-06-21 00:38:28 -07:00
|
|
|
nodeExecutionOrder.push(nodeName);
|
|
|
|
},
|
|
|
|
],
|
|
|
|
workflowExecuteAfter: [
|
|
|
|
async (fullRunData: IRun): Promise<void> => {
|
|
|
|
waitPromise.resolve(fullRunData);
|
|
|
|
},
|
|
|
|
],
|
|
|
|
};
|
|
|
|
|
2024-05-17 09:43:50 -07:00
|
|
|
return mock<IWorkflowExecuteAdditionalData>({
|
|
|
|
hooks: new WorkflowHooks(hookFunctions, 'trigger', '1', mock()),
|
|
|
|
});
|
2023-06-21 00:38:28 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
const preparePinData = (pinData: IDataObject) => {
|
|
|
|
const returnData = Object.keys(pinData).reduce(
|
|
|
|
(acc, key) => {
|
|
|
|
const data = pinData[key] as IDataObject[];
|
|
|
|
acc[key] = [data];
|
|
|
|
return acc;
|
|
|
|
},
|
|
|
|
{} as {
|
|
|
|
[key: string]: IDataObject[][];
|
|
|
|
},
|
|
|
|
);
|
|
|
|
return returnData;
|
|
|
|
};
|
|
|
|
|
|
|
|
const readJsonFileSync = <T>(filePath: string) =>
|
|
|
|
JSON.parse(readFileSync(path.join(BASE_DIR, filePath), 'utf-8')) as T;
|
|
|
|
|
|
|
|
export function getNodeTypes(testData: WorkflowTestData[] | WorkflowTestData) {
|
|
|
|
if (!Array.isArray(testData)) {
|
|
|
|
testData = [testData];
|
|
|
|
}
|
|
|
|
|
|
|
|
const nodeTypes: INodeTypeData = {};
|
|
|
|
|
|
|
|
const nodes = [...new Set(testData.flatMap((data) => data.input.workflowData.nodes))];
|
|
|
|
|
|
|
|
const nodeNames = nodes.map((n) => n.type);
|
|
|
|
|
|
|
|
const knownNodes = readJsonFileSync<Record<string, NodeLoadingDetails>>(
|
|
|
|
'nodes-base/dist/known/nodes.json',
|
|
|
|
);
|
|
|
|
|
|
|
|
for (const nodeName of nodeNames) {
|
|
|
|
if (!nodeName.startsWith('n8n-nodes-base.')) {
|
2023-11-30 00:06:19 -08:00
|
|
|
throw new ApplicationError('Unknown node type', { tags: { nodeType: nodeName } });
|
2023-06-21 00:38:28 -07:00
|
|
|
}
|
|
|
|
const loadInfo = knownNodes[nodeName.replace('n8n-nodes-base.', '')];
|
|
|
|
if (!loadInfo) {
|
2023-11-30 00:06:19 -08:00
|
|
|
throw new ApplicationError('Unknown node type', { tags: { nodeType: nodeName } });
|
2023-06-21 00:38:28 -07:00
|
|
|
}
|
|
|
|
const sourcePath = loadInfo.sourcePath.replace(/^dist\//, './').replace(/\.js$/, '.ts');
|
|
|
|
const nodeSourcePath = path.join(BASE_DIR, 'nodes-base', sourcePath);
|
|
|
|
const node = new (require(nodeSourcePath)[loadInfo.className])() as INodeType;
|
|
|
|
nodeTypes[nodeName] = {
|
|
|
|
sourcePath: '',
|
|
|
|
type: node,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
return nodeTypes;
|
|
|
|
}
|
|
|
|
|
|
|
|
const getWorkflowFilenames = (dirname: string, testFolder = 'workflows') => {
|
|
|
|
const workflows: string[] = [];
|
|
|
|
|
|
|
|
const filenames: string[] = readdirSync(`${dirname}${path.sep}${testFolder}`);
|
|
|
|
|
|
|
|
filenames.forEach((file) => {
|
|
|
|
if (file.endsWith('.json')) {
|
|
|
|
workflows.push(path.join('core', 'test', testFolder, file));
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
return workflows;
|
|
|
|
};
|
|
|
|
|
|
|
|
export const workflowToTests = (dirname: string, testFolder = 'workflows') => {
|
|
|
|
const workflowFiles: string[] = getWorkflowFilenames(dirname, testFolder);
|
|
|
|
|
|
|
|
const testCases: WorkflowTestData[] = [];
|
|
|
|
|
|
|
|
for (const filePath of workflowFiles) {
|
|
|
|
const description = filePath.replace('.json', '');
|
|
|
|
const workflowData = readJsonFileSync<IWorkflowBase>(filePath);
|
|
|
|
if (workflowData.pinData === undefined) {
|
2023-11-30 00:06:19 -08:00
|
|
|
throw new ApplicationError('Workflow data does not contain pinData');
|
2023-06-21 00:38:28 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
const nodeData = preparePinData(workflowData.pinData);
|
|
|
|
|
|
|
|
delete workflowData.pinData;
|
|
|
|
|
|
|
|
const input = { workflowData };
|
|
|
|
const output = { nodeData };
|
|
|
|
|
|
|
|
testCases.push({ description, input, output });
|
|
|
|
}
|
|
|
|
return testCases;
|
|
|
|
};
|