- Add validation for invalid fixedCollection structures in Switch, If, and Filter nodes - Detect and prevent nested 'conditions.values' patterns that cause n8n UI crashes - Support both 'n8n-nodes-base.x' and 'nodes-base.x' node type formats - Provide auto-fix suggestions for invalid structures - Add comprehensive test coverage for all edge cases This prevents AI agents from creating invalid node configurations that break n8n's UI.
413 lines
12 KiB
TypeScript
413 lines
12 KiB
TypeScript
/**
|
|
* Workflow Fixed Collection Validation Tests
|
|
* Tests that workflow validation catches fixedCollection structure errors at the workflow level
|
|
*/
|
|
|
|
import { describe, test, expect, beforeEach, vi } from 'vitest';
|
|
import { WorkflowValidator } from '../../../src/services/workflow-validator';
|
|
import { EnhancedConfigValidator } from '../../../src/services/enhanced-config-validator';
|
|
import { NodeRepository } from '../../../src/database/node-repository';
|
|
|
|
describe('Workflow FixedCollection Validation', () => {
|
|
let validator: WorkflowValidator;
|
|
let mockNodeRepository: any;
|
|
|
|
beforeEach(() => {
|
|
// Create mock repository that returns basic node info for common nodes
|
|
mockNodeRepository = {
|
|
getNode: vi.fn().mockImplementation((type: string) => {
|
|
const normalizedType = type.replace('n8n-nodes-base.', '').replace('nodes-base.', '');
|
|
switch (normalizedType) {
|
|
case 'webhook':
|
|
return {
|
|
nodeType: 'nodes-base.webhook',
|
|
displayName: 'Webhook',
|
|
properties: [
|
|
{ name: 'path', type: 'string', required: true },
|
|
{ name: 'httpMethod', type: 'options' }
|
|
]
|
|
};
|
|
case 'switch':
|
|
return {
|
|
nodeType: 'nodes-base.switch',
|
|
displayName: 'Switch',
|
|
properties: [
|
|
{ name: 'rules', type: 'fixedCollection', required: true }
|
|
]
|
|
};
|
|
case 'if':
|
|
return {
|
|
nodeType: 'nodes-base.if',
|
|
displayName: 'If',
|
|
properties: [
|
|
{ name: 'conditions', type: 'filter', required: true }
|
|
]
|
|
};
|
|
case 'filter':
|
|
return {
|
|
nodeType: 'nodes-base.filter',
|
|
displayName: 'Filter',
|
|
properties: [
|
|
{ name: 'conditions', type: 'filter', required: true }
|
|
]
|
|
};
|
|
default:
|
|
return null;
|
|
}
|
|
})
|
|
};
|
|
|
|
validator = new WorkflowValidator(mockNodeRepository, EnhancedConfigValidator);
|
|
});
|
|
|
|
test('should catch invalid Switch node structure in workflow validation', async () => {
|
|
const workflow = {
|
|
name: 'Test Workflow with Invalid Switch',
|
|
nodes: [
|
|
{
|
|
id: 'webhook',
|
|
name: 'Webhook',
|
|
type: 'n8n-nodes-base.webhook',
|
|
position: [0, 0] as [number, number],
|
|
parameters: {
|
|
path: 'test-webhook'
|
|
}
|
|
},
|
|
{
|
|
id: 'switch',
|
|
name: 'Switch',
|
|
type: 'n8n-nodes-base.switch',
|
|
position: [200, 0] as [number, number],
|
|
parameters: {
|
|
// This is the problematic structure that causes "propertyValues[itemName] is not iterable"
|
|
rules: {
|
|
conditions: {
|
|
values: [
|
|
{
|
|
value1: '={{$json.status}}',
|
|
operation: 'equals',
|
|
value2: 'active'
|
|
}
|
|
]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
],
|
|
connections: {
|
|
Webhook: {
|
|
main: [[{ node: 'Switch', type: 'main', index: 0 }]]
|
|
}
|
|
}
|
|
};
|
|
|
|
const result = await validator.validateWorkflow(workflow, {
|
|
validateNodes: true,
|
|
profile: 'ai-friendly'
|
|
});
|
|
|
|
expect(result.valid).toBe(false);
|
|
expect(result.errors).toHaveLength(1);
|
|
|
|
const switchError = result.errors.find(e => e.nodeId === 'switch');
|
|
expect(switchError).toBeDefined();
|
|
expect(switchError!.message).toContain('propertyValues[itemName] is not iterable');
|
|
expect(switchError!.message).toContain('Invalid structure for nodes-base.switch node');
|
|
});
|
|
|
|
test('should catch invalid If node structure in workflow validation', async () => {
|
|
const workflow = {
|
|
name: 'Test Workflow with Invalid If',
|
|
nodes: [
|
|
{
|
|
id: 'webhook',
|
|
name: 'Webhook',
|
|
type: 'n8n-nodes-base.webhook',
|
|
position: [0, 0] as [number, number],
|
|
parameters: {
|
|
path: 'test-webhook'
|
|
}
|
|
},
|
|
{
|
|
id: 'if',
|
|
name: 'If',
|
|
type: 'n8n-nodes-base.if',
|
|
position: [200, 0] as [number, number],
|
|
parameters: {
|
|
// This is the problematic structure
|
|
conditions: {
|
|
values: [
|
|
{
|
|
value1: '={{$json.age}}',
|
|
operation: 'largerEqual',
|
|
value2: 18
|
|
}
|
|
]
|
|
}
|
|
}
|
|
}
|
|
],
|
|
connections: {
|
|
Webhook: {
|
|
main: [[{ node: 'If', type: 'main', index: 0 }]]
|
|
}
|
|
}
|
|
};
|
|
|
|
const result = await validator.validateWorkflow(workflow, {
|
|
validateNodes: true,
|
|
profile: 'ai-friendly'
|
|
});
|
|
|
|
expect(result.valid).toBe(false);
|
|
expect(result.errors).toHaveLength(1);
|
|
|
|
const ifError = result.errors.find(e => e.nodeId === 'if');
|
|
expect(ifError).toBeDefined();
|
|
expect(ifError!.message).toContain('Invalid structure for nodes-base.if node');
|
|
});
|
|
|
|
test('should accept valid Switch node structure in workflow validation', async () => {
|
|
const workflow = {
|
|
name: 'Test Workflow with Valid Switch',
|
|
nodes: [
|
|
{
|
|
id: 'webhook',
|
|
name: 'Webhook',
|
|
type: 'n8n-nodes-base.webhook',
|
|
position: [0, 0] as [number, number],
|
|
parameters: {
|
|
path: 'test-webhook'
|
|
}
|
|
},
|
|
{
|
|
id: 'switch',
|
|
name: 'Switch',
|
|
type: 'n8n-nodes-base.switch',
|
|
position: [200, 0] as [number, number],
|
|
parameters: {
|
|
// This is the correct structure
|
|
rules: {
|
|
values: [
|
|
{
|
|
conditions: {
|
|
value1: '={{$json.status}}',
|
|
operation: 'equals',
|
|
value2: 'active'
|
|
},
|
|
outputKey: 'active'
|
|
}
|
|
]
|
|
}
|
|
}
|
|
}
|
|
],
|
|
connections: {
|
|
Webhook: {
|
|
main: [[{ node: 'Switch', type: 'main', index: 0 }]]
|
|
}
|
|
}
|
|
};
|
|
|
|
const result = await validator.validateWorkflow(workflow, {
|
|
validateNodes: true,
|
|
profile: 'ai-friendly'
|
|
});
|
|
|
|
// Should not have fixedCollection structure errors
|
|
const hasFixedCollectionError = result.errors.some(e =>
|
|
e.message.includes('propertyValues[itemName] is not iterable')
|
|
);
|
|
expect(hasFixedCollectionError).toBe(false);
|
|
});
|
|
|
|
test('should catch multiple fixedCollection errors in a single workflow', async () => {
|
|
const workflow = {
|
|
name: 'Test Workflow with Multiple Invalid Structures',
|
|
nodes: [
|
|
{
|
|
id: 'webhook',
|
|
name: 'Webhook',
|
|
type: 'n8n-nodes-base.webhook',
|
|
position: [0, 0] as [number, number],
|
|
parameters: {
|
|
path: 'test-webhook'
|
|
}
|
|
},
|
|
{
|
|
id: 'switch',
|
|
name: 'Switch',
|
|
type: 'n8n-nodes-base.switch',
|
|
position: [200, 0] as [number, number],
|
|
parameters: {
|
|
rules: {
|
|
conditions: {
|
|
values: [{ value1: 'test', operation: 'equals', value2: 'test' }]
|
|
}
|
|
}
|
|
}
|
|
},
|
|
{
|
|
id: 'if',
|
|
name: 'If',
|
|
type: 'n8n-nodes-base.if',
|
|
position: [400, 0] as [number, number],
|
|
parameters: {
|
|
conditions: {
|
|
values: [{ value1: 'test', operation: 'equals', value2: 'test' }]
|
|
}
|
|
}
|
|
},
|
|
{
|
|
id: 'filter',
|
|
name: 'Filter',
|
|
type: 'n8n-nodes-base.filter',
|
|
position: [600, 0] as [number, number],
|
|
parameters: {
|
|
conditions: {
|
|
values: [{ value1: 'test', operation: 'equals', value2: 'test' }]
|
|
}
|
|
}
|
|
}
|
|
],
|
|
connections: {
|
|
Webhook: {
|
|
main: [[{ node: 'Switch', type: 'main', index: 0 }]]
|
|
},
|
|
Switch: {
|
|
main: [
|
|
[{ node: 'If', type: 'main', index: 0 }],
|
|
[{ node: 'Filter', type: 'main', index: 0 }]
|
|
]
|
|
}
|
|
}
|
|
};
|
|
|
|
const result = await validator.validateWorkflow(workflow, {
|
|
validateNodes: true,
|
|
profile: 'ai-friendly'
|
|
});
|
|
|
|
expect(result.valid).toBe(false);
|
|
expect(result.errors.length).toBeGreaterThanOrEqual(3); // At least one error for each problematic node
|
|
|
|
// Check that each problematic node has an error
|
|
const switchError = result.errors.find(e => e.nodeId === 'switch');
|
|
const ifError = result.errors.find(e => e.nodeId === 'if');
|
|
const filterError = result.errors.find(e => e.nodeId === 'filter');
|
|
|
|
expect(switchError).toBeDefined();
|
|
expect(ifError).toBeDefined();
|
|
expect(filterError).toBeDefined();
|
|
});
|
|
|
|
test('should provide helpful statistics about fixedCollection errors', async () => {
|
|
const workflow = {
|
|
name: 'Test Workflow Statistics',
|
|
nodes: [
|
|
{
|
|
id: 'webhook',
|
|
name: 'Webhook',
|
|
type: 'n8n-nodes-base.webhook',
|
|
position: [0, 0] as [number, number],
|
|
parameters: { path: 'test' }
|
|
},
|
|
{
|
|
id: 'bad-switch',
|
|
name: 'Bad Switch',
|
|
type: 'n8n-nodes-base.switch',
|
|
position: [200, 0] as [number, number],
|
|
parameters: {
|
|
rules: {
|
|
conditions: { values: [{ value1: 'test', operation: 'equals', value2: 'test' }] }
|
|
}
|
|
}
|
|
},
|
|
{
|
|
id: 'good-switch',
|
|
name: 'Good Switch',
|
|
type: 'n8n-nodes-base.switch',
|
|
position: [400, 0] as [number, number],
|
|
parameters: {
|
|
rules: {
|
|
values: [{ conditions: { value1: 'test', operation: 'equals', value2: 'test' }, outputKey: 'out' }]
|
|
}
|
|
}
|
|
}
|
|
],
|
|
connections: {
|
|
Webhook: {
|
|
main: [
|
|
[{ node: 'Bad Switch', type: 'main', index: 0 }],
|
|
[{ node: 'Good Switch', type: 'main', index: 0 }]
|
|
]
|
|
}
|
|
}
|
|
};
|
|
|
|
const result = await validator.validateWorkflow(workflow, {
|
|
validateNodes: true,
|
|
profile: 'ai-friendly'
|
|
});
|
|
|
|
expect(result.statistics.totalNodes).toBe(3);
|
|
expect(result.statistics.enabledNodes).toBe(3);
|
|
expect(result.valid).toBe(false); // Should be invalid due to the bad switch
|
|
|
|
// Should have at least one error for the bad switch
|
|
const badSwitchError = result.errors.find(e => e.nodeId === 'bad-switch');
|
|
expect(badSwitchError).toBeDefined();
|
|
|
|
// Should not have errors for the good switch or webhook
|
|
const goodSwitchError = result.errors.find(e => e.nodeId === 'good-switch');
|
|
const webhookError = result.errors.find(e => e.nodeId === 'webhook');
|
|
|
|
// These might have other validation errors, but not fixedCollection errors
|
|
if (goodSwitchError) {
|
|
expect(goodSwitchError.message).not.toContain('propertyValues[itemName] is not iterable');
|
|
}
|
|
if (webhookError) {
|
|
expect(webhookError.message).not.toContain('propertyValues[itemName] is not iterable');
|
|
}
|
|
});
|
|
|
|
test('should work with different validation profiles', async () => {
|
|
const workflow = {
|
|
name: 'Test Profile Compatibility',
|
|
nodes: [
|
|
{
|
|
id: 'switch',
|
|
name: 'Switch',
|
|
type: 'n8n-nodes-base.switch',
|
|
position: [0, 0] as [number, number],
|
|
parameters: {
|
|
rules: {
|
|
conditions: {
|
|
values: [{ value1: 'test', operation: 'equals', value2: 'test' }]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
],
|
|
connections: {}
|
|
};
|
|
|
|
const profiles: Array<'strict' | 'runtime' | 'ai-friendly' | 'minimal'> =
|
|
['strict', 'runtime', 'ai-friendly', 'minimal'];
|
|
|
|
for (const profile of profiles) {
|
|
const result = await validator.validateWorkflow(workflow, {
|
|
validateNodes: true,
|
|
profile
|
|
});
|
|
|
|
// All profiles should catch this critical error
|
|
const hasCriticalError = result.errors.some(e =>
|
|
e.message.includes('propertyValues[itemName] is not iterable')
|
|
);
|
|
|
|
expect(hasCriticalError, `Profile ${profile} should catch critical fixedCollection error`).toBe(true);
|
|
expect(result.valid, `Profile ${profile} should mark workflow as invalid`).toBe(false);
|
|
}
|
|
});
|
|
}); |