mirror of
https://github.com/n8n-io/n8n.git
synced 2026-05-12 16:10:30 +02:00
434 lines
15 KiB
TypeScript
434 lines
15 KiB
TypeScript
/**
|
|
* Integration tests for execution context propagation across workflows.
|
|
* These tests verify that execution context is properly inherited by sub-workflows,
|
|
* error workflows, and preserved during workflow resume scenarios.
|
|
*/
|
|
|
|
import { testDb, createWorkflow, createActiveWorkflow } from '@n8n/backend-test-utils';
|
|
import { ExecutionRepository, type IWorkflowDb } from '@n8n/db';
|
|
import { Container } from '@n8n/di';
|
|
import { readFileSync } from 'fs';
|
|
import { UnrecognizedNodeTypeError } from 'n8n-core';
|
|
import type { IExecutionContext, INodeType, INodeTypeData, NodeLoadingDetails } from 'n8n-workflow';
|
|
import path from 'path';
|
|
|
|
import { WorkflowExecutionService } from '@/workflows/workflow-execution.service';
|
|
|
|
import { createOwner } from './shared/db/users';
|
|
import * as utils from './shared/utils';
|
|
import {
|
|
createSubWorkflowFixture,
|
|
createParentWorkflowFixture,
|
|
createMiddleWorkflowFixture,
|
|
createSimpleWorkflowFixture,
|
|
} from './shared/workflow-fixtures';
|
|
import {
|
|
validateRootContext,
|
|
validateChildContextInheritance,
|
|
validateContextInheritanceChain,
|
|
validateBasicContextStructure,
|
|
} from './shared/execution-context-helpers';
|
|
|
|
// ============================================================
|
|
// Helper to load nodes from dist folder
|
|
// ============================================================
|
|
|
|
const BASE_DIR = path.resolve(__dirname, '../../..');
|
|
|
|
function loadNodesFromDist(nodeNames: string[]): INodeTypeData {
|
|
const nodeTypes: INodeTypeData = {};
|
|
|
|
const knownNodes = JSON.parse(
|
|
readFileSync(path.join(BASE_DIR, 'nodes-base/dist/known/nodes.json'), 'utf-8'),
|
|
) as Record<string, NodeLoadingDetails>;
|
|
|
|
for (const nodeName of nodeNames) {
|
|
const loadInfo = knownNodes[nodeName.replace('n8n-nodes-base.', '')];
|
|
if (!loadInfo) {
|
|
throw new UnrecognizedNodeTypeError('n8n-nodes-base', nodeName);
|
|
}
|
|
// Load from dist .js files (sourcePath already includes 'dist/')
|
|
const nodeDistPath = path.join(BASE_DIR, 'nodes-base', loadInfo.sourcePath);
|
|
const node = new (require(nodeDistPath)[loadInfo.className])() as INodeType;
|
|
nodeTypes[nodeName] = {
|
|
sourcePath: '',
|
|
type: node,
|
|
};
|
|
}
|
|
|
|
return nodeTypes;
|
|
}
|
|
|
|
// Fixtures are now imported from './shared/workflow-fixtures'
|
|
|
|
describe('Execution Context Propagation Integration Tests', () => {
|
|
let owner: any;
|
|
let workflowExecutionService: WorkflowExecutionService;
|
|
let executionRepository: ExecutionRepository;
|
|
|
|
beforeAll(async () => {
|
|
await testDb.init();
|
|
|
|
owner = await createOwner();
|
|
|
|
// Load required node types from dist folder
|
|
const nodeTypes = loadNodesFromDist([
|
|
'n8n-nodes-base.manualTrigger',
|
|
'n8n-nodes-base.executeWorkflow',
|
|
'n8n-nodes-base.executeWorkflowTrigger',
|
|
]);
|
|
|
|
await utils.initNodeTypes(nodeTypes);
|
|
await utils.initBinaryDataService();
|
|
|
|
workflowExecutionService = Container.get(WorkflowExecutionService);
|
|
executionRepository = Container.get(ExecutionRepository);
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await testDb.truncate(['ExecutionEntity', 'WorkflowEntity']);
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await testDb.terminate();
|
|
});
|
|
|
|
// ============================================================
|
|
// Helper Functions
|
|
// ============================================================
|
|
|
|
/**
|
|
* Wait for an execution to complete by polling the database
|
|
*/
|
|
async function waitForExecution(executionId: string, timeout = 10000): Promise<void> {
|
|
const start = Date.now();
|
|
while (Date.now() - start < timeout) {
|
|
const execution = await executionRepository.findOneBy({ id: executionId });
|
|
if (execution?.finished) {
|
|
return;
|
|
}
|
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
}
|
|
throw new Error(`Execution ${executionId} did not complete within ${timeout}ms`);
|
|
}
|
|
|
|
/**
|
|
* Get execution data including the full execution context from the database
|
|
*/
|
|
async function getExecutionWithData(executionId: string) {
|
|
// Use ExecutionRepository's method to properly deserialize execution data
|
|
const executionWithData = await executionRepository.findSingleExecution(executionId, {
|
|
includeData: true,
|
|
unflattenData: true,
|
|
});
|
|
|
|
if (!executionWithData) {
|
|
throw new Error(`Execution ${executionId} not found`);
|
|
}
|
|
|
|
return executionWithData;
|
|
}
|
|
|
|
// ============================================================
|
|
// Tests
|
|
// ============================================================
|
|
|
|
describe('Sub-workflow context propagation', () => {
|
|
it('should propagate context from parent to child workflow', async () => {
|
|
// ============================================================
|
|
// SETUP: Create Child Workflow
|
|
// ============================================================
|
|
const childWorkflow = await createActiveWorkflow(
|
|
{
|
|
name: 'Child Workflow',
|
|
...createSubWorkflowFixture(),
|
|
} as any as IWorkflowDb,
|
|
owner,
|
|
);
|
|
|
|
// ============================================================
|
|
// SETUP: Create Parent Workflow
|
|
// ============================================================
|
|
const parentWorkflow = await createWorkflow(
|
|
{
|
|
name: 'Parent Workflow',
|
|
...createParentWorkflowFixture(childWorkflow.id),
|
|
} as any as IWorkflowDb,
|
|
owner,
|
|
);
|
|
|
|
// ============================================================
|
|
// EXECUTE: Run Parent Workflow
|
|
// ============================================================
|
|
const result = await workflowExecutionService.executeManually(
|
|
parentWorkflow,
|
|
{
|
|
triggerToStartFrom: { name: 'Trigger' },
|
|
},
|
|
owner,
|
|
);
|
|
|
|
hasExecutionId(result);
|
|
expect(result).toBeDefined();
|
|
expect(result.executionId).toBeDefined();
|
|
|
|
// Wait for parent execution to complete
|
|
await waitForExecution(result.executionId);
|
|
|
|
// ============================================================
|
|
// VERIFY: Fetch Parent Execution and Context
|
|
// ============================================================
|
|
const parentExecution = await getExecutionWithData(result.executionId);
|
|
|
|
const parentContext = parentExecution.data.executionData?.runtimeData as IExecutionContext;
|
|
|
|
// Validate parent is a root context with manual execution source
|
|
validateRootContext(parentContext, 'manual');
|
|
|
|
// ============================================================
|
|
// VERIFY: Find Child Execution
|
|
// ============================================================
|
|
// Child workflow should have been executed, find it by workflow ID
|
|
const childExecutions = await executionRepository.find({
|
|
where: { workflowId: childWorkflow.id },
|
|
order: { createdAt: 'DESC' },
|
|
});
|
|
|
|
expect(childExecutions.length).toBeGreaterThan(0);
|
|
|
|
// ============================================================
|
|
// VERIFY: Child Execution Context
|
|
// ============================================================
|
|
const childExecution = await getExecutionWithData(childExecutions[0].id);
|
|
const childContext = childExecution.data.executionData?.runtimeData as IExecutionContext;
|
|
|
|
// Validate child context properly inherits from parent
|
|
validateChildContextInheritance(childContext, parentContext, result.executionId);
|
|
|
|
// Verify execution finished successfully
|
|
expect(parentExecution.status).toBe('success');
|
|
expect(childExecution.status).toBe('success');
|
|
});
|
|
});
|
|
|
|
describe('Nested sub-workflow propagation', () => {
|
|
it('should propagate context through multiple workflow levels', async () => {
|
|
// ============================================================
|
|
// SETUP: Create Grandchild Workflow (Workflow C)
|
|
// ============================================================
|
|
const grandchildWorkflow = await createActiveWorkflow(
|
|
{
|
|
name: 'Grandchild Workflow',
|
|
...createSubWorkflowFixture(),
|
|
} as any as IWorkflowDb,
|
|
owner,
|
|
);
|
|
|
|
// ============================================================
|
|
// SETUP: Create Parent Workflow (Workflow B) - calls Grandchild
|
|
// ============================================================
|
|
const parentWorkflow = await createActiveWorkflow(
|
|
{
|
|
name: 'Parent Workflow (B)',
|
|
...createMiddleWorkflowFixture(grandchildWorkflow.id),
|
|
} as any as IWorkflowDb,
|
|
owner,
|
|
);
|
|
|
|
// ============================================================
|
|
// SETUP: Create Grandparent Workflow (Workflow A) - calls Parent
|
|
// ============================================================
|
|
const grandparentWorkflow = await createWorkflow(
|
|
{
|
|
name: 'Grandparent Workflow (A)',
|
|
...createParentWorkflowFixture(parentWorkflow.id),
|
|
} as any as IWorkflowDb,
|
|
owner,
|
|
);
|
|
|
|
// ============================================================
|
|
// EXECUTE: Run Grandparent Workflow
|
|
// ============================================================
|
|
const result = await workflowExecutionService.executeManually(
|
|
grandparentWorkflow,
|
|
{
|
|
triggerToStartFrom: { name: 'Trigger' },
|
|
},
|
|
owner,
|
|
);
|
|
|
|
hasExecutionId(result);
|
|
expect(result).toBeDefined();
|
|
expect(result.executionId).toBeDefined();
|
|
|
|
// Wait for grandparent execution to complete
|
|
await waitForExecution(result.executionId);
|
|
|
|
// ============================================================
|
|
// VERIFY: Fetch All Execution Contexts
|
|
// ============================================================
|
|
const grandparentExecution = await getExecutionWithData(result.executionId);
|
|
const grandparentContext = grandparentExecution.data.executionData
|
|
?.runtimeData as IExecutionContext;
|
|
|
|
const parentExecutions = await executionRepository.find({
|
|
where: { workflowId: parentWorkflow.id },
|
|
order: { createdAt: 'DESC' },
|
|
});
|
|
expect(parentExecutions.length).toBeGreaterThan(0);
|
|
|
|
const parentExecution = await getExecutionWithData(parentExecutions[0].id);
|
|
const parentContext = parentExecution.data.executionData?.runtimeData as IExecutionContext;
|
|
|
|
const grandchildExecutions = await executionRepository.find({
|
|
where: { workflowId: grandchildWorkflow.id },
|
|
order: { createdAt: 'DESC' },
|
|
});
|
|
expect(grandchildExecutions.length).toBeGreaterThan(0);
|
|
|
|
const grandchildExecution = await getExecutionWithData(grandchildExecutions[0].id);
|
|
const grandchildContext = grandchildExecution.data.executionData
|
|
?.runtimeData as IExecutionContext;
|
|
|
|
// ============================================================
|
|
// VERIFY: Complete Context Inheritance Chain
|
|
// ============================================================
|
|
validateContextInheritanceChain([
|
|
{
|
|
context: grandparentContext,
|
|
parentExecutionId: undefined,
|
|
},
|
|
{
|
|
context: parentContext,
|
|
parentExecutionId: result.executionId,
|
|
},
|
|
{
|
|
context: grandchildContext,
|
|
parentExecutionId: parentExecutions[0].id,
|
|
},
|
|
]);
|
|
|
|
// ============================================================
|
|
// VERIFY: All Executions Finished Successfully
|
|
// ============================================================
|
|
expect(grandparentExecution.status).toBe('success');
|
|
expect(parentExecution.status).toBe('success');
|
|
expect(grandchildExecution.status).toBe('success');
|
|
});
|
|
});
|
|
|
|
describe('Context isolation', () => {
|
|
it('should not leak context between independent workflows', async () => {
|
|
// ============================================================
|
|
// SETUP: Create Two Independent Workflows
|
|
// ============================================================
|
|
const workflow1 = await createWorkflow(
|
|
{
|
|
name: 'Independent Workflow 1',
|
|
...createSimpleWorkflowFixture(),
|
|
} as any as IWorkflowDb,
|
|
owner,
|
|
);
|
|
|
|
const workflow2 = await createWorkflow(
|
|
{
|
|
name: 'Independent Workflow 2',
|
|
...createSimpleWorkflowFixture(),
|
|
} as any as IWorkflowDb,
|
|
owner,
|
|
);
|
|
|
|
// ============================================================
|
|
// EXECUTE: Run Both Workflows Independently
|
|
// ============================================================
|
|
const result1 = await workflowExecutionService.executeManually(
|
|
workflow1,
|
|
{
|
|
triggerToStartFrom: { name: 'Trigger' },
|
|
},
|
|
owner,
|
|
);
|
|
hasExecutionId(result1);
|
|
|
|
expect(result1).toBeDefined();
|
|
expect(result1.executionId).toBeDefined();
|
|
|
|
// Wait for first execution to complete
|
|
await waitForExecution(result1.executionId);
|
|
|
|
const result2 = await workflowExecutionService.executeManually(
|
|
workflow2,
|
|
{
|
|
triggerToStartFrom: { name: 'Trigger' },
|
|
},
|
|
owner,
|
|
);
|
|
|
|
hasExecutionId(result2);
|
|
expect(result2).toBeDefined();
|
|
expect(result2.executionId).toBeDefined();
|
|
|
|
// Wait for second execution to complete
|
|
await waitForExecution(result2.executionId);
|
|
|
|
// ============================================================
|
|
// VERIFY: Fetch Both Execution Contexts
|
|
// ============================================================
|
|
const execution1 = await getExecutionWithData(result1.executionId);
|
|
const context1 = execution1.data.executionData?.runtimeData as IExecutionContext;
|
|
|
|
const execution2 = await getExecutionWithData(result2.executionId);
|
|
const context2 = execution2.data.executionData?.runtimeData as IExecutionContext;
|
|
|
|
// ============================================================
|
|
// VERIFY: Both Contexts Are Root Contexts
|
|
// ============================================================
|
|
validateRootContext(context1, 'manual');
|
|
validateRootContext(context2, 'manual');
|
|
|
|
// ============================================================
|
|
// VERIFY: Complete Context Isolation
|
|
// ============================================================
|
|
|
|
// Both should have valid basic structure
|
|
validateBasicContextStructure(context1);
|
|
validateBasicContextStructure(context2);
|
|
|
|
// Both should be root contexts with no parent
|
|
expect(context1.parentExecutionId).toBeUndefined();
|
|
expect(context2.parentExecutionId).toBeUndefined();
|
|
|
|
// Both should have manual execution source
|
|
expect(context1.source).toBe('manual');
|
|
expect(context2.source).toBe('manual');
|
|
|
|
// Both should have different establishedAt timestamps (since executed sequentially)
|
|
expect(context1.establishedAt).not.toBe(context2.establishedAt);
|
|
expect(context2.establishedAt).toBeGreaterThanOrEqual(context1.establishedAt);
|
|
|
|
// Verify no credential sharing between independent workflows
|
|
// Both should either have different credentials or no credentials at all
|
|
if (context1.credentials && context2.credentials) {
|
|
// If both have credentials, they should not be the same reference
|
|
expect(context1.credentials).not.toBe(context2.credentials);
|
|
}
|
|
|
|
// ============================================================
|
|
// VERIFY: Both Executions Finished Successfully
|
|
// ============================================================
|
|
expect(execution1.status).toBe('success');
|
|
expect(execution2.status).toBe('success');
|
|
});
|
|
});
|
|
});
|
|
|
|
function hasExecutionId(
|
|
data: Awaited<ReturnType<WorkflowExecutionService['executeManually']>>,
|
|
): asserts data is { executionId: string } {
|
|
if ('executionId' in data) {
|
|
return;
|
|
}
|
|
|
|
throw new Error(`Expected an '{executionId: string}', instead got ${JSON.stringify(data)}`);
|
|
}
|