mirror of
https://github.com/czlonkowski/n8n-mcp.git
synced 2026-02-06 05:23:08 +00:00
- 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
577 lines
19 KiB
TypeScript
577 lines
19 KiB
TypeScript
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);
|
|
});
|
|
});
|
|
}); |