Skip to main content
Glama

n8n-MCP

by 88-888
fixed-collection-validator.test.ts27.3 kB
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 }); }); });

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/88-888/n8n-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server