Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
290 lines
9.2 KiB
TypeScript
290 lines
9.2 KiB
TypeScript
import { mockInstance } from '@n8n/backend-test-utils';
|
|
import type { Project, Variables } from '@n8n/db';
|
|
import type { ITaskData, IWorkflowSettings } from 'n8n-workflow';
|
|
|
|
import { VariablesService } from '@/environments.ee/variables/variables.service.ee';
|
|
import { OwnershipService } from '@/services/ownership.service';
|
|
import {
|
|
getVariables,
|
|
preserveInputOverride,
|
|
removeDefaultValues,
|
|
shouldRestartParentExecution,
|
|
} from '@/workflow-helpers';
|
|
|
|
describe('workflow-helpers', () => {
|
|
beforeAll(() => {
|
|
mockInstance(VariablesService, {
|
|
async getAllCached() {
|
|
return [
|
|
{ id: '1', key: 'VAR1', value: 'value1' },
|
|
{ id: '2', key: 'VAR2', value: 'value2' },
|
|
{
|
|
id: '3',
|
|
key: 'VAR2',
|
|
value: 'value1Project',
|
|
project: { id: '1', name: 'project1' } as Project,
|
|
},
|
|
{
|
|
id: '4',
|
|
key: 'VAR4',
|
|
value: 'value4',
|
|
project: { id: '1', name: 'project1' } as Project,
|
|
},
|
|
{
|
|
id: '5',
|
|
key: 'VAR5',
|
|
value: 'value5',
|
|
project: { id: '2', name: 'project2' } as Project,
|
|
},
|
|
] as Variables[];
|
|
},
|
|
});
|
|
|
|
mockInstance(OwnershipService, {
|
|
async getWorkflowProjectCached(_workflowId: string) {
|
|
return { id: '1', name: 'project' } as unknown as Project;
|
|
},
|
|
});
|
|
});
|
|
|
|
describe('getVariables', () => {
|
|
it('should return all variables as key-value pairs if no parameters are given', async () => {
|
|
const variables = await getVariables();
|
|
expect(variables).toEqual({ VAR1: 'value1', VAR2: 'value2' });
|
|
});
|
|
|
|
it('should return global and project variables if projectId is given', async () => {
|
|
const variables = await getVariables(undefined, '1');
|
|
expect(variables).toEqual({ VAR1: 'value1', VAR2: 'value1Project', VAR4: 'value4' });
|
|
});
|
|
|
|
it('should return global and project variables if workflowId is given', async () => {
|
|
const variables = await getVariables('1');
|
|
expect(variables).toEqual({ VAR1: 'value1', VAR2: 'value1Project', VAR4: 'value4' });
|
|
});
|
|
|
|
it('should prioritize passed of projectId over workflowId', async () => {
|
|
const variables = await getVariables('1', '2');
|
|
expect(variables).toEqual({ VAR1: 'value1', VAR2: 'value2', VAR5: 'value5' });
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('shouldRestartParentExecution', () => {
|
|
it('should return false when parentExecution is undefined', () => {
|
|
expect(shouldRestartParentExecution(undefined)).toBe(false);
|
|
});
|
|
|
|
it('should return false when shouldResume is explicitly false', () => {
|
|
const parentExecution = {
|
|
executionId: 'parent-exec-id',
|
|
workflowId: 'parent-workflow-id',
|
|
shouldResume: false,
|
|
};
|
|
expect(shouldRestartParentExecution(parentExecution)).toBe(false);
|
|
});
|
|
|
|
it('should return true when shouldResume is undefined', () => {
|
|
const parentExecution = {
|
|
executionId: 'parent-exec-id',
|
|
workflowId: 'parent-workflow-id',
|
|
shouldResume: undefined,
|
|
};
|
|
expect(shouldRestartParentExecution(parentExecution)).toBe(true);
|
|
});
|
|
|
|
it('should return true when shouldResume is true', () => {
|
|
const parentExecution = {
|
|
executionId: 'parent-exec-id',
|
|
workflowId: 'parent-workflow-id',
|
|
shouldResume: true,
|
|
};
|
|
expect(shouldRestartParentExecution(parentExecution)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('preserveInputOverride', () => {
|
|
const makeEntry = (overrides: Partial<ITaskData> = {}): ITaskData => ({
|
|
startTime: 100,
|
|
executionTime: 50,
|
|
executionIndex: 1,
|
|
source: [],
|
|
...overrides,
|
|
});
|
|
|
|
it('should throw when the array is empty', () => {
|
|
expect(() => preserveInputOverride([])).toThrow();
|
|
});
|
|
|
|
it('should pop the entry and leave the array empty when there is no inputOverride', () => {
|
|
const runDataArray: ITaskData[] = [makeEntry()];
|
|
preserveInputOverride(runDataArray);
|
|
expect(runDataArray).toHaveLength(0);
|
|
});
|
|
|
|
it('should replace the entry with a zeroed placeholder when inputOverride is present', () => {
|
|
const inputOverride = { main: [[{ json: { key: 'value' } }]] };
|
|
const runDataArray: ITaskData[] = [makeEntry({ inputOverride })];
|
|
preserveInputOverride(runDataArray);
|
|
expect(runDataArray).toHaveLength(1);
|
|
expect(runDataArray[0]).toEqual({
|
|
startTime: 0,
|
|
executionTime: 0,
|
|
executionIndex: 0,
|
|
source: [],
|
|
inputOverride,
|
|
});
|
|
});
|
|
|
|
it('should carry source from the original entry over to the placeholder', () => {
|
|
const source = [{ previousNode: 'NodeA', previousNodeOutput: 0 }];
|
|
const inputOverride = { main: [[{ json: {} }]] };
|
|
const runDataArray: ITaskData[] = [makeEntry({ source, inputOverride })];
|
|
preserveInputOverride(runDataArray);
|
|
expect(runDataArray[0].source).toBe(source);
|
|
});
|
|
|
|
it('should not include data or other original fields in the placeholder', () => {
|
|
const inputOverride = { main: [[{ json: {} }]] };
|
|
const data = { main: [[{ json: { result: 'something' } }]] };
|
|
const runDataArray: ITaskData[] = [makeEntry({ inputOverride, data, executionTime: 999 })];
|
|
preserveInputOverride(runDataArray);
|
|
expect(runDataArray[0]).not.toHaveProperty('data');
|
|
expect(runDataArray[0].executionTime).toBe(0);
|
|
});
|
|
|
|
it('should only affect the last entry when inputOverride is present', () => {
|
|
const inputOverride = { main: [[{ json: {} }]] };
|
|
const first = makeEntry({ executionIndex: 0 });
|
|
const second = makeEntry({ executionIndex: 1 });
|
|
const last = makeEntry({ executionIndex: 2, inputOverride });
|
|
const runDataArray: ITaskData[] = [first, second, last];
|
|
preserveInputOverride(runDataArray);
|
|
expect(runDataArray).toHaveLength(3);
|
|
expect(runDataArray[0]).toBe(first);
|
|
expect(runDataArray[1]).toBe(second);
|
|
expect(runDataArray[2].inputOverride).toBe(inputOverride);
|
|
expect(runDataArray[2].startTime).toBe(0);
|
|
});
|
|
|
|
it('should remove only the last entry when no inputOverride and earlier entries remain', () => {
|
|
const first = makeEntry({ executionIndex: 0 });
|
|
const runDataArray: ITaskData[] = [first, makeEntry({ executionIndex: 1 })];
|
|
preserveInputOverride(runDataArray);
|
|
expect(runDataArray).toHaveLength(1);
|
|
expect(runDataArray[0]).toBe(first);
|
|
});
|
|
});
|
|
|
|
describe('removeDefaultValues', () => {
|
|
const DEFAULT_EXECUTION_TIMEOUT = 3600;
|
|
const DEFAULT = 'DEFAULT';
|
|
|
|
it('should remove errorWorkflow when set to DEFAULT', () => {
|
|
const settings: IWorkflowSettings = {
|
|
errorWorkflow: DEFAULT,
|
|
timezone: 'America/New_York',
|
|
};
|
|
const result = removeDefaultValues(settings, DEFAULT_EXECUTION_TIMEOUT);
|
|
expect(result).toEqual({
|
|
timezone: 'America/New_York',
|
|
});
|
|
});
|
|
|
|
it('should remove all keys set to DEFAULT', () => {
|
|
const settings: IWorkflowSettings = {
|
|
errorWorkflow: DEFAULT,
|
|
timezone: DEFAULT,
|
|
saveDataErrorExecution: DEFAULT,
|
|
saveDataSuccessExecution: DEFAULT,
|
|
saveManualExecutions: DEFAULT,
|
|
saveExecutionProgress: DEFAULT,
|
|
callerPolicy: 'workflowsFromSameOwner',
|
|
};
|
|
const result = removeDefaultValues(settings, DEFAULT_EXECUTION_TIMEOUT);
|
|
expect(result).toEqual({
|
|
callerPolicy: 'workflowsFromSameOwner',
|
|
});
|
|
});
|
|
|
|
it('should remove executionTimeout when it matches default', () => {
|
|
const settings: IWorkflowSettings = {
|
|
executionTimeout: 3600,
|
|
timezone: 'America/New_York',
|
|
};
|
|
const result = removeDefaultValues(settings, DEFAULT_EXECUTION_TIMEOUT);
|
|
expect(result).toEqual({
|
|
timezone: 'America/New_York',
|
|
});
|
|
});
|
|
|
|
it('should keep executionTimeout when it differs from default', () => {
|
|
const settings: IWorkflowSettings = {
|
|
executionTimeout: 7200,
|
|
timezone: 'America/New_York',
|
|
};
|
|
const result = removeDefaultValues(settings, DEFAULT_EXECUTION_TIMEOUT);
|
|
expect(result).toEqual({
|
|
executionTimeout: 7200,
|
|
timezone: 'America/New_York',
|
|
});
|
|
});
|
|
|
|
it('should keep non-DEFAULT values', () => {
|
|
const settings: IWorkflowSettings = {
|
|
errorWorkflow: 'some-workflow-id',
|
|
timezone: 'America/New_York',
|
|
saveDataErrorExecution: 'all',
|
|
};
|
|
const result = removeDefaultValues(settings, DEFAULT_EXECUTION_TIMEOUT);
|
|
expect(result).toEqual({
|
|
errorWorkflow: 'some-workflow-id',
|
|
timezone: 'America/New_York',
|
|
saveDataErrorExecution: 'all',
|
|
});
|
|
});
|
|
|
|
it('should remove credentialResolverId when empty string', () => {
|
|
const settings: IWorkflowSettings = {
|
|
credentialResolverId: '',
|
|
timezone: 'America/New_York',
|
|
};
|
|
const result = removeDefaultValues(settings, DEFAULT_EXECUTION_TIMEOUT);
|
|
expect(result).toEqual({
|
|
timezone: 'America/New_York',
|
|
});
|
|
});
|
|
|
|
it('should remove credentialResolverId when undefined', () => {
|
|
const settings: IWorkflowSettings = {
|
|
credentialResolverId: undefined,
|
|
timezone: 'America/New_York',
|
|
};
|
|
const result = removeDefaultValues(settings, DEFAULT_EXECUTION_TIMEOUT);
|
|
expect(result).not.toHaveProperty('credentialResolverId');
|
|
});
|
|
|
|
it('should keep credentialResolverId when set to a valid ID', () => {
|
|
const settings: IWorkflowSettings = {
|
|
credentialResolverId: 'resolver-id-123',
|
|
timezone: 'America/New_York',
|
|
};
|
|
const result = removeDefaultValues(settings, DEFAULT_EXECUTION_TIMEOUT);
|
|
expect(result).toEqual({
|
|
credentialResolverId: 'resolver-id-123',
|
|
timezone: 'America/New_York',
|
|
});
|
|
});
|
|
|
|
it('should not mutate the original settings object', () => {
|
|
const settings = {
|
|
errorWorkflow: DEFAULT,
|
|
timezone: 'America/New_York',
|
|
executionTimeout: 3600,
|
|
};
|
|
const originalSettings = { ...settings };
|
|
removeDefaultValues(settings, DEFAULT_EXECUTION_TIMEOUT);
|
|
expect(settings).toEqual(originalSettings);
|
|
});
|
|
});
|