node-repository.test.tsโข19.9 kB
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 || []
};
}