- Fixed MCP_MODE type assignment in console-manager.test.ts - Fixed prototype pollution test TypeScript errors in fixed-collection-validator.test.ts - All linting checks now pass 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
786 lines
27 KiB
TypeScript
786 lines
27 KiB
TypeScript
import { describe, test, expect } from 'vitest';
|
|
import { FixedCollectionValidator, NodeConfig, NodeConfigValue } from '../../../src/utils/fixed-collection-validator';
|
|
|
|
// Type guard helper for tests
|
|
function isNodeConfig(value: NodeConfig | NodeConfigValue[] | undefined): value is NodeConfig {
|
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
}
|
|
|
|
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(isNodeConfig(result.autofix)).toBe(true);
|
|
if (isNodeConfig(result.autofix)) {
|
|
expect(result.autofix.rules).toBeDefined();
|
|
expect((result.autofix.rules as any).values).toBeDefined();
|
|
expect((result.autofix.rules as any).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(isNodeConfig(result.autofix)).toBe(true);
|
|
if (isNodeConfig(result.autofix)) {
|
|
expect((result.autofix.rules as any).values).toHaveLength(2);
|
|
expect((result.autofix.rules as any).values[0].outputKey).toBe('output1');
|
|
expect((result.autofix.rules as any).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(isNodeConfig(result.autofix)).toBe(true);
|
|
if (isNodeConfig(result.autofix)) {
|
|
expect((result.autofix.fieldsToSummarize as any).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(isNodeConfig(result.autofix)).toBe(true);
|
|
if (isNodeConfig(result.autofix)) {
|
|
expect((result.autofix.mergeByFields as any).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(isNodeConfig(result.autofix)).toBe(true);
|
|
if (isNodeConfig(result.autofix)) {
|
|
expect((result.autofix.sortFieldsUi as any).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(isNodeConfig(result.autofix)).toBe(true);
|
|
if (isNodeConfig(result.autofix)) {
|
|
expect((result.autofix.fieldsToAggregate as any).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(isNodeConfig(result.autofix)).toBe(true);
|
|
if (isNodeConfig(result.autofix)) {
|
|
expect((result.autofix.fields as any).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(isNodeConfig(result.autofix)).toBe(true);
|
|
if (isNodeConfig(result.autofix)) {
|
|
expect((result.autofix.extractionValues as any).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(isNodeConfig(result.autofix)).toBe(true);
|
|
if (isNodeConfig(result.autofix)) {
|
|
expect((result.autofix.body as any).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(isNodeConfig(result.autofix)).toBe(true);
|
|
if (isNodeConfig(result.autofix)) {
|
|
expect((result.autofix.sort as any).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('Private Method Testing (through public API)', () => {
|
|
describe('isNodeConfig Type Guard', () => {
|
|
test('should return true for plain objects', () => {
|
|
const validConfig = { property: 'value' };
|
|
const result = FixedCollectionValidator.validate('switch', validConfig);
|
|
// Type guard is tested indirectly through validation
|
|
expect(result).toBeDefined();
|
|
});
|
|
|
|
test('should handle null values correctly', () => {
|
|
const result = FixedCollectionValidator.validate('switch', null as any);
|
|
expect(result.isValid).toBe(true);
|
|
expect(result.errors).toHaveLength(0);
|
|
});
|
|
|
|
test('should handle undefined values correctly', () => {
|
|
const result = FixedCollectionValidator.validate('switch', undefined as any);
|
|
expect(result.isValid).toBe(true);
|
|
expect(result.errors).toHaveLength(0);
|
|
});
|
|
|
|
test('should handle arrays correctly', () => {
|
|
const result = FixedCollectionValidator.validate('switch', [] as any);
|
|
expect(result.isValid).toBe(true);
|
|
expect(result.errors).toHaveLength(0);
|
|
});
|
|
|
|
test('should handle primitive values correctly', () => {
|
|
const result1 = FixedCollectionValidator.validate('switch', 'string' as any);
|
|
expect(result1.isValid).toBe(true);
|
|
|
|
const result2 = FixedCollectionValidator.validate('switch', 123 as any);
|
|
expect(result2.isValid).toBe(true);
|
|
|
|
const result3 = FixedCollectionValidator.validate('switch', true as any);
|
|
expect(result3.isValid).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('getNestedValue Testing', () => {
|
|
test('should handle simple nested paths', () => {
|
|
const config = {
|
|
rules: {
|
|
conditions: {
|
|
values: [{ test: 'value' }]
|
|
}
|
|
}
|
|
};
|
|
|
|
const result = FixedCollectionValidator.validate('switch', config);
|
|
expect(result.isValid).toBe(false); // This tests the nested value extraction
|
|
});
|
|
|
|
test('should handle non-existent paths gracefully', () => {
|
|
const config = {
|
|
rules: {
|
|
// missing conditions property
|
|
}
|
|
};
|
|
|
|
const result = FixedCollectionValidator.validate('switch', config);
|
|
expect(result.isValid).toBe(true); // Should not find invalid structure
|
|
});
|
|
|
|
test('should handle interrupted paths (null/undefined in middle)', () => {
|
|
const config = {
|
|
rules: null
|
|
};
|
|
|
|
const result = FixedCollectionValidator.validate('switch', config);
|
|
expect(result.isValid).toBe(true);
|
|
});
|
|
|
|
test('should handle array interruptions in path', () => {
|
|
const config = {
|
|
rules: [1, 2, 3] // array instead of object
|
|
};
|
|
|
|
const result = FixedCollectionValidator.validate('switch', config);
|
|
expect(result.isValid).toBe(true); // Should not find the pattern
|
|
});
|
|
});
|
|
|
|
describe('Circular Reference Protection', () => {
|
|
test('should handle circular references in config', () => {
|
|
const config: any = {
|
|
rules: {
|
|
conditions: {}
|
|
}
|
|
};
|
|
// Create circular reference
|
|
config.rules.conditions.circular = config.rules;
|
|
|
|
const result = FixedCollectionValidator.validate('switch', config);
|
|
// Should not crash and should detect the pattern (result is false because it finds rules.conditions)
|
|
expect(result.isValid).toBe(false);
|
|
expect(result.errors.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
test('should handle self-referencing objects', () => {
|
|
const config: any = {
|
|
rules: {}
|
|
};
|
|
config.rules.self = config.rules;
|
|
|
|
const result = FixedCollectionValidator.validate('switch', config);
|
|
expect(result.isValid).toBe(true);
|
|
});
|
|
|
|
test('should handle deeply nested circular references', () => {
|
|
const config: any = {
|
|
rules: {
|
|
conditions: {
|
|
values: {}
|
|
}
|
|
}
|
|
};
|
|
config.rules.conditions.values.back = config;
|
|
|
|
const result = FixedCollectionValidator.validate('switch', config);
|
|
// Should detect the problematic pattern: rules.conditions.values exists
|
|
expect(result.isValid).toBe(false);
|
|
expect(result.errors.length).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
|
|
describe('Deep Copying in getAllPatterns', () => {
|
|
test('should return independent copies of patterns', () => {
|
|
const patterns1 = FixedCollectionValidator.getAllPatterns();
|
|
const patterns2 = FixedCollectionValidator.getAllPatterns();
|
|
|
|
// Modify one copy
|
|
patterns1[0].invalidPatterns.push('test.pattern');
|
|
|
|
// Other copy should be unaffected
|
|
expect(patterns2[0].invalidPatterns).not.toContain('test.pattern');
|
|
});
|
|
|
|
test('should deep copy invalidPatterns arrays', () => {
|
|
const patterns = FixedCollectionValidator.getAllPatterns();
|
|
const switchPattern = patterns.find(p => p.nodeType === 'switch')!;
|
|
|
|
expect(switchPattern.invalidPatterns).toBeInstanceOf(Array);
|
|
expect(switchPattern.invalidPatterns.length).toBeGreaterThan(0);
|
|
|
|
// Ensure it's a different array instance
|
|
const originalPatterns = FixedCollectionValidator.getAllPatterns();
|
|
const originalSwitch = originalPatterns.find(p => p.nodeType === 'switch')!;
|
|
|
|
expect(switchPattern.invalidPatterns).not.toBe(originalSwitch.invalidPatterns);
|
|
expect(switchPattern.invalidPatterns).toEqual(originalSwitch.invalidPatterns);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Enhanced Edge Cases', () => {
|
|
test('should handle hasOwnProperty edge case', () => {
|
|
const config = Object.create(null);
|
|
config.rules = {
|
|
conditions: {
|
|
values: [{ test: 'value' }]
|
|
}
|
|
};
|
|
|
|
const result = FixedCollectionValidator.validate('switch', config);
|
|
expect(result.isValid).toBe(false); // Should still detect the pattern
|
|
});
|
|
|
|
test('should handle prototype pollution attempts', () => {
|
|
const config = {
|
|
rules: {
|
|
conditions: {
|
|
values: [{ test: 'value' }]
|
|
}
|
|
}
|
|
};
|
|
|
|
// Add prototype property (should be ignored by hasOwnProperty check)
|
|
(Object.prototype as any).maliciousProperty = 'evil';
|
|
|
|
try {
|
|
const result = FixedCollectionValidator.validate('switch', config);
|
|
expect(result.isValid).toBe(false);
|
|
expect(result.errors).toHaveLength(2);
|
|
} finally {
|
|
delete (Object.prototype as any).maliciousProperty;
|
|
}
|
|
});
|
|
|
|
test('should handle objects with numeric keys', () => {
|
|
const config = {
|
|
rules: {
|
|
'0': {
|
|
values: [{ test: 'value' }]
|
|
}
|
|
}
|
|
};
|
|
|
|
const result = FixedCollectionValidator.validate('switch', config);
|
|
expect(result.isValid).toBe(true); // Should not match 'conditions' pattern
|
|
});
|
|
|
|
test('should handle very deep nesting without crashing', () => {
|
|
let deepConfig: any = {};
|
|
let current = deepConfig;
|
|
|
|
// Create 100 levels deep
|
|
for (let i = 0; i < 100; i++) {
|
|
current.next = {};
|
|
current = current.next;
|
|
}
|
|
|
|
const result = FixedCollectionValidator.validate('switch', deepConfig);
|
|
expect(result.isValid).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('Alternative Node Type Formats', () => {
|
|
test('should handle all node type normalization cases', () => {
|
|
const testCases = [
|
|
'n8n-nodes-base.switch',
|
|
'nodes-base.switch',
|
|
'@n8n/n8n-nodes-langchain.switch',
|
|
'SWITCH',
|
|
'Switch',
|
|
'sWiTcH'
|
|
];
|
|
|
|
testCases.forEach(nodeType => {
|
|
expect(FixedCollectionValidator.isNodeSusceptible(nodeType)).toBe(true);
|
|
});
|
|
});
|
|
|
|
test('should handle empty and invalid node types', () => {
|
|
expect(FixedCollectionValidator.isNodeSusceptible('')).toBe(false);
|
|
expect(FixedCollectionValidator.isNodeSusceptible('unknown-node')).toBe(false);
|
|
expect(FixedCollectionValidator.isNodeSusceptible('n8n-nodes-base.unknown')).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('Complex Autofix Scenarios', () => {
|
|
test('should handle switch autofix with non-array values', () => {
|
|
const invalidConfig = {
|
|
rules: {
|
|
conditions: {
|
|
values: { single: 'condition' } // Object instead of array
|
|
}
|
|
}
|
|
};
|
|
|
|
const result = FixedCollectionValidator.validate('switch', invalidConfig);
|
|
expect(result.isValid).toBe(false);
|
|
expect(isNodeConfig(result.autofix)).toBe(true);
|
|
|
|
if (isNodeConfig(result.autofix)) {
|
|
const values = (result.autofix.rules as any).values;
|
|
expect(values).toHaveLength(1);
|
|
expect(values[0].conditions).toEqual({ single: 'condition' });
|
|
expect(values[0].outputKey).toBe('output1');
|
|
}
|
|
});
|
|
|
|
test('should handle if/filter autofix with object values', () => {
|
|
const invalidConfig = {
|
|
conditions: {
|
|
values: { type: 'single', condition: 'test' }
|
|
}
|
|
};
|
|
|
|
const result = FixedCollectionValidator.validate('if', invalidConfig);
|
|
expect(result.isValid).toBe(false);
|
|
expect(result.autofix).toEqual({ type: 'single', condition: 'test' });
|
|
});
|
|
|
|
test('should handle applyAutofix for if/filter with null values', () => {
|
|
const invalidConfig = {
|
|
conditions: {
|
|
values: null
|
|
}
|
|
};
|
|
|
|
const pattern = FixedCollectionValidator.getAllPatterns().find(p => p.nodeType === 'if')!;
|
|
const fixed = FixedCollectionValidator.applyAutofix(invalidConfig, pattern);
|
|
|
|
// Should return the original config when values is null
|
|
expect(fixed).toEqual(invalidConfig);
|
|
});
|
|
|
|
test('should handle applyAutofix for if/filter with undefined values', () => {
|
|
const invalidConfig = {
|
|
conditions: {
|
|
values: undefined
|
|
}
|
|
};
|
|
|
|
const pattern = FixedCollectionValidator.getAllPatterns().find(p => p.nodeType === 'if')!;
|
|
const fixed = FixedCollectionValidator.applyAutofix(invalidConfig, pattern);
|
|
|
|
// Should return the original config when values is undefined
|
|
expect(fixed).toEqual(invalidConfig);
|
|
});
|
|
});
|
|
|
|
describe('applyAutofix Method', () => {
|
|
test('should apply autofix correctly for if/filter nodes', () => {
|
|
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' }
|
|
]);
|
|
});
|
|
|
|
test('should return original config for non-if/filter nodes', () => {
|
|
const invalidConfig = {
|
|
fieldsToSummarize: {
|
|
values: {
|
|
values: [{ field: 'test' }]
|
|
}
|
|
}
|
|
};
|
|
|
|
const pattern = FixedCollectionValidator.getAllPatterns().find(p => p.nodeType === 'summarize');
|
|
const fixed = FixedCollectionValidator.applyAutofix(invalidConfig, pattern!);
|
|
|
|
expect(isNodeConfig(fixed)).toBe(true);
|
|
if (isNodeConfig(fixed)) {
|
|
expect((fixed.fieldsToSummarize as any).values).toEqual([{ field: 'test' }]);
|
|
}
|
|
});
|
|
|
|
test('should handle filter node applyAutofix edge cases', () => {
|
|
const invalidConfig = {
|
|
conditions: {
|
|
values: 'string-value' // Invalid type
|
|
}
|
|
};
|
|
|
|
const pattern = FixedCollectionValidator.getAllPatterns().find(p => p.nodeType === 'filter');
|
|
const fixed = FixedCollectionValidator.applyAutofix(invalidConfig, pattern!);
|
|
|
|
// Should return original config when values is not object/array
|
|
expect(fixed).toEqual(invalidConfig);
|
|
});
|
|
});
|
|
|
|
describe('Missing Function Coverage Tests', () => {
|
|
test('should test all generateFixMessage cases', () => {
|
|
// Test each node type's fix message generation through validation
|
|
const nodeConfigs = [
|
|
{ nodeType: 'switch', config: { rules: { conditions: { values: [] } } } },
|
|
{ nodeType: 'if', config: { conditions: { values: [] } } },
|
|
{ nodeType: 'filter', config: { conditions: { values: [] } } },
|
|
{ nodeType: 'summarize', config: { fieldsToSummarize: { values: { values: [] } } } },
|
|
{ nodeType: 'comparedatasets', config: { mergeByFields: { values: { values: [] } } } },
|
|
{ nodeType: 'sort', config: { sortFieldsUi: { sortField: { values: [] } } } },
|
|
{ nodeType: 'aggregate', config: { fieldsToAggregate: { fieldToAggregate: { values: [] } } } },
|
|
{ nodeType: 'set', config: { fields: { values: { values: [] } } } },
|
|
{ nodeType: 'html', config: { extractionValues: { values: { values: [] } } } },
|
|
{ nodeType: 'httprequest', config: { body: { parameters: { values: [] } } } },
|
|
{ nodeType: 'airtable', config: { sort: { sortField: { values: [] } } } },
|
|
];
|
|
|
|
nodeConfigs.forEach(({ nodeType, config }) => {
|
|
const result = FixedCollectionValidator.validate(nodeType, config);
|
|
expect(result.isValid).toBe(false);
|
|
expect(result.errors.length).toBeGreaterThan(0);
|
|
expect(result.errors[0].fix).toBeDefined();
|
|
expect(typeof result.errors[0].fix).toBe('string');
|
|
});
|
|
});
|
|
|
|
test('should test default case in generateFixMessage', () => {
|
|
// Create a custom pattern with unknown nodeType to test default case
|
|
const mockPattern = {
|
|
nodeType: 'unknown-node-type',
|
|
property: 'testProperty',
|
|
expectedStructure: 'test.structure',
|
|
invalidPatterns: ['test.invalid.pattern']
|
|
};
|
|
|
|
// We can't directly test the private generateFixMessage method,
|
|
// but we can test through the validation logic by temporarily adding to KNOWN_PATTERNS
|
|
// Instead, let's verify the method works by checking error messages contain the expected structure
|
|
const patterns = FixedCollectionValidator.getAllPatterns();
|
|
expect(patterns.length).toBeGreaterThan(0);
|
|
|
|
// Ensure we have patterns that would exercise different fix message paths
|
|
const switchPattern = patterns.find(p => p.nodeType === 'switch');
|
|
expect(switchPattern).toBeDefined();
|
|
expect(switchPattern!.expectedStructure).toBe('rules.values array');
|
|
});
|
|
|
|
test('should exercise hasInvalidStructure edge cases', () => {
|
|
// Test with property that exists but is not at the end of the pattern
|
|
const config = {
|
|
rules: {
|
|
conditions: 'string-value' // Not an object, so traversal should stop
|
|
}
|
|
};
|
|
|
|
const result = FixedCollectionValidator.validate('switch', config);
|
|
expect(result.isValid).toBe(false); // Should still detect rules.conditions pattern
|
|
});
|
|
|
|
test('should test getNestedValue with complex paths', () => {
|
|
// Test through hasInvalidStructure which uses getNestedValue
|
|
const config = {
|
|
deeply: {
|
|
nested: {
|
|
path: {
|
|
to: {
|
|
value: 'exists'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
// This would exercise the getNestedValue function through hasInvalidStructure
|
|
const result = FixedCollectionValidator.validate('switch', config);
|
|
expect(result.isValid).toBe(true); // No matching patterns
|
|
});
|
|
});
|
|
}); |