Skip to main content
Glama

n8n-MCP

by 88-888
metadata-operations.test.ts20.7 kB
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { TemplateService } from '../../../src/templates/template-service'; import { TemplateRepository } from '../../../src/templates/template-repository'; import { MetadataGenerator } from '../../../src/templates/metadata-generator'; import { BatchProcessor } from '../../../src/templates/batch-processor'; import { DatabaseAdapter, createDatabaseAdapter } from '../../../src/database/database-adapter'; import { tmpdir } from 'os'; import * as path from 'path'; import { unlinkSync, existsSync, readFileSync } from 'fs'; // Mock logger vi.mock('../../../src/utils/logger', () => ({ logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() } })); // Mock template sanitizer vi.mock('../../../src/utils/template-sanitizer', () => { class MockTemplateSanitizer { sanitizeWorkflow = vi.fn((workflow) => ({ sanitized: workflow, wasModified: false })); detectTokens = vi.fn(() => []); } return { TemplateSanitizer: MockTemplateSanitizer }; }); // Mock OpenAI for MetadataGenerator and BatchProcessor vi.mock('openai', () => { const mockClient = { chat: { completions: { create: vi.fn() } }, files: { create: vi.fn(), content: vi.fn(), del: vi.fn() }, batches: { create: vi.fn(), retrieve: vi.fn() } }; return { default: vi.fn().mockImplementation(() => mockClient) }; }); describe('Template Metadata Operations - Integration Tests', () => { let adapter: DatabaseAdapter; let repository: TemplateRepository; let service: TemplateService; let dbPath: string; beforeEach(async () => { // Create temporary database dbPath = path.join(tmpdir(), `test-metadata-${Date.now()}.db`); adapter = await createDatabaseAdapter(dbPath); // Initialize database schema const schemaPath = path.join(__dirname, '../../../src/database/schema.sql'); const schema = readFileSync(schemaPath, 'utf8'); adapter.exec(schema); // Initialize repository and service repository = new TemplateRepository(adapter); service = new TemplateService(adapter); // Create test templates await createTestTemplates(); }); afterEach(() => { if (adapter) { adapter.close(); } if (existsSync(dbPath)) { unlinkSync(dbPath); } vi.clearAllMocks(); }); async function createTestTemplates() { // Create test templates with metadata const templates = [ { workflow: { id: 1, name: 'Simple Webhook Slack', description: 'Basic webhook to Slack automation', user: { id: 1, name: 'Test User', username: 'test', verified: true }, nodes: [ { id: 1, name: 'n8n-nodes-base.webhook', icon: 'fa:webhook' }, { id: 2, name: 'n8n-nodes-base.slack', icon: 'fa:slack' } ], totalViews: 150, createdAt: '2024-01-01T00:00:00Z' }, detail: { id: 1, name: 'Simple Webhook Slack', description: 'Basic webhook to Slack automation', views: 150, createdAt: '2024-01-01T00:00:00Z', workflow: { nodes: [ { type: 'n8n-nodes-base.webhook', name: 'Webhook', id: '1', position: [0, 0], parameters: {}, typeVersion: 1 }, { type: 'n8n-nodes-base.slack', name: 'Slack', id: '2', position: [100, 0], parameters: {}, typeVersion: 1 } ], connections: { '1': { main: [[{ node: '2', type: 'main', index: 0 }]] } }, settings: {} } }, categories: ['automation', 'communication'], metadata: { categories: ['automation', 'communication'], complexity: 'simple' as const, use_cases: ['Webhook processing', 'Slack notifications'], estimated_setup_minutes: 15, required_services: ['Slack API'], key_features: ['Real-time notifications', 'Easy setup'], target_audience: ['developers', 'marketers'] } }, { workflow: { id: 2, name: 'Complex AI Data Pipeline', description: 'Advanced data processing with AI analysis', user: { id: 2, name: 'AI Expert', username: 'aiexpert', verified: true }, nodes: [ { id: 1, name: 'n8n-nodes-base.webhook', icon: 'fa:webhook' }, { id: 2, name: '@n8n/n8n-nodes-langchain.openAi', icon: 'fa:brain' }, { id: 3, name: 'n8n-nodes-base.postgres', icon: 'fa:database' }, { id: 4, name: 'n8n-nodes-base.googleSheets', icon: 'fa:sheet' } ], totalViews: 450, createdAt: '2024-01-15T00:00:00Z' }, detail: { id: 2, name: 'Complex AI Data Pipeline', description: 'Advanced data processing with AI analysis', views: 450, createdAt: '2024-01-15T00:00:00Z', workflow: { nodes: [ { type: 'n8n-nodes-base.webhook', name: 'Webhook', id: '1', position: [0, 0], parameters: {}, typeVersion: 1 }, { type: '@n8n/n8n-nodes-langchain.openAi', name: 'OpenAI', id: '2', position: [100, 0], parameters: {}, typeVersion: 1 }, { type: 'n8n-nodes-base.postgres', name: 'Postgres', id: '3', position: [200, 0], parameters: {}, typeVersion: 1 }, { type: 'n8n-nodes-base.googleSheets', name: 'Google Sheets', id: '4', position: [300, 0], parameters: {}, typeVersion: 1 } ], connections: { '1': { main: [[{ node: '2', type: 'main', index: 0 }]] }, '2': { main: [[{ node: '3', type: 'main', index: 0 }]] }, '3': { main: [[{ node: '4', type: 'main', index: 0 }]] } }, settings: {} } }, categories: ['ai', 'data_processing'], metadata: { categories: ['ai', 'data_processing', 'automation'], complexity: 'complex' as const, use_cases: ['Data analysis', 'AI processing', 'Report generation'], estimated_setup_minutes: 120, required_services: ['OpenAI API', 'PostgreSQL', 'Google Sheets API'], key_features: ['AI analysis', 'Database integration', 'Automated reports'], target_audience: ['developers', 'analysts'] } }, { workflow: { id: 3, name: 'Medium Email Automation', description: 'Email automation with moderate complexity', user: { id: 3, name: 'Marketing User', username: 'marketing', verified: false }, nodes: [ { id: 1, name: 'n8n-nodes-base.cron', icon: 'fa:clock' }, { id: 2, name: 'n8n-nodes-base.gmail', icon: 'fa:mail' }, { id: 3, name: 'n8n-nodes-base.googleSheets', icon: 'fa:sheet' } ], totalViews: 200, createdAt: '2024-02-01T00:00:00Z' }, detail: { id: 3, name: 'Medium Email Automation', description: 'Email automation with moderate complexity', views: 200, createdAt: '2024-02-01T00:00:00Z', workflow: { nodes: [ { type: 'n8n-nodes-base.cron', name: 'Cron', id: '1', position: [0, 0], parameters: {}, typeVersion: 1 }, { type: 'n8n-nodes-base.gmail', name: 'Gmail', id: '2', position: [100, 0], parameters: {}, typeVersion: 1 }, { type: 'n8n-nodes-base.googleSheets', name: 'Google Sheets', id: '3', position: [200, 0], parameters: {}, typeVersion: 1 } ], connections: { '1': { main: [[{ node: '2', type: 'main', index: 0 }]] }, '2': { main: [[{ node: '3', type: 'main', index: 0 }]] } }, settings: {} } }, categories: ['email_automation', 'scheduling'], metadata: { categories: ['email_automation', 'scheduling'], complexity: 'medium' as const, use_cases: ['Email campaigns', 'Scheduled reports'], estimated_setup_minutes: 45, required_services: ['Gmail API', 'Google Sheets API'], key_features: ['Scheduled execution', 'Email automation'], target_audience: ['marketers'] } } ]; // Save templates for (const template of templates) { repository.saveTemplate(template.workflow, template.detail, template.categories); repository.updateTemplateMetadata(template.workflow.id, template.metadata); } } describe('Repository Metadata Operations', () => { it('should update template metadata successfully', () => { const newMetadata = { categories: ['test', 'updated'], complexity: 'simple' as const, use_cases: ['Testing'], estimated_setup_minutes: 10, required_services: [], key_features: ['Test feature'], target_audience: ['testers'] }; repository.updateTemplateMetadata(1, newMetadata); // Verify metadata was updated const templates = repository.searchTemplatesByMetadata({ category: 'test'}, 10, 0); expect(templates).toHaveLength(1); expect(templates[0].id).toBe(1); }); it('should batch update metadata for multiple templates', () => { const metadataMap = new Map([ [1, { categories: ['batch_test'], complexity: 'simple' as const, use_cases: ['Batch testing'], estimated_setup_minutes: 20, required_services: [], key_features: ['Batch update'], target_audience: ['developers'] }], [2, { categories: ['batch_test'], complexity: 'complex' as const, use_cases: ['Complex batch testing'], estimated_setup_minutes: 60, required_services: ['OpenAI'], key_features: ['Advanced batch'], target_audience: ['developers'] }] ]); repository.batchUpdateMetadata(metadataMap); // Verify both templates were updated const templates = repository.searchTemplatesByMetadata({ category: 'batch_test'}, 10, 0); expect(templates).toHaveLength(2); expect(templates.map(t => t.id).sort()).toEqual([1, 2]); }); it('should search templates by category', () => { const templates = repository.searchTemplatesByMetadata({ category: 'automation'}, 10, 0); expect(templates.length).toBeGreaterThan(0); expect(templates[0]).toHaveProperty('id'); expect(templates[0]).toHaveProperty('name'); }); it('should search templates by complexity', () => { const simpleTemplates = repository.searchTemplatesByMetadata({ complexity: 'simple'}, 10, 0); const complexTemplates = repository.searchTemplatesByMetadata({ complexity: 'complex'}, 10, 0); expect(simpleTemplates).toHaveLength(1); expect(complexTemplates).toHaveLength(1); expect(simpleTemplates[0].id).toBe(1); expect(complexTemplates[0].id).toBe(2); }); it('should search templates by setup time', () => { const quickTemplates = repository.searchTemplatesByMetadata({ maxSetupMinutes: 30}, 10, 0); const longTemplates = repository.searchTemplatesByMetadata({ minSetupMinutes: 60}, 10, 0); expect(quickTemplates).toHaveLength(1); // Only 15 min template (45 min > 30) expect(longTemplates).toHaveLength(1); // 120 min template }); it('should search templates by required service', () => { const slackTemplates = repository.searchTemplatesByMetadata({ requiredService: 'slack'}, 10, 0); const openaiTemplates = repository.searchTemplatesByMetadata({ requiredService: 'OpenAI'}, 10, 0); expect(slackTemplates).toHaveLength(1); expect(openaiTemplates).toHaveLength(1); }); it('should search templates by target audience', () => { const developerTemplates = repository.searchTemplatesByMetadata({ targetAudience: 'developers'}, 10, 0); const marketerTemplates = repository.searchTemplatesByMetadata({ targetAudience: 'marketers'}, 10, 0); expect(developerTemplates).toHaveLength(2); expect(marketerTemplates).toHaveLength(2); }); it('should handle combined filters correctly', () => { const filteredTemplates = repository.searchTemplatesByMetadata({ complexity: 'medium', targetAudience: 'marketers', maxSetupMinutes: 60}, 10, 0); expect(filteredTemplates).toHaveLength(1); expect(filteredTemplates[0].id).toBe(3); }); it('should return correct counts for metadata searches', () => { const automationCount = repository.getSearchTemplatesByMetadataCount({ category: 'automation' }); const complexCount = repository.getSearchTemplatesByMetadataCount({ complexity: 'complex' }); expect(automationCount).toBeGreaterThan(0); expect(complexCount).toBe(1); }); it('should get unique categories', () => { const categories = repository.getUniqueCategories(); expect(categories).toContain('automation'); expect(categories).toContain('communication'); expect(categories).toContain('ai'); expect(categories).toContain('data_processing'); expect(categories).toContain('email_automation'); expect(categories).toContain('scheduling'); }); it('should get unique target audiences', () => { const audiences = repository.getUniqueTargetAudiences(); expect(audiences).toContain('developers'); expect(audiences).toContain('marketers'); expect(audiences).toContain('analysts'); }); it('should get templates by category', () => { const aiTemplates = repository.getTemplatesByCategory('ai'); // Both template 2 has 'ai', and template 1 has 'automation' which contains 'ai' as substring // due to LIKE '%ai%' matching expect(aiTemplates).toHaveLength(2); // Template 2 should be first due to higher view count (450 vs 150) expect(aiTemplates[0].id).toBe(2); }); it('should get templates by complexity', () => { const simpleTemplates = repository.getTemplatesByComplexity('simple'); expect(simpleTemplates).toHaveLength(1); expect(simpleTemplates[0].id).toBe(1); }); it('should get templates without metadata', () => { // Create a template without metadata const workflow = { id: 999, name: 'No Metadata Template', description: 'Template without metadata', user: { id: 999, name: 'Test', username: 'test', verified: true }, nodes: [{ id: 1, name: 'n8n-nodes-base.webhook', icon: 'fa:webhook' }], totalViews: 50, // Must be > 10 to not be filtered out createdAt: '2024-03-01T00:00:00Z' }; const detail = { id: 999, name: 'No Metadata Template', description: 'Template without metadata', views: 50, // Must be > 10 to not be filtered out createdAt: '2024-03-01T00:00:00Z', workflow: { nodes: [{ type: 'n8n-nodes-base.webhook', name: 'Webhook', id: '1', position: [0, 0], parameters: {}, typeVersion: 1 }], connections: {}, settings: {} } }; repository.saveTemplate(workflow, detail, []); // Don't update metadata for this template, so it remains without metadata const templatesWithoutMetadata = repository.getTemplatesWithoutMetadata(); expect(templatesWithoutMetadata.some(t => t.workflow_id === 999)).toBe(true); }); it('should get outdated metadata templates', () => { // This test would require manipulating timestamps, // for now just verify the method doesn't throw const outdatedTemplates = repository.getTemplatesWithOutdatedMetadata(30); expect(Array.isArray(outdatedTemplates)).toBe(true); }); it('should get metadata statistics', () => { const stats = repository.getMetadataStats(); expect(stats).toHaveProperty('withMetadata'); expect(stats).toHaveProperty('total'); expect(stats).toHaveProperty('withoutMetadata'); expect(stats).toHaveProperty('outdated'); expect(stats.withMetadata).toBeGreaterThan(0); expect(stats.total).toBeGreaterThan(0); }); }); describe('Service Layer Integration', () => { it('should search templates with metadata through service', async () => { const results = await service.searchTemplatesByMetadata({ complexity: 'simple'}, 10, 0); expect(results).toHaveProperty('items'); expect(results).toHaveProperty('total'); expect(results).toHaveProperty('hasMore'); expect(results.items.length).toBeGreaterThan(0); expect(results.items[0]).toHaveProperty('metadata'); }); it('should handle pagination correctly in metadata search', async () => { const page1 = await service.searchTemplatesByMetadata( {}, // empty filters 1, // limit 0 // offset ); const page2 = await service.searchTemplatesByMetadata( {}, // empty filters 1, // limit 1 // offset ); expect(page1.items).toHaveLength(1); expect(page2.items).toHaveLength(1); expect(page1.items[0].id).not.toBe(page2.items[0].id); }); it('should return templates with metadata information', async () => { const results = await service.searchTemplatesByMetadata({ category: 'automation'}, 10, 0); expect(results.items.length).toBeGreaterThan(0); const template = results.items[0]; expect(template).toHaveProperty('metadata'); expect(template.metadata).toHaveProperty('categories'); expect(template.metadata).toHaveProperty('complexity'); expect(template.metadata).toHaveProperty('estimated_setup_minutes'); }); }); describe('Security and Error Handling', () => { it('should handle malicious input safely in metadata search', () => { const maliciousInputs = [ { category: "'; DROP TABLE templates; --" }, { requiredService: "'; UNION SELECT * FROM sqlite_master; --" }, { targetAudience: "administrators'; DELETE FROM templates WHERE '1'='1" } ]; maliciousInputs.forEach(input => { expect(() => { repository.searchTemplatesByMetadata({ ...input}, 10, 0); }).not.toThrow(); }); }); it('should handle invalid metadata gracefully', () => { const invalidMetadata = { categories: null, complexity: 'invalid_complexity', use_cases: 'not_an_array', estimated_setup_minutes: 'not_a_number', required_services: undefined, key_features: {}, target_audience: 42 }; expect(() => { repository.updateTemplateMetadata(1, invalidMetadata); }).not.toThrow(); }); it('should handle empty search results gracefully', () => { const results = repository.searchTemplatesByMetadata({ category: 'nonexistent_category'}, 10, 0); expect(results).toHaveLength(0); }); it('should handle edge case parameters', () => { // Test extreme values const results = repository.searchTemplatesByMetadata({ maxSetupMinutes: 0, minSetupMinutes: 999999 }, 0, -1); // offset -1 to test edge case expect(Array.isArray(results)).toBe(true); }); }); describe('Performance and Scalability', () => { it('should handle large result sets efficiently', () => { // Test with maximum limit const startTime = Date.now(); const results = repository.searchTemplatesByMetadata({}, 100, 0); const endTime = Date.now(); expect(endTime - startTime).toBeLessThan(1000); // Should complete within 1 second expect(Array.isArray(results)).toBe(true); }); it('should handle concurrent metadata updates', () => { const updates: any[] = []; for (let i = 0; i < 10; i++) { updates.push(() => { repository.updateTemplateMetadata(1, { categories: [`concurrent_test_${i}`], complexity: 'simple' as const, use_cases: ['Testing'], estimated_setup_minutes: 10, required_services: [], key_features: ['Concurrent'], target_audience: ['developers'] }); }); } // Execute all updates expect(() => { updates.forEach(update => update()); }).not.toThrow(); }); }); });

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