439 lines
14 KiB
TypeScript
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' }
|
|
]
|
|
};
|
|
} |