Skip to main content
Glama

n8n-MCP

by 88-888
metadata-generator.test.ts15.2 kB
import { describe, it, expect, vi, beforeEach } from 'vitest'; import { MetadataGenerator, TemplateMetadataSchema, MetadataRequest } from '../../../src/templates/metadata-generator'; // Mock OpenAI vi.mock('openai', () => { return { default: vi.fn().mockImplementation(() => ({ chat: { completions: { create: vi.fn() } } })) }; }); describe('MetadataGenerator', () => { let generator: MetadataGenerator; beforeEach(() => { generator = new MetadataGenerator('test-api-key', 'gpt-5-mini-2025-08-07'); }); describe('createBatchRequest', () => { it('should create a valid batch request', () => { const template: MetadataRequest = { templateId: 123, name: 'Test Workflow', description: 'A test workflow', nodes: ['n8n-nodes-base.webhook', 'n8n-nodes-base.httpRequest', 'n8n-nodes-base.slack'] }; const request = generator.createBatchRequest(template); expect(request.custom_id).toBe('template-123'); expect(request.method).toBe('POST'); expect(request.url).toBe('/v1/chat/completions'); expect(request.body.model).toBe('gpt-5-mini-2025-08-07'); expect(request.body.response_format.type).toBe('json_schema'); expect(request.body.response_format.json_schema.strict).toBe(true); expect(request.body.messages).toHaveLength(2); }); it('should summarize nodes effectively', () => { const template: MetadataRequest = { templateId: 456, name: 'Complex Workflow', nodes: [ 'n8n-nodes-base.webhook', 'n8n-nodes-base.httpRequest', 'n8n-nodes-base.httpRequest', 'n8n-nodes-base.postgres', 'n8n-nodes-base.slack', '@n8n/n8n-nodes-langchain.agent' ] }; const request = generator.createBatchRequest(template); const userMessage = request.body.messages[1].content; expect(userMessage).toContain('Complex Workflow'); expect(userMessage).toContain('Nodes Used (6)'); expect(userMessage).toContain('HTTP/Webhooks'); }); }); describe('parseResult', () => { it('should parse a successful result', () => { const mockResult = { custom_id: 'template-789', response: { body: { choices: [{ message: { content: JSON.stringify({ categories: ['automation', 'integration'], complexity: 'medium', use_cases: ['API integration', 'Data sync'], estimated_setup_minutes: 30, required_services: ['Slack API'], key_features: ['Webhook triggers', 'API calls'], target_audience: ['developers'] }) }, finish_reason: 'stop' }] } } }; const result = generator.parseResult(mockResult); expect(result.templateId).toBe(789); expect(result.metadata.categories).toEqual(['automation', 'integration']); expect(result.metadata.complexity).toBe('medium'); expect(result.error).toBeUndefined(); }); it('should handle error results', () => { const mockResult = { custom_id: 'template-999', error: { message: 'API error' } }; const result = generator.parseResult(mockResult); expect(result.templateId).toBe(999); expect(result.error).toBe('API error'); expect(result.metadata).toBeDefined(); expect(result.metadata.complexity).toBe('medium'); // Default metadata }); it('should handle malformed responses', () => { const mockResult = { custom_id: 'template-111', response: { body: { choices: [{ message: { content: 'not valid json' }, finish_reason: 'stop' }] } } }; const result = generator.parseResult(mockResult); expect(result.templateId).toBe(111); expect(result.error).toContain('Unexpected token'); expect(result.metadata).toBeDefined(); }); }); describe('TemplateMetadataSchema', () => { it('should validate correct metadata', () => { const validMetadata = { categories: ['automation', 'integration'], complexity: 'simple' as const, use_cases: ['API calls', 'Data processing'], estimated_setup_minutes: 15, required_services: [], key_features: ['Fast processing'], target_audience: ['developers'] }; const result = TemplateMetadataSchema.safeParse(validMetadata); expect(result.success).toBe(true); }); it('should reject invalid complexity', () => { const invalidMetadata = { categories: ['automation'], complexity: 'very-hard', // Invalid use_cases: ['API calls'], estimated_setup_minutes: 15, required_services: [], key_features: ['Fast'], target_audience: ['developers'] }; const result = TemplateMetadataSchema.safeParse(invalidMetadata); expect(result.success).toBe(false); }); it('should enforce array limits', () => { const tooManyCategories = { categories: ['a', 'b', 'c', 'd', 'e', 'f'], // Max 5 complexity: 'simple' as const, use_cases: ['API calls'], estimated_setup_minutes: 15, required_services: [], key_features: ['Fast'], target_audience: ['developers'] }; const result = TemplateMetadataSchema.safeParse(tooManyCategories); expect(result.success).toBe(false); }); it('should enforce time limits', () => { const tooLongSetup = { categories: ['automation'], complexity: 'complex' as const, use_cases: ['API calls'], estimated_setup_minutes: 500, // Max 480 required_services: [], key_features: ['Fast'], target_audience: ['developers'] }; const result = TemplateMetadataSchema.safeParse(tooLongSetup); expect(result.success).toBe(false); }); }); describe('Input Sanitization and Security', () => { it('should handle malicious template names safely', () => { const maliciousTemplate: MetadataRequest = { templateId: 123, name: '<script>alert("xss")</script>', description: 'javascript:alert(1)', nodes: ['n8n-nodes-base.webhook'] }; const request = generator.createBatchRequest(maliciousTemplate); const userMessage = request.body.messages[1].content; // Should contain the malicious content as-is (OpenAI will handle it) // but should not cause any injection in our code expect(userMessage).toContain('<script>alert("xss")</script>'); expect(userMessage).toContain('javascript:alert(1)'); expect(request.body.model).toBe('gpt-5-mini-2025-08-07'); }); it('should handle extremely long template names', () => { const longName = 'A'.repeat(10000); // Very long name const template: MetadataRequest = { templateId: 456, name: longName, nodes: ['n8n-nodes-base.webhook'] }; const request = generator.createBatchRequest(template); expect(request.custom_id).toBe('template-456'); expect(request.body.messages[1].content).toContain(longName); }); it('should handle special characters in node names', () => { const template: MetadataRequest = { templateId: 789, name: 'Test Workflow', nodes: [ 'n8n-nodes-base.webhook', '@n8n/custom-node.with.dots', 'custom-package/node-with-slashes', 'node_with_underscore', 'node-with-unicode-名前' ] }; const request = generator.createBatchRequest(template); const userMessage = request.body.messages[1].content; expect(userMessage).toContain('HTTP/Webhooks'); expect(userMessage).toContain('custom-node.with.dots'); }); it('should handle empty or undefined descriptions safely', () => { const template: MetadataRequest = { templateId: 100, name: 'Test', description: undefined, nodes: ['n8n-nodes-base.webhook'] }; const request = generator.createBatchRequest(template); const userMessage = request.body.messages[1].content; // Should not include undefined or null in the message expect(userMessage).not.toContain('undefined'); expect(userMessage).not.toContain('null'); expect(userMessage).toContain('Test'); }); it('should limit context size for very large workflows', () => { const manyNodes = Array.from({ length: 1000 }, (_, i) => `n8n-nodes-base.node${i}`); const template: MetadataRequest = { templateId: 200, name: 'Huge Workflow', nodes: manyNodes, workflow: { nodes: Array.from({ length: 500 }, (_, i) => ({ id: `node${i}` })), connections: {} } }; const request = generator.createBatchRequest(template); const userMessage = request.body.messages[1].content; // Should handle large amounts of data gracefully expect(userMessage.length).toBeLessThan(50000); // Reasonable limit expect(userMessage).toContain('Huge Workflow'); }); }); describe('Error Handling and Edge Cases', () => { it('should handle malformed OpenAI responses', () => { const malformedResults = [ { custom_id: 'template-111', response: { body: { choices: [{ message: { content: '{"invalid": json syntax}' }, finish_reason: 'stop' }] } } }, { custom_id: 'template-222', response: { body: { choices: [{ message: { content: null }, finish_reason: 'stop' }] } } }, { custom_id: 'template-333', response: { body: { choices: [] } } } ]; malformedResults.forEach(result => { const parsed = generator.parseResult(result); expect(parsed.error).toBeDefined(); expect(parsed.metadata).toBeDefined(); expect(parsed.metadata.complexity).toBe('medium'); // Default metadata }); }); it('should handle Zod validation failures', () => { const invalidResponse = { custom_id: 'template-444', response: { body: { choices: [{ message: { content: JSON.stringify({ categories: ['too', 'many', 'categories', 'here', 'way', 'too', 'many'], complexity: 'invalid-complexity', use_cases: [], estimated_setup_minutes: -5, // Invalid negative time required_services: 'not-an-array', key_features: null, target_audience: ['too', 'many', 'audiences', 'here'] }) }, finish_reason: 'stop' }] } } }; const result = generator.parseResult(invalidResponse); expect(result.templateId).toBe(444); expect(result.error).toBeDefined(); expect(result.metadata).toEqual(generator['getDefaultMetadata']()); }); it('should handle network timeouts gracefully in generateSingle', async () => { // Create a new generator with mocked OpenAI client const mockClient = { chat: { completions: { create: vi.fn().mockRejectedValue(new Error('Request timed out')) } } }; // Override the client property using Object.defineProperty Object.defineProperty(generator, 'client', { value: mockClient, writable: true }); const template: MetadataRequest = { templateId: 555, name: 'Timeout Test', nodes: ['n8n-nodes-base.webhook'] }; const result = await generator.generateSingle(template); // Should return default metadata instead of throwing expect(result).toEqual(generator['getDefaultMetadata']()); }); }); describe('Node Summarization Logic', () => { it('should group similar nodes correctly', () => { const template: MetadataRequest = { templateId: 666, name: 'Complex Workflow', nodes: [ 'n8n-nodes-base.webhook', 'n8n-nodes-base.httpRequest', 'n8n-nodes-base.postgres', 'n8n-nodes-base.mysql', 'n8n-nodes-base.slack', 'n8n-nodes-base.gmail', '@n8n/n8n-nodes-langchain.openAi', '@n8n/n8n-nodes-langchain.agent', 'n8n-nodes-base.googleSheets', 'n8n-nodes-base.excel' ] }; const request = generator.createBatchRequest(template); const userMessage = request.body.messages[1].content; expect(userMessage).toContain('HTTP/Webhooks (2)'); expect(userMessage).toContain('Database (2)'); expect(userMessage).toContain('Communication (2)'); expect(userMessage).toContain('AI/ML (2)'); expect(userMessage).toContain('Spreadsheets (2)'); }); it('should handle unknown node types gracefully', () => { const template: MetadataRequest = { templateId: 777, name: 'Unknown Nodes', nodes: [ 'custom-package.unknownNode', 'another-package.weirdNodeType', 'someNodeTrigger', 'anotherNode' ] }; const request = generator.createBatchRequest(template); const userMessage = request.body.messages[1].content; // Should handle unknown nodes without crashing expect(userMessage).toContain('unknownNode'); expect(userMessage).toContain('weirdNodeType'); expect(userMessage).toContain('someNode'); // Trigger suffix removed }); it('should limit node summary length', () => { const manyNodes = Array.from({ length: 50 }, (_, i) => `n8n-nodes-base.customNode${i}` ); const template: MetadataRequest = { templateId: 888, name: 'Many Nodes', nodes: manyNodes }; const request = generator.createBatchRequest(template); const userMessage = request.body.messages[1].content; // Should limit to top 10 groups const summaryLine = userMessage.split('\n').find((line: string) => line.includes('Nodes Used (50)') ); expect(summaryLine).toBeDefined(); const nodeGroups = summaryLine!.split(': ')[1].split(', '); expect(nodeGroups.length).toBeLessThanOrEqual(10); }); }); });

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