Skip to main content
Glama

n8n-MCP

by 88-888
workflow-validator-edge-cases.test.ts20.8 kB
import { describe, it, expect, vi, beforeEach } from 'vitest'; import { WorkflowValidator } from '@/services/workflow-validator'; import { NodeRepository } from '@/database/node-repository'; import { EnhancedConfigValidator } from '@/services/enhanced-config-validator'; import type { WorkflowValidationResult } from '@/services/workflow-validator'; // NOTE: Mocking EnhancedConfigValidator is challenging because: // 1. WorkflowValidator expects the class itself, not an instance // 2. The class has static methods that are called directly // 3. vi.mock() hoisting makes it difficult to mock properly // // For properly mocked tests, see workflow-validator-with-mocks.test.ts // These tests use a partially mocked approach that may still access the database // Mock dependencies vi.mock('@/database/node-repository'); vi.mock('@/services/expression-validator'); vi.mock('@/utils/logger'); // Mock EnhancedConfigValidator with static methods vi.mock('@/services/enhanced-config-validator', () => ({ EnhancedConfigValidator: { validate: vi.fn().mockReturnValue({ valid: true, errors: [], warnings: [], suggestions: [], visibleProperties: [], hiddenProperties: [] }), validateWithMode: vi.fn().mockReturnValue({ valid: true, errors: [], warnings: [], fixedConfig: null }) } })); describe('WorkflowValidator - Edge Cases', () => { let validator: WorkflowValidator; let mockNodeRepository: any; let mockEnhancedConfigValidator: any; beforeEach(() => { vi.clearAllMocks(); // Create mock repository that returns node info for test nodes and common n8n nodes mockNodeRepository = { getNode: vi.fn().mockImplementation((type: string) => { if (type === 'test.node' || type === 'test.agent' || type === 'test.tool') { return { name: 'Test Node', type: type, typeVersion: 1, properties: [], package: 'test-package', version: 1, displayName: 'Test Node', isVersioned: false }; } // Handle common n8n node types if (type.startsWith('n8n-nodes-base.') || type.startsWith('nodes-base.')) { const nodeName = type.split('.')[1]; return { name: nodeName, type: type, typeVersion: 1, properties: [], package: 'n8n-nodes-base', version: 1, displayName: nodeName.charAt(0).toUpperCase() + nodeName.slice(1), isVersioned: ['set', 'httpRequest'].includes(nodeName) }; } return null; }), findByType: vi.fn().mockReturnValue({ name: 'Test Node', type: 'test.node', typeVersion: 1, properties: [] }), searchNodes: vi.fn().mockReturnValue([]) }; // Ensure EnhancedConfigValidator.validate always returns a valid result vi.mocked(EnhancedConfigValidator.validate).mockReturnValue({ valid: true, errors: [], warnings: [], suggestions: [], visibleProperties: [], hiddenProperties: [] }); // Create validator instance with mocked dependencies validator = new WorkflowValidator(mockNodeRepository, EnhancedConfigValidator); }); describe('Null and Undefined Handling', () => { it('should handle null workflow gracefully', async () => { const result = await validator.validateWorkflow(null as any); expect(result.valid).toBe(false); expect(result.errors.some(e => e.message.includes('Invalid workflow structure'))).toBe(true); }); it('should handle undefined workflow gracefully', async () => { const result = await validator.validateWorkflow(undefined as any); expect(result.valid).toBe(false); expect(result.errors.some(e => e.message.includes('Invalid workflow structure'))).toBe(true); }); it('should handle workflow with null nodes array', async () => { const workflow = { nodes: null, connections: {} }; const result = await validator.validateWorkflow(workflow as any); expect(result.valid).toBe(false); expect(result.errors.some(e => e.message.includes('nodes must be an array'))).toBe(true); }); it('should handle workflow with null connections', async () => { const workflow = { nodes: [], connections: null }; const result = await validator.validateWorkflow(workflow as any); expect(result.valid).toBe(false); expect(result.errors.some(e => e.message.includes('connections must be an object'))).toBe(true); }); it('should handle nodes with null/undefined properties', async () => { const workflow = { nodes: [ { id: '1', name: null, type: 'test.node', position: [0, 0], parameters: undefined } ], connections: {} }; const result = await validator.validateWorkflow(workflow as any); expect(result.valid).toBe(false); expect(result.errors.length).toBeGreaterThan(0); }); }); describe('Boundary Value Testing', () => { it('should handle empty workflow', async () => { const workflow = { nodes: [], connections: {} }; const result = await validator.validateWorkflow(workflow as any); expect(result.valid).toBe(true); expect(result.warnings.some(w => w.message.includes('empty'))).toBe(true); }); it('should handle very large workflows', async () => { const nodes = Array(1000).fill(null).map((_, i) => ({ id: `node${i}`, name: `Node ${i}`, type: 'test.node', position: [i * 100, 0] as [number, number], parameters: {} })); const connections: any = {}; for (let i = 0; i < 999; i++) { connections[`Node ${i}`] = { main: [[{ node: `Node ${i + 1}`, type: 'main', index: 0 }]] }; } const workflow = { nodes, connections }; const start = Date.now(); const result = await validator.validateWorkflow(workflow as any); const duration = Date.now() - start; expect(result).toBeDefined(); // Use longer timeout for CI environments const isCI = process.env.CI === 'true' || process.env.GITHUB_ACTIONS === 'true'; const timeout = isCI ? 10000 : 5000; // 10 seconds for CI, 5 seconds for local expect(duration).toBeLessThan(timeout); }); it('should handle deeply nested connections', async () => { const workflow = { nodes: [ { id: '1', name: 'Start', type: 'test.node', position: [0, 0] as [number, number], parameters: {} }, { id: '2', name: 'Middle', type: 'test.node', position: [100, 0] as [number, number], parameters: {} }, { id: '3', name: 'End', type: 'test.node', position: [200, 0] as [number, number], parameters: {} } ], connections: { 'Start': { main: [[{ node: 'Middle', type: 'main', index: 0 }]], error: [[{ node: 'End', type: 'main', index: 0 }]], ai_tool: [[{ node: 'Middle', type: 'ai_tool', index: 0 }]] } } }; const result = await validator.validateWorkflow(workflow as any); expect(result.statistics.invalidConnections).toBe(0); }); it.skip('should handle nodes at extreme positions - FIXME: mock issues', async () => { const workflow = { nodes: [ { id: '1', name: 'FarLeft', type: 'n8n-nodes-base.set', position: [-999999, -999999] as [number, number], parameters: {} }, { id: '2', name: 'FarRight', type: 'n8n-nodes-base.set', position: [999999, 999999] as [number, number], parameters: {} }, { id: '3', name: 'Zero', type: 'n8n-nodes-base.set', position: [0, 0] as [number, number], parameters: {} } ], connections: { 'FarLeft': { main: [[{ node: 'FarRight', type: 'main', index: 0 }]] }, 'FarRight': { main: [[{ node: 'Zero', type: 'main', index: 0 }]] } } }; const result = await validator.validateWorkflow(workflow as any); expect(result.valid).toBe(true); }); }); describe('Invalid Data Type Handling', () => { it('should handle non-array nodes', async () => { const workflow = { nodes: 'not-an-array', connections: {} }; const result = await validator.validateWorkflow(workflow as any); expect(result.valid).toBe(false); expect(result.errors[0].message).toContain('nodes must be an array'); }); it('should handle non-object connections', async () => { const workflow = { nodes: [], connections: [] }; const result = await validator.validateWorkflow(workflow as any); expect(result.valid).toBe(false); expect(result.errors[0].message).toContain('connections must be an object'); }); it('should handle invalid position values', async () => { const workflow = { nodes: [ { id: '1', name: 'InvalidPos', type: 'test.node', position: 'invalid' as any, parameters: {} }, { id: '2', name: 'NaNPos', type: 'test.node', position: [NaN, NaN] as [number, number], parameters: {} }, { id: '3', name: 'InfinityPos', type: 'test.node', position: [Infinity, -Infinity] as [number, number], parameters: {} } ], connections: {} }; const result = await validator.validateWorkflow(workflow as any); expect(result.errors.length).toBeGreaterThan(0); }); it('should handle circular references in workflow object', async () => { const workflow: any = { nodes: [], connections: {} }; workflow.circular = workflow; await expect(validator.validateWorkflow(workflow)).resolves.toBeDefined(); }); }); describe('Connection Validation Edge Cases', () => { it('should detect self-referencing nodes', async () => { const workflow = { nodes: [ { id: '1', name: 'SelfLoop', type: 'test.node', position: [0, 0] as [number, number], parameters: {} } ], connections: { 'SelfLoop': { main: [[{ node: 'SelfLoop', type: 'main', index: 0 }]] } } }; const result = await validator.validateWorkflow(workflow as any); expect(result.warnings.some(w => w.message.includes('self-referencing'))).toBe(true); }); it('should handle non-existent node references', async () => { const workflow = { nodes: [ { id: '1', name: 'Node1', type: 'test.node', position: [0, 0] as [number, number], parameters: {} } ], connections: { 'Node1': { main: [[{ node: 'NonExistent', type: 'main', index: 0 }]] } } }; const result = await validator.validateWorkflow(workflow as any); expect(result.errors.some(e => e.message.includes('non-existent'))).toBe(true); }); it('should handle invalid connection formats', async () => { const workflow = { nodes: [ { id: '1', name: 'Node1', type: 'test.node', position: [0, 0] as [number, number], parameters: {} } ], connections: { 'Node1': { main: 'invalid-format' as any } } }; const result = await validator.validateWorkflow(workflow as any); expect(result.errors.length).toBeGreaterThan(0); }); it('should handle missing connection properties', async () => { const workflow = { nodes: [ { id: '1', name: 'Node1', type: 'test.node', position: [0, 0] as [number, number], parameters: {} }, { id: '2', name: 'Node2', type: 'test.node', position: [100, 0] as [number, number], parameters: {} } ], connections: { 'Node1': { main: [[{ node: 'Node2' }]] // Missing type and index } } as any }; const result = await validator.validateWorkflow(workflow as any); // Should still work as type and index can have defaults expect(result.statistics.validConnections).toBeGreaterThan(0); }); it('should handle negative output indices', async () => { const workflow = { nodes: [ { id: '1', name: 'Node1', type: 'test.node', position: [0, 0] as [number, number], parameters: {} }, { id: '2', name: 'Node2', type: 'test.node', position: [100, 0] as [number, number], parameters: {} } ], connections: { 'Node1': { main: [[{ node: 'Node2', type: 'main', index: -1 }]] } } }; const result = await validator.validateWorkflow(workflow as any); expect(result.errors.some(e => e.message.includes('Invalid'))).toBe(true); }); }); describe('Special Characters and Unicode', () => { // Note: These tests are skipped because WorkflowValidator also needs special character // normalization (similar to WorkflowDiffEngine fix in #270). Will be addressed in a future PR. it.skip('should handle apostrophes in node names - TODO: needs WorkflowValidator normalization', async () => { // Test default n8n Manual Trigger node name with apostrophes const workflow = { nodes: [ { id: '1', name: "When clicking 'Execute workflow'", type: 'n8n-nodes-base.manualTrigger', position: [0, 0] as [number, number], parameters: {} }, { id: '2', name: 'HTTP Request', type: 'n8n-nodes-base.httpRequest', position: [100, 0] as [number, number], parameters: {} } ], connections: { "When clicking 'Execute workflow'": { main: [[{ node: 'HTTP Request', type: 'main', index: 0 }]] } } }; const result = await validator.validateWorkflow(workflow as any); expect(result.valid).toBe(true); expect(result.errors).toHaveLength(0); }); it.skip('should handle special characters in node names - TODO: needs WorkflowValidator normalization', async () => { const workflow = { nodes: [ { id: '1', name: 'Node@#$%', type: 'n8n-nodes-base.set', position: [0, 0] as [number, number], parameters: {} }, { id: '2', name: 'Node 中文', type: 'n8n-nodes-base.set', position: [100, 0] as [number, number], parameters: {} }, { id: '3', name: 'Node😊', type: 'n8n-nodes-base.set', position: [200, 0] as [number, number], parameters: {} } ], connections: { 'Node@#$%': { main: [[{ node: 'Node 中文', type: 'main', index: 0 }]] }, 'Node 中文': { main: [[{ node: 'Node😊', type: 'main', index: 0 }]] } } }; const result = await validator.validateWorkflow(workflow as any); expect(result.valid).toBe(true); expect(result.errors).toHaveLength(0); }); it('should handle very long node names', async () => { const longName = 'A'.repeat(1000); const workflow = { nodes: [ { id: '1', name: longName, type: 'test.node', position: [0, 0] as [number, number], parameters: {} } ], connections: {} }; const result = await validator.validateWorkflow(workflow as any); expect(result.warnings.some(w => w.message.includes('very long'))).toBe(true); }); }); describe('Batch Validation', () => { it.skip('should handle batch validation with mixed valid/invalid workflows - FIXME: mock issues', async () => { const workflows = [ { nodes: [ { id: '1', name: 'Node1', type: 'n8n-nodes-base.set', position: [0, 0] as [number, number], parameters: {} }, { id: '2', name: 'Node2', type: 'n8n-nodes-base.set', position: [100, 0] as [number, number], parameters: {} } ], connections: { 'Node1': { main: [[{ node: 'Node2', type: 'main', index: 0 }]] } } }, null as any, { nodes: 'invalid' as any, connections: {} } ]; const promises = workflows.map(w => validator.validateWorkflow(w)); const results = await Promise.all(promises); expect(results[0].valid).toBe(true); expect(results[1].valid).toBe(false); expect(results[2].valid).toBe(false); }); it.skip('should handle concurrent validation requests - FIXME: mock issues', async () => { const workflow = { nodes: [{ id: '1', name: 'Test', type: 'n8n-nodes-base.webhook', position: [0, 0] as [number, number], parameters: {} }], connections: {} }; const promises = Array(10).fill(null).map(() => validator.validateWorkflow(workflow)); const results = await Promise.all(promises); expect(results.every(r => r.valid)).toBe(true); }); }); describe('Expression Validation Edge Cases', () => { it('should skip expression validation when option is false', async () => { const workflow = { nodes: [{ id: '1', name: 'Node1', type: 'test.node', position: [0, 0] as [number, number], parameters: { value: '{{ $json.invalid.expression }}' } }], connections: {} }; const result = await validator.validateWorkflow(workflow, { validateExpressions: false }); expect(result.statistics.expressionsValidated).toBe(0); }); }); describe('Connection Type Validation', () => { it('should validate different connection types', async () => { const workflow = { nodes: [ { id: '1', name: 'Agent', type: 'test.agent', position: [0, 0] as [number, number], parameters: {} }, { id: '2', name: 'Tool', type: 'test.tool', position: [100, 0] as [number, number], parameters: {} } ], connections: { 'Tool': { ai_tool: [[{ node: 'Agent', type: 'ai_tool', index: 0 }]] } } }; const result = await validator.validateWorkflow(workflow as any); expect(result.statistics.validConnections).toBeGreaterThan(0); }); }); describe('Error Recovery', () => { it('should continue validation after encountering errors', async () => { const workflow = { nodes: [ { id: '1', name: null as any, type: 'test.node', position: [0, 0] as [number, number], parameters: {} }, { id: '2', name: 'Valid', type: 'test.node', position: [100, 0] as [number, number], parameters: {} }, { id: '3', name: 'AlsoValid', type: 'test.node', position: [200, 0] as [number, number], parameters: {} } ], connections: { 'Valid': { main: [[{ node: 'AlsoValid', type: 'main', index: 0 }]] } } }; const result = await validator.validateWorkflow(workflow as any); expect(result.errors.length).toBeGreaterThan(0); expect(result.statistics.validConnections).toBeGreaterThan(0); }); }); describe('Static Method Alternatives', () => { it('should validate workflow connections only', async () => { const workflow = { nodes: [ { id: '1', name: 'Node1', type: 'test.node', position: [0, 0] as [number, number], parameters: {} }, { id: '2', name: 'Node2', type: 'test.node', position: [100, 0] as [number, number], parameters: {} } ], connections: { 'Node1': { main: [[{ node: 'Node2', type: 'main', index: 0 }]] } } }; const result = await validator.validateWorkflow(workflow, { validateNodes: false, validateExpressions: false, validateConnections: true }); expect(result.statistics.validConnections).toBe(1); }); it('should validate workflow expressions only', async () => { const workflow = { nodes: [{ id: '1', name: 'Node1', type: 'test.node', position: [0, 0] as [number, number], parameters: { value: '{{ $json.data }}' } }], connections: {} }; const result = await validator.validateWorkflow(workflow, { validateNodes: false, validateExpressions: true, validateConnections: false }); expect(result.statistics.expressionsValidated).toBeGreaterThan(0); }); }); });

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