diff --git a/packages/cli/test/integration/task-runners/js-task-runner-execution.integration.test.ts b/packages/cli/test/integration/task-runners/js-task-runner-execution.integration.test.ts new file mode 100644 index 0000000000..a69f60d198 --- /dev/null +++ b/packages/cli/test/integration/task-runners/js-task-runner-execution.integration.test.ts @@ -0,0 +1,269 @@ +import { TaskRunnersConfig } from '@n8n/config'; +import { Container } from '@n8n/di'; +import { mock } from 'jest-mock-extended'; +import type { + IExecuteFunctions, + INode, + INodeExecutionData, + INodeParameters, + INodeTypes, + IRunExecutionData, + ITaskDataConnections, + IWorkflowExecuteAdditionalData, + WorkflowExecuteMode, +} from 'n8n-workflow'; +import { createEnvProviderState, NodeConnectionType, Workflow } from 'n8n-workflow'; + +import { LocalTaskRequester } from '@/task-runners/task-managers/local-task-requester'; +import { TaskRunnerModule } from '@/task-runners/task-runner-module'; + +/** + * Integration tests for the JS TaskRunner execution. Starts the TaskRunner + * as a child process and executes tasks on it via the broker. + */ +describe('JS TaskRunner execution on internal mode', () => { + const runnerConfig = Container.get(TaskRunnersConfig); + runnerConfig.mode = 'internal'; + runnerConfig.enabled = true; + runnerConfig.port = 45678; + + const taskRunnerModule = Container.get(TaskRunnerModule); + const taskRequester = Container.get(LocalTaskRequester); + + /** + * Sets up task data that includes a workflow with manual trigger and a + * code node with the given JS code. The input data is a single item: + * ```json + * { + * "input": "item" + * } + * ``` + */ + const newTaskData = (jsCode: string) => { + const taskSettings = { + code: jsCode, + nodeMode: 'runOnceForAllItems', + workflowMode: 'manual', + continueOnFail: false, + }; + + const codeNode: INode = { + parameters: { + jsCode, + }, + type: 'n8n-nodes-base.code', + typeVersion: 2, + position: [200, 80], + id: 'b35fd455-32e4-4d52-b840-36aa28dd1910', + name: 'Code', + }; + + const workflow = new Workflow({ + id: 'testWorkflow', + name: 'testWorkflow', + nodes: [ + { + parameters: {}, + type: 'n8n-nodes-base.manualTrigger', + typeVersion: 1, + position: [0, 0], + id: 'a39a566a-283a-433e-88bc-b3857aab706f', + name: 'ManualTrigger', + }, + codeNode, + ], + connections: { + ManualTrigger: { + main: [ + [ + { + node: 'Code', + type: NodeConnectionType.Main, + index: 0, + }, + ], + ], + }, + }, + active: true, + nodeTypes: mock(), + }); + + const inputData: INodeExecutionData[] = [ + { + json: { + input: 'item', + }, + }, + ]; + + const inputConnections: ITaskDataConnections = { + main: [inputData], + }; + + const runExecutionData: IRunExecutionData = { + startData: {}, + resultData: { + runData: { + ManualTrigger: [ + { + startTime: Date.now(), + executionTime: 0, + executionStatus: 'success', + source: [], + data: { + main: [inputData], + }, + }, + ], + }, + lastNodeExecuted: 'ManualTrigger', + }, + executionData: { + contextData: {}, + nodeExecutionStack: [], + metadata: {}, + waitingExecution: {}, + waitingExecutionSource: {}, + }, + }; + + return { + additionalData: mock(), + executeFunctions: mock(), + taskSettings, + codeNode, + workflow, + inputData, + inputConnections, + runExecutionData, + envProviderState: createEnvProviderState(), + }; + }; + + const runTaskWithCode = async (jsCode: string) => { + const { + additionalData, + taskSettings, + codeNode, + workflow, + inputData, + inputConnections, + runExecutionData, + executeFunctions, + envProviderState, + } = newTaskData(jsCode); + + return await taskRequester.startTask( + additionalData, + 'javascript', + taskSettings, + executeFunctions, + inputConnections, + codeNode, + workflow, + runExecutionData, + 0, + 0, + codeNode.name, + inputData, + mock(), + mock(), + envProviderState, + ); + }; + + describe('Basic code execution', () => { + beforeAll(async () => { + await taskRunnerModule.start(); + }); + + afterAll(async () => { + await taskRunnerModule.stop(); + }); + + it('should execute a simple JS task', async () => { + // Act + const result = await runTaskWithCode('return [{ hello: "world" }]'); + + // Assert + expect(result).toEqual({ + ok: true, + result: [{ json: { hello: 'world' } }], + }); + }); + }); + + describe('Internal and external libs', () => { + beforeAll(async () => { + process.env.NODE_FUNCTION_ALLOW_BUILTIN = 'crypto'; + process.env.NODE_FUNCTION_ALLOW_EXTERNAL = 'moment'; + await taskRunnerModule.start(); + }); + + afterAll(async () => { + await taskRunnerModule.stop(); + }); + + it('should allow importing allowed internal module', async () => { + // Act + const result = await runTaskWithCode(` + const crypto = require("crypto"); + return [{ + digest: crypto + .createHmac("sha256", Buffer.from("MySecretKey")) + .update("MESSAGE") + .digest("base64") + }] + `); + + expect(result).toEqual({ + ok: true, + result: [{ json: { digest: 'T09DMv7upNDKMD3Ht36FkwzrmWSgWpPiUNlcIX9/yaI=' } }], + }); + }); + + it('should not allow importing disallowed internal module', async () => { + // Act + const result = await runTaskWithCode(` + const fs = require("fs"); + return [{ file: fs.readFileSync("test.txt") }] + `); + + expect(result).toEqual({ + ok: false, + error: expect.objectContaining({ + message: "Cannot find module 'fs' [line 2]", + }), + }); + }); + + it('should allow importing allowed external module', async () => { + // Act + const result = await runTaskWithCode(` + const moment = require("moment"); + return [{ time: moment("1995-12-25").format("YYYY-MM-DD") }] + `); + + expect(result).toEqual({ + ok: true, + result: [{ json: { time: '1995-12-25' } }], + }); + }); + + it('should not allow importing disallowed external module', async () => { + // Act + const result = await runTaskWithCode(` + const lodash = require("lodash"); + return [{ obj: lodash.cloneDeep({}) }] + `); + + expect(result).toEqual({ + ok: false, + error: expect.objectContaining({ + message: "Cannot find module 'lodash' [line 2]", + }), + }); + }); + }); +}); diff --git a/packages/cli/test/integration/runners/task-runner-module.external.test.ts b/packages/cli/test/integration/task-runners/task-runner-module.external.test.ts similarity index 100% rename from packages/cli/test/integration/runners/task-runner-module.external.test.ts rename to packages/cli/test/integration/task-runners/task-runner-module.external.test.ts diff --git a/packages/cli/test/integration/runners/task-runner-module.internal.test.ts b/packages/cli/test/integration/task-runners/task-runner-module.internal.test.ts similarity index 100% rename from packages/cli/test/integration/runners/task-runner-module.internal.test.ts rename to packages/cli/test/integration/task-runners/task-runner-module.internal.test.ts diff --git a/packages/cli/test/integration/runners/task-runner-process.test.ts b/packages/cli/test/integration/task-runners/task-runner-process.test.ts similarity index 100% rename from packages/cli/test/integration/runners/task-runner-process.test.ts rename to packages/cli/test/integration/task-runners/task-runner-process.test.ts diff --git a/packages/cli/test/integration/runners/task-runner-server.test.ts b/packages/cli/test/integration/task-runners/task-runner-server.test.ts similarity index 100% rename from packages/cli/test/integration/runners/task-runner-server.test.ts rename to packages/cli/test/integration/task-runners/task-runner-server.test.ts