import { describe, it, expect, vi, beforeEach } from 'vitest'; import { NodeRepository, CommunityNodeFields } from '@/database/node-repository'; import { DatabaseAdapter, PreparedStatement, RunResult } from '@/database/database-adapter'; import { ParsedNode } from '@/parsers/node-parser'; /** * Mock DatabaseAdapter for testing community node methods */ class MockDatabaseAdapter implements DatabaseAdapter { private statements = new Map(); private mockData: Map = new Map(); prepare = vi.fn((sql: string) => { if (!this.statements.has(sql)) { this.statements.set(sql, new MockPreparedStatement(sql, this.mockData, this)); } 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 helpers _setMockData(key: string, data: any[]) { this.mockData.set(key, data); } _getMockData(key: string): any[] { return this.mockData.get(key) || []; } } 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, private adapter: MockDatabaseAdapter ) { this.setupMockBehavior(); } private setupMockBehavior() { // Community nodes queries if (this.sql.includes('SELECT * FROM nodes WHERE is_community = 1')) { this.all = vi.fn((...params: any[]) => { let nodes = this.mockData.get('community_nodes') || []; // Handle verified filter if (this.sql.includes('AND is_verified = ?')) { const isVerified = params[0] === 1; nodes = nodes.filter((n: any) => n.is_verified === (isVerified ? 1 : 0)); } // Handle limit if (this.sql.includes('LIMIT ?')) { const limitParam = params[params.length - 1]; nodes = nodes.slice(0, limitParam); } return nodes; }); } // Community stats - total count if (this.sql.includes('SELECT COUNT(*) as count FROM nodes WHERE is_community = 1') && !this.sql.includes('AND is_verified')) { this.get = vi.fn(() => { const nodes = this.mockData.get('community_nodes') || []; return { count: nodes.length }; }); } // Community stats - verified count if (this.sql.includes('SELECT COUNT(*) as count FROM nodes WHERE is_community = 1 AND is_verified = 1')) { this.get = vi.fn(() => { const nodes = this.mockData.get('community_nodes') || []; return { count: nodes.filter((n: any) => n.is_verified === 1).length }; }); } // hasNodeByNpmPackage if (this.sql.includes('SELECT 1 FROM nodes WHERE npm_package_name = ?')) { this.get = vi.fn((npmPackageName: string) => { const nodes = this.mockData.get('community_nodes') || []; const found = nodes.find((n: any) => n.npm_package_name === npmPackageName); return found ? { '1': 1 } : undefined; }); } // getNodeByNpmPackage if (this.sql.includes('SELECT * FROM nodes WHERE npm_package_name = ?')) { this.get = vi.fn((npmPackageName: string) => { const nodes = this.mockData.get('community_nodes') || []; return nodes.find((n: any) => n.npm_package_name === npmPackageName); }); } // deleteCommunityNodes if (this.sql.includes('DELETE FROM nodes WHERE is_community = 1')) { this.run = vi.fn(() => { const nodes = this.mockData.get('community_nodes') || []; const count = nodes.length; this.mockData.set('community_nodes', []); return { changes: count, lastInsertRowid: 0 }; }); } // saveNode - INSERT OR REPLACE if (this.sql.includes('INSERT OR REPLACE INTO nodes')) { this.run = vi.fn((...params: any[]): RunResult => { const nodes = this.mockData.get('community_nodes') || []; const nodeType = params[0]; // Remove existing node with same type const filteredNodes = nodes.filter((n: any) => n.node_type !== nodeType); // Add new node (simplified) const newNode = { node_type: params[0], package_name: params[1], display_name: params[2], description: params[3], is_community: params[20] || 0, is_verified: params[21] || 0, npm_package_name: params[24], npm_version: params[25], npm_downloads: params[26] || 0, author_name: params[22], }; filteredNodes.push(newNode); this.mockData.set('community_nodes', filteredNodes); return { changes: 1, lastInsertRowid: filteredNodes.length }; }); } } } describe('NodeRepository - Community Node Methods', () => { let repository: NodeRepository; let mockAdapter: MockDatabaseAdapter; // Sample community node data const sampleCommunityNodes = [ { node_type: 'n8n-nodes-verified.testNode', package_name: 'n8n-nodes-verified', display_name: 'Verified Test Node', description: 'A verified community node', category: 'Community', development_style: 'declarative', is_ai_tool: 0, is_trigger: 0, is_webhook: 0, is_versioned: 0, is_tool_variant: 0, has_tool_variant: 0, version: '1.0.0', properties_schema: '[]', operations: '[]', credentials_required: '[]', is_community: 1, is_verified: 1, author_name: 'Verified Author', author_github_url: 'https://github.com/verified', npm_package_name: 'n8n-nodes-verified', npm_version: '1.0.0', npm_downloads: 5000, community_fetched_at: '2024-01-01T00:00:00.000Z', }, { node_type: 'n8n-nodes-unverified.testNode', package_name: 'n8n-nodes-unverified', display_name: 'Unverified Test Node', description: 'An unverified community node', category: 'Community', development_style: 'declarative', is_ai_tool: 0, is_trigger: 1, is_webhook: 0, is_versioned: 0, is_tool_variant: 0, has_tool_variant: 0, version: '0.5.0', properties_schema: '[]', operations: '[]', credentials_required: '[]', is_community: 1, is_verified: 0, author_name: 'Community Author', author_github_url: 'https://github.com/community', npm_package_name: 'n8n-nodes-unverified', npm_version: '0.5.0', npm_downloads: 1000, community_fetched_at: '2024-01-02T00:00:00.000Z', }, { node_type: 'n8n-nodes-popular.testNode', package_name: 'n8n-nodes-popular', display_name: 'Popular Test Node', description: 'A popular verified community node', category: 'Community', development_style: 'declarative', is_ai_tool: 0, is_trigger: 0, is_webhook: 1, is_versioned: 1, is_tool_variant: 0, has_tool_variant: 0, version: '2.0.0', properties_schema: '[]', operations: '[]', credentials_required: '[]', is_community: 1, is_verified: 1, author_name: 'Popular Author', author_github_url: 'https://github.com/popular', npm_package_name: 'n8n-nodes-popular', npm_version: '2.0.0', npm_downloads: 50000, community_fetched_at: '2024-01-03T00:00:00.000Z', }, ]; beforeEach(() => { vi.clearAllMocks(); mockAdapter = new MockDatabaseAdapter(); repository = new NodeRepository(mockAdapter); }); describe('getCommunityNodes', () => { beforeEach(() => { mockAdapter._setMockData('community_nodes', [...sampleCommunityNodes]); }); it('should return all community nodes', () => { const nodes = repository.getCommunityNodes(); expect(nodes).toHaveLength(3); expect(nodes[0].isCommunity).toBe(true); }); it('should filter by verified status', () => { const verifiedNodes = repository.getCommunityNodes({ verified: true }); const unverifiedNodes = repository.getCommunityNodes({ verified: false }); expect(verifiedNodes).toHaveLength(2); expect(unverifiedNodes).toHaveLength(1); expect(verifiedNodes.every((n: any) => n.isVerified)).toBe(true); expect(unverifiedNodes.every((n: any) => !n.isVerified)).toBe(true); }); it('should respect limit parameter', () => { const nodes = repository.getCommunityNodes({ limit: 2 }); expect(nodes).toHaveLength(2); }); it('should correctly parse community node fields', () => { const nodes = repository.getCommunityNodes(); const verifiedNode = nodes.find((n: any) => n.nodeType === 'n8n-nodes-verified.testNode'); expect(verifiedNode).toBeDefined(); expect(verifiedNode.isCommunity).toBe(true); expect(verifiedNode.isVerified).toBe(true); expect(verifiedNode.authorName).toBe('Verified Author'); expect(verifiedNode.npmPackageName).toBe('n8n-nodes-verified'); expect(verifiedNode.npmVersion).toBe('1.0.0'); expect(verifiedNode.npmDownloads).toBe(5000); }); it('should handle empty result', () => { mockAdapter._setMockData('community_nodes', []); const nodes = repository.getCommunityNodes(); expect(nodes).toHaveLength(0); }); it('should handle order by downloads', () => { const nodes = repository.getCommunityNodes({ orderBy: 'downloads' }); // The mock doesn't actually sort, but we verify the query is made expect(nodes).toBeDefined(); }); it('should handle order by updated', () => { const nodes = repository.getCommunityNodes({ orderBy: 'updated' }); expect(nodes).toBeDefined(); }); }); describe('getCommunityStats', () => { beforeEach(() => { mockAdapter._setMockData('community_nodes', [...sampleCommunityNodes]); }); it('should return correct community statistics', () => { const stats = repository.getCommunityStats(); expect(stats.total).toBe(3); expect(stats.verified).toBe(2); expect(stats.unverified).toBe(1); }); it('should handle empty database', () => { mockAdapter._setMockData('community_nodes', []); const stats = repository.getCommunityStats(); expect(stats.total).toBe(0); expect(stats.verified).toBe(0); expect(stats.unverified).toBe(0); }); it('should handle all verified nodes', () => { mockAdapter._setMockData( 'community_nodes', sampleCommunityNodes.filter((n) => n.is_verified === 1) ); const stats = repository.getCommunityStats(); expect(stats.total).toBe(2); expect(stats.verified).toBe(2); expect(stats.unverified).toBe(0); }); it('should handle all unverified nodes', () => { mockAdapter._setMockData( 'community_nodes', sampleCommunityNodes.filter((n) => n.is_verified === 0) ); const stats = repository.getCommunityStats(); expect(stats.total).toBe(1); expect(stats.verified).toBe(0); expect(stats.unverified).toBe(1); }); }); describe('hasNodeByNpmPackage', () => { beforeEach(() => { mockAdapter._setMockData('community_nodes', [...sampleCommunityNodes]); }); it('should return true for existing package', () => { const exists = repository.hasNodeByNpmPackage('n8n-nodes-verified'); expect(exists).toBe(true); }); it('should return false for non-existent package', () => { const exists = repository.hasNodeByNpmPackage('n8n-nodes-nonexistent'); expect(exists).toBe(false); }); it('should handle empty package name', () => { const exists = repository.hasNodeByNpmPackage(''); expect(exists).toBe(false); }); }); describe('getNodeByNpmPackage', () => { beforeEach(() => { mockAdapter._setMockData('community_nodes', [...sampleCommunityNodes]); }); it('should return node for existing package', () => { const node = repository.getNodeByNpmPackage('n8n-nodes-verified'); expect(node).toBeDefined(); expect(node.npmPackageName).toBe('n8n-nodes-verified'); expect(node.displayName).toBe('Verified Test Node'); }); it('should return null for non-existent package', () => { const node = repository.getNodeByNpmPackage('n8n-nodes-nonexistent'); expect(node).toBeNull(); }); it('should correctly parse all community fields', () => { const node = repository.getNodeByNpmPackage('n8n-nodes-popular'); expect(node).toBeDefined(); expect(node.isCommunity).toBe(true); expect(node.isVerified).toBe(true); expect(node.isWebhook).toBe(true); expect(node.isVersioned).toBe(true); expect(node.npmDownloads).toBe(50000); }); }); describe('deleteCommunityNodes', () => { beforeEach(() => { mockAdapter._setMockData('community_nodes', [...sampleCommunityNodes]); }); it('should delete all community nodes and return count', () => { const deletedCount = repository.deleteCommunityNodes(); expect(deletedCount).toBe(3); expect(mockAdapter._getMockData('community_nodes')).toHaveLength(0); }); it('should handle empty database', () => { mockAdapter._setMockData('community_nodes', []); const deletedCount = repository.deleteCommunityNodes(); expect(deletedCount).toBe(0); }); }); describe('saveNode with community fields', () => { it('should save a community node with all fields', () => { const communityNode: ParsedNode & CommunityNodeFields = { nodeType: 'n8n-nodes-new.newNode', packageName: 'n8n-nodes-new', displayName: 'New Community Node', description: 'A brand new community node', category: 'Community', style: 'declarative', properties: [], credentials: [], operations: [], isAITool: false, isTrigger: false, isWebhook: false, isVersioned: false, version: '1.0.0', isCommunity: true, isVerified: true, authorName: 'New Author', authorGithubUrl: 'https://github.com/newauthor', npmPackageName: 'n8n-nodes-new', npmVersion: '1.0.0', npmDownloads: 100, communityFetchedAt: new Date().toISOString(), }; repository.saveNode(communityNode); const savedNodes = mockAdapter._getMockData('community_nodes'); expect(savedNodes).toHaveLength(1); expect(savedNodes[0].node_type).toBe('n8n-nodes-new.newNode'); expect(savedNodes[0].is_community).toBe(1); expect(savedNodes[0].is_verified).toBe(1); }); it('should save a core node without community fields', () => { const coreNode: ParsedNode = { nodeType: 'nodes-base.httpRequest', packageName: 'n8n-nodes-base', displayName: 'HTTP Request', description: 'Makes an HTTP request', category: 'Core', style: 'declarative', properties: [], credentials: [], operations: [], isAITool: false, isTrigger: false, isWebhook: false, isVersioned: true, version: '4.0', }; repository.saveNode(coreNode); const savedNodes = mockAdapter._getMockData('community_nodes'); expect(savedNodes).toHaveLength(1); expect(savedNodes[0].is_community).toBe(0); }); it('should update existing community node', () => { mockAdapter._setMockData('community_nodes', [...sampleCommunityNodes]); const updatedNode: ParsedNode & CommunityNodeFields = { nodeType: 'n8n-nodes-verified.testNode', packageName: 'n8n-nodes-verified', displayName: 'Updated Verified Node', description: 'Updated description', category: 'Community', style: 'declarative', properties: [], credentials: [], operations: [], isAITool: false, isTrigger: false, isWebhook: false, isVersioned: false, version: '1.1.0', isCommunity: true, isVerified: true, authorName: 'Verified Author', npmPackageName: 'n8n-nodes-verified', npmVersion: '1.1.0', npmDownloads: 6000, communityFetchedAt: new Date().toISOString(), }; repository.saveNode(updatedNode); const savedNodes = mockAdapter._getMockData('community_nodes'); const updatedSaved = savedNodes.find( (n: any) => n.node_type === 'n8n-nodes-verified.testNode' ); expect(updatedSaved).toBeDefined(); expect(updatedSaved.display_name).toBe('Updated Verified Node'); }); }); describe('edge cases', () => { it('should handle null values in community fields', () => { const nodeWithNulls = { ...sampleCommunityNodes[0], author_name: null, author_github_url: null, npm_package_name: null, npm_version: null, community_fetched_at: null, }; mockAdapter._setMockData('community_nodes', [nodeWithNulls]); const nodes = repository.getCommunityNodes(); expect(nodes).toHaveLength(1); expect(nodes[0].authorName).toBeNull(); expect(nodes[0].npmPackageName).toBeNull(); }); it('should handle zero downloads', () => { const nodeWithZeroDownloads = { ...sampleCommunityNodes[0], npm_downloads: 0, }; mockAdapter._setMockData('community_nodes', [nodeWithZeroDownloads]); const nodes = repository.getCommunityNodes(); expect(nodes[0].npmDownloads).toBe(0); }); it('should handle very large download counts', () => { const nodeWithManyDownloads = { ...sampleCommunityNodes[0], npm_downloads: 10000000, }; mockAdapter._setMockData('community_nodes', [nodeWithManyDownloads]); const nodes = repository.getCommunityNodes(); expect(nodes[0].npmDownloads).toBe(10000000); }); it('should handle special characters in author name', () => { const nodeWithSpecialChars = { ...sampleCommunityNodes[0], author_name: "O'Brien & Sons ", }; mockAdapter._setMockData('community_nodes', [nodeWithSpecialChars]); const nodes = repository.getCommunityNodes(); expect(nodes[0].authorName).toBe("O'Brien & Sons "); }); it('should handle Unicode in display name', () => { const nodeWithUnicode = { ...sampleCommunityNodes[0], display_name: 'Test Node', }; mockAdapter._setMockData('community_nodes', [nodeWithUnicode]); const nodes = repository.getCommunityNodes(); expect(nodes[0].displayName).toBe('Test Node'); }); it('should handle combined filters', () => { mockAdapter._setMockData('community_nodes', [...sampleCommunityNodes]); const nodes = repository.getCommunityNodes({ verified: true, limit: 1, orderBy: 'downloads', }); expect(nodes).toHaveLength(1); expect(nodes[0].isVerified).toBe(true); }); }); });