mirror of
https://github.com/czlonkowski/n8n-mcp.git
synced 2026-01-30 06:22:04 +00:00
Changed minimal default settings from executionOrder: 'v0' (legacy) to executionOrder: 'v1' (modern default) when providing fallback settings. This ensures workflows use the modern execution order by default, which provides better performance and more predictable execution behavior. Updated all affected tests to expect 'v1' instead of 'v0'. Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1414 lines
46 KiB
TypeScript
1414 lines
46 KiB
TypeScript
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
import {
|
|
workflowNodeSchema,
|
|
workflowConnectionSchema,
|
|
workflowSettingsSchema,
|
|
defaultWorkflowSettings,
|
|
validateWorkflowNode,
|
|
validateWorkflowConnections,
|
|
validateWorkflowSettings,
|
|
cleanWorkflowForCreate,
|
|
cleanWorkflowForUpdate,
|
|
validateWorkflowStructure,
|
|
hasWebhookTrigger,
|
|
getWebhookUrl,
|
|
getWorkflowStructureExample,
|
|
getWorkflowFixSuggestions,
|
|
} from '../../../src/services/n8n-validation';
|
|
import { WorkflowBuilder } from '../../utils/builders/workflow.builder';
|
|
import { z } from 'zod';
|
|
import { WorkflowNode, WorkflowConnection, Workflow } from '../../../src/types/n8n-api';
|
|
|
|
describe('n8n-validation', () => {
|
|
describe('Zod Schemas', () => {
|
|
describe('workflowNodeSchema', () => {
|
|
it('should validate a complete valid node', () => {
|
|
const validNode = {
|
|
id: 'node-1',
|
|
name: 'Test Node',
|
|
type: 'n8n-nodes-base.set',
|
|
typeVersion: 3,
|
|
position: [100, 200],
|
|
parameters: { key: 'value' },
|
|
credentials: { api: 'cred-id' },
|
|
disabled: false,
|
|
notes: 'Test notes',
|
|
notesInFlow: true,
|
|
continueOnFail: true,
|
|
retryOnFail: true,
|
|
maxTries: 3,
|
|
waitBetweenTries: 1000,
|
|
alwaysOutputData: true,
|
|
executeOnce: false,
|
|
};
|
|
|
|
const result = workflowNodeSchema.parse(validNode);
|
|
expect(result).toEqual(validNode);
|
|
});
|
|
|
|
it('should validate a minimal valid node', () => {
|
|
const minimalNode = {
|
|
id: 'node-1',
|
|
name: 'Test Node',
|
|
type: 'n8n-nodes-base.set',
|
|
typeVersion: 3,
|
|
position: [100, 200],
|
|
parameters: {},
|
|
};
|
|
|
|
const result = workflowNodeSchema.parse(minimalNode);
|
|
expect(result).toEqual(minimalNode);
|
|
});
|
|
|
|
it('should reject node with missing required fields', () => {
|
|
const invalidNode = {
|
|
name: 'Test Node',
|
|
type: 'n8n-nodes-base.set',
|
|
};
|
|
|
|
expect(() => workflowNodeSchema.parse(invalidNode)).toThrow();
|
|
});
|
|
|
|
it('should reject node with invalid position format', () => {
|
|
const invalidNode = {
|
|
id: 'node-1',
|
|
name: 'Test Node',
|
|
type: 'n8n-nodes-base.set',
|
|
typeVersion: 3,
|
|
position: [100], // Should be tuple of 2 numbers
|
|
parameters: {},
|
|
};
|
|
|
|
expect(() => workflowNodeSchema.parse(invalidNode)).toThrow();
|
|
});
|
|
|
|
it('should reject node with invalid type values', () => {
|
|
const invalidNode = {
|
|
id: 'node-1',
|
|
name: 'Test Node',
|
|
type: 'n8n-nodes-base.set',
|
|
typeVersion: '3', // Should be number
|
|
position: [100, 200],
|
|
parameters: {},
|
|
};
|
|
|
|
expect(() => workflowNodeSchema.parse(invalidNode)).toThrow();
|
|
});
|
|
});
|
|
|
|
describe('workflowConnectionSchema', () => {
|
|
it('should validate valid connections', () => {
|
|
const validConnections = {
|
|
'node-1': {
|
|
main: [[{ node: 'node-2', type: 'main', index: 0 }]],
|
|
},
|
|
'node-2': {
|
|
main: [
|
|
[
|
|
{ node: 'node-3', type: 'main', index: 0 },
|
|
{ node: 'node-4', type: 'main', index: 0 },
|
|
],
|
|
],
|
|
},
|
|
};
|
|
|
|
const result = workflowConnectionSchema.parse(validConnections);
|
|
expect(result).toEqual(validConnections);
|
|
});
|
|
|
|
it('should validate empty connections', () => {
|
|
const emptyConnections = {};
|
|
const result = workflowConnectionSchema.parse(emptyConnections);
|
|
expect(result).toEqual(emptyConnections);
|
|
});
|
|
|
|
it('should reject invalid connection structure', () => {
|
|
const invalidConnections = {
|
|
'node-1': {
|
|
main: [{ node: 'node-2', type: 'main', index: 0 }], // Should be array of arrays
|
|
},
|
|
};
|
|
|
|
expect(() => workflowConnectionSchema.parse(invalidConnections)).toThrow();
|
|
});
|
|
|
|
it('should reject connections missing required fields', () => {
|
|
const invalidConnections = {
|
|
'node-1': {
|
|
main: [[{ node: 'node-2' }]], // Missing type and index
|
|
},
|
|
};
|
|
|
|
expect(() => workflowConnectionSchema.parse(invalidConnections)).toThrow();
|
|
});
|
|
});
|
|
|
|
describe('workflowSettingsSchema', () => {
|
|
it('should validate complete settings', () => {
|
|
const completeSettings = {
|
|
executionOrder: 'v1' as const,
|
|
timezone: 'America/New_York',
|
|
saveDataErrorExecution: 'all' as const,
|
|
saveDataSuccessExecution: 'all' as const,
|
|
saveManualExecutions: true,
|
|
saveExecutionProgress: true,
|
|
executionTimeout: 300,
|
|
errorWorkflow: 'error-handler-workflow',
|
|
};
|
|
|
|
const result = workflowSettingsSchema.parse(completeSettings);
|
|
expect(result).toEqual(completeSettings);
|
|
});
|
|
|
|
it('should apply defaults for missing fields', () => {
|
|
const minimalSettings = {};
|
|
const result = workflowSettingsSchema.parse(minimalSettings);
|
|
|
|
expect(result).toEqual({
|
|
executionOrder: 'v1',
|
|
saveDataErrorExecution: 'all',
|
|
saveDataSuccessExecution: 'all',
|
|
saveManualExecutions: true,
|
|
saveExecutionProgress: true,
|
|
});
|
|
});
|
|
|
|
it('should reject invalid enum values', () => {
|
|
const invalidSettings = {
|
|
executionOrder: 'v2', // Invalid enum value
|
|
};
|
|
|
|
expect(() => workflowSettingsSchema.parse(invalidSettings)).toThrow();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Validation Functions', () => {
|
|
describe('validateWorkflowNode', () => {
|
|
it('should validate and return a valid node', () => {
|
|
const node = {
|
|
id: 'test-1',
|
|
name: 'Test',
|
|
type: 'n8n-nodes-base.webhook',
|
|
typeVersion: 2,
|
|
position: [250, 300] as [number, number],
|
|
parameters: {},
|
|
};
|
|
|
|
const result = validateWorkflowNode(node);
|
|
expect(result).toEqual(node);
|
|
});
|
|
|
|
it('should throw for invalid node', () => {
|
|
const invalidNode = { name: 'Test' };
|
|
expect(() => validateWorkflowNode(invalidNode)).toThrow();
|
|
});
|
|
});
|
|
|
|
describe('validateWorkflowConnections', () => {
|
|
it('should validate and return valid connections', () => {
|
|
const connections = {
|
|
'Node1': {
|
|
main: [[{ node: 'Node2', type: 'main', index: 0 }]],
|
|
},
|
|
};
|
|
|
|
const result = validateWorkflowConnections(connections);
|
|
expect(result).toEqual(connections);
|
|
});
|
|
|
|
it('should throw for invalid connections', () => {
|
|
const invalidConnections = {
|
|
'Node1': {
|
|
main: 'invalid', // Should be array
|
|
},
|
|
};
|
|
|
|
expect(() => validateWorkflowConnections(invalidConnections)).toThrow();
|
|
});
|
|
});
|
|
|
|
describe('validateWorkflowSettings', () => {
|
|
it('should validate and return valid settings', () => {
|
|
const settings = {
|
|
executionOrder: 'v1' as const,
|
|
timezone: 'UTC',
|
|
};
|
|
|
|
const result = validateWorkflowSettings(settings);
|
|
expect(result).toMatchObject(settings);
|
|
});
|
|
|
|
it('should apply defaults and validate', () => {
|
|
const result = validateWorkflowSettings({});
|
|
expect(result).toMatchObject(defaultWorkflowSettings);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Workflow Cleaning Functions', () => {
|
|
describe('cleanWorkflowForCreate', () => {
|
|
it('should remove read-only fields', () => {
|
|
const workflow = {
|
|
id: 'should-be-removed',
|
|
name: 'Test Workflow',
|
|
nodes: [],
|
|
connections: {},
|
|
createdAt: '2023-01-01',
|
|
updatedAt: '2023-01-01',
|
|
versionId: 'v123',
|
|
meta: { test: 'data' },
|
|
active: true,
|
|
tags: ['tag1'],
|
|
};
|
|
|
|
const cleaned = cleanWorkflowForCreate(workflow as any);
|
|
|
|
expect(cleaned).not.toHaveProperty('id');
|
|
expect(cleaned).not.toHaveProperty('createdAt');
|
|
expect(cleaned).not.toHaveProperty('updatedAt');
|
|
expect(cleaned).not.toHaveProperty('versionId');
|
|
expect(cleaned).not.toHaveProperty('meta');
|
|
expect(cleaned).not.toHaveProperty('active');
|
|
expect(cleaned).not.toHaveProperty('tags');
|
|
expect(cleaned.name).toBe('Test Workflow');
|
|
});
|
|
|
|
it('should add default settings if not present', () => {
|
|
const workflow = {
|
|
name: 'Test Workflow',
|
|
nodes: [],
|
|
connections: {},
|
|
};
|
|
|
|
const cleaned = cleanWorkflowForCreate(workflow as Workflow);
|
|
expect(cleaned.settings).toEqual(defaultWorkflowSettings);
|
|
});
|
|
|
|
it('should preserve existing settings', () => {
|
|
const customSettings = {
|
|
executionOrder: 'v0' as const,
|
|
timezone: 'America/New_York',
|
|
};
|
|
|
|
const workflow = {
|
|
name: 'Test Workflow',
|
|
nodes: [],
|
|
connections: {},
|
|
settings: customSettings,
|
|
};
|
|
|
|
const cleaned = cleanWorkflowForCreate(workflow as Workflow);
|
|
expect(cleaned.settings).toEqual(customSettings);
|
|
});
|
|
});
|
|
|
|
describe('cleanWorkflowForUpdate', () => {
|
|
it('should remove all read-only and computed fields', () => {
|
|
const workflow = {
|
|
id: 'keep-id',
|
|
name: 'Updated Workflow',
|
|
nodes: [],
|
|
connections: {},
|
|
createdAt: '2023-01-01',
|
|
updatedAt: '2023-01-01',
|
|
versionId: 'v123',
|
|
versionCounter: 5, // n8n 1.118.1+ field
|
|
meta: { test: 'data' },
|
|
staticData: { some: 'data' },
|
|
pinData: { pin: 'data' },
|
|
tags: ['tag1'],
|
|
isArchived: false,
|
|
usedCredentials: ['cred1'],
|
|
sharedWithProjects: ['proj1'],
|
|
triggerCount: 5,
|
|
shared: true,
|
|
active: true,
|
|
settings: { executionOrder: 'v1' },
|
|
} as any;
|
|
|
|
const cleaned = cleanWorkflowForUpdate(workflow);
|
|
|
|
// Should remove all these fields
|
|
expect(cleaned).not.toHaveProperty('id');
|
|
expect(cleaned).not.toHaveProperty('createdAt');
|
|
expect(cleaned).not.toHaveProperty('updatedAt');
|
|
expect(cleaned).not.toHaveProperty('versionId');
|
|
expect(cleaned).not.toHaveProperty('versionCounter'); // n8n 1.118.1+ compatibility
|
|
expect(cleaned).not.toHaveProperty('meta');
|
|
expect(cleaned).not.toHaveProperty('staticData');
|
|
expect(cleaned).not.toHaveProperty('pinData');
|
|
expect(cleaned).not.toHaveProperty('tags');
|
|
expect(cleaned).not.toHaveProperty('isArchived');
|
|
expect(cleaned).not.toHaveProperty('usedCredentials');
|
|
expect(cleaned).not.toHaveProperty('sharedWithProjects');
|
|
expect(cleaned).not.toHaveProperty('triggerCount');
|
|
expect(cleaned).not.toHaveProperty('shared');
|
|
expect(cleaned).not.toHaveProperty('active');
|
|
|
|
// Should keep name and filter settings to safe properties
|
|
expect(cleaned.name).toBe('Updated Workflow');
|
|
expect(cleaned.settings).toEqual({ executionOrder: 'v1' });
|
|
});
|
|
|
|
it('should exclude versionCounter for n8n 1.118.1+ compatibility', () => {
|
|
const workflow = {
|
|
name: 'Test Workflow',
|
|
nodes: [],
|
|
connections: {},
|
|
versionId: 'v123',
|
|
versionCounter: 5, // n8n 1.118.1 returns this but rejects it in PUT
|
|
} as any;
|
|
|
|
const cleaned = cleanWorkflowForUpdate(workflow);
|
|
|
|
expect(cleaned).not.toHaveProperty('versionCounter');
|
|
expect(cleaned).not.toHaveProperty('versionId');
|
|
expect(cleaned.name).toBe('Test Workflow');
|
|
});
|
|
|
|
it('should exclude description field for n8n API compatibility (Issue #431)', () => {
|
|
const workflow = {
|
|
name: 'Test Workflow',
|
|
description: 'This is a test workflow description',
|
|
nodes: [],
|
|
connections: {},
|
|
versionId: 'v123',
|
|
} as any;
|
|
|
|
const cleaned = cleanWorkflowForUpdate(workflow);
|
|
|
|
expect(cleaned).not.toHaveProperty('description');
|
|
expect(cleaned).not.toHaveProperty('versionId');
|
|
expect(cleaned.name).toBe('Test Workflow');
|
|
});
|
|
|
|
it('should provide minimal default settings when no settings provided (Issue #431)', () => {
|
|
const workflow = {
|
|
name: 'Test Workflow',
|
|
nodes: [],
|
|
connections: {},
|
|
} as any;
|
|
|
|
const cleaned = cleanWorkflowForUpdate(workflow);
|
|
// n8n API requires settings to be present, so we provide minimal defaults (v1 is modern default)
|
|
expect(cleaned.settings).toEqual({ executionOrder: 'v1' });
|
|
});
|
|
|
|
it('should filter settings to safe properties to prevent API errors (Issue #248 - final fix)', () => {
|
|
const workflow = {
|
|
name: 'Test Workflow',
|
|
nodes: [],
|
|
connections: {},
|
|
settings: {
|
|
executionOrder: 'v1' as const,
|
|
saveDataSuccessExecution: 'none' as const,
|
|
callerPolicy: 'workflowsFromSameOwner' as const, // Filtered out (not in OpenAPI spec)
|
|
timeSavedPerExecution: 5, // Filtered out (UI-only property)
|
|
},
|
|
} as any;
|
|
|
|
const cleaned = cleanWorkflowForUpdate(workflow);
|
|
|
|
// Unsafe properties filtered out, safe properties kept
|
|
expect(cleaned.settings).toEqual({
|
|
executionOrder: 'v1',
|
|
saveDataSuccessExecution: 'none'
|
|
});
|
|
expect(cleaned.settings).not.toHaveProperty('callerPolicy');
|
|
expect(cleaned.settings).not.toHaveProperty('timeSavedPerExecution');
|
|
});
|
|
|
|
it('should filter out callerPolicy (Issue #248 - API limitation)', () => {
|
|
const workflow = {
|
|
name: 'Test Workflow',
|
|
nodes: [],
|
|
connections: {},
|
|
settings: {
|
|
executionOrder: 'v1' as const,
|
|
callerPolicy: 'workflowsFromSameOwner' as const, // Filtered out
|
|
errorWorkflow: 'N2O2nZy3aUiBRGFN',
|
|
},
|
|
} as any;
|
|
|
|
const cleaned = cleanWorkflowForUpdate(workflow);
|
|
|
|
// callerPolicy filtered out (causes API errors), safe properties kept
|
|
expect(cleaned.settings).toEqual({
|
|
executionOrder: 'v1',
|
|
errorWorkflow: 'N2O2nZy3aUiBRGFN'
|
|
});
|
|
expect(cleaned.settings).not.toHaveProperty('callerPolicy');
|
|
});
|
|
|
|
it('should filter all settings properties correctly (Issue #248 - API design)', () => {
|
|
const workflow = {
|
|
name: 'Test Workflow',
|
|
nodes: [],
|
|
connections: {},
|
|
settings: {
|
|
executionOrder: 'v0' as const,
|
|
timezone: 'UTC',
|
|
saveDataErrorExecution: 'all' as const,
|
|
saveDataSuccessExecution: 'none' as const,
|
|
saveManualExecutions: false,
|
|
saveExecutionProgress: false,
|
|
executionTimeout: 300,
|
|
errorWorkflow: 'error-workflow-id',
|
|
callerPolicy: 'workflowsFromAList' as const, // Filtered out (not in OpenAPI spec)
|
|
},
|
|
} as any;
|
|
|
|
const cleaned = cleanWorkflowForUpdate(workflow);
|
|
|
|
// Safe properties kept, unsafe properties filtered out
|
|
// See: https://community.n8n.io/t/api-workflow-update-endpoint-doesnt-support-setting-callerpolicy/161916
|
|
expect(cleaned.settings).toEqual({
|
|
executionOrder: 'v0',
|
|
timezone: 'UTC',
|
|
saveDataErrorExecution: 'all',
|
|
saveDataSuccessExecution: 'none',
|
|
saveManualExecutions: false,
|
|
saveExecutionProgress: false,
|
|
executionTimeout: 300,
|
|
errorWorkflow: 'error-workflow-id'
|
|
});
|
|
expect(cleaned.settings).not.toHaveProperty('callerPolicy');
|
|
});
|
|
|
|
it('should handle workflows without settings gracefully', () => {
|
|
const workflow = {
|
|
name: 'Test Workflow',
|
|
nodes: [],
|
|
connections: {},
|
|
} as any;
|
|
|
|
const cleaned = cleanWorkflowForUpdate(workflow);
|
|
// n8n API requires settings, so we provide minimal defaults (v1 is modern default)
|
|
expect(cleaned.settings).toEqual({ executionOrder: 'v1' });
|
|
});
|
|
|
|
it('should provide minimal settings when only non-whitelisted properties exist (Issue #431)', () => {
|
|
const workflow = {
|
|
name: 'Test Workflow',
|
|
nodes: [],
|
|
connections: {},
|
|
settings: {
|
|
callerPolicy: 'workflowsFromSameOwner' as const, // Filtered out
|
|
timeSavedPerExecution: 5, // Filtered out (UI-only)
|
|
someOtherProperty: 'value', // Filtered out
|
|
},
|
|
} as any;
|
|
|
|
const cleaned = cleanWorkflowForUpdate(workflow);
|
|
// All properties were filtered out, but n8n API requires settings
|
|
// so we provide minimal defaults (v1 is modern default) to avoid both
|
|
// "additional properties" and "required property" API errors
|
|
expect(cleaned.settings).toEqual({ executionOrder: 'v1' });
|
|
});
|
|
|
|
it('should preserve whitelisted settings when mixed with non-whitelisted (Issue #431)', () => {
|
|
const workflow = {
|
|
name: 'Test Workflow',
|
|
nodes: [],
|
|
connections: {},
|
|
settings: {
|
|
executionOrder: 'v1' as const, // Whitelisted
|
|
callerPolicy: 'workflowsFromSameOwner' as const, // Filtered out
|
|
timezone: 'America/New_York', // Whitelisted
|
|
someOtherProperty: 'value', // Filtered out
|
|
},
|
|
} as any;
|
|
|
|
const cleaned = cleanWorkflowForUpdate(workflow);
|
|
// Should keep only whitelisted properties
|
|
expect(cleaned.settings).toEqual({
|
|
executionOrder: 'v1',
|
|
timezone: 'America/New_York'
|
|
});
|
|
expect(cleaned.settings).not.toHaveProperty('callerPolicy');
|
|
expect(cleaned.settings).not.toHaveProperty('someOtherProperty');
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('validateWorkflowStructure', () => {
|
|
it('should return no errors for valid workflow', () => {
|
|
const workflow = new WorkflowBuilder('Valid Workflow')
|
|
.addWebhookNode({ id: 'webhook-1', name: 'Webhook' })
|
|
.addSlackNode({ id: 'slack-1', name: 'Send Slack' })
|
|
.connect('Webhook', 'Send Slack')
|
|
.build();
|
|
|
|
const errors = validateWorkflowStructure(workflow as any);
|
|
expect(errors).toEqual([]);
|
|
});
|
|
|
|
it('should detect missing workflow name', () => {
|
|
const workflow = {
|
|
nodes: [],
|
|
connections: {},
|
|
};
|
|
|
|
const errors = validateWorkflowStructure(workflow as any);
|
|
expect(errors).toContain('Workflow name is required');
|
|
});
|
|
|
|
it('should detect missing nodes', () => {
|
|
const workflow = {
|
|
name: 'Test',
|
|
connections: {},
|
|
};
|
|
|
|
const errors = validateWorkflowStructure(workflow as any);
|
|
expect(errors).toContain('Workflow must have at least one node');
|
|
});
|
|
|
|
it('should detect empty nodes array', () => {
|
|
const workflow = {
|
|
name: 'Test',
|
|
nodes: [],
|
|
connections: {},
|
|
};
|
|
|
|
const errors = validateWorkflowStructure(workflow as any);
|
|
expect(errors).toContain('Workflow must have at least one node');
|
|
});
|
|
|
|
it('should detect missing connections', () => {
|
|
const workflow = {
|
|
name: 'Test',
|
|
nodes: [{ id: 'node-1', name: 'Node 1', type: 'n8n-nodes-base.set', typeVersion: 1, position: [0, 0] as [number, number], parameters: {} }],
|
|
};
|
|
|
|
const errors = validateWorkflowStructure(workflow as any);
|
|
expect(errors).toContain('Workflow connections are required');
|
|
});
|
|
|
|
it('should allow single webhook node workflow', () => {
|
|
const workflow = {
|
|
name: 'Webhook Only',
|
|
nodes: [{
|
|
id: 'webhook-1',
|
|
name: 'Webhook',
|
|
type: 'n8n-nodes-base.webhook',
|
|
typeVersion: 2,
|
|
position: [250, 300] as [number, number],
|
|
parameters: {},
|
|
}],
|
|
connections: {},
|
|
};
|
|
|
|
const errors = validateWorkflowStructure(workflow as any);
|
|
expect(errors).toEqual([]);
|
|
});
|
|
|
|
it('should reject single non-webhook node workflow', () => {
|
|
const workflow = {
|
|
name: 'Invalid Single Node',
|
|
nodes: [{
|
|
id: 'set-1',
|
|
name: 'Set',
|
|
type: 'n8n-nodes-base.set',
|
|
typeVersion: 3,
|
|
position: [250, 300] as [number, number],
|
|
parameters: {},
|
|
}],
|
|
connections: {},
|
|
};
|
|
|
|
const errors = validateWorkflowStructure(workflow);
|
|
expect(errors.some(e => e.includes('Single non-webhook node workflow is invalid'))).toBe(true);
|
|
});
|
|
|
|
it('should detect empty connections in multi-node workflow', () => {
|
|
const workflow = {
|
|
name: 'Disconnected Nodes',
|
|
nodes: [
|
|
{
|
|
id: 'node-1',
|
|
name: 'Node 1',
|
|
type: 'n8n-nodes-base.set',
|
|
typeVersion: 3,
|
|
position: [250, 300] as [number, number],
|
|
parameters: {},
|
|
},
|
|
{
|
|
id: 'node-2',
|
|
name: 'Node 2',
|
|
type: 'n8n-nodes-base.set',
|
|
typeVersion: 3,
|
|
position: [550, 300] as [number, number],
|
|
parameters: {},
|
|
},
|
|
],
|
|
connections: {},
|
|
};
|
|
|
|
const errors = validateWorkflowStructure(workflow);
|
|
expect(errors.some(e => e.includes('Multi-node workflow has no connections between nodes'))).toBe(true);
|
|
});
|
|
|
|
it('should validate node type format - missing package prefix', () => {
|
|
const workflow = {
|
|
name: 'Invalid Node Type',
|
|
nodes: [{
|
|
id: 'node-1',
|
|
name: 'Node 1',
|
|
type: 'webhook', // Missing package prefix
|
|
typeVersion: 2,
|
|
position: [250, 300] as [number, number],
|
|
parameters: {},
|
|
}],
|
|
connections: {},
|
|
};
|
|
|
|
const errors = validateWorkflowStructure(workflow);
|
|
expect(errors).toContain('Invalid node type "webhook" at index 0. Node types must include package prefix (e.g., "n8n-nodes-base.webhook").');
|
|
});
|
|
|
|
it('should validate node type format - wrong prefix format', () => {
|
|
const workflow = {
|
|
name: 'Invalid Node Type',
|
|
nodes: [{
|
|
id: 'node-1',
|
|
name: 'Node 1',
|
|
type: 'nodes-base.webhook', // Wrong prefix
|
|
typeVersion: 2,
|
|
position: [250, 300] as [number, number],
|
|
parameters: {},
|
|
}],
|
|
connections: {},
|
|
};
|
|
|
|
const errors = validateWorkflowStructure(workflow);
|
|
expect(errors).toContain('Invalid node type "nodes-base.webhook" at index 0. Use "n8n-nodes-base.webhook" instead.');
|
|
});
|
|
|
|
it('should detect invalid node structure', () => {
|
|
const workflow = {
|
|
name: 'Invalid Node',
|
|
nodes: [{
|
|
name: 'Missing Required Fields',
|
|
// Missing id, type, typeVersion, position, parameters
|
|
} as any],
|
|
connections: {},
|
|
};
|
|
|
|
const errors = validateWorkflowStructure(workflow);
|
|
// The validation will fail because the node is missing required fields
|
|
expect(errors.some(e => e.includes('Invalid node at index 0'))).toBe(true);
|
|
});
|
|
|
|
it('should detect non-existent connection source by name', () => {
|
|
const workflow = {
|
|
name: 'Bad Connection',
|
|
nodes: [{
|
|
id: 'node-1',
|
|
name: 'Node 1',
|
|
type: 'n8n-nodes-base.set',
|
|
typeVersion: 3,
|
|
position: [250, 300] as [number, number],
|
|
parameters: {},
|
|
}],
|
|
connections: {
|
|
'Non-existent Node': {
|
|
main: [[{ node: 'Node 1', type: 'main', index: 0 }]],
|
|
},
|
|
},
|
|
};
|
|
|
|
const errors = validateWorkflowStructure(workflow);
|
|
expect(errors).toContain('Connection references non-existent node: Non-existent Node');
|
|
});
|
|
|
|
it('should detect non-existent connection target by name', () => {
|
|
const workflow = {
|
|
name: 'Bad Connection Target',
|
|
nodes: [{
|
|
id: 'node-1',
|
|
name: 'Node 1',
|
|
type: 'n8n-nodes-base.set',
|
|
typeVersion: 3,
|
|
position: [250, 300] as [number, number],
|
|
parameters: {},
|
|
}],
|
|
connections: {
|
|
'Node 1': {
|
|
main: [[{ node: 'Non-existent Node', type: 'main', index: 0 }]],
|
|
},
|
|
},
|
|
};
|
|
|
|
const errors = validateWorkflowStructure(workflow);
|
|
expect(errors).toContain('Connection references non-existent target node: Non-existent Node (from Node 1[0][0])');
|
|
});
|
|
|
|
it('should detect when node ID is used instead of name in connection source', () => {
|
|
const workflow = {
|
|
name: 'ID Instead of Name',
|
|
nodes: [
|
|
{
|
|
id: 'node-1',
|
|
name: 'First Node',
|
|
type: 'n8n-nodes-base.set',
|
|
typeVersion: 3,
|
|
position: [250, 300] as [number, number],
|
|
parameters: {},
|
|
},
|
|
{
|
|
id: 'node-2',
|
|
name: 'Second Node',
|
|
type: 'n8n-nodes-base.set',
|
|
typeVersion: 3,
|
|
position: [550, 300] as [number, number],
|
|
parameters: {},
|
|
},
|
|
],
|
|
connections: {
|
|
'node-1': { // Using ID instead of name
|
|
main: [[{ node: 'Second Node', type: 'main', index: 0 }]],
|
|
},
|
|
},
|
|
};
|
|
|
|
const errors = validateWorkflowStructure(workflow);
|
|
expect(errors).toContain("Connection uses node ID 'node-1' but must use node name 'First Node'. Change connections.node-1 to connections['First Node']");
|
|
});
|
|
|
|
it('should detect when node ID is used instead of name in connection target', () => {
|
|
const workflow = {
|
|
name: 'ID Instead of Name in Target',
|
|
nodes: [
|
|
{
|
|
id: 'node-1',
|
|
name: 'First Node',
|
|
type: 'n8n-nodes-base.set',
|
|
typeVersion: 3,
|
|
position: [250, 300] as [number, number],
|
|
parameters: {},
|
|
},
|
|
{
|
|
id: 'node-2',
|
|
name: 'Second Node',
|
|
type: 'n8n-nodes-base.set',
|
|
typeVersion: 3,
|
|
position: [550, 300] as [number, number],
|
|
parameters: {},
|
|
},
|
|
],
|
|
connections: {
|
|
'First Node': {
|
|
main: [[{ node: 'node-2', type: 'main', index: 0 }]], // Using ID instead of name
|
|
},
|
|
},
|
|
};
|
|
|
|
const errors = validateWorkflowStructure(workflow);
|
|
expect(errors).toContain("Connection target uses node ID 'node-2' but must use node name 'Second Node' (from First Node[0][0])");
|
|
});
|
|
|
|
it('should handle complex multi-output connections', () => {
|
|
const workflow = {
|
|
name: 'Complex Connections',
|
|
nodes: [
|
|
{
|
|
id: 'if-1',
|
|
name: 'IF Node',
|
|
type: 'n8n-nodes-base.if',
|
|
typeVersion: 2,
|
|
position: [250, 300] as [number, number],
|
|
parameters: {},
|
|
},
|
|
{
|
|
id: 'true-1',
|
|
name: 'True Branch',
|
|
type: 'n8n-nodes-base.set',
|
|
typeVersion: 3,
|
|
position: [450, 200] as [number, number],
|
|
parameters: {},
|
|
},
|
|
{
|
|
id: 'false-1',
|
|
name: 'False Branch',
|
|
type: 'n8n-nodes-base.set',
|
|
typeVersion: 3,
|
|
position: [450, 400] as [number, number],
|
|
parameters: {},
|
|
},
|
|
],
|
|
connections: {
|
|
'IF Node': {
|
|
main: [
|
|
[{ node: 'True Branch', type: 'main', index: 0 }],
|
|
[{ node: 'False Branch', type: 'main', index: 0 }],
|
|
],
|
|
},
|
|
},
|
|
};
|
|
|
|
const errors = validateWorkflowStructure(workflow);
|
|
expect(errors).toEqual([]);
|
|
});
|
|
|
|
it('should validate invalid connections structure', () => {
|
|
const workflow = {
|
|
name: 'Invalid Connections',
|
|
nodes: [
|
|
{
|
|
id: 'node-1',
|
|
name: 'Node 1',
|
|
type: 'n8n-nodes-base.set',
|
|
typeVersion: 3,
|
|
position: [250, 300] as [number, number],
|
|
parameters: {},
|
|
},
|
|
{
|
|
id: 'node-2',
|
|
name: 'Node 2',
|
|
type: 'n8n-nodes-base.set',
|
|
typeVersion: 3,
|
|
position: [550, 300] as [number, number],
|
|
parameters: {},
|
|
}
|
|
],
|
|
connections: {
|
|
'Node 1': 'invalid', // Should be an object
|
|
} as any,
|
|
};
|
|
|
|
const errors = validateWorkflowStructure(workflow);
|
|
expect(errors.some(e => e.includes('Invalid connections'))).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('hasWebhookTrigger', () => {
|
|
it('should return true for workflow with webhook node', () => {
|
|
const workflow = new WorkflowBuilder()
|
|
.addWebhookNode()
|
|
.build() as Workflow;
|
|
|
|
expect(hasWebhookTrigger(workflow)).toBe(true);
|
|
});
|
|
|
|
it('should return true for workflow with webhookTrigger node', () => {
|
|
const workflow = {
|
|
name: 'Test',
|
|
nodes: [{
|
|
id: 'webhook-1',
|
|
name: 'Webhook Trigger',
|
|
type: 'n8n-nodes-base.webhookTrigger',
|
|
typeVersion: 1,
|
|
position: [250, 300] as [number, number],
|
|
parameters: {},
|
|
}],
|
|
connections: {},
|
|
} as Workflow;
|
|
|
|
expect(hasWebhookTrigger(workflow)).toBe(true);
|
|
});
|
|
|
|
it('should return false for workflow without webhook nodes', () => {
|
|
const workflow = new WorkflowBuilder()
|
|
.addSlackNode()
|
|
.addHttpRequestNode()
|
|
.build() as Workflow;
|
|
|
|
expect(hasWebhookTrigger(workflow)).toBe(false);
|
|
});
|
|
|
|
it('should return true even if webhook is not the first node', () => {
|
|
const workflow = new WorkflowBuilder()
|
|
.addSlackNode()
|
|
.addWebhookNode()
|
|
.addHttpRequestNode()
|
|
.build() as Workflow;
|
|
|
|
expect(hasWebhookTrigger(workflow)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('getWebhookUrl', () => {
|
|
it('should return webhook path from webhook node', () => {
|
|
const workflow = {
|
|
name: 'Test',
|
|
nodes: [{
|
|
id: 'webhook-1',
|
|
name: 'Webhook',
|
|
type: 'n8n-nodes-base.webhook',
|
|
typeVersion: 2,
|
|
position: [250, 300] as [number, number],
|
|
parameters: {
|
|
path: 'my-custom-webhook',
|
|
},
|
|
}],
|
|
connections: {},
|
|
} as Workflow;
|
|
|
|
expect(getWebhookUrl(workflow)).toBe('my-custom-webhook');
|
|
});
|
|
|
|
it('should return webhook path from webhookTrigger node', () => {
|
|
const workflow = {
|
|
name: 'Test',
|
|
nodes: [{
|
|
id: 'webhook-1',
|
|
name: 'Webhook Trigger',
|
|
type: 'n8n-nodes-base.webhookTrigger',
|
|
typeVersion: 1,
|
|
position: [250, 300] as [number, number],
|
|
parameters: {
|
|
path: 'trigger-webhook-path',
|
|
},
|
|
}],
|
|
connections: {},
|
|
} as Workflow;
|
|
|
|
expect(getWebhookUrl(workflow)).toBe('trigger-webhook-path');
|
|
});
|
|
|
|
it('should return null if no webhook node exists', () => {
|
|
const workflow = new WorkflowBuilder()
|
|
.addSlackNode()
|
|
.build() as Workflow;
|
|
|
|
expect(getWebhookUrl(workflow)).toBe(null);
|
|
});
|
|
|
|
it('should return null if webhook node has no parameters', () => {
|
|
const workflow = {
|
|
name: 'Test',
|
|
nodes: [{
|
|
id: 'webhook-1',
|
|
name: 'Webhook',
|
|
type: 'n8n-nodes-base.webhook',
|
|
typeVersion: 2,
|
|
position: [250, 300] as [number, number],
|
|
parameters: undefined as any,
|
|
}],
|
|
connections: {},
|
|
} as Workflow;
|
|
|
|
expect(getWebhookUrl(workflow)).toBe(null);
|
|
});
|
|
|
|
it('should return null if webhook node has no path parameter', () => {
|
|
const workflow = {
|
|
name: 'Test',
|
|
nodes: [{
|
|
id: 'webhook-1',
|
|
name: 'Webhook',
|
|
type: 'n8n-nodes-base.webhook',
|
|
typeVersion: 2,
|
|
position: [250, 300] as [number, number],
|
|
parameters: {
|
|
method: 'POST',
|
|
// No path parameter
|
|
},
|
|
}],
|
|
connections: {},
|
|
} as Workflow;
|
|
|
|
expect(getWebhookUrl(workflow)).toBe(null);
|
|
});
|
|
|
|
it('should return first webhook path when multiple webhooks exist', () => {
|
|
const workflow = {
|
|
name: 'Test',
|
|
nodes: [
|
|
{
|
|
id: 'webhook-1',
|
|
name: 'Webhook 1',
|
|
type: 'n8n-nodes-base.webhook',
|
|
typeVersion: 2,
|
|
position: [250, 300] as [number, number],
|
|
parameters: {
|
|
path: 'first-webhook',
|
|
},
|
|
},
|
|
{
|
|
id: 'webhook-2',
|
|
name: 'Webhook 2',
|
|
type: 'n8n-nodes-base.webhook',
|
|
typeVersion: 2,
|
|
position: [550, 300] as [number, number],
|
|
parameters: {
|
|
path: 'second-webhook',
|
|
},
|
|
},
|
|
],
|
|
connections: {},
|
|
} as Workflow;
|
|
|
|
expect(getWebhookUrl(workflow)).toBe('first-webhook');
|
|
});
|
|
});
|
|
|
|
describe('getWorkflowStructureExample', () => {
|
|
it('should return a string containing example workflow structure', () => {
|
|
const example = getWorkflowStructureExample();
|
|
|
|
expect(example).toContain('Minimal Workflow Example');
|
|
expect(example).toContain('Manual Trigger');
|
|
expect(example).toContain('Set Data');
|
|
expect(example).toContain('connections');
|
|
expect(example).toContain('IMPORTANT: In connections, use the node NAME');
|
|
});
|
|
|
|
it('should contain valid JSON structure in example', () => {
|
|
const example = getWorkflowStructureExample();
|
|
// Extract the JSON part between the first { and last }
|
|
const match = example.match(/\{[\s\S]*\}/);
|
|
expect(match).toBeTruthy();
|
|
|
|
if (match) {
|
|
// Should not throw when parsing
|
|
expect(() => JSON.parse(match[0])).not.toThrow();
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('getWorkflowFixSuggestions', () => {
|
|
it('should suggest fixes for empty connections', () => {
|
|
const errors = ['Multi-node workflow has empty connections'];
|
|
const suggestions = getWorkflowFixSuggestions(errors);
|
|
|
|
expect(suggestions).toContain('Add connections between your nodes. Each node (except endpoints) should connect to another node.');
|
|
expect(suggestions).toContain('Connection format: connections: { "Source Node Name": { "main": [[{ "node": "Target Node Name", "type": "main", "index": 0 }]] } }');
|
|
});
|
|
|
|
it('should suggest fixes for single-node workflows', () => {
|
|
const errors = ['Single-node workflows are only valid for webhooks'];
|
|
const suggestions = getWorkflowFixSuggestions(errors);
|
|
|
|
expect(suggestions).toContain('Add at least one more node to process data. Common patterns: Trigger → Process → Output');
|
|
expect(suggestions).toContain('Examples: Manual Trigger → Set, Webhook → HTTP Request, Schedule Trigger → Database Query');
|
|
});
|
|
|
|
it('should suggest fixes for node ID usage instead of names', () => {
|
|
const errors = ["Connection uses node ID 'set-1' but must use node name 'Set Data' instead of node name"];
|
|
const suggestions = getWorkflowFixSuggestions(errors);
|
|
|
|
expect(suggestions.some(s => s.includes('Replace node IDs with node names'))).toBe(true);
|
|
expect(suggestions.some(s => s.includes('connections: { "set-1": {...} }'))).toBe(true);
|
|
});
|
|
|
|
it('should return empty array for no errors', () => {
|
|
const suggestions = getWorkflowFixSuggestions([]);
|
|
expect(suggestions).toEqual([]);
|
|
});
|
|
|
|
it('should handle multiple error types', () => {
|
|
const errors = [
|
|
'Multi-node workflow has empty connections',
|
|
'Single-node workflows are only valid for webhooks',
|
|
"Connection uses node ID instead of node name",
|
|
];
|
|
const suggestions = getWorkflowFixSuggestions(errors);
|
|
|
|
expect(suggestions.length).toBeGreaterThan(3);
|
|
expect(suggestions).toContain('Add connections between your nodes. Each node (except endpoints) should connect to another node.');
|
|
expect(suggestions).toContain('Add at least one more node to process data. Common patterns: Trigger → Process → Output');
|
|
expect(suggestions).toContain('Replace node IDs with node names in connections. The name is what appears in the node header.');
|
|
});
|
|
|
|
it('should not duplicate suggestions for similar errors', () => {
|
|
const errors = [
|
|
"Connection uses node ID 'id1' instead of node name",
|
|
"Connection uses node ID 'id2' instead of node name",
|
|
];
|
|
const suggestions = getWorkflowFixSuggestions(errors);
|
|
|
|
// Should only have 2 suggestions for this error type
|
|
const idSuggestions = suggestions.filter(s => s.includes('Replace node IDs'));
|
|
expect(idSuggestions.length).toBe(1);
|
|
});
|
|
});
|
|
|
|
describe('Edge Cases and Error Conditions', () => {
|
|
it('should handle workflow with null values gracefully', () => {
|
|
const workflow = {
|
|
name: 'Test',
|
|
nodes: null as any,
|
|
connections: null as any,
|
|
};
|
|
|
|
const errors = validateWorkflowStructure(workflow);
|
|
expect(errors).toContain('Workflow must have at least one node');
|
|
expect(errors).toContain('Workflow connections are required');
|
|
});
|
|
|
|
it('should handle undefined parameters in cleaning functions', () => {
|
|
const workflow = {
|
|
name: undefined as any,
|
|
nodes: undefined as any,
|
|
connections: undefined as any,
|
|
};
|
|
|
|
expect(() => cleanWorkflowForCreate(workflow)).not.toThrow();
|
|
expect(() => cleanWorkflowForUpdate(workflow as any)).not.toThrow();
|
|
});
|
|
|
|
it('should handle circular references in workflow structure', () => {
|
|
const node1: any = {
|
|
id: 'node-1',
|
|
name: 'Node 1',
|
|
type: 'n8n-nodes-base.set',
|
|
typeVersion: 3,
|
|
position: [250, 300],
|
|
parameters: {},
|
|
};
|
|
|
|
// Create circular reference
|
|
node1.parameters.circular = node1;
|
|
|
|
const workflow = {
|
|
name: 'Circular Ref',
|
|
nodes: [node1],
|
|
connections: {},
|
|
};
|
|
|
|
// Should handle circular references without crashing
|
|
expect(() => validateWorkflowStructure(workflow)).not.toThrow();
|
|
});
|
|
|
|
it('should validate very large position values', () => {
|
|
const node = {
|
|
id: 'node-1',
|
|
name: 'Test Node',
|
|
type: 'n8n-nodes-base.set',
|
|
typeVersion: 3,
|
|
position: [Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER] as [number, number],
|
|
parameters: {},
|
|
};
|
|
|
|
expect(() => validateWorkflowNode(node)).not.toThrow();
|
|
});
|
|
|
|
it('should handle special characters in node names', () => {
|
|
const workflow = {
|
|
name: 'Special Chars',
|
|
nodes: [
|
|
{
|
|
id: 'node-1',
|
|
name: 'Node with "quotes" & special <chars>',
|
|
type: 'n8n-nodes-base.set',
|
|
typeVersion: 3,
|
|
position: [250, 300] as [number, number],
|
|
parameters: {},
|
|
},
|
|
{
|
|
id: 'node-2',
|
|
name: 'Normal Node',
|
|
type: 'n8n-nodes-base.set',
|
|
typeVersion: 3,
|
|
position: [550, 300] as [number, number],
|
|
parameters: {},
|
|
},
|
|
],
|
|
connections: {
|
|
'Node with "quotes" & special <chars>': {
|
|
main: [[{ node: 'Normal Node', type: 'main', index: 0 }]],
|
|
},
|
|
},
|
|
};
|
|
|
|
const errors = validateWorkflowStructure(workflow);
|
|
expect(errors).toEqual([]);
|
|
});
|
|
|
|
it('should handle empty string values', () => {
|
|
const workflow = {
|
|
name: '',
|
|
nodes: [{
|
|
id: '',
|
|
name: '',
|
|
type: '',
|
|
typeVersion: 1,
|
|
position: [0, 0] as [number, number],
|
|
parameters: {},
|
|
}],
|
|
connections: {},
|
|
};
|
|
|
|
const errors = validateWorkflowStructure(workflow);
|
|
expect(errors).toContain('Workflow name is required');
|
|
// Empty string for type will be caught as invalid
|
|
expect(errors.some(e => e.includes('Invalid node at index 0') || e.includes('Node types must include package prefix'))).toBe(true);
|
|
});
|
|
|
|
it('should handle negative position values', () => {
|
|
const node = {
|
|
id: 'node-1',
|
|
name: 'Test Node',
|
|
type: 'n8n-nodes-base.set',
|
|
typeVersion: 3,
|
|
position: [-100, -200] as [number, number],
|
|
parameters: {},
|
|
};
|
|
|
|
// Negative positions are valid
|
|
expect(() => validateWorkflowNode(node)).not.toThrow();
|
|
});
|
|
|
|
it('should validate settings with additional unknown properties', () => {
|
|
const settings = {
|
|
executionOrder: 'v1' as const,
|
|
timezone: 'UTC',
|
|
unknownProperty: 'should be allowed',
|
|
anotherUnknown: { nested: 'object' },
|
|
};
|
|
|
|
// Zod by default strips unknown properties
|
|
const result = validateWorkflowSettings(settings);
|
|
expect(result).toHaveProperty('executionOrder', 'v1');
|
|
expect(result).toHaveProperty('timezone', 'UTC');
|
|
expect(result).not.toHaveProperty('unknownProperty');
|
|
expect(result).not.toHaveProperty('anotherUnknown');
|
|
});
|
|
});
|
|
|
|
describe('Integration Tests', () => {
|
|
it('should validate a complete real-world workflow', () => {
|
|
const workflow = new WorkflowBuilder('Production Workflow')
|
|
.addWebhookNode({
|
|
id: 'webhook-1',
|
|
name: 'Order Webhook',
|
|
parameters: {
|
|
path: 'new-order',
|
|
method: 'POST',
|
|
},
|
|
})
|
|
.addIfNode({
|
|
id: 'if-1',
|
|
name: 'Check Order Value',
|
|
parameters: {
|
|
conditions: {
|
|
options: { caseSensitive: true, leftValue: '', typeValidation: 'strict' },
|
|
conditions: [{
|
|
id: '1',
|
|
leftValue: '={{ $json.orderValue }}',
|
|
rightValue: '100',
|
|
operator: { type: 'number', operation: 'gte' },
|
|
}],
|
|
combinator: 'and',
|
|
},
|
|
},
|
|
})
|
|
.addSlackNode({
|
|
id: 'slack-1',
|
|
name: 'Notify High Value',
|
|
parameters: {
|
|
channel: '#high-value-orders',
|
|
text: 'High value order received: ${{ $json.orderId }}',
|
|
},
|
|
})
|
|
.addHttpRequestNode({
|
|
id: 'http-1',
|
|
name: 'Update Inventory',
|
|
parameters: {
|
|
method: 'POST',
|
|
url: 'https://api.inventory.com/update',
|
|
sendBody: true,
|
|
bodyParametersJson: '={{ $json }}',
|
|
},
|
|
})
|
|
.connect('Order Webhook', 'Check Order Value')
|
|
.connect('Check Order Value', 'Notify High Value', 0) // True output
|
|
.connect('Check Order Value', 'Update Inventory', 1) // False output
|
|
.setSettings({
|
|
executionOrder: 'v1',
|
|
timezone: 'America/New_York',
|
|
saveDataErrorExecution: 'all',
|
|
saveDataSuccessExecution: 'none',
|
|
executionTimeout: 300,
|
|
})
|
|
.build();
|
|
|
|
const errors = validateWorkflowStructure(workflow as any);
|
|
expect(errors).toEqual([]);
|
|
|
|
// Validate individual components
|
|
workflow.nodes.forEach(node => {
|
|
expect(() => validateWorkflowNode(node)).not.toThrow();
|
|
});
|
|
expect(() => validateWorkflowConnections(workflow.connections)).not.toThrow();
|
|
expect(() => validateWorkflowSettings(workflow.settings!)).not.toThrow();
|
|
});
|
|
|
|
it('should clean and validate workflow for API operations', () => {
|
|
const originalWorkflow = {
|
|
id: 'wf-123',
|
|
name: 'API Test Workflow',
|
|
nodes: [
|
|
{
|
|
id: 'manual-1',
|
|
name: 'Manual Trigger',
|
|
type: 'n8n-nodes-base.manualTrigger',
|
|
typeVersion: 1,
|
|
position: [250, 300] as [number, number],
|
|
parameters: {},
|
|
},
|
|
{
|
|
id: 'set-1',
|
|
name: 'Set Data',
|
|
type: 'n8n-nodes-base.set',
|
|
typeVersion: 3.4,
|
|
position: [450, 300] as [number, number],
|
|
parameters: {
|
|
mode: 'manual',
|
|
assignments: {
|
|
assignments: [{
|
|
id: '1',
|
|
name: 'testKey',
|
|
value: 'testValue',
|
|
type: 'string',
|
|
}],
|
|
},
|
|
},
|
|
}
|
|
],
|
|
connections: {
|
|
'Manual Trigger': {
|
|
main: [[{
|
|
node: 'Set Data',
|
|
type: 'main',
|
|
index: 0,
|
|
}]],
|
|
},
|
|
},
|
|
createdAt: '2023-01-01T00:00:00Z',
|
|
updatedAt: '2023-01-02T00:00:00Z',
|
|
versionId: 'v123',
|
|
active: true,
|
|
tags: ['test', 'api'],
|
|
meta: { instanceId: 'instance-123' },
|
|
};
|
|
|
|
// Test create cleaning
|
|
const forCreate = cleanWorkflowForCreate(originalWorkflow);
|
|
expect(forCreate).not.toHaveProperty('id');
|
|
expect(forCreate).not.toHaveProperty('createdAt');
|
|
expect(forCreate).not.toHaveProperty('updatedAt');
|
|
expect(forCreate).not.toHaveProperty('versionId');
|
|
expect(forCreate).not.toHaveProperty('active');
|
|
expect(forCreate).not.toHaveProperty('tags');
|
|
expect(forCreate).not.toHaveProperty('meta');
|
|
expect(forCreate).toHaveProperty('settings');
|
|
expect(validateWorkflowStructure(forCreate)).toEqual([]);
|
|
|
|
// Test update cleaning
|
|
const forUpdate = cleanWorkflowForUpdate(originalWorkflow as any);
|
|
expect(forUpdate).not.toHaveProperty('id');
|
|
expect(forUpdate).not.toHaveProperty('createdAt');
|
|
expect(forUpdate).not.toHaveProperty('updatedAt');
|
|
expect(forUpdate).not.toHaveProperty('versionId');
|
|
expect(forUpdate).not.toHaveProperty('active');
|
|
expect(forUpdate).not.toHaveProperty('tags');
|
|
expect(forUpdate).not.toHaveProperty('meta');
|
|
// n8n API requires settings in updates, so minimal defaults (v1) are provided (Issue #431)
|
|
expect(forUpdate.settings).toEqual({ executionOrder: 'v1' });
|
|
expect(validateWorkflowStructure(forUpdate)).toEqual([]);
|
|
});
|
|
});
|
|
}); |