workflow-fixed-collection-validation.test.tsโข12.7 kB
/**
* 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);
}
});
});