Spaces:
Sleeping
Sleeping
| import { describe, it, expect, beforeEach, afterEach } from 'vitest'; | |
| import Database from 'better-sqlite3'; | |
| import { NodeRepository } from '../../../src/database/node-repository'; | |
| import { DatabaseAdapter } from '../../../src/database/database-adapter'; | |
| import { TestDatabase, TestDataGenerator, MOCK_NODES, createTestDatabaseAdapter } from './test-utils'; | |
| import { ParsedNode } from '../../../src/parsers/node-parser'; | |
| describe('NodeRepository Integration Tests', () => { | |
| let testDb: TestDatabase; | |
| let db: Database.Database; | |
| let repository: NodeRepository; | |
| let adapter: DatabaseAdapter; | |
| beforeEach(async () => { | |
| testDb = new TestDatabase({ mode: 'memory' }); | |
| db = await testDb.initialize(); | |
| adapter = createTestDatabaseAdapter(db); | |
| repository = new NodeRepository(adapter); | |
| }); | |
| afterEach(async () => { | |
| await testDb.cleanup(); | |
| }); | |
| describe('saveNode', () => { | |
| it('should save single node successfully', () => { | |
| const node = createParsedNode(MOCK_NODES.webhook); | |
| repository.saveNode(node); | |
| const saved = repository.getNode(node.nodeType); | |
| expect(saved).toBeTruthy(); | |
| expect(saved.nodeType).toBe(node.nodeType); | |
| expect(saved.displayName).toBe(node.displayName); | |
| }); | |
| it('should update existing nodes', () => { | |
| const node = createParsedNode(MOCK_NODES.webhook); | |
| // Save initial version | |
| repository.saveNode(node); | |
| // Update and save again | |
| const updated = { ...node, displayName: 'Updated Webhook' }; | |
| repository.saveNode(updated); | |
| const saved = repository.getNode(node.nodeType); | |
| expect(saved?.displayName).toBe('Updated Webhook'); | |
| // Should not create duplicate | |
| const count = repository.getNodeCount(); | |
| expect(count).toBe(1); | |
| }); | |
| it('should handle nodes with complex properties', () => { | |
| const complexNode: ParsedNode = { | |
| nodeType: 'n8n-nodes-base.complex', | |
| packageName: 'n8n-nodes-base', | |
| displayName: 'Complex Node', | |
| description: 'A complex node with many properties', | |
| category: 'automation', | |
| style: 'programmatic', | |
| isAITool: false, | |
| isTrigger: false, | |
| isWebhook: false, | |
| isVersioned: true, | |
| version: '1', | |
| documentation: 'Complex node documentation', | |
| properties: [ | |
| { | |
| displayName: 'Resource', | |
| name: 'resource', | |
| type: 'options', | |
| options: [ | |
| { name: 'User', value: 'user' }, | |
| { name: 'Post', value: 'post' } | |
| ], | |
| default: 'user' | |
| }, | |
| { | |
| displayName: 'Operation', | |
| name: 'operation', | |
| type: 'options', | |
| displayOptions: { | |
| show: { | |
| resource: ['user'] | |
| } | |
| }, | |
| options: [ | |
| { name: 'Create', value: 'create' }, | |
| { name: 'Get', value: 'get' } | |
| ] | |
| } | |
| ], | |
| operations: [ | |
| { resource: 'user', operation: 'create' }, | |
| { resource: 'user', operation: 'get' } | |
| ], | |
| credentials: [ | |
| { | |
| name: 'httpBasicAuth', | |
| required: false | |
| } | |
| ] | |
| }; | |
| repository.saveNode(complexNode); | |
| const saved = repository.getNode(complexNode.nodeType); | |
| expect(saved).toBeTruthy(); | |
| expect(saved.properties).toHaveLength(2); | |
| expect(saved.credentials).toHaveLength(1); | |
| expect(saved.operations).toHaveLength(2); | |
| }); | |
| it('should handle very large nodes', () => { | |
| const largeNode: ParsedNode = { | |
| nodeType: 'n8n-nodes-base.large', | |
| packageName: 'n8n-nodes-base', | |
| displayName: 'Large Node', | |
| description: 'A very large node', | |
| category: 'automation', | |
| style: 'programmatic', | |
| isAITool: false, | |
| isTrigger: false, | |
| isWebhook: false, | |
| isVersioned: true, | |
| version: '1', | |
| properties: Array.from({ length: 100 }, (_, i) => ({ | |
| displayName: `Property ${i}`, | |
| name: `prop${i}`, | |
| type: 'string', | |
| default: '' | |
| })), | |
| operations: [], | |
| credentials: [] | |
| }; | |
| repository.saveNode(largeNode); | |
| const saved = repository.getNode(largeNode.nodeType); | |
| expect(saved?.properties).toHaveLength(100); | |
| }); | |
| }); | |
| describe('getNode', () => { | |
| beforeEach(() => { | |
| repository.saveNode(createParsedNode(MOCK_NODES.webhook)); | |
| repository.saveNode(createParsedNode(MOCK_NODES.httpRequest)); | |
| }); | |
| it('should retrieve node by type', () => { | |
| const node = repository.getNode('n8n-nodes-base.webhook'); | |
| expect(node).toBeTruthy(); | |
| expect(node.displayName).toBe('Webhook'); | |
| expect(node.nodeType).toBe('n8n-nodes-base.webhook'); | |
| expect(node.package).toBe('n8n-nodes-base'); | |
| }); | |
| it('should return null for non-existent node', () => { | |
| const node = repository.getNode('n8n-nodes-base.nonExistent'); | |
| expect(node).toBeNull(); | |
| }); | |
| it('should handle special characters in node types', () => { | |
| const specialNode: ParsedNode = { | |
| nodeType: 'n8n-nodes-base.special-chars_v2.node', | |
| packageName: 'n8n-nodes-base', | |
| displayName: 'Special Node', | |
| description: 'Node with special characters', | |
| category: 'automation', | |
| style: 'programmatic', | |
| isAITool: false, | |
| isTrigger: false, | |
| isWebhook: false, | |
| isVersioned: true, | |
| version: '2', | |
| properties: [], | |
| operations: [], | |
| credentials: [] | |
| }; | |
| repository.saveNode(specialNode); | |
| const retrieved = repository.getNode(specialNode.nodeType); | |
| expect(retrieved).toBeTruthy(); | |
| }); | |
| }); | |
| describe('getAllNodes', () => { | |
| it('should return empty array when no nodes', () => { | |
| const nodes = repository.getAllNodes(); | |
| expect(nodes).toHaveLength(0); | |
| }); | |
| it('should return all nodes with limit', () => { | |
| const nodes = Array.from({ length: 20 }, (_, i) => | |
| createParsedNode({ | |
| ...MOCK_NODES.webhook, | |
| nodeType: `n8n-nodes-base.node${i}`, | |
| displayName: `Node ${i}` | |
| }) | |
| ); | |
| nodes.forEach(node => repository.saveNode(node)); | |
| const retrieved = repository.getAllNodes(10); | |
| expect(retrieved).toHaveLength(10); | |
| }); | |
| it('should return all nodes without limit', () => { | |
| const nodes = Array.from({ length: 20 }, (_, i) => | |
| createParsedNode({ | |
| ...MOCK_NODES.webhook, | |
| nodeType: `n8n-nodes-base.node${i}`, | |
| displayName: `Node ${i}` | |
| }) | |
| ); | |
| nodes.forEach(node => repository.saveNode(node)); | |
| const retrieved = repository.getAllNodes(); | |
| expect(retrieved).toHaveLength(20); | |
| }); | |
| it('should handle very large result sets efficiently', () => { | |
| const nodes = Array.from({ length: 1000 }, (_, i) => | |
| createParsedNode({ | |
| ...MOCK_NODES.webhook, | |
| nodeType: `n8n-nodes-base.node${i}`, | |
| displayName: `Node ${i}` | |
| }) | |
| ); | |
| const insertMany = db.transaction((nodes: ParsedNode[]) => { | |
| nodes.forEach(node => repository.saveNode(node)); | |
| }); | |
| const start = Date.now(); | |
| insertMany(nodes); | |
| const duration = Date.now() - start; | |
| expect(duration).toBeLessThan(1000); // Should complete in under 1 second | |
| const retrieved = repository.getAllNodes(); | |
| expect(retrieved).toHaveLength(1000); | |
| }); | |
| }); | |
| describe('getNodesByPackage', () => { | |
| beforeEach(() => { | |
| const nodes = [ | |
| createParsedNode({ | |
| ...MOCK_NODES.webhook, | |
| nodeType: 'n8n-nodes-base.node1', | |
| packageName: 'n8n-nodes-base' | |
| }), | |
| createParsedNode({ | |
| ...MOCK_NODES.webhook, | |
| nodeType: 'n8n-nodes-base.node2', | |
| packageName: 'n8n-nodes-base' | |
| }), | |
| createParsedNode({ | |
| ...MOCK_NODES.webhook, | |
| nodeType: '@n8n/n8n-nodes-langchain.node3', | |
| packageName: '@n8n/n8n-nodes-langchain' | |
| }) | |
| ]; | |
| nodes.forEach(node => repository.saveNode(node)); | |
| }); | |
| it('should filter nodes by package', () => { | |
| const baseNodes = repository.getNodesByPackage('n8n-nodes-base'); | |
| expect(baseNodes).toHaveLength(2); | |
| const langchainNodes = repository.getNodesByPackage('@n8n/n8n-nodes-langchain'); | |
| expect(langchainNodes).toHaveLength(1); | |
| }); | |
| it('should return empty array for non-existent package', () => { | |
| const nodes = repository.getNodesByPackage('non-existent-package'); | |
| expect(nodes).toHaveLength(0); | |
| }); | |
| }); | |
| describe('getNodesByCategory', () => { | |
| beforeEach(() => { | |
| const nodes = [ | |
| createParsedNode({ | |
| ...MOCK_NODES.webhook, | |
| nodeType: 'n8n-nodes-base.webhook', | |
| category: 'trigger' | |
| }), | |
| createParsedNode({ | |
| ...MOCK_NODES.webhook, | |
| nodeType: 'n8n-nodes-base.schedule', | |
| displayName: 'Schedule', | |
| category: 'trigger' | |
| }), | |
| createParsedNode({ | |
| ...MOCK_NODES.httpRequest, | |
| nodeType: 'n8n-nodes-base.httpRequest', | |
| category: 'automation' | |
| }) | |
| ]; | |
| nodes.forEach(node => repository.saveNode(node)); | |
| }); | |
| it('should filter nodes by category', () => { | |
| const triggers = repository.getNodesByCategory('trigger'); | |
| expect(triggers).toHaveLength(2); | |
| expect(triggers.every(n => n.category === 'trigger')).toBe(true); | |
| const automation = repository.getNodesByCategory('automation'); | |
| expect(automation).toHaveLength(1); | |
| expect(automation[0].category).toBe('automation'); | |
| }); | |
| }); | |
| describe('searchNodes', () => { | |
| beforeEach(() => { | |
| const nodes = [ | |
| createParsedNode({ | |
| ...MOCK_NODES.webhook, | |
| description: 'Starts the workflow when webhook is called' | |
| }), | |
| createParsedNode({ | |
| ...MOCK_NODES.httpRequest, | |
| description: 'Makes HTTP requests to external APIs' | |
| }), | |
| createParsedNode({ | |
| nodeType: 'n8n-nodes-base.emailSend', | |
| packageName: 'n8n-nodes-base', | |
| displayName: 'Send Email', | |
| description: 'Sends emails via SMTP protocol', | |
| category: 'communication', | |
| developmentStyle: 'programmatic', | |
| isAITool: false, | |
| isTrigger: false, | |
| isWebhook: false, | |
| isVersioned: true, | |
| version: '1', | |
| properties: [], | |
| operations: [], | |
| credentials: [] | |
| }) | |
| ]; | |
| nodes.forEach(node => repository.saveNode(node)); | |
| }); | |
| it('should search by node type', () => { | |
| const results = repository.searchNodes('webhook'); | |
| expect(results).toHaveLength(1); | |
| expect(results[0].nodeType).toBe('n8n-nodes-base.webhook'); | |
| }); | |
| it('should search by display name', () => { | |
| const results = repository.searchNodes('Send Email'); | |
| expect(results).toHaveLength(1); | |
| expect(results[0].nodeType).toBe('n8n-nodes-base.emailSend'); | |
| }); | |
| it('should search by description', () => { | |
| const results = repository.searchNodes('SMTP'); | |
| expect(results).toHaveLength(1); | |
| expect(results[0].nodeType).toBe('n8n-nodes-base.emailSend'); | |
| }); | |
| it('should handle OR mode (default)', () => { | |
| const results = repository.searchNodes('webhook email', 'OR'); | |
| expect(results).toHaveLength(2); | |
| const nodeTypes = results.map(r => r.nodeType); | |
| expect(nodeTypes).toContain('n8n-nodes-base.webhook'); | |
| expect(nodeTypes).toContain('n8n-nodes-base.emailSend'); | |
| }); | |
| it('should handle AND mode', () => { | |
| const results = repository.searchNodes('HTTP request', 'AND'); | |
| expect(results).toHaveLength(1); | |
| expect(results[0].nodeType).toBe('n8n-nodes-base.httpRequest'); | |
| }); | |
| it('should handle FUZZY mode', () => { | |
| const results = repository.searchNodes('HTT', 'FUZZY'); | |
| expect(results).toHaveLength(1); | |
| expect(results[0].nodeType).toBe('n8n-nodes-base.httpRequest'); | |
| }); | |
| it('should handle case-insensitive search', () => { | |
| const results = repository.searchNodes('WEBHOOK'); | |
| expect(results).toHaveLength(1); | |
| expect(results[0].nodeType).toBe('n8n-nodes-base.webhook'); | |
| }); | |
| it('should return empty array for no matches', () => { | |
| const results = repository.searchNodes('nonexistent'); | |
| expect(results).toHaveLength(0); | |
| }); | |
| it('should respect limit parameter', () => { | |
| // Add more nodes | |
| const nodes = Array.from({ length: 10 }, (_, i) => | |
| createParsedNode({ | |
| ...MOCK_NODES.webhook, | |
| nodeType: `n8n-nodes-base.test${i}`, | |
| displayName: `Test Node ${i}`, | |
| description: 'Test description' | |
| }) | |
| ); | |
| nodes.forEach(node => repository.saveNode(node)); | |
| const results = repository.searchNodes('test', 'OR', 5); | |
| expect(results).toHaveLength(5); | |
| }); | |
| }); | |
| describe('getAITools', () => { | |
| it('should return only AI tool nodes', () => { | |
| const nodes = [ | |
| createParsedNode({ | |
| ...MOCK_NODES.webhook, | |
| nodeType: 'n8n-nodes-base.webhook', | |
| isAITool: false | |
| }), | |
| createParsedNode({ | |
| ...MOCK_NODES.webhook, | |
| nodeType: '@n8n/n8n-nodes-langchain.agent', | |
| displayName: 'AI Agent', | |
| packageName: '@n8n/n8n-nodes-langchain', | |
| isAITool: true | |
| }), | |
| createParsedNode({ | |
| ...MOCK_NODES.webhook, | |
| nodeType: '@n8n/n8n-nodes-langchain.tool', | |
| displayName: 'AI Tool', | |
| packageName: '@n8n/n8n-nodes-langchain', | |
| isAITool: true | |
| }) | |
| ]; | |
| nodes.forEach(node => repository.saveNode(node)); | |
| const aiTools = repository.getAITools(); | |
| expect(aiTools).toHaveLength(2); | |
| expect(aiTools.every(node => node.package.includes('langchain'))).toBe(true); | |
| expect(aiTools[0].displayName).toBe('AI Agent'); | |
| expect(aiTools[1].displayName).toBe('AI Tool'); | |
| }); | |
| }); | |
| describe('getNodeCount', () => { | |
| it('should return correct node count', () => { | |
| expect(repository.getNodeCount()).toBe(0); | |
| repository.saveNode(createParsedNode(MOCK_NODES.webhook)); | |
| expect(repository.getNodeCount()).toBe(1); | |
| repository.saveNode(createParsedNode(MOCK_NODES.httpRequest)); | |
| expect(repository.getNodeCount()).toBe(2); | |
| }); | |
| }); | |
| describe('searchNodeProperties', () => { | |
| beforeEach(() => { | |
| const node: ParsedNode = { | |
| nodeType: 'n8n-nodes-base.complex', | |
| packageName: 'n8n-nodes-base', | |
| displayName: 'Complex Node', | |
| description: 'A complex node', | |
| category: 'automation', | |
| style: 'programmatic', | |
| isAITool: false, | |
| isTrigger: false, | |
| isWebhook: false, | |
| isVersioned: true, | |
| version: '1', | |
| properties: [ | |
| { | |
| displayName: 'Authentication', | |
| name: 'authentication', | |
| type: 'options', | |
| options: [ | |
| { name: 'Basic', value: 'basic' }, | |
| { name: 'OAuth2', value: 'oauth2' } | |
| ] | |
| }, | |
| { | |
| displayName: 'Headers', | |
| name: 'headers', | |
| type: 'collection', | |
| default: {}, | |
| options: [ | |
| { | |
| displayName: 'Header', | |
| name: 'header', | |
| type: 'string' | |
| } | |
| ] | |
| } | |
| ], | |
| operations: [], | |
| credentials: [] | |
| }; | |
| repository.saveNode(node); | |
| }); | |
| it('should find properties by name', () => { | |
| const results = repository.searchNodeProperties('n8n-nodes-base.complex', 'auth'); | |
| expect(results.length).toBeGreaterThan(0); | |
| expect(results.some(r => r.path.includes('authentication'))).toBe(true); | |
| }); | |
| it('should find nested properties', () => { | |
| const results = repository.searchNodeProperties('n8n-nodes-base.complex', 'header'); | |
| expect(results.length).toBeGreaterThan(0); | |
| }); | |
| it('should return empty array for non-existent node', () => { | |
| const results = repository.searchNodeProperties('non-existent', 'test'); | |
| expect(results).toHaveLength(0); | |
| }); | |
| }); | |
| describe('Transaction handling', () => { | |
| it('should handle errors gracefully', () => { | |
| // Test with a node that violates database constraints | |
| const invalidNode = { | |
| nodeType: '', // Empty string should violate PRIMARY KEY constraint | |
| packageName: null, // NULL should violate NOT NULL constraint | |
| displayName: null, // NULL should violate NOT NULL constraint | |
| description: '', | |
| category: 'automation', | |
| style: 'programmatic', | |
| isAITool: false, | |
| isTrigger: false, | |
| isWebhook: false, | |
| isVersioned: false, | |
| version: '1', | |
| properties: [], | |
| operations: [], | |
| credentials: [] | |
| } as any; | |
| expect(() => { | |
| repository.saveNode(invalidNode); | |
| }).toThrow(); | |
| // Repository should still be functional | |
| const count = repository.getNodeCount(); | |
| expect(count).toBe(0); | |
| }); | |
| it('should handle concurrent saves', () => { | |
| const node = createParsedNode(MOCK_NODES.webhook); | |
| // Simulate concurrent saves of the same node with different display names | |
| const promises = Array.from({ length: 10 }, (_, i) => { | |
| const updatedNode = { | |
| ...node, | |
| displayName: `Display ${i}` | |
| }; | |
| return Promise.resolve(repository.saveNode(updatedNode)); | |
| }); | |
| Promise.all(promises); | |
| // Should have only one node | |
| const count = repository.getNodeCount(); | |
| expect(count).toBe(1); | |
| // Should have the last update | |
| const saved = repository.getNode(node.nodeType); | |
| expect(saved).toBeTruthy(); | |
| }); | |
| }); | |
| describe('Performance characteristics', () => { | |
| it('should handle bulk operations efficiently', () => { | |
| const nodeCount = 1000; | |
| const nodes = Array.from({ length: nodeCount }, (_, i) => | |
| createParsedNode({ | |
| ...MOCK_NODES.webhook, | |
| nodeType: `n8n-nodes-base.node${i}`, | |
| displayName: `Node ${i}`, | |
| description: `Description for node ${i}` | |
| }) | |
| ); | |
| const insertMany = db.transaction((nodes: ParsedNode[]) => { | |
| nodes.forEach(node => repository.saveNode(node)); | |
| }); | |
| const start = Date.now(); | |
| insertMany(nodes); | |
| const saveDuration = Date.now() - start; | |
| expect(saveDuration).toBeLessThan(1000); // Should complete in under 1 second | |
| // Test search performance | |
| const searchStart = Date.now(); | |
| const results = repository.searchNodes('node', 'OR', 100); | |
| const searchDuration = Date.now() - searchStart; | |
| expect(searchDuration).toBeLessThan(50); // Search should be fast | |
| expect(results.length).toBe(100); // Respects limit | |
| }); | |
| }); | |
| }); | |
| // Helper function to create ParsedNode from test data | |
| function createParsedNode(data: any): ParsedNode { | |
| return { | |
| nodeType: data.nodeType, | |
| packageName: data.packageName, | |
| displayName: data.displayName, | |
| description: data.description || '', | |
| category: data.category || 'automation', | |
| style: data.developmentStyle || 'programmatic', | |
| isAITool: data.isAITool || false, | |
| isTrigger: data.isTrigger || false, | |
| isWebhook: data.isWebhook || false, | |
| isVersioned: data.isVersioned !== undefined ? data.isVersioned : true, | |
| version: data.version || '1', | |
| documentation: data.documentation || null, | |
| properties: data.properties || [], | |
| operations: data.operations || [], | |
| credentials: data.credentials || [] | |
| }; | |
| } |