mirror of
https://github.com/czlonkowski/n8n-mcp.git
synced 2026-01-30 06:22:04 +00:00
fix: update tests for template compression, pagination, and quality filtering
- Fix parameter validation tests to expect mode parameter in getTemplate calls - Update database utils tests to use totalViews > 10 for quality filter - Add comprehensive tests for template service functionality - Fix integration tests for new pagination parameters All CI tests now passing after template system enhancements
This commit is contained in:
@@ -163,7 +163,8 @@ describe('Database Integration Tests', () => {
|
|||||||
expect(template!.name).toBe('AI Content Generator');
|
expect(template!.name).toBe('AI Content Generator');
|
||||||
|
|
||||||
// Parse workflow JSON
|
// Parse workflow JSON
|
||||||
const workflow = JSON.parse(template!.workflow_json);
|
expect(template!.workflow_json).toBeTruthy();
|
||||||
|
const workflow = JSON.parse(template!.workflow_json!);
|
||||||
expect(workflow.nodes).toHaveLength(3);
|
expect(workflow.nodes).toHaveLength(3);
|
||||||
expect(workflow.nodes[0].name).toBe('Webhook');
|
expect(workflow.nodes[0].name).toBe('Webhook');
|
||||||
expect(workflow.nodes[1].name).toBe('OpenAI');
|
expect(workflow.nodes[1].name).toBe('OpenAI');
|
||||||
|
|||||||
@@ -131,7 +131,8 @@ describe('TemplateRepository Integration Tests', () => {
|
|||||||
const saved = repository.getTemplate(template.id);
|
const saved = repository.getTemplate(template.id);
|
||||||
expect(saved).toBeTruthy();
|
expect(saved).toBeTruthy();
|
||||||
|
|
||||||
const workflowJson = JSON.parse(saved!.workflow_json);
|
expect(saved!.workflow_json).toBeTruthy();
|
||||||
|
const workflowJson = JSON.parse(saved!.workflow_json!);
|
||||||
expect(workflowJson.pinData).toBeUndefined();
|
expect(workflowJson.pinData).toBeUndefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -239,6 +240,32 @@ describe('TemplateRepository Integration Tests', () => {
|
|||||||
const results = repository.searchTemplates('special');
|
const results = repository.searchTemplates('special');
|
||||||
expect(results.length).toBeGreaterThan(0);
|
expect(results.length).toBeGreaterThan(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should support pagination in search results', () => {
|
||||||
|
for (let i = 1; i <= 15; i++) {
|
||||||
|
const template = createTemplateWorkflow({
|
||||||
|
id: i,
|
||||||
|
name: `Search Template ${i}`,
|
||||||
|
description: 'Common search term'
|
||||||
|
});
|
||||||
|
const detail = createTemplateDetail({ id: i });
|
||||||
|
repository.saveTemplate(template, detail);
|
||||||
|
}
|
||||||
|
|
||||||
|
const page1 = repository.searchTemplates('search', 5, 0);
|
||||||
|
expect(page1).toHaveLength(5);
|
||||||
|
|
||||||
|
const page2 = repository.searchTemplates('search', 5, 5);
|
||||||
|
expect(page2).toHaveLength(5);
|
||||||
|
|
||||||
|
const page3 = repository.searchTemplates('search', 5, 10);
|
||||||
|
expect(page3).toHaveLength(5);
|
||||||
|
|
||||||
|
// Should be different templates on each page
|
||||||
|
const page1Ids = page1.map(t => t.id);
|
||||||
|
const page2Ids = page2.map(t => t.id);
|
||||||
|
expect(page1Ids.filter(id => page2Ids.includes(id))).toHaveLength(0);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getTemplatesByNodeTypes', () => {
|
describe('getTemplatesByNodeTypes', () => {
|
||||||
@@ -319,6 +346,17 @@ describe('TemplateRepository Integration Tests', () => {
|
|||||||
const results = repository.getTemplatesByNodes(['n8n-nodes-base.webhook'], 1);
|
const results = repository.getTemplatesByNodes(['n8n-nodes-base.webhook'], 1);
|
||||||
expect(results).toHaveLength(1);
|
expect(results).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should support pagination with offset', () => {
|
||||||
|
const results1 = repository.getTemplatesByNodes(['n8n-nodes-base.webhook'], 1, 0);
|
||||||
|
expect(results1).toHaveLength(1);
|
||||||
|
|
||||||
|
const results2 = repository.getTemplatesByNodes(['n8n-nodes-base.webhook'], 1, 1);
|
||||||
|
expect(results2).toHaveLength(1);
|
||||||
|
|
||||||
|
// Results should be different
|
||||||
|
expect(results1[0].id).not.toBe(results2[0].id);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getAllTemplates', () => {
|
describe('getAllTemplates', () => {
|
||||||
@@ -338,6 +376,51 @@ describe('TemplateRepository Integration Tests', () => {
|
|||||||
expect(templates).toHaveLength(10);
|
expect(templates).toHaveLength(10);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should support pagination with offset', () => {
|
||||||
|
for (let i = 1; i <= 15; i++) {
|
||||||
|
const template = createTemplateWorkflow({ id: i });
|
||||||
|
const detail = createTemplateDetail({ id: i });
|
||||||
|
repository.saveTemplate(template, detail);
|
||||||
|
}
|
||||||
|
|
||||||
|
const page1 = repository.getAllTemplates(5, 0);
|
||||||
|
expect(page1).toHaveLength(5);
|
||||||
|
|
||||||
|
const page2 = repository.getAllTemplates(5, 5);
|
||||||
|
expect(page2).toHaveLength(5);
|
||||||
|
|
||||||
|
const page3 = repository.getAllTemplates(5, 10);
|
||||||
|
expect(page3).toHaveLength(5);
|
||||||
|
|
||||||
|
// Should be different templates on each page
|
||||||
|
const page1Ids = page1.map(t => t.id);
|
||||||
|
const page2Ids = page2.map(t => t.id);
|
||||||
|
const page3Ids = page3.map(t => t.id);
|
||||||
|
|
||||||
|
expect(page1Ids.filter(id => page2Ids.includes(id))).toHaveLength(0);
|
||||||
|
expect(page2Ids.filter(id => page3Ids.includes(id))).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support different sort orders', () => {
|
||||||
|
const template1 = createTemplateWorkflow({ id: 1, name: 'Alpha Template', totalViews: 50 });
|
||||||
|
const detail1 = createTemplateDetail({ id: 1 });
|
||||||
|
repository.saveTemplate(template1, detail1);
|
||||||
|
|
||||||
|
const template2 = createTemplateWorkflow({ id: 2, name: 'Beta Template', totalViews: 100 });
|
||||||
|
const detail2 = createTemplateDetail({ id: 2 });
|
||||||
|
repository.saveTemplate(template2, detail2);
|
||||||
|
|
||||||
|
// Sort by views (default) - highest first
|
||||||
|
const byViews = repository.getAllTemplates(10, 0, 'views');
|
||||||
|
expect(byViews[0].name).toBe('Beta Template');
|
||||||
|
expect(byViews[1].name).toBe('Alpha Template');
|
||||||
|
|
||||||
|
// Sort by name - alphabetical
|
||||||
|
const byName = repository.getAllTemplates(10, 0, 'name');
|
||||||
|
expect(byName[0].name).toBe('Alpha Template');
|
||||||
|
expect(byName[1].name).toBe('Beta Template');
|
||||||
|
});
|
||||||
|
|
||||||
it('should order templates by views and created_at descending', () => {
|
it('should order templates by views and created_at descending', () => {
|
||||||
// Save templates with different views to ensure predictable ordering
|
// Save templates with different views to ensure predictable ordering
|
||||||
const template1 = createTemplateWorkflow({ id: 1, name: 'First', totalViews: 50 });
|
const template1 = createTemplateWorkflow({ id: 1, name: 'First', totalViews: 50 });
|
||||||
@@ -365,7 +448,7 @@ describe('TemplateRepository Integration Tests', () => {
|
|||||||
const saved = repository.getTemplate(1);
|
const saved = repository.getTemplate(1);
|
||||||
expect(saved).toBeTruthy();
|
expect(saved).toBeTruthy();
|
||||||
expect(saved?.workflow_json).toBeTruthy();
|
expect(saved?.workflow_json).toBeTruthy();
|
||||||
const workflow = JSON.parse(saved!.workflow_json);
|
const workflow = JSON.parse(saved!.workflow_json!);
|
||||||
expect(workflow.nodes).toHaveLength(detail.workflow.nodes.length);
|
expect(workflow.nodes).toHaveLength(detail.workflow.nodes.length);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -462,6 +545,104 @@ describe('TemplateRepository Integration Tests', () => {
|
|||||||
expect(results).toHaveLength(50);
|
expect(results).toHaveLength(50);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('New pagination count methods', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// Set up test data
|
||||||
|
for (let i = 1; i <= 25; i++) {
|
||||||
|
const template = createTemplateWorkflow({
|
||||||
|
id: i,
|
||||||
|
name: `Template ${i}`,
|
||||||
|
description: i <= 10 ? 'webhook automation' : 'data processing'
|
||||||
|
});
|
||||||
|
const detail = createTemplateDetail({
|
||||||
|
id: i,
|
||||||
|
workflow: {
|
||||||
|
nodes: i <= 15 ? [
|
||||||
|
{ id: 'node1', type: 'n8n-nodes-base.webhook', name: 'Webhook', position: [0, 0], parameters: {}, typeVersion: 1 }
|
||||||
|
] : [
|
||||||
|
{ id: 'node1', type: 'n8n-nodes-base.httpRequest', name: 'HTTP', position: [0, 0], parameters: {}, typeVersion: 1 }
|
||||||
|
],
|
||||||
|
connections: {},
|
||||||
|
settings: {}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
repository.saveTemplate(template, detail);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getNodeTemplatesCount', () => {
|
||||||
|
it('should return correct count for node type searches', () => {
|
||||||
|
const webhookCount = repository.getNodeTemplatesCount(['n8n-nodes-base.webhook']);
|
||||||
|
expect(webhookCount).toBe(15);
|
||||||
|
|
||||||
|
const httpCount = repository.getNodeTemplatesCount(['n8n-nodes-base.httpRequest']);
|
||||||
|
expect(httpCount).toBe(10);
|
||||||
|
|
||||||
|
const bothCount = repository.getNodeTemplatesCount([
|
||||||
|
'n8n-nodes-base.webhook',
|
||||||
|
'n8n-nodes-base.httpRequest'
|
||||||
|
]);
|
||||||
|
expect(bothCount).toBe(25); // OR query, so all templates
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 0 for non-existent node types', () => {
|
||||||
|
const count = repository.getNodeTemplatesCount(['non-existent-node']);
|
||||||
|
expect(count).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getSearchCount', () => {
|
||||||
|
it('should return correct count for search queries', () => {
|
||||||
|
const webhookSearchCount = repository.getSearchCount('webhook');
|
||||||
|
expect(webhookSearchCount).toBe(10);
|
||||||
|
|
||||||
|
const processingSearchCount = repository.getSearchCount('processing');
|
||||||
|
expect(processingSearchCount).toBe(15);
|
||||||
|
|
||||||
|
const noResultsCount = repository.getSearchCount('nonexistent');
|
||||||
|
expect(noResultsCount).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getTaskTemplatesCount', () => {
|
||||||
|
it('should return correct count for task-based searches', () => {
|
||||||
|
const webhookTaskCount = repository.getTaskTemplatesCount('webhook_processing');
|
||||||
|
expect(webhookTaskCount).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
const unknownTaskCount = repository.getTaskTemplatesCount('unknown_task');
|
||||||
|
expect(unknownTaskCount).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getTemplateCount', () => {
|
||||||
|
it('should return total template count', () => {
|
||||||
|
const totalCount = repository.getTemplateCount();
|
||||||
|
expect(totalCount).toBe(25);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 0 for empty database', () => {
|
||||||
|
repository.clearTemplates();
|
||||||
|
const count = repository.getTemplateCount();
|
||||||
|
expect(count).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getTemplatesForTask with pagination', () => {
|
||||||
|
it('should support pagination for task-based searches', () => {
|
||||||
|
const page1 = repository.getTemplatesForTask('webhook_processing', 5, 0);
|
||||||
|
const page2 = repository.getTemplatesForTask('webhook_processing', 5, 5);
|
||||||
|
|
||||||
|
expect(page1).toHaveLength(5);
|
||||||
|
expect(page2).toHaveLength(5);
|
||||||
|
|
||||||
|
// Should be different results
|
||||||
|
const page1Ids = page1.map(t => t.id);
|
||||||
|
const page2Ids = page2.map(t => t.id);
|
||||||
|
expect(page1Ids.filter(id => page2Ids.includes(id))).toHaveLength(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Helper functions
|
// Helper functions
|
||||||
|
|||||||
@@ -173,6 +173,7 @@ describe('TemplateRepository - Core Functionality', () => {
|
|||||||
call => call[0].includes('INSERT OR REPLACE INTO templates')
|
call => call[0].includes('INSERT OR REPLACE INTO templates')
|
||||||
)?.[0] || '');
|
)?.[0] || '');
|
||||||
|
|
||||||
|
// The implementation now uses gzip compression, so we just verify the call happened
|
||||||
expect(stmt?.run).toHaveBeenCalledWith(
|
expect(stmt?.run).toHaveBeenCalledWith(
|
||||||
123, // id
|
123, // id
|
||||||
123, // workflow_id
|
123, // workflow_id
|
||||||
@@ -182,14 +183,7 @@ describe('TemplateRepository - Core Functionality', () => {
|
|||||||
'johndoe',
|
'johndoe',
|
||||||
1, // verified
|
1, // verified
|
||||||
JSON.stringify(['n8n-nodes-base.httpRequest', 'n8n-nodes-base.slack']),
|
JSON.stringify(['n8n-nodes-base.httpRequest', 'n8n-nodes-base.slack']),
|
||||||
JSON.stringify({
|
expect.any(String), // compressed workflow JSON
|
||||||
nodes: [
|
|
||||||
{ type: 'n8n-nodes-base.httpRequest', name: 'HTTP Request', id: '1', position: [0, 0], parameters: {}, typeVersion: 1 },
|
|
||||||
{ type: 'n8n-nodes-base.slack', name: 'Slack', id: '2', position: [100, 0], parameters: {}, typeVersion: 1 }
|
|
||||||
],
|
|
||||||
connections: {},
|
|
||||||
settings: {}
|
|
||||||
}),
|
|
||||||
JSON.stringify(['automation', 'integration']),
|
JSON.stringify(['automation', 'integration']),
|
||||||
1000, // views
|
1000, // views
|
||||||
'2024-01-01T00:00:00Z',
|
'2024-01-01T00:00:00Z',
|
||||||
@@ -316,7 +310,7 @@ describe('TemplateRepository - Core Functionality', () => {
|
|||||||
|
|
||||||
const results = repository.getTemplatesByNodes(['n8n-nodes-base.httpRequest'], 5);
|
const results = repository.getTemplatesByNodes(['n8n-nodes-base.httpRequest'], 5);
|
||||||
|
|
||||||
expect(stmt.all).toHaveBeenCalledWith('%"n8n-nodes-base.httpRequest"%', 5);
|
expect(stmt.all).toHaveBeenCalledWith('%"n8n-nodes-base.httpRequest"%', 5, 0);
|
||||||
expect(results).toEqual(mockTemplates);
|
expect(results).toEqual(mockTemplates);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -391,6 +385,102 @@ describe('TemplateRepository - Core Functionality', () => {
|
|||||||
expect(stats.topUsedNodes).toContainEqual({ node: 'n8n-nodes-base.httpRequest', count: 2 });
|
expect(stats.topUsedNodes).toContainEqual({ node: 'n8n-nodes-base.httpRequest', count: 2 });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('pagination count methods', () => {
|
||||||
|
it('should get node templates count', () => {
|
||||||
|
mockAdapter._setMockData('node_templates_count', 15);
|
||||||
|
|
||||||
|
const stmt = new MockPreparedStatement('', new Map());
|
||||||
|
stmt.get = vi.fn(() => ({ count: 15 }));
|
||||||
|
mockAdapter.prepare = vi.fn(() => stmt);
|
||||||
|
|
||||||
|
const count = repository.getNodeTemplatesCount(['n8n-nodes-base.webhook']);
|
||||||
|
|
||||||
|
expect(count).toBe(15);
|
||||||
|
expect(stmt.get).toHaveBeenCalledWith('%"n8n-nodes-base.webhook"%');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get search count', () => {
|
||||||
|
const stmt = new MockPreparedStatement('', new Map());
|
||||||
|
stmt.get = vi.fn(() => ({ count: 8 }));
|
||||||
|
mockAdapter.prepare = vi.fn(() => stmt);
|
||||||
|
|
||||||
|
const count = repository.getSearchCount('webhook');
|
||||||
|
|
||||||
|
expect(count).toBe(8);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get task templates count', () => {
|
||||||
|
const stmt = new MockPreparedStatement('', new Map());
|
||||||
|
stmt.get = vi.fn(() => ({ count: 12 }));
|
||||||
|
mockAdapter.prepare = vi.fn(() => stmt);
|
||||||
|
|
||||||
|
const count = repository.getTaskTemplatesCount('ai_automation');
|
||||||
|
|
||||||
|
expect(count).toBe(12);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle pagination in getAllTemplates', () => {
|
||||||
|
const mockTemplates = [
|
||||||
|
{ id: 1, name: 'Template 1' },
|
||||||
|
{ id: 2, name: 'Template 2' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const stmt = new MockPreparedStatement('', new Map());
|
||||||
|
stmt.all = vi.fn(() => mockTemplates);
|
||||||
|
mockAdapter.prepare = vi.fn(() => stmt);
|
||||||
|
|
||||||
|
const results = repository.getAllTemplates(10, 5, 'name');
|
||||||
|
|
||||||
|
expect(results).toEqual(mockTemplates);
|
||||||
|
expect(stmt.all).toHaveBeenCalledWith(10, 5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle pagination in getTemplatesByNodes', () => {
|
||||||
|
const mockTemplates = [
|
||||||
|
{ id: 1, nodes_used: '["n8n-nodes-base.webhook"]' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const stmt = new MockPreparedStatement('', new Map());
|
||||||
|
stmt.all = vi.fn(() => mockTemplates);
|
||||||
|
mockAdapter.prepare = vi.fn(() => stmt);
|
||||||
|
|
||||||
|
const results = repository.getTemplatesByNodes(['n8n-nodes-base.webhook'], 5, 10);
|
||||||
|
|
||||||
|
expect(results).toEqual(mockTemplates);
|
||||||
|
expect(stmt.all).toHaveBeenCalledWith('%"n8n-nodes-base.webhook"%', 5, 10);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle pagination in searchTemplates', () => {
|
||||||
|
const mockTemplates = [
|
||||||
|
{ id: 1, name: 'Search Result 1' }
|
||||||
|
];
|
||||||
|
|
||||||
|
mockAdapter._setMockData('fts_results', mockTemplates);
|
||||||
|
|
||||||
|
const stmt = new MockPreparedStatement('', new Map());
|
||||||
|
stmt.all = vi.fn(() => mockTemplates);
|
||||||
|
mockAdapter.prepare = vi.fn(() => stmt);
|
||||||
|
|
||||||
|
const results = repository.searchTemplates('webhook', 20, 40);
|
||||||
|
|
||||||
|
expect(results).toEqual(mockTemplates);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle pagination in getTemplatesForTask', () => {
|
||||||
|
const mockTemplates = [
|
||||||
|
{ id: 1, categories: '["ai"]' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const stmt = new MockPreparedStatement('', new Map());
|
||||||
|
stmt.all = vi.fn(() => mockTemplates);
|
||||||
|
mockAdapter.prepare = vi.fn(() => stmt);
|
||||||
|
|
||||||
|
const results = repository.getTemplatesForTask('ai_automation', 15, 30);
|
||||||
|
|
||||||
|
expect(results).toEqual(mockTemplates);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('maintenance operations', () => {
|
describe('maintenance operations', () => {
|
||||||
it('should clear all templates', () => {
|
it('should clear all templates', () => {
|
||||||
|
|||||||
@@ -371,7 +371,7 @@ describe('Parameter Validation', () => {
|
|||||||
templateId: 123
|
templateId: 123
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(mockGetTemplate).toHaveBeenCalledWith(123);
|
expect(mockGetTemplate).toHaveBeenCalledWith(123, 'full');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should convert string templateId to number', async () => {
|
it('should convert string templateId to number', async () => {
|
||||||
@@ -381,7 +381,7 @@ describe('Parameter Validation', () => {
|
|||||||
templateId: '123'
|
templateId: '123'
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(mockGetTemplate).toHaveBeenCalledWith(123);
|
expect(mockGetTemplate).toHaveBeenCalledWith(123, 'full');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
481
tests/unit/mcp/template-handlers.test.ts
Normal file
481
tests/unit/mcp/template-handlers.test.ts
Normal file
@@ -0,0 +1,481 @@
|
|||||||
|
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
|
||||||
|
import { N8NDocumentationMCPServer } from '../../../src/mcp/server';
|
||||||
|
import { NodeRepository } from '../../../src/database/node-repository';
|
||||||
|
import { TemplateService, PaginatedResponse, TemplateMinimal } from '../../../src/templates/template-service';
|
||||||
|
import { DatabaseAdapter } from '../../../src/database/database-adapter';
|
||||||
|
|
||||||
|
// Mock dependencies
|
||||||
|
vi.mock('../../../src/utils/logger', () => ({
|
||||||
|
logger: {
|
||||||
|
info: vi.fn(),
|
||||||
|
warn: vi.fn(),
|
||||||
|
error: vi.fn(),
|
||||||
|
debug: vi.fn()
|
||||||
|
},
|
||||||
|
Logger: vi.fn().mockImplementation(() => ({
|
||||||
|
info: vi.fn(),
|
||||||
|
warn: vi.fn(),
|
||||||
|
error: vi.fn(),
|
||||||
|
debug: vi.fn()
|
||||||
|
}))
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../../../src/database/node-repository');
|
||||||
|
vi.mock('../../../src/templates/template-service');
|
||||||
|
vi.mock('../../../src/database/database-adapter');
|
||||||
|
|
||||||
|
// Create testable server class to access private methods
|
||||||
|
class TestableMCPServer extends N8NDocumentationMCPServer {
|
||||||
|
public async testListTemplates(limit?: number, offset?: number, sortBy?: 'views' | 'created_at' | 'name'): Promise<any> {
|
||||||
|
return (this as any).listTemplates(limit, offset, sortBy);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async testGetTemplate(templateId: number, mode?: 'full' | 'nodes_only' | 'structure'): Promise<any> {
|
||||||
|
return (this as any).getTemplate(templateId, mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async testGetDatabaseStatistics(): Promise<any> {
|
||||||
|
return (this as any).getDatabaseStatistics();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async testListNodeTemplates(nodeTypes: string[], limit?: number, offset?: number): Promise<any> {
|
||||||
|
return (this as any).listNodeTemplates(nodeTypes, limit, offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async testSearchTemplates(query: string, limit?: number, offset?: number): Promise<any> {
|
||||||
|
return (this as any).searchTemplates(query, limit, offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async testGetTemplatesForTask(task: string, limit?: number, offset?: number): Promise<any> {
|
||||||
|
return (this as any).getTemplatesForTask(task, limit, offset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('MCP Template Handlers', () => {
|
||||||
|
let server: TestableMCPServer;
|
||||||
|
let mockDb: DatabaseAdapter;
|
||||||
|
let mockNodeRepository: NodeRepository;
|
||||||
|
let mockTemplateService: TemplateService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
|
||||||
|
mockDb = {
|
||||||
|
prepare: vi.fn(),
|
||||||
|
exec: vi.fn(),
|
||||||
|
close: vi.fn(),
|
||||||
|
pragma: vi.fn(),
|
||||||
|
transaction: vi.fn(),
|
||||||
|
checkFTS5Support: vi.fn(() => true),
|
||||||
|
inTransaction: false
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
mockNodeRepository = {
|
||||||
|
getTotalNodes: vi.fn(() => 500),
|
||||||
|
getAIToolsCount: vi.fn(() => 263),
|
||||||
|
getTriggersCount: vi.fn(() => 104),
|
||||||
|
getDocsCount: vi.fn(() => 435)
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
mockTemplateService = {
|
||||||
|
listTemplates: vi.fn(),
|
||||||
|
getTemplate: vi.fn(),
|
||||||
|
listNodeTemplates: vi.fn(),
|
||||||
|
searchTemplates: vi.fn(),
|
||||||
|
getTemplatesForTask: vi.fn(),
|
||||||
|
getTemplateStats: vi.fn()
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
(NodeRepository as any).mockImplementation(() => mockNodeRepository);
|
||||||
|
(TemplateService as any).mockImplementation(() => mockTemplateService);
|
||||||
|
|
||||||
|
// Set environment variable for in-memory database
|
||||||
|
process.env.NODE_DB_PATH = ':memory:';
|
||||||
|
server = new TestableMCPServer();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('listTemplates', () => {
|
||||||
|
it('should return paginated template list with default parameters', async () => {
|
||||||
|
const mockResponse: PaginatedResponse<TemplateMinimal> = {
|
||||||
|
items: [
|
||||||
|
{ id: 1, name: 'Template A', views: 200, nodeCount: 3 },
|
||||||
|
{ id: 2, name: 'Template B', views: 150, nodeCount: 2 }
|
||||||
|
],
|
||||||
|
total: 25,
|
||||||
|
limit: 10,
|
||||||
|
offset: 0,
|
||||||
|
hasMore: true
|
||||||
|
};
|
||||||
|
|
||||||
|
mockTemplateService.listTemplates = vi.fn().mockResolvedValue(mockResponse);
|
||||||
|
|
||||||
|
const result = await server.testListTemplates();
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
templates: mockResponse.items,
|
||||||
|
pagination: {
|
||||||
|
total: 25,
|
||||||
|
limit: 10,
|
||||||
|
offset: 0,
|
||||||
|
hasMore: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockTemplateService.listTemplates).toHaveBeenCalledWith(10, 0, 'views');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle custom pagination parameters', async () => {
|
||||||
|
const mockResponse: PaginatedResponse<TemplateMinimal> = {
|
||||||
|
items: [
|
||||||
|
{ id: 3, name: 'Template C', views: 100, nodeCount: 1 }
|
||||||
|
],
|
||||||
|
total: 25,
|
||||||
|
limit: 5,
|
||||||
|
offset: 20,
|
||||||
|
hasMore: false
|
||||||
|
};
|
||||||
|
|
||||||
|
mockTemplateService.listTemplates = vi.fn().mockResolvedValue(mockResponse);
|
||||||
|
|
||||||
|
const result = await server.testListTemplates(5, 20, 'name');
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
templates: mockResponse.items,
|
||||||
|
pagination: {
|
||||||
|
total: 25,
|
||||||
|
limit: 5,
|
||||||
|
offset: 20,
|
||||||
|
hasMore: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockTemplateService.listTemplates).toHaveBeenCalledWith(5, 20, 'name');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle empty results', async () => {
|
||||||
|
const mockResponse: PaginatedResponse<TemplateMinimal> = {
|
||||||
|
items: [],
|
||||||
|
total: 0,
|
||||||
|
limit: 10,
|
||||||
|
offset: 0,
|
||||||
|
hasMore: false
|
||||||
|
};
|
||||||
|
|
||||||
|
mockTemplateService.listTemplates = vi.fn().mockResolvedValue(mockResponse);
|
||||||
|
|
||||||
|
const result = await server.testListTemplates();
|
||||||
|
|
||||||
|
expect(result.templates).toHaveLength(0);
|
||||||
|
expect(result.pagination.total).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getTemplate with mode parameter', () => {
|
||||||
|
it('should return full template by default', async () => {
|
||||||
|
const mockTemplate = {
|
||||||
|
id: 1,
|
||||||
|
name: 'Test Template',
|
||||||
|
description: 'Test description',
|
||||||
|
author: { name: 'Test Author', username: 'test', verified: true },
|
||||||
|
nodes: ['n8n-nodes-base.webhook'],
|
||||||
|
views: 100,
|
||||||
|
created: '2024-01-01T00:00:00Z',
|
||||||
|
url: 'https://n8n.io/workflows/1',
|
||||||
|
workflow: {
|
||||||
|
nodes: [{ id: 'node1', type: 'n8n-nodes-base.webhook' }],
|
||||||
|
connections: {},
|
||||||
|
settings: {}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
mockTemplateService.getTemplate = vi.fn().mockResolvedValue(mockTemplate);
|
||||||
|
|
||||||
|
const result = await server.testGetTemplate(1);
|
||||||
|
|
||||||
|
expect(result).toEqual(mockTemplate);
|
||||||
|
expect(mockTemplateService.getTemplate).toHaveBeenCalledWith(1, 'full');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return nodes_only mode correctly', async () => {
|
||||||
|
const mockTemplate = {
|
||||||
|
id: 1,
|
||||||
|
name: 'Test Template',
|
||||||
|
nodes: [
|
||||||
|
{ type: 'n8n-nodes-base.webhook', name: 'Webhook' },
|
||||||
|
{ type: 'n8n-nodes-base.slack', name: 'Slack' }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
mockTemplateService.getTemplate = vi.fn().mockResolvedValue(mockTemplate);
|
||||||
|
|
||||||
|
const result = await server.testGetTemplate(1, 'nodes_only');
|
||||||
|
|
||||||
|
expect(result).toEqual(mockTemplate);
|
||||||
|
expect(mockTemplateService.getTemplate).toHaveBeenCalledWith(1, 'nodes_only');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return structure mode correctly', async () => {
|
||||||
|
const mockTemplate = {
|
||||||
|
id: 1,
|
||||||
|
name: 'Test Template',
|
||||||
|
nodes: [
|
||||||
|
{ id: 'node1', type: 'n8n-nodes-base.webhook', name: 'Webhook', position: [100, 100] }
|
||||||
|
],
|
||||||
|
connections: { node1: { main: [[{ node: 'node2', type: 'main', index: 0 }]] } }
|
||||||
|
};
|
||||||
|
|
||||||
|
mockTemplateService.getTemplate = vi.fn().mockResolvedValue(mockTemplate);
|
||||||
|
|
||||||
|
const result = await server.testGetTemplate(1, 'structure');
|
||||||
|
|
||||||
|
expect(result).toEqual(mockTemplate);
|
||||||
|
expect(mockTemplateService.getTemplate).toHaveBeenCalledWith(1, 'structure');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle non-existent template', async () => {
|
||||||
|
mockTemplateService.getTemplate = vi.fn().mockResolvedValue(null);
|
||||||
|
|
||||||
|
const result = await server.testGetTemplate(999);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
error: 'Template not found',
|
||||||
|
tip: "Use list_templates, list_node_templates or search_templates to find available templates"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Enhanced template tools with pagination', () => {
|
||||||
|
describe('listNodeTemplates', () => {
|
||||||
|
it('should handle pagination correctly', async () => {
|
||||||
|
const mockResponse = {
|
||||||
|
items: [
|
||||||
|
{ id: 1, name: 'Webhook Template', nodes: ['n8n-nodes-base.webhook'], views: 200 }
|
||||||
|
],
|
||||||
|
total: 15,
|
||||||
|
limit: 10,
|
||||||
|
offset: 5,
|
||||||
|
hasMore: true
|
||||||
|
};
|
||||||
|
|
||||||
|
mockTemplateService.listNodeTemplates = vi.fn().mockResolvedValue(mockResponse);
|
||||||
|
|
||||||
|
const result = await server.testListNodeTemplates(['n8n-nodes-base.webhook'], 10, 5);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
templates: mockResponse.items,
|
||||||
|
pagination: {
|
||||||
|
total: 15,
|
||||||
|
limit: 10,
|
||||||
|
offset: 5,
|
||||||
|
hasMore: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockTemplateService.listNodeTemplates).toHaveBeenCalledWith(['n8n-nodes-base.webhook'], 10, 5);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('searchTemplates', () => {
|
||||||
|
it('should handle pagination correctly', async () => {
|
||||||
|
const mockResponse = {
|
||||||
|
items: [
|
||||||
|
{ id: 2, name: 'Search Result', description: 'Found template', views: 150 }
|
||||||
|
],
|
||||||
|
total: 8,
|
||||||
|
limit: 20,
|
||||||
|
offset: 0,
|
||||||
|
hasMore: false
|
||||||
|
};
|
||||||
|
|
||||||
|
mockTemplateService.searchTemplates = vi.fn().mockResolvedValue(mockResponse);
|
||||||
|
|
||||||
|
const result = await server.testSearchTemplates('webhook', 20, 0);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
templates: mockResponse.items,
|
||||||
|
pagination: {
|
||||||
|
total: 8,
|
||||||
|
limit: 20,
|
||||||
|
offset: 0,
|
||||||
|
hasMore: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockTemplateService.searchTemplates).toHaveBeenCalledWith('webhook', 20, 0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getTemplatesForTask', () => {
|
||||||
|
it('should handle pagination correctly', async () => {
|
||||||
|
const mockResponse = {
|
||||||
|
items: [
|
||||||
|
{ id: 3, name: 'AI Template', nodes: ['@n8n/n8n-nodes-langchain.openAi'], views: 300 }
|
||||||
|
],
|
||||||
|
total: 12,
|
||||||
|
limit: 10,
|
||||||
|
offset: 10,
|
||||||
|
hasMore: true
|
||||||
|
};
|
||||||
|
|
||||||
|
mockTemplateService.getTemplatesForTask = vi.fn().mockResolvedValue(mockResponse);
|
||||||
|
|
||||||
|
const result = await server.testGetTemplatesForTask('ai_automation', 10, 10);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
templates: mockResponse.items,
|
||||||
|
pagination: {
|
||||||
|
total: 12,
|
||||||
|
limit: 10,
|
||||||
|
offset: 10,
|
||||||
|
hasMore: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockTemplateService.getTemplatesForTask).toHaveBeenCalledWith('ai_automation', 10, 10);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getDatabaseStatistics with template metrics', () => {
|
||||||
|
it('should include template statistics', async () => {
|
||||||
|
const mockTemplateStats = {
|
||||||
|
totalTemplates: 100,
|
||||||
|
averageViews: 250,
|
||||||
|
minViews: 10,
|
||||||
|
maxViews: 1000,
|
||||||
|
topUsedNodes: [
|
||||||
|
{ node: 'n8n-nodes-base.webhook', count: 45 },
|
||||||
|
{ node: 'n8n-nodes-base.slack', count: 30 }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
mockTemplateService.getTemplateStats = vi.fn().mockResolvedValue(mockTemplateStats);
|
||||||
|
|
||||||
|
const result = await server.testGetDatabaseStatistics();
|
||||||
|
|
||||||
|
expect(result).toEqual(expect.objectContaining({
|
||||||
|
nodeStatistics: {
|
||||||
|
totalNodes: 500,
|
||||||
|
aiTools: 263,
|
||||||
|
triggers: 104,
|
||||||
|
docsAvailable: 435,
|
||||||
|
docsCoverage: '87%'
|
||||||
|
},
|
||||||
|
templateStatistics: {
|
||||||
|
totalTemplates: 100,
|
||||||
|
averageViews: 250,
|
||||||
|
minViews: 10,
|
||||||
|
maxViews: 1000,
|
||||||
|
topUsedNodes: [
|
||||||
|
{ node: 'n8n-nodes-base.webhook', count: 45 },
|
||||||
|
{ node: 'n8n-nodes-base.slack', count: 30 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
expect(mockTemplateService.getTemplateStats).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle template service errors gracefully', async () => {
|
||||||
|
mockTemplateService.getTemplateStats = vi.fn().mockRejectedValue(new Error('Template stats failed'));
|
||||||
|
|
||||||
|
const result = await server.testGetDatabaseStatistics();
|
||||||
|
|
||||||
|
expect(result).toEqual(expect.objectContaining({
|
||||||
|
nodeStatistics: expect.any(Object),
|
||||||
|
templateStatistics: {
|
||||||
|
error: 'Unable to fetch template statistics'
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle missing template service', async () => {
|
||||||
|
// Create server without template service
|
||||||
|
const serverWithoutTemplates = new TestableMCPServer();
|
||||||
|
(serverWithoutTemplates as any).templateService = undefined;
|
||||||
|
|
||||||
|
const result = await serverWithoutTemplates.testGetDatabaseStatistics();
|
||||||
|
|
||||||
|
expect(result).toEqual(expect.objectContaining({
|
||||||
|
nodeStatistics: expect.any(Object),
|
||||||
|
templateStatistics: {
|
||||||
|
error: 'Template service not available'
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Error handling', () => {
|
||||||
|
it('should handle service errors in listTemplates', async () => {
|
||||||
|
mockTemplateService.listTemplates = vi.fn().mockRejectedValue(new Error('Database error'));
|
||||||
|
|
||||||
|
await expect(server.testListTemplates()).rejects.toThrow('Database error');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle service errors in getTemplate', async () => {
|
||||||
|
mockTemplateService.getTemplate = vi.fn().mockRejectedValue(new Error('Template fetch error'));
|
||||||
|
|
||||||
|
await expect(server.testGetTemplate(1)).rejects.toThrow('Template fetch error');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should validate template mode parameter', async () => {
|
||||||
|
const result = await server.testGetTemplate(1, 'invalid_mode' as any);
|
||||||
|
|
||||||
|
expect(mockTemplateService.getTemplate).toHaveBeenCalledWith(1, 'invalid_mode');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle invalid pagination parameters', async () => {
|
||||||
|
const mockResponse = {
|
||||||
|
items: [],
|
||||||
|
total: 0,
|
||||||
|
limit: 10,
|
||||||
|
offset: 0,
|
||||||
|
hasMore: false
|
||||||
|
};
|
||||||
|
|
||||||
|
mockTemplateService.listTemplates = vi.fn().mockResolvedValue(mockResponse);
|
||||||
|
|
||||||
|
// Should handle negative values gracefully
|
||||||
|
const result = await server.testListTemplates(-5, -10, 'views');
|
||||||
|
|
||||||
|
expect(mockTemplateService.listTemplates).toHaveBeenCalledWith(-5, -10, 'views');
|
||||||
|
expect(result.pagination.limit).toBe(10);
|
||||||
|
expect(result.pagination.offset).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Integration with existing functionality', () => {
|
||||||
|
it('should maintain backward compatibility', async () => {
|
||||||
|
// Existing getTemplate method should work without mode parameter
|
||||||
|
const mockTemplate = { id: 1, name: 'Test' };
|
||||||
|
mockTemplateService.getTemplate = vi.fn().mockResolvedValue(mockTemplate);
|
||||||
|
|
||||||
|
const result = await server.testGetTemplate(1);
|
||||||
|
|
||||||
|
expect(mockTemplateService.getTemplate).toHaveBeenCalledWith(1, 'full');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with existing template tools', async () => {
|
||||||
|
// Test that existing functionality isn't broken
|
||||||
|
const mockResponse = {
|
||||||
|
items: [{ id: 1, name: 'Template' }],
|
||||||
|
total: 1,
|
||||||
|
limit: 10,
|
||||||
|
offset: 0,
|
||||||
|
hasMore: false
|
||||||
|
};
|
||||||
|
|
||||||
|
mockTemplateService.listNodeTemplates = vi.fn().mockResolvedValue(mockResponse);
|
||||||
|
|
||||||
|
const result = await server.testListNodeTemplates(['n8n-nodes-base.webhook']);
|
||||||
|
|
||||||
|
expect(mockTemplateService.listNodeTemplates).toHaveBeenCalledWith(['n8n-nodes-base.webhook'], 10, 0);
|
||||||
|
expect(result.templates).toBeDefined();
|
||||||
|
expect(result.pagination).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -254,7 +254,7 @@ describe('n8nDocumentationToolsFinal', () => {
|
|||||||
discovery: ['list_nodes', 'search_nodes', 'list_ai_tools'],
|
discovery: ['list_nodes', 'search_nodes', 'list_ai_tools'],
|
||||||
configuration: ['get_node_info', 'get_node_essentials', 'get_node_documentation'],
|
configuration: ['get_node_info', 'get_node_essentials', 'get_node_documentation'],
|
||||||
validation: ['validate_node_operation', 'validate_workflow', 'validate_node_minimal'],
|
validation: ['validate_node_operation', 'validate_workflow', 'validate_node_minimal'],
|
||||||
templates: ['list_tasks', 'get_node_for_task', 'search_templates'],
|
templates: ['list_tasks', 'get_node_for_task', 'search_templates', 'list_templates', 'get_template', 'list_node_templates'],
|
||||||
documentation: ['tools_documentation']
|
documentation: ['tools_documentation']
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -317,4 +317,83 @@ describe('n8nDocumentationToolsFinal', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('New Template Tools', () => {
|
||||||
|
describe('list_templates', () => {
|
||||||
|
const tool = n8nDocumentationToolsFinal.find(t => t.name === 'list_templates');
|
||||||
|
|
||||||
|
it('should exist and be properly defined', () => {
|
||||||
|
expect(tool).toBeDefined();
|
||||||
|
expect(tool?.description).toContain('minimal data');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have correct parameters', () => {
|
||||||
|
expect(tool?.inputSchema.properties).toHaveProperty('limit');
|
||||||
|
expect(tool?.inputSchema.properties).toHaveProperty('offset');
|
||||||
|
expect(tool?.inputSchema.properties).toHaveProperty('sortBy');
|
||||||
|
|
||||||
|
const limitParam = tool?.inputSchema.properties.limit;
|
||||||
|
expect(limitParam.type).toBe('number');
|
||||||
|
expect(limitParam.minimum).toBe(1);
|
||||||
|
expect(limitParam.maximum).toBe(100);
|
||||||
|
|
||||||
|
const offsetParam = tool?.inputSchema.properties.offset;
|
||||||
|
expect(offsetParam.type).toBe('number');
|
||||||
|
expect(offsetParam.minimum).toBe(0);
|
||||||
|
|
||||||
|
const sortByParam = tool?.inputSchema.properties.sortBy;
|
||||||
|
expect(sortByParam.enum).toEqual(['views', 'created_at', 'name']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have no required parameters', () => {
|
||||||
|
expect(tool?.inputSchema.required).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('get_template (enhanced)', () => {
|
||||||
|
const tool = n8nDocumentationToolsFinal.find(t => t.name === 'get_template');
|
||||||
|
|
||||||
|
it('should exist and support mode parameter', () => {
|
||||||
|
expect(tool).toBeDefined();
|
||||||
|
expect(tool?.description).toContain('mode');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have mode parameter with correct values', () => {
|
||||||
|
expect(tool?.inputSchema.properties).toHaveProperty('mode');
|
||||||
|
|
||||||
|
const modeParam = tool?.inputSchema.properties.mode;
|
||||||
|
expect(modeParam.enum).toEqual(['nodes_only', 'structure', 'full']);
|
||||||
|
expect(modeParam.default).toBe('full');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should require templateId parameter', () => {
|
||||||
|
expect(tool?.inputSchema.required).toContain('templateId');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Enhanced pagination support', () => {
|
||||||
|
const paginatedTools = ['list_node_templates', 'search_templates', 'get_templates_for_task'];
|
||||||
|
|
||||||
|
paginatedTools.forEach(toolName => {
|
||||||
|
describe(toolName, () => {
|
||||||
|
const tool = n8nDocumentationToolsFinal.find(t => t.name === toolName);
|
||||||
|
|
||||||
|
it('should support limit parameter', () => {
|
||||||
|
expect(tool?.inputSchema.properties).toHaveProperty('limit');
|
||||||
|
const limitParam = tool?.inputSchema.properties.limit;
|
||||||
|
expect(limitParam.type).toBe('number');
|
||||||
|
expect(limitParam.minimum).toBeGreaterThanOrEqual(1);
|
||||||
|
expect(limitParam.maximum).toBeGreaterThanOrEqual(50);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support offset parameter', () => {
|
||||||
|
expect(tool?.inputSchema.properties).toHaveProperty('offset');
|
||||||
|
const offsetParam = tool?.inputSchema.properties.offset;
|
||||||
|
expect(offsetParam.type).toBe('number');
|
||||||
|
expect(offsetParam.minimum).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
577
tests/unit/services/template-service.test.ts
Normal file
577
tests/unit/services/template-service.test.ts
Normal file
@@ -0,0 +1,577 @@
|
|||||||
|
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
|
||||||
|
import { TemplateService, PaginatedResponse, TemplateInfo, TemplateMinimal } from '../../../src/templates/template-service';
|
||||||
|
import { TemplateRepository, StoredTemplate } from '../../../src/templates/template-repository';
|
||||||
|
import { DatabaseAdapter } from '../../../src/database/database-adapter';
|
||||||
|
|
||||||
|
// Mock the logger
|
||||||
|
vi.mock('../../../src/utils/logger', () => ({
|
||||||
|
logger: {
|
||||||
|
info: vi.fn(),
|
||||||
|
warn: vi.fn(),
|
||||||
|
error: vi.fn(),
|
||||||
|
debug: vi.fn()
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock the template repository
|
||||||
|
vi.mock('../../../src/templates/template-repository');
|
||||||
|
|
||||||
|
// Mock template fetcher - only imported when needed
|
||||||
|
vi.mock('../../../src/templates/template-fetcher', () => ({
|
||||||
|
TemplateFetcher: vi.fn().mockImplementation(() => ({
|
||||||
|
fetchTemplates: vi.fn(),
|
||||||
|
fetchAllTemplateDetails: vi.fn()
|
||||||
|
}))
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('TemplateService', () => {
|
||||||
|
let service: TemplateService;
|
||||||
|
let mockDb: DatabaseAdapter;
|
||||||
|
let mockRepository: TemplateRepository;
|
||||||
|
|
||||||
|
const createMockTemplate = (id: number, overrides: any = {}): StoredTemplate => ({
|
||||||
|
id,
|
||||||
|
workflow_id: id,
|
||||||
|
name: overrides.name || `Template ${id}`,
|
||||||
|
description: overrides.description || `Description for template ${id}`,
|
||||||
|
author_name: overrides.author_name || 'Test Author',
|
||||||
|
author_username: overrides.author_username || 'testuser',
|
||||||
|
author_verified: overrides.author_verified !== undefined ? overrides.author_verified : 1,
|
||||||
|
nodes_used: JSON.stringify(overrides.nodes_used || ['n8n-nodes-base.webhook']),
|
||||||
|
workflow_json: JSON.stringify(overrides.workflow || {
|
||||||
|
nodes: [
|
||||||
|
{
|
||||||
|
id: 'node1',
|
||||||
|
type: 'n8n-nodes-base.webhook',
|
||||||
|
name: 'Webhook',
|
||||||
|
position: [100, 100],
|
||||||
|
parameters: {}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
connections: {},
|
||||||
|
settings: {}
|
||||||
|
}),
|
||||||
|
categories: JSON.stringify(overrides.categories || ['automation']),
|
||||||
|
views: overrides.views || 100,
|
||||||
|
created_at: overrides.created_at || '2024-01-01T00:00:00Z',
|
||||||
|
updated_at: overrides.updated_at || '2024-01-01T00:00:00Z',
|
||||||
|
url: overrides.url || `https://n8n.io/workflows/${id}`,
|
||||||
|
scraped_at: '2024-01-01T00:00:00Z'
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
|
||||||
|
mockDb = {} as DatabaseAdapter;
|
||||||
|
|
||||||
|
// Create mock repository with all methods
|
||||||
|
mockRepository = {
|
||||||
|
getTemplatesByNodes: vi.fn(),
|
||||||
|
getNodeTemplatesCount: vi.fn(),
|
||||||
|
getTemplate: vi.fn(),
|
||||||
|
searchTemplates: vi.fn(),
|
||||||
|
getSearchCount: vi.fn(),
|
||||||
|
getTemplatesForTask: vi.fn(),
|
||||||
|
getTaskTemplatesCount: vi.fn(),
|
||||||
|
getAllTemplates: vi.fn(),
|
||||||
|
getTemplateCount: vi.fn(),
|
||||||
|
getTemplateStats: vi.fn(),
|
||||||
|
getExistingTemplateIds: vi.fn(),
|
||||||
|
clearTemplates: vi.fn(),
|
||||||
|
saveTemplate: vi.fn(),
|
||||||
|
rebuildTemplateFTS: vi.fn()
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
// Mock the constructor
|
||||||
|
(TemplateRepository as any).mockImplementation(() => mockRepository);
|
||||||
|
|
||||||
|
service = new TemplateService(mockDb);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('listNodeTemplates', () => {
|
||||||
|
it('should return paginated node templates', async () => {
|
||||||
|
const mockTemplates = [
|
||||||
|
createMockTemplate(1, { name: 'Webhook Template' }),
|
||||||
|
createMockTemplate(2, { name: 'HTTP Template' })
|
||||||
|
];
|
||||||
|
|
||||||
|
mockRepository.getTemplatesByNodes = vi.fn().mockReturnValue(mockTemplates);
|
||||||
|
mockRepository.getNodeTemplatesCount = vi.fn().mockReturnValue(10);
|
||||||
|
|
||||||
|
const result = await service.listNodeTemplates(['n8n-nodes-base.webhook'], 5, 0);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
items: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: 1,
|
||||||
|
name: 'Webhook Template',
|
||||||
|
author: expect.objectContaining({
|
||||||
|
name: 'Test Author',
|
||||||
|
username: 'testuser',
|
||||||
|
verified: true
|
||||||
|
}),
|
||||||
|
nodes: ['n8n-nodes-base.webhook'],
|
||||||
|
views: 100
|
||||||
|
})
|
||||||
|
]),
|
||||||
|
total: 10,
|
||||||
|
limit: 5,
|
||||||
|
offset: 0,
|
||||||
|
hasMore: true
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockRepository.getTemplatesByNodes).toHaveBeenCalledWith(['n8n-nodes-base.webhook'], 5, 0);
|
||||||
|
expect(mockRepository.getNodeTemplatesCount).toHaveBeenCalledWith(['n8n-nodes-base.webhook']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle pagination correctly', async () => {
|
||||||
|
mockRepository.getTemplatesByNodes = vi.fn().mockReturnValue([]);
|
||||||
|
mockRepository.getNodeTemplatesCount = vi.fn().mockReturnValue(25);
|
||||||
|
|
||||||
|
const result = await service.listNodeTemplates(['n8n-nodes-base.webhook'], 10, 20);
|
||||||
|
|
||||||
|
expect(result.hasMore).toBe(false); // 20 + 10 >= 25
|
||||||
|
expect(result.offset).toBe(20);
|
||||||
|
expect(result.limit).toBe(10);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use default pagination parameters', async () => {
|
||||||
|
mockRepository.getTemplatesByNodes = vi.fn().mockReturnValue([]);
|
||||||
|
mockRepository.getNodeTemplatesCount = vi.fn().mockReturnValue(0);
|
||||||
|
|
||||||
|
await service.listNodeTemplates(['n8n-nodes-base.webhook']);
|
||||||
|
|
||||||
|
expect(mockRepository.getTemplatesByNodes).toHaveBeenCalledWith(['n8n-nodes-base.webhook'], 10, 0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getTemplate', () => {
|
||||||
|
const mockWorkflow = {
|
||||||
|
nodes: [
|
||||||
|
{
|
||||||
|
id: 'node1',
|
||||||
|
type: 'n8n-nodes-base.webhook',
|
||||||
|
name: 'Webhook',
|
||||||
|
position: [100, 100],
|
||||||
|
parameters: { path: 'test' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'node2',
|
||||||
|
type: 'n8n-nodes-base.slack',
|
||||||
|
name: 'Slack',
|
||||||
|
position: [300, 100],
|
||||||
|
parameters: { channel: '#general' }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
connections: {
|
||||||
|
'node1': {
|
||||||
|
'main': [
|
||||||
|
[{ 'node': 'node2', 'type': 'main', 'index': 0 }]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
settings: { timezone: 'UTC' }
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should return template in nodes_only mode', async () => {
|
||||||
|
const mockTemplate = createMockTemplate(1, { workflow: mockWorkflow });
|
||||||
|
mockRepository.getTemplate = vi.fn().mockReturnValue(mockTemplate);
|
||||||
|
|
||||||
|
const result = await service.getTemplate(1, 'nodes_only');
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
id: 1,
|
||||||
|
name: 'Template 1',
|
||||||
|
nodes: [
|
||||||
|
{ type: 'n8n-nodes-base.webhook', name: 'Webhook' },
|
||||||
|
{ type: 'n8n-nodes-base.slack', name: 'Slack' }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return template in structure mode', async () => {
|
||||||
|
const mockTemplate = createMockTemplate(1, { workflow: mockWorkflow });
|
||||||
|
mockRepository.getTemplate = vi.fn().mockReturnValue(mockTemplate);
|
||||||
|
|
||||||
|
const result = await service.getTemplate(1, 'structure');
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
id: 1,
|
||||||
|
name: 'Template 1',
|
||||||
|
nodes: [
|
||||||
|
{
|
||||||
|
id: 'node1',
|
||||||
|
type: 'n8n-nodes-base.webhook',
|
||||||
|
name: 'Webhook',
|
||||||
|
position: [100, 100]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'node2',
|
||||||
|
type: 'n8n-nodes-base.slack',
|
||||||
|
name: 'Slack',
|
||||||
|
position: [300, 100]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
connections: mockWorkflow.connections
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return full template in full mode', async () => {
|
||||||
|
const mockTemplate = createMockTemplate(1, { workflow: mockWorkflow });
|
||||||
|
mockRepository.getTemplate = vi.fn().mockReturnValue(mockTemplate);
|
||||||
|
|
||||||
|
const result = await service.getTemplate(1, 'full');
|
||||||
|
|
||||||
|
expect(result).toEqual(expect.objectContaining({
|
||||||
|
id: 1,
|
||||||
|
name: 'Template 1',
|
||||||
|
description: 'Description for template 1',
|
||||||
|
author: {
|
||||||
|
name: 'Test Author',
|
||||||
|
username: 'testuser',
|
||||||
|
verified: true
|
||||||
|
},
|
||||||
|
nodes: ['n8n-nodes-base.webhook'],
|
||||||
|
views: 100,
|
||||||
|
workflow: mockWorkflow
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return null for non-existent template', async () => {
|
||||||
|
mockRepository.getTemplate = vi.fn().mockReturnValue(null);
|
||||||
|
|
||||||
|
const result = await service.getTemplate(999);
|
||||||
|
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle templates with no workflow nodes', async () => {
|
||||||
|
const mockTemplate = createMockTemplate(1, { workflow: { connections: {}, settings: {} } });
|
||||||
|
mockRepository.getTemplate = vi.fn().mockReturnValue(mockTemplate);
|
||||||
|
|
||||||
|
const result = await service.getTemplate(1, 'nodes_only');
|
||||||
|
|
||||||
|
expect(result.nodes).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('searchTemplates', () => {
|
||||||
|
it('should return paginated search results', async () => {
|
||||||
|
const mockTemplates = [
|
||||||
|
createMockTemplate(1, { name: 'Webhook Automation' }),
|
||||||
|
createMockTemplate(2, { name: 'Webhook Processing' })
|
||||||
|
];
|
||||||
|
|
||||||
|
mockRepository.searchTemplates = vi.fn().mockReturnValue(mockTemplates);
|
||||||
|
mockRepository.getSearchCount = vi.fn().mockReturnValue(15);
|
||||||
|
|
||||||
|
const result = await service.searchTemplates('webhook', 10, 5);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
items: expect.arrayContaining([
|
||||||
|
expect.objectContaining({ id: 1, name: 'Webhook Automation' }),
|
||||||
|
expect.objectContaining({ id: 2, name: 'Webhook Processing' })
|
||||||
|
]),
|
||||||
|
total: 15,
|
||||||
|
limit: 10,
|
||||||
|
offset: 5,
|
||||||
|
hasMore: false // 5 + 10 >= 15
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockRepository.searchTemplates).toHaveBeenCalledWith('webhook', 10, 5);
|
||||||
|
expect(mockRepository.getSearchCount).toHaveBeenCalledWith('webhook');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use default parameters', async () => {
|
||||||
|
mockRepository.searchTemplates = vi.fn().mockReturnValue([]);
|
||||||
|
mockRepository.getSearchCount = vi.fn().mockReturnValue(0);
|
||||||
|
|
||||||
|
await service.searchTemplates('test');
|
||||||
|
|
||||||
|
expect(mockRepository.searchTemplates).toHaveBeenCalledWith('test', 20, 0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getTemplatesForTask', () => {
|
||||||
|
it('should return paginated task templates', async () => {
|
||||||
|
const mockTemplates = [
|
||||||
|
createMockTemplate(1, { name: 'AI Workflow' }),
|
||||||
|
createMockTemplate(2, { name: 'ML Pipeline' })
|
||||||
|
];
|
||||||
|
|
||||||
|
mockRepository.getTemplatesForTask = vi.fn().mockReturnValue(mockTemplates);
|
||||||
|
mockRepository.getTaskTemplatesCount = vi.fn().mockReturnValue(8);
|
||||||
|
|
||||||
|
const result = await service.getTemplatesForTask('ai_automation', 5, 3);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
items: expect.arrayContaining([
|
||||||
|
expect.objectContaining({ id: 1, name: 'AI Workflow' }),
|
||||||
|
expect.objectContaining({ id: 2, name: 'ML Pipeline' })
|
||||||
|
]),
|
||||||
|
total: 8,
|
||||||
|
limit: 5,
|
||||||
|
offset: 3,
|
||||||
|
hasMore: false // 3 + 5 >= 8
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockRepository.getTemplatesForTask).toHaveBeenCalledWith('ai_automation', 5, 3);
|
||||||
|
expect(mockRepository.getTaskTemplatesCount).toHaveBeenCalledWith('ai_automation');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('listTemplates', () => {
|
||||||
|
it('should return paginated minimal template data', async () => {
|
||||||
|
const mockTemplates = [
|
||||||
|
createMockTemplate(1, {
|
||||||
|
name: 'Template A',
|
||||||
|
nodes_used: ['n8n-nodes-base.webhook', 'n8n-nodes-base.slack'],
|
||||||
|
views: 200
|
||||||
|
}),
|
||||||
|
createMockTemplate(2, {
|
||||||
|
name: 'Template B',
|
||||||
|
nodes_used: ['n8n-nodes-base.httpRequest'],
|
||||||
|
views: 150
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
mockRepository.getAllTemplates = vi.fn().mockReturnValue(mockTemplates);
|
||||||
|
mockRepository.getTemplateCount = vi.fn().mockReturnValue(50);
|
||||||
|
|
||||||
|
const result = await service.listTemplates(10, 20, 'views');
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
items: [
|
||||||
|
{ id: 1, name: 'Template A', views: 200, nodeCount: 2 },
|
||||||
|
{ id: 2, name: 'Template B', views: 150, nodeCount: 1 }
|
||||||
|
],
|
||||||
|
total: 50,
|
||||||
|
limit: 10,
|
||||||
|
offset: 20,
|
||||||
|
hasMore: true // 20 + 10 < 50
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockRepository.getAllTemplates).toHaveBeenCalledWith(10, 20, 'views');
|
||||||
|
expect(mockRepository.getTemplateCount).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use default parameters', async () => {
|
||||||
|
mockRepository.getAllTemplates = vi.fn().mockReturnValue([]);
|
||||||
|
mockRepository.getTemplateCount = vi.fn().mockReturnValue(0);
|
||||||
|
|
||||||
|
await service.listTemplates();
|
||||||
|
|
||||||
|
expect(mockRepository.getAllTemplates).toHaveBeenCalledWith(10, 0, 'views');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle different sort orders', async () => {
|
||||||
|
mockRepository.getAllTemplates = vi.fn().mockReturnValue([]);
|
||||||
|
mockRepository.getTemplateCount = vi.fn().mockReturnValue(0);
|
||||||
|
|
||||||
|
await service.listTemplates(5, 0, 'name');
|
||||||
|
|
||||||
|
expect(mockRepository.getAllTemplates).toHaveBeenCalledWith(5, 0, 'name');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('listAvailableTasks', () => {
|
||||||
|
it('should return list of available tasks', () => {
|
||||||
|
const tasks = service.listAvailableTasks();
|
||||||
|
|
||||||
|
expect(tasks).toEqual([
|
||||||
|
'ai_automation',
|
||||||
|
'data_sync',
|
||||||
|
'webhook_processing',
|
||||||
|
'email_automation',
|
||||||
|
'slack_integration',
|
||||||
|
'data_transformation',
|
||||||
|
'file_processing',
|
||||||
|
'scheduling',
|
||||||
|
'api_integration',
|
||||||
|
'database_operations'
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getTemplateStats', () => {
|
||||||
|
it('should return template statistics', async () => {
|
||||||
|
const mockStats = {
|
||||||
|
totalTemplates: 100,
|
||||||
|
averageViews: 250,
|
||||||
|
topUsedNodes: [
|
||||||
|
{ node: 'n8n-nodes-base.webhook', count: 45 },
|
||||||
|
{ node: 'n8n-nodes-base.slack', count: 30 }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
mockRepository.getTemplateStats = vi.fn().mockReturnValue(mockStats);
|
||||||
|
|
||||||
|
const result = await service.getTemplateStats();
|
||||||
|
|
||||||
|
expect(result).toEqual(mockStats);
|
||||||
|
expect(mockRepository.getTemplateStats).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('fetchAndUpdateTemplates', () => {
|
||||||
|
it('should handle rebuild mode', async () => {
|
||||||
|
const mockFetcher = {
|
||||||
|
fetchTemplates: vi.fn().mockResolvedValue([
|
||||||
|
{ id: 1, name: 'Template 1' },
|
||||||
|
{ id: 2, name: 'Template 2' }
|
||||||
|
]),
|
||||||
|
fetchAllTemplateDetails: vi.fn().mockResolvedValue(new Map([
|
||||||
|
[1, { id: 1, workflow: { nodes: [], connections: {}, settings: {} } }],
|
||||||
|
[2, { id: 2, workflow: { nodes: [], connections: {}, settings: {} } }]
|
||||||
|
]))
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mock dynamic import
|
||||||
|
vi.doMock('../../../src/templates/template-fetcher', () => ({
|
||||||
|
TemplateFetcher: vi.fn(() => mockFetcher)
|
||||||
|
}));
|
||||||
|
|
||||||
|
mockRepository.clearTemplates = vi.fn();
|
||||||
|
mockRepository.saveTemplate = vi.fn();
|
||||||
|
mockRepository.rebuildTemplateFTS = vi.fn();
|
||||||
|
|
||||||
|
const progressCallback = vi.fn();
|
||||||
|
|
||||||
|
await service.fetchAndUpdateTemplates(progressCallback, 'rebuild');
|
||||||
|
|
||||||
|
expect(mockRepository.clearTemplates).toHaveBeenCalled();
|
||||||
|
expect(mockRepository.saveTemplate).toHaveBeenCalledTimes(2);
|
||||||
|
expect(mockRepository.rebuildTemplateFTS).toHaveBeenCalled();
|
||||||
|
expect(progressCallback).toHaveBeenCalledWith('Complete', 2, 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle update mode with existing templates', async () => {
|
||||||
|
const mockFetcher = {
|
||||||
|
fetchTemplates: vi.fn().mockResolvedValue([
|
||||||
|
{ id: 1, name: 'Template 1' },
|
||||||
|
{ id: 2, name: 'Template 2' },
|
||||||
|
{ id: 3, name: 'Template 3' }
|
||||||
|
]),
|
||||||
|
fetchAllTemplateDetails: vi.fn().mockResolvedValue(new Map([
|
||||||
|
[3, { id: 3, workflow: { nodes: [], connections: {}, settings: {} } }]
|
||||||
|
]))
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mock dynamic import
|
||||||
|
vi.doMock('../../../src/templates/template-fetcher', () => ({
|
||||||
|
TemplateFetcher: vi.fn(() => mockFetcher)
|
||||||
|
}));
|
||||||
|
|
||||||
|
mockRepository.getExistingTemplateIds = vi.fn().mockReturnValue(new Set([1, 2]));
|
||||||
|
mockRepository.saveTemplate = vi.fn();
|
||||||
|
mockRepository.rebuildTemplateFTS = vi.fn();
|
||||||
|
|
||||||
|
const progressCallback = vi.fn();
|
||||||
|
|
||||||
|
await service.fetchAndUpdateTemplates(progressCallback, 'update');
|
||||||
|
|
||||||
|
expect(mockRepository.getExistingTemplateIds).toHaveBeenCalled();
|
||||||
|
expect(mockRepository.saveTemplate).toHaveBeenCalledTimes(1); // Only new template
|
||||||
|
expect(mockRepository.rebuildTemplateFTS).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle update mode with no new templates', async () => {
|
||||||
|
const mockFetcher = {
|
||||||
|
fetchTemplates: vi.fn().mockResolvedValue([
|
||||||
|
{ id: 1, name: 'Template 1' },
|
||||||
|
{ id: 2, name: 'Template 2' }
|
||||||
|
]),
|
||||||
|
fetchAllTemplateDetails: vi.fn().mockResolvedValue(new Map())
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mock dynamic import
|
||||||
|
vi.doMock('../../../src/templates/template-fetcher', () => ({
|
||||||
|
TemplateFetcher: vi.fn(() => mockFetcher)
|
||||||
|
}));
|
||||||
|
|
||||||
|
mockRepository.getExistingTemplateIds = vi.fn().mockReturnValue(new Set([1, 2]));
|
||||||
|
mockRepository.saveTemplate = vi.fn();
|
||||||
|
mockRepository.rebuildTemplateFTS = vi.fn();
|
||||||
|
|
||||||
|
const progressCallback = vi.fn();
|
||||||
|
|
||||||
|
await service.fetchAndUpdateTemplates(progressCallback, 'update');
|
||||||
|
|
||||||
|
expect(mockRepository.saveTemplate).not.toHaveBeenCalled();
|
||||||
|
expect(mockRepository.rebuildTemplateFTS).not.toHaveBeenCalled();
|
||||||
|
expect(progressCallback).toHaveBeenCalledWith('No new templates', 0, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle errors during fetch', async () => {
|
||||||
|
// Mock the import to fail during constructor
|
||||||
|
const mockFetcher = function() {
|
||||||
|
throw new Error('Fetch failed');
|
||||||
|
};
|
||||||
|
|
||||||
|
vi.doMock('../../../src/templates/template-fetcher', () => ({
|
||||||
|
TemplateFetcher: mockFetcher
|
||||||
|
}));
|
||||||
|
|
||||||
|
await expect(service.fetchAndUpdateTemplates()).rejects.toThrow('Fetch failed');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('formatTemplateInfo (private method behavior)', () => {
|
||||||
|
it('should format template data correctly through public methods', async () => {
|
||||||
|
const mockTemplate = createMockTemplate(1, {
|
||||||
|
name: 'Test Template',
|
||||||
|
description: 'Test Description',
|
||||||
|
author_name: 'John Doe',
|
||||||
|
author_username: 'johndoe',
|
||||||
|
author_verified: 1,
|
||||||
|
nodes_used: ['n8n-nodes-base.webhook', 'n8n-nodes-base.slack'],
|
||||||
|
views: 500,
|
||||||
|
created_at: '2024-01-15T10:30:00Z',
|
||||||
|
url: 'https://n8n.io/workflows/123'
|
||||||
|
});
|
||||||
|
|
||||||
|
mockRepository.searchTemplates = vi.fn().mockReturnValue([mockTemplate]);
|
||||||
|
mockRepository.getSearchCount = vi.fn().mockReturnValue(1);
|
||||||
|
|
||||||
|
const result = await service.searchTemplates('test');
|
||||||
|
|
||||||
|
expect(result.items[0]).toEqual({
|
||||||
|
id: 1,
|
||||||
|
name: 'Test Template',
|
||||||
|
description: 'Test Description',
|
||||||
|
author: {
|
||||||
|
name: 'John Doe',
|
||||||
|
username: 'johndoe',
|
||||||
|
verified: true
|
||||||
|
},
|
||||||
|
nodes: ['n8n-nodes-base.webhook', 'n8n-nodes-base.slack'],
|
||||||
|
views: 500,
|
||||||
|
created: '2024-01-15T10:30:00Z',
|
||||||
|
url: 'https://n8n.io/workflows/123'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle unverified authors', async () => {
|
||||||
|
const mockTemplate = createMockTemplate(1, {
|
||||||
|
author_verified: 0 // Explicitly set to 0 for unverified
|
||||||
|
});
|
||||||
|
|
||||||
|
// Override the helper to return exactly what we want
|
||||||
|
const unverifiedTemplate = {
|
||||||
|
...mockTemplate,
|
||||||
|
author_verified: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
mockRepository.searchTemplates = vi.fn().mockReturnValue([unverifiedTemplate]);
|
||||||
|
mockRepository.getSearchCount = vi.fn().mockReturnValue(1);
|
||||||
|
|
||||||
|
const result = await service.searchTemplates('test');
|
||||||
|
|
||||||
|
expect(result.items[0].author.verified).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -280,7 +280,7 @@ export function createTestTemplate(overrides: Partial<TemplateWorkflow> = {}): T
|
|||||||
verified: false
|
verified: false
|
||||||
},
|
},
|
||||||
createdAt: overrides.createdAt || new Date().toISOString(),
|
createdAt: overrides.createdAt || new Date().toISOString(),
|
||||||
totalViews: overrides.totalViews || 0,
|
totalViews: overrides.totalViews || 100,
|
||||||
...overrides
|
...overrides
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user