mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 01:56:46 +00:00
test(core): Add integration test for JS task runner (#12804)
Co-authored-by: Iván Ovejero <ivov.src@gmail.com>
This commit is contained in:
@@ -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<INodeTypes>(),
|
||||
});
|
||||
|
||||
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<IWorkflowExecuteAdditionalData>(),
|
||||
executeFunctions: mock<IExecuteFunctions>(),
|
||||
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<INodeExecutionData[], Error>(
|
||||
additionalData,
|
||||
'javascript',
|
||||
taskSettings,
|
||||
executeFunctions,
|
||||
inputConnections,
|
||||
codeNode,
|
||||
workflow,
|
||||
runExecutionData,
|
||||
0,
|
||||
0,
|
||||
codeNode.name,
|
||||
inputData,
|
||||
mock<INodeParameters>(),
|
||||
mock<WorkflowExecuteMode>(),
|
||||
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]",
|
||||
}),
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
import { TaskRunnersConfig } from '@n8n/config';
|
||||
import { Container } from '@n8n/di';
|
||||
import { mock } from 'jest-mock-extended';
|
||||
|
||||
import { MissingAuthTokenError } from '@/task-runners/errors/missing-auth-token.error';
|
||||
import { TaskRunnerModule } from '@/task-runners/task-runner-module';
|
||||
|
||||
import { DefaultTaskRunnerDisconnectAnalyzer } from '../../../src/task-runners/default-task-runner-disconnect-analyzer';
|
||||
import { TaskRunnerWsServer } from '../../../src/task-runners/task-runner-ws-server';
|
||||
|
||||
describe('TaskRunnerModule in external mode', () => {
|
||||
const runnerConfig = Container.get(TaskRunnersConfig);
|
||||
runnerConfig.mode = 'external';
|
||||
runnerConfig.port = 0;
|
||||
runnerConfig.authToken = 'test';
|
||||
const module = Container.get(TaskRunnerModule);
|
||||
|
||||
afterEach(async () => {
|
||||
await module.stop();
|
||||
});
|
||||
|
||||
describe('start', () => {
|
||||
it('should throw if the task runner is disabled', async () => {
|
||||
runnerConfig.enabled = false;
|
||||
|
||||
// Act
|
||||
await expect(module.start()).rejects.toThrow('Task runner is disabled');
|
||||
});
|
||||
|
||||
it('should throw if auth token is missing', async () => {
|
||||
const runnerConfig = new TaskRunnersConfig();
|
||||
runnerConfig.mode = 'external';
|
||||
runnerConfig.enabled = true;
|
||||
runnerConfig.authToken = '';
|
||||
|
||||
const module = new TaskRunnerModule(mock(), mock(), runnerConfig);
|
||||
|
||||
await expect(module.start()).rejects.toThrowError(MissingAuthTokenError);
|
||||
});
|
||||
|
||||
it('should start the task runner', async () => {
|
||||
runnerConfig.enabled = true;
|
||||
|
||||
// Act
|
||||
await module.start();
|
||||
});
|
||||
|
||||
it('should use DefaultTaskRunnerDisconnectAnalyzer', () => {
|
||||
const wsServer = Container.get(TaskRunnerWsServer);
|
||||
|
||||
expect(wsServer.getDisconnectAnalyzer()).toBeInstanceOf(DefaultTaskRunnerDisconnectAnalyzer);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
import { TaskRunnersConfig } from '@n8n/config';
|
||||
import { Container } from '@n8n/di';
|
||||
|
||||
import { TaskRunnerModule } from '@/task-runners/task-runner-module';
|
||||
|
||||
import { InternalTaskRunnerDisconnectAnalyzer } from '../../../src/task-runners/internal-task-runner-disconnect-analyzer';
|
||||
import { TaskRunnerWsServer } from '../../../src/task-runners/task-runner-ws-server';
|
||||
|
||||
describe('TaskRunnerModule in internal mode', () => {
|
||||
const runnerConfig = Container.get(TaskRunnersConfig);
|
||||
runnerConfig.port = 0; // Random port
|
||||
runnerConfig.mode = 'internal';
|
||||
runnerConfig.enabled = true;
|
||||
const module = Container.get(TaskRunnerModule);
|
||||
|
||||
afterEach(async () => {
|
||||
await module.stop();
|
||||
});
|
||||
|
||||
describe('start', () => {
|
||||
it('should throw if the task runner is disabled', async () => {
|
||||
runnerConfig.enabled = false;
|
||||
|
||||
// Act
|
||||
await expect(module.start()).rejects.toThrow('Task runner is disabled');
|
||||
});
|
||||
|
||||
it('should start the task runner', async () => {
|
||||
runnerConfig.enabled = true;
|
||||
|
||||
// Act
|
||||
await module.start();
|
||||
});
|
||||
|
||||
it('should use InternalTaskRunnerDisconnectAnalyzer', () => {
|
||||
const wsServer = Container.get(TaskRunnerWsServer);
|
||||
|
||||
expect(wsServer.getDisconnectAnalyzer()).toBeInstanceOf(InternalTaskRunnerDisconnectAnalyzer);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,117 @@
|
||||
import { Container } from '@n8n/di';
|
||||
|
||||
import { TaskBroker } from '@/task-runners/task-broker.service';
|
||||
import { TaskRunnerProcess } from '@/task-runners/task-runner-process';
|
||||
import { TaskRunnerProcessRestartLoopDetector } from '@/task-runners/task-runner-process-restart-loop-detector';
|
||||
import { TaskRunnerWsServer } from '@/task-runners/task-runner-ws-server';
|
||||
import { retryUntil } from '@test-integration/retry-until';
|
||||
import { setupBrokerTestServer } from '@test-integration/utils/task-broker-test-server';
|
||||
|
||||
describe('TaskRunnerProcess', () => {
|
||||
const { config, server: taskRunnerServer } = setupBrokerTestServer({
|
||||
mode: 'internal',
|
||||
});
|
||||
const runnerProcess = Container.get(TaskRunnerProcess);
|
||||
const taskBroker = Container.get(TaskBroker);
|
||||
const taskRunnerService = Container.get(TaskRunnerWsServer);
|
||||
|
||||
beforeAll(async () => {
|
||||
await taskRunnerServer.start();
|
||||
// Set the port to the actually used port
|
||||
config.port = taskRunnerServer.port;
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await taskRunnerServer.stop();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await runnerProcess.stop();
|
||||
});
|
||||
|
||||
const getNumConnectedRunners = () => taskRunnerService.runnerConnections.size;
|
||||
const getNumRegisteredRunners = () => taskBroker.getKnownRunners().size;
|
||||
|
||||
it('should start and connect the task runner', async () => {
|
||||
// Act
|
||||
await runnerProcess.start();
|
||||
|
||||
// Assert
|
||||
expect(runnerProcess.isRunning).toBeTruthy();
|
||||
|
||||
// Wait until the runner has connected
|
||||
await retryUntil(() => expect(getNumConnectedRunners()).toBe(1));
|
||||
expect(getNumRegisteredRunners()).toBe(1);
|
||||
});
|
||||
|
||||
it('should stop an disconnect the task runner', async () => {
|
||||
// Arrange
|
||||
await runnerProcess.start();
|
||||
|
||||
// Wait until the runner has connected
|
||||
await retryUntil(() => expect(getNumConnectedRunners()).toBe(1));
|
||||
expect(getNumRegisteredRunners()).toBe(1);
|
||||
|
||||
// Act
|
||||
await runnerProcess.stop();
|
||||
|
||||
// Assert
|
||||
// Wait until the runner has disconnected
|
||||
await retryUntil(() => expect(getNumConnectedRunners()).toBe(0));
|
||||
|
||||
expect(runnerProcess.isRunning).toBeFalsy();
|
||||
expect(getNumRegisteredRunners()).toBe(0);
|
||||
});
|
||||
|
||||
it('should restart the task runner if it exits', async () => {
|
||||
// Arrange
|
||||
await runnerProcess.start();
|
||||
|
||||
// Wait until the runner has connected
|
||||
await retryUntil(() => expect(getNumConnectedRunners()).toBe(1));
|
||||
const processId = runnerProcess.pid;
|
||||
|
||||
// Act
|
||||
// @ts-expect-error private property
|
||||
runnerProcess.process?.kill('SIGKILL');
|
||||
|
||||
// Wait until the runner has exited
|
||||
await runnerProcess.runPromise;
|
||||
|
||||
// Assert
|
||||
// Wait until the runner has connected again
|
||||
await retryUntil(() => expect(getNumConnectedRunners()).toBe(1));
|
||||
expect(getNumConnectedRunners()).toBe(1);
|
||||
expect(getNumRegisteredRunners()).toBe(1);
|
||||
expect(runnerProcess.pid).not.toBe(processId);
|
||||
});
|
||||
|
||||
it('should work together with restart loop detector', async () => {
|
||||
// Arrange
|
||||
const restartLoopDetector = new TaskRunnerProcessRestartLoopDetector(runnerProcess);
|
||||
let restartLoopDetectedEventEmitted = false;
|
||||
restartLoopDetector.once('restart-loop-detected', () => {
|
||||
restartLoopDetectedEventEmitted = true;
|
||||
});
|
||||
|
||||
// Act
|
||||
await runnerProcess.start();
|
||||
|
||||
// Simulate a restart loop
|
||||
for (let i = 0; i < 5; i++) {
|
||||
await retryUntil(() => {
|
||||
expect(runnerProcess.pid).toBeDefined();
|
||||
});
|
||||
|
||||
// @ts-expect-error private property
|
||||
runnerProcess.process?.kill();
|
||||
|
||||
await new Promise((resolve) => {
|
||||
runnerProcess.once('exit', resolve);
|
||||
});
|
||||
}
|
||||
|
||||
// Assert
|
||||
expect(restartLoopDetectedEventEmitted).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,44 @@
|
||||
import { setupBrokerTestServer } from '@test-integration/utils/task-broker-test-server';
|
||||
|
||||
describe('TaskRunnerServer', () => {
|
||||
const { agent, server } = setupBrokerTestServer({
|
||||
authToken: 'token',
|
||||
mode: 'external',
|
||||
});
|
||||
|
||||
beforeAll(async () => {
|
||||
await server.start();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await server.stop();
|
||||
});
|
||||
|
||||
describe('/healthz', () => {
|
||||
it('should return 200', async () => {
|
||||
await agent.get('/healthz').expect(200);
|
||||
});
|
||||
});
|
||||
|
||||
describe('/runners/_ws', () => {
|
||||
it('should return 429 when too many requests are made', async () => {
|
||||
await agent.post('/runners/_ws').send({}).expect(401);
|
||||
await agent.post('/runners/_ws').send({}).expect(401);
|
||||
await agent.post('/runners/_ws').send({}).expect(401);
|
||||
await agent.post('/runners/_ws').send({}).expect(401);
|
||||
await agent.post('/runners/_ws').send({}).expect(401);
|
||||
await agent.post('/runners/_ws').send({}).expect(429);
|
||||
});
|
||||
});
|
||||
|
||||
describe('/runners/auth', () => {
|
||||
it('should return 429 when too many requests are made', async () => {
|
||||
await agent.post('/runners/auth').send({ token: 'invalid' }).expect(403);
|
||||
await agent.post('/runners/auth').send({ token: 'invalid' }).expect(403);
|
||||
await agent.post('/runners/auth').send({ token: 'invalid' }).expect(403);
|
||||
await agent.post('/runners/auth').send({ token: 'invalid' }).expect(403);
|
||||
await agent.post('/runners/auth').send({ token: 'invalid' }).expect(403);
|
||||
await agent.post('/runners/auth').send({ token: 'invalid' }).expect(429);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user