- Add FixedCollectionValidator utility to handle all fixedCollection patterns - Support validation for 12 different node types including Switch, If, Filter, Summarize, Compare Datasets, Sort, Aggregate, Set, HTML, HTTP Request, and Airtable - Refactor enhanced-config-validator to use the generic utility - Add comprehensive tests with 19 test cases covering all node types - Maintain backward compatibility with existing validation behavior This prevents the 'propertyValues[itemName] is not iterable' error across all susceptible n8n nodes, not just Switch/If/Filter.
336 lines
10 KiB
TypeScript
336 lines
10 KiB
TypeScript
import { describe, test, expect } from 'vitest';
|
|
import { FixedCollectionValidator } from '../../../src/utils/fixed-collection-validator';
|
|
|
|
describe('FixedCollectionValidator', () => {
|
|
describe('Core Functionality', () => {
|
|
test('should return valid for non-susceptible nodes', () => {
|
|
const result = FixedCollectionValidator.validate('n8n-nodes-base.cron', {
|
|
triggerTimes: { hour: 10, minute: 30 }
|
|
});
|
|
|
|
expect(result.isValid).toBe(true);
|
|
expect(result.errors).toHaveLength(0);
|
|
});
|
|
|
|
test('should normalize node types correctly', () => {
|
|
const nodeTypes = [
|
|
'n8n-nodes-base.switch',
|
|
'nodes-base.switch',
|
|
'@n8n/n8n-nodes-langchain.switch',
|
|
'SWITCH'
|
|
];
|
|
|
|
nodeTypes.forEach(nodeType => {
|
|
expect(FixedCollectionValidator.isNodeSusceptible(nodeType)).toBe(true);
|
|
});
|
|
});
|
|
|
|
test('should get all known patterns', () => {
|
|
const patterns = FixedCollectionValidator.getAllPatterns();
|
|
expect(patterns.length).toBeGreaterThan(10); // We have at least 11 patterns
|
|
expect(patterns.some(p => p.nodeType === 'switch')).toBe(true);
|
|
expect(patterns.some(p => p.nodeType === 'summarize')).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('Switch Node Validation', () => {
|
|
test('should detect invalid nested conditions structure', () => {
|
|
const invalidConfig = {
|
|
rules: {
|
|
conditions: {
|
|
values: [
|
|
{
|
|
value1: '={{$json.status}}',
|
|
operation: 'equals',
|
|
value2: 'active'
|
|
}
|
|
]
|
|
}
|
|
}
|
|
};
|
|
|
|
const result = FixedCollectionValidator.validate('n8n-nodes-base.switch', invalidConfig);
|
|
|
|
expect(result.isValid).toBe(false);
|
|
expect(result.errors).toHaveLength(2); // Both rules.conditions and rules.conditions.values match
|
|
// Check that we found the specific pattern
|
|
const conditionsValuesError = result.errors.find(e => e.pattern === 'rules.conditions.values');
|
|
expect(conditionsValuesError).toBeDefined();
|
|
expect(conditionsValuesError!.message).toContain('propertyValues[itemName] is not iterable');
|
|
expect(result.autofix).toBeDefined();
|
|
expect(result.autofix.rules.values).toBeDefined();
|
|
expect(result.autofix.rules.values[0].outputKey).toBe('output1');
|
|
});
|
|
|
|
test('should provide correct autofix for switch node', () => {
|
|
const invalidConfig = {
|
|
rules: {
|
|
conditions: {
|
|
values: [
|
|
{ value1: '={{$json.a}}', operation: 'equals', value2: '1' },
|
|
{ value1: '={{$json.b}}', operation: 'equals', value2: '2' }
|
|
]
|
|
}
|
|
}
|
|
};
|
|
|
|
const result = FixedCollectionValidator.validate('switch', invalidConfig);
|
|
|
|
expect(result.autofix.rules.values).toHaveLength(2);
|
|
expect(result.autofix.rules.values[0].outputKey).toBe('output1');
|
|
expect(result.autofix.rules.values[1].outputKey).toBe('output2');
|
|
});
|
|
});
|
|
|
|
describe('If/Filter Node Validation', () => {
|
|
test('should detect invalid nested values structure', () => {
|
|
const invalidConfig = {
|
|
conditions: {
|
|
values: [
|
|
{
|
|
value1: '={{$json.age}}',
|
|
operation: 'largerEqual',
|
|
value2: 18
|
|
}
|
|
]
|
|
}
|
|
};
|
|
|
|
const ifResult = FixedCollectionValidator.validate('n8n-nodes-base.if', invalidConfig);
|
|
const filterResult = FixedCollectionValidator.validate('n8n-nodes-base.filter', invalidConfig);
|
|
|
|
expect(ifResult.isValid).toBe(false);
|
|
expect(ifResult.errors[0].fix).toContain('directly, not nested under "values"');
|
|
expect(ifResult.autofix).toEqual([
|
|
{
|
|
value1: '={{$json.age}}',
|
|
operation: 'largerEqual',
|
|
value2: 18
|
|
}
|
|
]);
|
|
|
|
expect(filterResult.isValid).toBe(false);
|
|
expect(filterResult.autofix).toEqual(ifResult.autofix);
|
|
});
|
|
});
|
|
|
|
describe('New Nodes Validation', () => {
|
|
test('should validate Summarize node', () => {
|
|
const invalidConfig = {
|
|
fieldsToSummarize: {
|
|
values: {
|
|
values: [
|
|
{ field: 'amount', aggregation: 'sum' },
|
|
{ field: 'count', aggregation: 'count' }
|
|
]
|
|
}
|
|
}
|
|
};
|
|
|
|
const result = FixedCollectionValidator.validate('summarize', invalidConfig);
|
|
|
|
expect(result.isValid).toBe(false);
|
|
expect(result.errors[0].pattern).toBe('fieldsToSummarize.values.values');
|
|
expect(result.errors[0].fix).toContain('not nested values.values');
|
|
expect(result.autofix.fieldsToSummarize.values).toHaveLength(2);
|
|
});
|
|
|
|
test('should validate Compare Datasets node', () => {
|
|
const invalidConfig = {
|
|
mergeByFields: {
|
|
values: {
|
|
values: [
|
|
{ field1: 'id', field2: 'userId' }
|
|
]
|
|
}
|
|
}
|
|
};
|
|
|
|
const result = FixedCollectionValidator.validate('compareDatasets', invalidConfig);
|
|
|
|
expect(result.isValid).toBe(false);
|
|
expect(result.errors[0].pattern).toBe('mergeByFields.values.values');
|
|
expect(result.autofix.mergeByFields.values).toHaveLength(1);
|
|
});
|
|
|
|
test('should validate Sort node', () => {
|
|
const invalidConfig = {
|
|
sortFieldsUi: {
|
|
sortField: {
|
|
values: [
|
|
{ fieldName: 'date', order: 'descending' }
|
|
]
|
|
}
|
|
}
|
|
};
|
|
|
|
const result = FixedCollectionValidator.validate('sort', invalidConfig);
|
|
|
|
expect(result.isValid).toBe(false);
|
|
expect(result.errors[0].pattern).toBe('sortFieldsUi.sortField.values');
|
|
expect(result.errors[0].fix).toContain('not sortField.values');
|
|
expect(result.autofix.sortFieldsUi.sortField).toHaveLength(1);
|
|
});
|
|
|
|
test('should validate Aggregate node', () => {
|
|
const invalidConfig = {
|
|
fieldsToAggregate: {
|
|
fieldToAggregate: {
|
|
values: [
|
|
{ fieldToAggregate: 'price', aggregation: 'average' }
|
|
]
|
|
}
|
|
}
|
|
};
|
|
|
|
const result = FixedCollectionValidator.validate('aggregate', invalidConfig);
|
|
|
|
expect(result.isValid).toBe(false);
|
|
expect(result.errors[0].pattern).toBe('fieldsToAggregate.fieldToAggregate.values');
|
|
expect(result.autofix.fieldsToAggregate.fieldToAggregate).toHaveLength(1);
|
|
});
|
|
|
|
test('should validate Set node', () => {
|
|
const invalidConfig = {
|
|
fields: {
|
|
values: {
|
|
values: [
|
|
{ name: 'status', value: 'active' }
|
|
]
|
|
}
|
|
}
|
|
};
|
|
|
|
const result = FixedCollectionValidator.validate('set', invalidConfig);
|
|
|
|
expect(result.isValid).toBe(false);
|
|
expect(result.errors[0].pattern).toBe('fields.values.values');
|
|
expect(result.autofix.fields.values).toHaveLength(1);
|
|
});
|
|
|
|
test('should validate HTML node', () => {
|
|
const invalidConfig = {
|
|
extractionValues: {
|
|
values: {
|
|
values: [
|
|
{ key: 'title', cssSelector: 'h1' }
|
|
]
|
|
}
|
|
}
|
|
};
|
|
|
|
const result = FixedCollectionValidator.validate('html', invalidConfig);
|
|
|
|
expect(result.isValid).toBe(false);
|
|
expect(result.errors[0].pattern).toBe('extractionValues.values.values');
|
|
expect(result.autofix.extractionValues.values).toHaveLength(1);
|
|
});
|
|
|
|
test('should validate HTTP Request node', () => {
|
|
const invalidConfig = {
|
|
body: {
|
|
parameters: {
|
|
values: [
|
|
{ name: 'api_key', value: '123' }
|
|
]
|
|
}
|
|
}
|
|
};
|
|
|
|
const result = FixedCollectionValidator.validate('httpRequest', invalidConfig);
|
|
|
|
expect(result.isValid).toBe(false);
|
|
expect(result.errors[0].pattern).toBe('body.parameters.values');
|
|
expect(result.errors[0].fix).toContain('not parameters.values');
|
|
expect(result.autofix.body.parameters).toHaveLength(1);
|
|
});
|
|
|
|
test('should validate Airtable node', () => {
|
|
const invalidConfig = {
|
|
sort: {
|
|
sortField: {
|
|
values: [
|
|
{ fieldName: 'Created', direction: 'desc' }
|
|
]
|
|
}
|
|
}
|
|
};
|
|
|
|
const result = FixedCollectionValidator.validate('airtable', invalidConfig);
|
|
|
|
expect(result.isValid).toBe(false);
|
|
expect(result.errors[0].pattern).toBe('sort.sortField.values');
|
|
expect(result.autofix.sort.sortField).toHaveLength(1);
|
|
});
|
|
});
|
|
|
|
describe('Edge Cases', () => {
|
|
test('should handle empty config', () => {
|
|
const result = FixedCollectionValidator.validate('switch', {});
|
|
expect(result.isValid).toBe(true);
|
|
});
|
|
|
|
test('should handle null/undefined properties', () => {
|
|
const result = FixedCollectionValidator.validate('switch', {
|
|
rules: null
|
|
});
|
|
expect(result.isValid).toBe(true);
|
|
});
|
|
|
|
test('should handle valid structures', () => {
|
|
const validSwitch = {
|
|
rules: {
|
|
values: [
|
|
{
|
|
conditions: { value1: '={{$json.x}}', operation: 'equals', value2: 1 },
|
|
outputKey: 'output1'
|
|
}
|
|
]
|
|
}
|
|
};
|
|
|
|
const result = FixedCollectionValidator.validate('switch', validSwitch);
|
|
expect(result.isValid).toBe(true);
|
|
expect(result.errors).toHaveLength(0);
|
|
});
|
|
|
|
test('should handle deeply nested invalid structures', () => {
|
|
const deeplyNested = {
|
|
rules: {
|
|
conditions: {
|
|
values: [
|
|
{
|
|
value1: '={{$json.deep}}',
|
|
operation: 'equals',
|
|
value2: 'nested'
|
|
}
|
|
]
|
|
}
|
|
}
|
|
};
|
|
|
|
const result = FixedCollectionValidator.validate('switch', deeplyNested);
|
|
expect(result.isValid).toBe(false);
|
|
expect(result.errors).toHaveLength(2); // Both patterns match
|
|
});
|
|
});
|
|
|
|
describe('applyAutofix Method', () => {
|
|
test('should apply autofix correctly', () => {
|
|
const invalidConfig = {
|
|
conditions: {
|
|
values: [
|
|
{ value1: '={{$json.test}}', operation: 'equals', value2: 'yes' }
|
|
]
|
|
}
|
|
};
|
|
|
|
const pattern = FixedCollectionValidator.getAllPatterns().find(p => p.nodeType === 'if');
|
|
const fixed = FixedCollectionValidator.applyAutofix(invalidConfig, pattern!);
|
|
|
|
expect(fixed).toEqual([
|
|
{ value1: '={{$json.test}}', operation: 'equals', value2: 'yes' }
|
|
]);
|
|
});
|
|
});
|
|
}); |