Files
n8n-mcp/tests/integration/database/template-repository.test.ts

439 lines
14 KiB
TypeScript

import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import * as Database from 'better-sqlite3';
import { TemplateRepository } from '../../../src/templates/template-repository';
import { DatabaseAdapter } from '../../../src/database/database-adapter';
import { TestDatabase, TestDataGenerator } from './test-utils';
import { TemplateWorkflow, TemplateDetail } from '../../../src/templates/template-fetcher';
describe('TemplateRepository Integration Tests', () => {
let testDb: TestDatabase;
let db: Database;
let repository: TemplateRepository;
let adapter: DatabaseAdapter;
beforeEach(async () => {
testDb = new TestDatabase({ mode: 'memory', enableFTS5: true });
db = await testDb.initialize();
adapter = new DatabaseAdapter(db);
repository = new TemplateRepository(adapter);
});
afterEach(async () => {
await testDb.cleanup();
});
describe('saveTemplate', () => {
it('should save single template successfully', () => {
const template = createTemplateWorkflow();
repository.saveTemplate(template);
const saved = repository.getTemplate(template.id);
expect(saved).toBeTruthy();
expect(saved?.workflow_id).toBe(template.id);
expect(saved?.name).toBe(template.name);
});
it('should update existing template', () => {
const template = createTemplateWorkflow();
// Save initial version
repository.saveTemplate(template);
// Update and save again
const updated: TemplateWorkflow = { ...template, name: 'Updated Template' };
repository.saveTemplate(updated);
const saved = repository.getTemplate(template.id);
expect(saved?.name).toBe('Updated Template');
// Should not create duplicate
const all = repository.getAllTemplates();
expect(all).toHaveLength(1);
});
it('should handle templates with complex node types', () => {
const template = createTemplateWorkflow({
id: 1,
nodes: [
{
id: 'node1',
name: 'Webhook',
type: 'n8n-nodes-base.webhook',
typeVersion: 1,
position: [100, 100],
parameters: {}
},
{
id: 'node2',
name: 'HTTP Request',
type: 'n8n-nodes-base.httpRequest',
typeVersion: 3,
position: [300, 100],
parameters: {
url: 'https://api.example.com',
method: 'POST'
}
}
]
});
repository.saveTemplate(template);
const saved = repository.getTemplate(template.id);
expect(saved).toBeTruthy();
const nodesUsed = JSON.parse(saved!.nodes_used);
expect(nodesUsed).toContain('n8n-nodes-base.webhook');
expect(nodesUsed).toContain('n8n-nodes-base.httpRequest');
});
it('should sanitize workflow data before saving', () => {
const template = createTemplateWorkflow({
workflowInfo: {
nodeCount: 5,
webhookCount: 1,
// Add some data that should be sanitized
executionId: 'should-be-removed',
pinData: { node1: { data: 'sensitive' } }
}
});
repository.saveTemplate(template);
const saved = repository.getTemplate(template.id);
expect(saved).toBeTruthy();
const workflowJson = JSON.parse(saved!.workflow_json);
expect(workflowJson.pinData).toBeUndefined();
});
});
describe('getTemplate', () => {
beforeEach(() => {
const templates = [
createTemplateWorkflow({ id: 1, name: 'Template 1' }),
createTemplateWorkflow({ id: 2, name: 'Template 2' })
];
templates.forEach(t => repository.saveTemplate(t));
});
it('should retrieve template by id', () => {
const template = repository.getTemplate(1);
expect(template).toBeTruthy();
expect(template?.name).toBe('Template 1');
});
it('should return null for non-existent template', () => {
const template = repository.getTemplate(999);
expect(template).toBeNull();
});
});
describe('searchTemplates with FTS5', () => {
beforeEach(() => {
const templates = [
createTemplateWorkflow({
id: 1,
name: 'Webhook to Slack',
description: 'Send Slack notifications when webhook received'
}),
createTemplateWorkflow({
id: 2,
name: 'HTTP Data Processing',
description: 'Process data from HTTP requests'
}),
createTemplateWorkflow({
id: 3,
name: 'Email Automation',
description: 'Automate email sending workflow'
})
];
templates.forEach(t => repository.saveTemplate(t));
});
it('should search templates by name', () => {
const results = repository.searchTemplates('webhook');
expect(results).toHaveLength(1);
expect(results[0].name).toBe('Webhook to Slack');
});
it('should search templates by description', () => {
const results = repository.searchTemplates('automate');
expect(results).toHaveLength(1);
expect(results[0].name).toBe('Email Automation');
});
it('should handle multiple search terms', () => {
const results = repository.searchTemplates('data process');
expect(results).toHaveLength(1);
expect(results[0].name).toBe('HTTP Data Processing');
});
it('should limit search results', () => {
// Add more templates
for (let i = 4; i <= 20; i++) {
repository.saveTemplate(createTemplateWorkflow({
id: i,
name: `Test Template ${i}`,
description: 'Test description'
}));
}
const results = repository.searchTemplates('test', 5);
expect(results).toHaveLength(5);
});
it('should handle special characters in search', () => {
repository.saveTemplate(createTemplateWorkflow({
id: 100,
name: 'Special @ # $ Template',
description: 'Template with special characters'
}));
const results = repository.searchTemplates('special');
expect(results.length).toBeGreaterThan(0);
});
});
describe('getTemplatesByNodeTypes', () => {
beforeEach(() => {
const templates = [
createTemplateWorkflow({
id: 1,
nodes: [
{ type: 'n8n-nodes-base.webhook' },
{ type: 'n8n-nodes-base.slack' }
]
}),
createTemplateWorkflow({
id: 2,
nodes: [
{ type: 'n8n-nodes-base.httpRequest' },
{ type: 'n8n-nodes-base.set' }
]
}),
createTemplateWorkflow({
id: 3,
nodes: [
{ type: 'n8n-nodes-base.webhook' },
{ type: 'n8n-nodes-base.httpRequest' }
]
})
];
templates.forEach(t => repository.saveTemplate(t));
});
it('should find templates using specific node types', () => {
const results = repository.getTemplatesByNodeTypes(['n8n-nodes-base.webhook']);
expect(results).toHaveLength(2);
expect(results.map(r => r.workflow_id)).toContain(1);
expect(results.map(r => r.workflow_id)).toContain(3);
});
it('should find templates using multiple node types', () => {
const results = repository.getTemplatesByNodeTypes([
'n8n-nodes-base.webhook',
'n8n-nodes-base.slack'
]);
expect(results).toHaveLength(1);
expect(results[0].workflow_id).toBe(1);
});
it('should return empty array for non-existent node types', () => {
const results = repository.getTemplatesByNodeTypes(['non-existent-node']);
expect(results).toHaveLength(0);
});
it('should limit results', () => {
const results = repository.getTemplatesByNodeTypes(['n8n-nodes-base.webhook'], 1);
expect(results).toHaveLength(1);
});
});
describe('getAllTemplates', () => {
it('should return empty array when no templates', () => {
const templates = repository.getAllTemplates();
expect(templates).toHaveLength(0);
});
it('should return all templates with limit', () => {
for (let i = 1; i <= 20; i++) {
repository.saveTemplate(createTemplateWorkflow({ id: i }));
}
const templates = repository.getAllTemplates(10);
expect(templates).toHaveLength(10);
});
it('should order templates by updated_at descending', () => {
// Save templates with slight delay to ensure different timestamps
const template1 = createTemplateWorkflow({ id: 1, name: 'First' });
repository.saveTemplate(template1);
// Small delay
const template2 = createTemplateWorkflow({ id: 2, name: 'Second' });
repository.saveTemplate(template2);
const templates = repository.getAllTemplates();
expect(templates).toHaveLength(2);
// Most recent should be first
expect(templates[0].name).toBe('Second');
});
});
describe('getTemplateDetail', () => {
it('should return template with full workflow data', () => {
const template = createTemplateDetail();
repository.saveTemplateDetail(template);
const saved = repository.getTemplateDetail(template.id);
expect(saved).toBeTruthy();
expect(saved?.workflow).toBeTruthy();
expect(saved?.workflow.nodes).toHaveLength(template.workflow.nodes.length);
});
it('should handle missing workflow gracefully', () => {
const template = createTemplateWorkflow({ id: 1 });
repository.saveTemplate(template);
const detail = repository.getTemplateDetail(1);
expect(detail).toBeNull();
});
});
describe('clearOldTemplates', () => {
it('should remove templates older than specified days', () => {
// Insert old template (30 days ago)
db.prepare(`
INSERT INTO templates (
id, workflow_id, name, description,
nodes_used, workflow_json, categories, views,
created_at, updated_at
) VALUES (?, ?, ?, ?, '[]', '{}', '[]', 0, datetime('now', '-31 days'), datetime('now', '-31 days'))
`).run(1, 1001, 'Old Template', 'Old template');
// Insert recent template
repository.saveTemplate(createTemplateWorkflow({ id: 2, name: 'Recent Template' }));
// Clear templates older than 30 days
const deleted = repository.clearOldTemplates(30);
expect(deleted).toBe(1);
const remaining = repository.getAllTemplates();
expect(remaining).toHaveLength(1);
expect(remaining[0].name).toBe('Recent Template');
});
});
describe('Transaction handling', () => {
it('should rollback on error during bulk save', () => {
const templates = [
createTemplateWorkflow({ id: 1 }),
createTemplateWorkflow({ id: 2 }),
{ id: null } as any // Invalid template
];
expect(() => {
templates.forEach(t => repository.saveTemplate(t));
}).toThrow();
// No templates should be saved due to error
const all = repository.getAllTemplates();
expect(all).toHaveLength(0);
});
});
describe('FTS5 performance', () => {
it('should handle large dataset searches efficiently', () => {
// Insert 1000 templates
const templates = Array.from({ length: 1000 }, (_, i) =>
createTemplateWorkflow({
id: i + 1,
name: `Template ${i}`,
description: `Description for ${['webhook', 'http', 'automation', 'data'][i % 4]} workflow ${i}`
})
);
const insertMany = db.transaction((templates: TemplateWorkflow[]) => {
templates.forEach(t => repository.saveTemplate(t));
});
const start = Date.now();
insertMany(templates);
const insertDuration = Date.now() - start;
expect(insertDuration).toBeLessThan(2000); // Should complete in under 2 seconds
// Test search performance
const searchStart = Date.now();
const results = repository.searchTemplates('webhook', 50);
const searchDuration = Date.now() - searchStart;
expect(searchDuration).toBeLessThan(50); // Search should be very fast
expect(results).toHaveLength(50);
});
});
});
// Helper functions
function createTemplateWorkflow(overrides: any = {}): TemplateWorkflow {
const id = overrides.id || Math.floor(Math.random() * 10000);
const nodes = overrides.nodes || [
{
id: 'node1',
name: 'Start',
type: 'n8n-nodes-base.start',
typeVersion: 1,
position: [100, 100],
parameters: {}
}
];
return {
id,
name: overrides.name || `Test Workflow ${id}`,
workflow: {
nodes: nodes.map((n: any) => ({
id: n.id || 'node1',
name: n.name || 'Node',
type: n.type || 'n8n-nodes-base.start',
typeVersion: n.typeVersion || 1,
position: n.position || [100, 100],
parameters: n.parameters || {}
})),
connections: overrides.connections || {},
settings: overrides.settings || {}
},
user: {
username: overrides.username || 'testuser'
},
views: overrides.views || 100,
totalViews: overrides.totalViews || 100,
createdAt: overrides.createdAt || new Date().toISOString(),
updatedAt: overrides.updatedAt || new Date().toISOString(),
description: overrides.description,
workflowInfo: overrides.workflowInfo || {
nodeCount: nodes.length,
webhookCount: nodes.filter((n: any) => n.type?.includes('webhook')).length
},
...overrides
};
}
function createTemplateDetail(overrides: any = {}): TemplateDetail {
const base = createTemplateWorkflow(overrides);
return {
...base,
workflow: {
id: base.id.toString(),
name: base.name,
nodes: base.workflow.nodes,
connections: base.workflow.connections,
settings: base.workflow.settings,
pinData: overrides.pinData
},
categories: overrides.categories || [
{ id: 1, name: 'automation' }
]
};
}