Skip to main content
Glama

n8n-MCP

by 88-888
node-repository-core.test.tsβ€’12.5 kB
import { describe, it, expect, beforeEach, vi } from 'vitest'; import { NodeRepository } from '../../../src/database/node-repository'; import { DatabaseAdapter, PreparedStatement, RunResult } from '../../../src/database/database-adapter'; import { ParsedNode } from '../../../src/parsers/node-parser'; // Create a complete mock for DatabaseAdapter class MockDatabaseAdapter implements DatabaseAdapter { private statements = new Map<string, MockPreparedStatement>(); private mockData = new Map<string, any>(); prepare = vi.fn((sql: string) => { if (!this.statements.has(sql)) { this.statements.set(sql, new MockPreparedStatement(sql, this.mockData)); } return this.statements.get(sql)!; }); exec = vi.fn(); close = vi.fn(); pragma = vi.fn(); transaction = vi.fn((fn: () => any) => fn()); checkFTS5Support = vi.fn(() => true); inTransaction = false; // Test helper to set mock data _setMockData(key: string, value: any) { this.mockData.set(key, value); } // Test helper to get statement by SQL _getStatement(sql: string) { return this.statements.get(sql); } } class MockPreparedStatement implements PreparedStatement { run = vi.fn((...params: any[]): RunResult => ({ changes: 1, lastInsertRowid: 1 })); get = vi.fn(); all = vi.fn(() => []); iterate = vi.fn(); pluck = vi.fn(() => this); expand = vi.fn(() => this); raw = vi.fn(() => this); columns = vi.fn(() => []); bind = vi.fn(() => this); constructor(private sql: string, private mockData: Map<string, any>) { // Configure get() based on SQL pattern if (sql.includes('SELECT * FROM nodes WHERE node_type = ?')) { this.get = vi.fn((nodeType: string) => this.mockData.get(`node:${nodeType}`)); } // Configure all() for getAITools if (sql.includes('WHERE is_ai_tool = 1')) { this.all = vi.fn(() => this.mockData.get('ai_tools') || []); } } } describe('NodeRepository - Core Functionality', () => { let repository: NodeRepository; let mockAdapter: MockDatabaseAdapter; beforeEach(() => { mockAdapter = new MockDatabaseAdapter(); repository = new NodeRepository(mockAdapter); }); describe('saveNode', () => { it('should save a node with proper JSON serialization', () => { const parsedNode: ParsedNode = { nodeType: 'nodes-base.httpRequest', displayName: 'HTTP Request', description: 'Makes HTTP requests', category: 'transform', style: 'declarative', packageName: 'n8n-nodes-base', properties: [{ name: 'url', type: 'string' }], operations: [{ name: 'execute', displayName: 'Execute' }], credentials: [{ name: 'httpBasicAuth' }], isAITool: false, isTrigger: false, isWebhook: false, isVersioned: true, version: '1.0', documentation: 'HTTP Request documentation', outputs: undefined, outputNames: undefined }; repository.saveNode(parsedNode); // Verify prepare was called with correct SQL expect(mockAdapter.prepare).toHaveBeenCalledWith(expect.stringContaining('INSERT OR REPLACE INTO nodes')); // Get the prepared statement and verify run was called const stmt = mockAdapter._getStatement(mockAdapter.prepare.mock.lastCall?.[0] || ''); expect(stmt?.run).toHaveBeenCalledWith( 'nodes-base.httpRequest', 'n8n-nodes-base', 'HTTP Request', 'Makes HTTP requests', 'transform', 'declarative', 0, // isAITool 0, // isTrigger 0, // isWebhook 1, // isVersioned '1.0', 'HTTP Request documentation', JSON.stringify([{ name: 'url', type: 'string' }], null, 2), JSON.stringify([{ name: 'execute', displayName: 'Execute' }], null, 2), JSON.stringify([{ name: 'httpBasicAuth' }], null, 2), null, // outputs null // outputNames ); }); it('should handle nodes without optional fields', () => { const minimalNode: ParsedNode = { nodeType: 'nodes-base.simple', displayName: 'Simple Node', category: 'core', style: 'programmatic', packageName: 'n8n-nodes-base', properties: [], operations: [], credentials: [], isAITool: true, isTrigger: true, isWebhook: true, isVersioned: false, outputs: undefined, outputNames: undefined }; repository.saveNode(minimalNode); const stmt = mockAdapter._getStatement(mockAdapter.prepare.mock.lastCall?.[0] || ''); const runCall = stmt?.run.mock.lastCall; expect(runCall?.[2]).toBe('Simple Node'); // displayName expect(runCall?.[3]).toBeUndefined(); // description expect(runCall?.[10]).toBeUndefined(); // version expect(runCall?.[11]).toBeNull(); // documentation }); }); describe('getNode', () => { it('should retrieve and deserialize a node correctly', () => { const mockRow = { node_type: 'nodes-base.httpRequest', display_name: 'HTTP Request', description: 'Makes HTTP requests', category: 'transform', development_style: 'declarative', package_name: 'n8n-nodes-base', is_ai_tool: 0, is_trigger: 0, is_webhook: 0, is_versioned: 1, version: '1.0', properties_schema: JSON.stringify([{ name: 'url', type: 'string' }]), operations: JSON.stringify([{ name: 'execute' }]), credentials_required: JSON.stringify([{ name: 'httpBasicAuth' }]), documentation: 'HTTP docs', outputs: null, output_names: null }; mockAdapter._setMockData('node:nodes-base.httpRequest', mockRow); const result = repository.getNode('nodes-base.httpRequest'); expect(result).toEqual({ nodeType: 'nodes-base.httpRequest', displayName: 'HTTP Request', description: 'Makes HTTP requests', category: 'transform', developmentStyle: 'declarative', package: 'n8n-nodes-base', isAITool: false, isTrigger: false, isWebhook: false, isVersioned: true, version: '1.0', properties: [{ name: 'url', type: 'string' }], operations: [{ name: 'execute' }], credentials: [{ name: 'httpBasicAuth' }], hasDocumentation: true, outputs: null, outputNames: null }); }); it('should return null for non-existent nodes', () => { const result = repository.getNode('non-existent'); expect(result).toBeNull(); }); it('should handle invalid JSON gracefully', () => { const mockRow = { node_type: 'nodes-base.broken', display_name: 'Broken Node', description: 'Node with broken JSON', category: 'transform', development_style: 'declarative', package_name: 'n8n-nodes-base', is_ai_tool: 0, is_trigger: 0, is_webhook: 0, is_versioned: 0, version: null, properties_schema: '{invalid json', operations: 'not json at all', credentials_required: '{"valid": "json"}', documentation: null, outputs: null, output_names: null }; mockAdapter._setMockData('node:nodes-base.broken', mockRow); const result = repository.getNode('nodes-base.broken'); expect(result?.properties).toEqual([]); // defaultValue from safeJsonParse expect(result?.operations).toEqual([]); // defaultValue from safeJsonParse expect(result?.credentials).toEqual({ valid: 'json' }); // successfully parsed }); }); describe('getAITools', () => { it('should retrieve all AI tools sorted by display name', () => { const mockAITools = [ { node_type: 'nodes-base.openai', display_name: 'OpenAI', description: 'OpenAI integration', package_name: 'n8n-nodes-base' }, { node_type: 'nodes-base.agent', display_name: 'AI Agent', description: 'AI Agent node', package_name: '@n8n/n8n-nodes-langchain' } ]; mockAdapter._setMockData('ai_tools', mockAITools); const result = repository.getAITools(); expect(result).toEqual([ { nodeType: 'nodes-base.openai', displayName: 'OpenAI', description: 'OpenAI integration', package: 'n8n-nodes-base' }, { nodeType: 'nodes-base.agent', displayName: 'AI Agent', description: 'AI Agent node', package: '@n8n/n8n-nodes-langchain' } ]); }); it('should return empty array when no AI tools exist', () => { mockAdapter._setMockData('ai_tools', []); const result = repository.getAITools(); expect(result).toEqual([]); }); }); describe('safeJsonParse', () => { it('should parse valid JSON', () => { // Access private method through the class const parseMethod = (repository as any).safeJsonParse.bind(repository); const validJson = '{"key": "value", "number": 42}'; const result = parseMethod(validJson, {}); expect(result).toEqual({ key: 'value', number: 42 }); }); it('should return default value for invalid JSON', () => { const parseMethod = (repository as any).safeJsonParse.bind(repository); const invalidJson = '{invalid json}'; const defaultValue = { default: true }; const result = parseMethod(invalidJson, defaultValue); expect(result).toEqual(defaultValue); }); it('should handle empty strings', () => { const parseMethod = (repository as any).safeJsonParse.bind(repository); const result = parseMethod('', []); expect(result).toEqual([]); }); it('should handle null and undefined', () => { const parseMethod = (repository as any).safeJsonParse.bind(repository); // JSON.parse(null) returns null, not an error expect(parseMethod(null, 'default')).toBe(null); expect(parseMethod(undefined, 'default')).toBe('default'); }); }); describe('Edge Cases', () => { it('should handle very large JSON properties', () => { const largeProperties = Array(1000).fill(null).map((_, i) => ({ name: `prop${i}`, type: 'string', description: 'A'.repeat(100) })); const node: ParsedNode = { nodeType: 'nodes-base.large', displayName: 'Large Node', category: 'test', style: 'declarative', packageName: 'test', properties: largeProperties, operations: [], credentials: [], isAITool: false, isTrigger: false, isWebhook: false, isVersioned: false, outputs: undefined, outputNames: undefined }; repository.saveNode(node); const stmt = mockAdapter._getStatement(mockAdapter.prepare.mock.lastCall?.[0] || ''); const runCall = stmt?.run.mock.lastCall; const savedProperties = runCall?.[12]; expect(savedProperties).toBe(JSON.stringify(largeProperties, null, 2)); }); it('should handle boolean conversion for integer fields', () => { const mockRow = { node_type: 'nodes-base.bool-test', display_name: 'Bool Test', description: 'Testing boolean conversion', category: 'test', development_style: 'declarative', package_name: 'test', is_ai_tool: 1, is_trigger: 0, is_webhook: '1', // String that should be converted is_versioned: '0', // String that should be converted version: null, properties_schema: '[]', operations: '[]', credentials_required: '[]', documentation: null, outputs: null, output_names: null }; mockAdapter._setMockData('node:nodes-base.bool-test', mockRow); const result = repository.getNode('nodes-base.bool-test'); expect(result?.isAITool).toBe(true); expect(result?.isTrigger).toBe(false); expect(result?.isWebhook).toBe(true); expect(result?.isVersioned).toBe(false); }); }); });

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