mirror of
https://github.com/czlonkowski/n8n-mcp.git
synced 2026-01-30 06:22:04 +00:00
* feat: rename n8n_trigger_webhook_workflow to n8n_test_workflow with multi-trigger support - Rename tool from n8n_trigger_webhook_workflow to n8n_test_workflow - Add support for webhook, form, and chat triggers (auto-detection) - Implement modular trigger system with registry pattern - Add trigger detector for automatic trigger type inference - Remove execute trigger type (n8n public API limitation) - Add comprehensive tests for trigger detection and handlers The tool now auto-detects trigger type from workflow structure and supports all externally-triggerable workflows via n8n's public API. Note: Direct workflow execution (Schedule/Manual triggers) requires n8n's instance-level MCP access, not available via REST API. Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix: add SSRF protection to webhook handler and update tests - Add SSRF URL validation to webhook-handler.ts (critical security fix) Aligns with existing SSRF protection in form-handler.ts and chat-handler.ts - Update parameter-validation.test.ts to use new n8n_test_workflow tool name Conceived by Romuald Członkowski - www.aiadvisors.pl/en 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * feat: n8n_test_workflow unified trigger tool (v2.28.0) Added new `n8n_test_workflow` tool replacing `n8n_trigger_webhook_workflow`: Features: - Auto-detects trigger type (webhook/form/chat) from workflow - Supports multiple trigger types with type-specific parameters - SSRF protection for all trigger handlers - Extensible handler architecture with registry pattern Changes: - Fixed Zod schema to remove invalid 'execute' trigger type - Updated README.md tool documentation - Added CHANGELOG entry for v2.28.0 - Bumped version to 2.28.0 Conceived by Romuald Członkowski - www.aiadvisors.pl/en 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * test: add comprehensive unit tests for trigger handlers Added 87 unit tests across 4 test files to improve code coverage: - base-handler.test.ts (19 tests) - 100% coverage - webhook-handler.test.ts (22 tests) - 100% coverage - chat-handler.test.ts (23 tests) - 100% coverage - form-handler.test.ts (23 tests) - 100% coverage Tests cover: - Input validation and parameter handling - SSRF protection integration - HTTP method handling and URL building - Error response formatting - Execution paths for all trigger types Conceived by Romuald Członkowski - www.aiadvisors.pl/en 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
551 lines
23 KiB
TypeScript
551 lines
23 KiB
TypeScript
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
import { N8NDocumentationMCPServer } from '../../../src/mcp/server';
|
|
|
|
// Mock the database and dependencies
|
|
vi.mock('../../../src/database/database-adapter');
|
|
vi.mock('../../../src/database/node-repository');
|
|
vi.mock('../../../src/templates/template-service');
|
|
vi.mock('../../../src/utils/logger');
|
|
|
|
class TestableN8NMCPServer extends N8NDocumentationMCPServer {
|
|
// Expose the private validateToolParams method for testing
|
|
public testValidateToolParams(toolName: string, args: any, requiredParams: string[]): void {
|
|
return (this as any).validateToolParams(toolName, args, requiredParams);
|
|
}
|
|
|
|
// Expose the private executeTool method for testing
|
|
public async testExecuteTool(name: string, args: any): Promise<any> {
|
|
return (this as any).executeTool(name, args);
|
|
}
|
|
}
|
|
|
|
describe('Parameter Validation', () => {
|
|
let server: TestableN8NMCPServer;
|
|
|
|
beforeEach(() => {
|
|
// Set environment variable to use in-memory database
|
|
process.env.NODE_DB_PATH = ':memory:';
|
|
server = new TestableN8NMCPServer();
|
|
});
|
|
|
|
afterEach(() => {
|
|
delete process.env.NODE_DB_PATH;
|
|
});
|
|
|
|
describe('validateToolParams', () => {
|
|
describe('Basic Parameter Validation', () => {
|
|
it('should pass validation when all required parameters are provided', () => {
|
|
const args = { nodeType: 'nodes-base.httpRequest', config: {} };
|
|
|
|
expect(() => {
|
|
server.testValidateToolParams('test_tool', args, ['nodeType', 'config']);
|
|
}).not.toThrow();
|
|
});
|
|
|
|
it('should throw error when required parameter is missing', () => {
|
|
const args = { config: {} };
|
|
|
|
expect(() => {
|
|
server.testValidateToolParams('test_tool', args, ['nodeType', 'config']);
|
|
}).toThrow('Missing required parameters for test_tool: nodeType');
|
|
});
|
|
|
|
it('should throw error when multiple required parameters are missing', () => {
|
|
const args = {};
|
|
|
|
expect(() => {
|
|
server.testValidateToolParams('test_tool', args, ['nodeType', 'config', 'query']);
|
|
}).toThrow('Missing required parameters for test_tool: nodeType, config, query');
|
|
});
|
|
|
|
it('should throw error when required parameter is undefined', () => {
|
|
const args = { nodeType: undefined, config: {} };
|
|
|
|
expect(() => {
|
|
server.testValidateToolParams('test_tool', args, ['nodeType', 'config']);
|
|
}).toThrow('Missing required parameters for test_tool: nodeType');
|
|
});
|
|
|
|
it('should throw error when required parameter is null', () => {
|
|
const args = { nodeType: null, config: {} };
|
|
|
|
expect(() => {
|
|
server.testValidateToolParams('test_tool', args, ['nodeType', 'config']);
|
|
}).toThrow('Missing required parameters for test_tool: nodeType');
|
|
});
|
|
|
|
it('should reject when required parameter is empty string (Issue #275 fix)', () => {
|
|
const args = { query: '', limit: 10 };
|
|
|
|
expect(() => {
|
|
server.testValidateToolParams('test_tool', args, ['query']);
|
|
}).toThrow('String parameters cannot be empty');
|
|
});
|
|
|
|
it('should pass when required parameter is zero', () => {
|
|
const args = { limit: 0, query: 'test' };
|
|
|
|
expect(() => {
|
|
server.testValidateToolParams('test_tool', args, ['limit']);
|
|
}).not.toThrow();
|
|
});
|
|
|
|
it('should pass when required parameter is false', () => {
|
|
const args = { includeData: false, id: '123' };
|
|
|
|
expect(() => {
|
|
server.testValidateToolParams('test_tool', args, ['includeData']);
|
|
}).not.toThrow();
|
|
});
|
|
});
|
|
|
|
describe('Edge Cases', () => {
|
|
it('should handle empty args object', () => {
|
|
expect(() => {
|
|
server.testValidateToolParams('test_tool', {}, ['param1']);
|
|
}).toThrow('Missing required parameters for test_tool: param1');
|
|
});
|
|
|
|
it('should handle null args', () => {
|
|
expect(() => {
|
|
server.testValidateToolParams('test_tool', null, ['param1']);
|
|
}).toThrow();
|
|
});
|
|
|
|
it('should handle undefined args', () => {
|
|
expect(() => {
|
|
server.testValidateToolParams('test_tool', undefined, ['param1']);
|
|
}).toThrow();
|
|
});
|
|
|
|
it('should pass when no required parameters are specified', () => {
|
|
const args = { optionalParam: 'value' };
|
|
|
|
expect(() => {
|
|
server.testValidateToolParams('test_tool', args, []);
|
|
}).not.toThrow();
|
|
});
|
|
|
|
it('should handle special characters in parameter names', () => {
|
|
const args = { 'param-with-dash': 'value', 'param_with_underscore': 'value' };
|
|
|
|
expect(() => {
|
|
server.testValidateToolParams('test_tool', args, ['param-with-dash', 'param_with_underscore']);
|
|
}).not.toThrow();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Tool-Specific Parameter Validation', () => {
|
|
// Mock the actual tool methods to avoid database calls
|
|
beforeEach(() => {
|
|
// Mock all the tool methods that would be called
|
|
vi.spyOn(server as any, 'getNode').mockResolvedValue({ mockResult: true });
|
|
vi.spyOn(server as any, 'searchNodes').mockResolvedValue({ results: [] });
|
|
vi.spyOn(server as any, 'getNodeDocumentation').mockResolvedValue({ docs: 'test' });
|
|
vi.spyOn(server as any, 'searchNodeProperties').mockResolvedValue({ properties: [] });
|
|
// Note: getNodeForTask removed in v2.15.0
|
|
vi.spyOn(server as any, 'validateNodeConfig').mockResolvedValue({ valid: true });
|
|
vi.spyOn(server as any, 'validateNodeMinimal').mockResolvedValue({ missing: [] });
|
|
vi.spyOn(server as any, 'getPropertyDependencies').mockResolvedValue({ dependencies: {} });
|
|
vi.spyOn(server as any, 'getNodeAsToolInfo').mockResolvedValue({ toolInfo: true });
|
|
vi.spyOn(server as any, 'listNodeTemplates').mockResolvedValue({ templates: [] });
|
|
vi.spyOn(server as any, 'getTemplate').mockResolvedValue({ template: {} });
|
|
vi.spyOn(server as any, 'searchTemplates').mockResolvedValue({ templates: [] });
|
|
vi.spyOn(server as any, 'getTemplatesForTask').mockResolvedValue({ templates: [] });
|
|
vi.spyOn(server as any, 'validateWorkflow').mockResolvedValue({ valid: true });
|
|
vi.spyOn(server as any, 'validateWorkflowConnections').mockResolvedValue({ valid: true });
|
|
vi.spyOn(server as any, 'validateWorkflowExpressions').mockResolvedValue({ valid: true });
|
|
});
|
|
|
|
describe('get_node', () => {
|
|
it('should require nodeType parameter', async () => {
|
|
await expect(server.testExecuteTool('get_node', {}))
|
|
.rejects.toThrow('Missing required parameters for get_node: nodeType');
|
|
});
|
|
|
|
it('should succeed with valid nodeType', async () => {
|
|
const result = await server.testExecuteTool('get_node', {
|
|
nodeType: 'nodes-base.httpRequest'
|
|
});
|
|
expect(result).toEqual({ mockResult: true });
|
|
});
|
|
});
|
|
|
|
describe('search_nodes', () => {
|
|
it('should require query parameter', async () => {
|
|
await expect(server.testExecuteTool('search_nodes', {}))
|
|
.rejects.toThrow('search_nodes: Validation failed:\n • query: query is required');
|
|
});
|
|
|
|
it('should succeed with valid query', async () => {
|
|
const result = await server.testExecuteTool('search_nodes', {
|
|
query: 'http'
|
|
});
|
|
expect(result).toEqual({ results: [] });
|
|
});
|
|
|
|
it('should handle optional limit parameter', async () => {
|
|
const result = await server.testExecuteTool('search_nodes', {
|
|
query: 'http',
|
|
limit: 10
|
|
});
|
|
expect(result).toEqual({ results: [] });
|
|
});
|
|
|
|
it('should reject invalid limit value', async () => {
|
|
await expect(server.testExecuteTool('search_nodes', {
|
|
query: 'http',
|
|
limit: 'invalid'
|
|
})).rejects.toThrow('search_nodes: Validation failed:\n • limit: limit must be a number, got string');
|
|
});
|
|
});
|
|
|
|
describe('validate_node (consolidated)', () => {
|
|
it('should require nodeType and config parameters', async () => {
|
|
await expect(server.testExecuteTool('validate_node', {}))
|
|
.rejects.toThrow('validate_node: Validation failed:\n • nodeType: nodeType is required\n • config: config is required');
|
|
});
|
|
|
|
it('should require nodeType parameter when config is provided', async () => {
|
|
await expect(server.testExecuteTool('validate_node', { config: {} }))
|
|
.rejects.toThrow('validate_node: Validation failed:\n • nodeType: nodeType is required');
|
|
});
|
|
|
|
it('should require config parameter when nodeType is provided', async () => {
|
|
await expect(server.testExecuteTool('validate_node', { nodeType: 'nodes-base.httpRequest' }))
|
|
.rejects.toThrow('validate_node: Validation failed:\n • config: config is required');
|
|
});
|
|
|
|
it('should succeed with valid parameters (full mode)', async () => {
|
|
const result = await server.testExecuteTool('validate_node', {
|
|
nodeType: 'nodes-base.httpRequest',
|
|
config: { method: 'GET', url: 'https://api.example.com' },
|
|
mode: 'full'
|
|
});
|
|
expect(result).toEqual({ valid: true });
|
|
});
|
|
|
|
it('should succeed with valid parameters (minimal mode)', async () => {
|
|
const result = await server.testExecuteTool('validate_node', {
|
|
nodeType: 'nodes-base.httpRequest',
|
|
config: {},
|
|
mode: 'minimal'
|
|
});
|
|
expect(result).toBeDefined();
|
|
});
|
|
});
|
|
|
|
describe('get_node mode=search_properties (consolidated)', () => {
|
|
it('should require nodeType and propertyQuery parameters', async () => {
|
|
await expect(server.testExecuteTool('get_node', { mode: 'search_properties' }))
|
|
.rejects.toThrow('Missing required parameters for get_node: nodeType');
|
|
});
|
|
|
|
it('should succeed with valid parameters', async () => {
|
|
const result = await server.testExecuteTool('get_node', {
|
|
nodeType: 'nodes-base.httpRequest',
|
|
mode: 'search_properties',
|
|
propertyQuery: 'auth'
|
|
});
|
|
expect(result).toEqual({ properties: [] });
|
|
});
|
|
|
|
it('should handle optional maxPropertyResults parameter', async () => {
|
|
const result = await server.testExecuteTool('get_node', {
|
|
nodeType: 'nodes-base.httpRequest',
|
|
mode: 'search_properties',
|
|
propertyQuery: 'auth',
|
|
maxPropertyResults: 5
|
|
});
|
|
expect(result).toEqual({ properties: [] });
|
|
});
|
|
});
|
|
|
|
describe('search_templates searchMode=by_nodes (consolidated)', () => {
|
|
it('should require nodeTypes parameter for by_nodes searchMode', async () => {
|
|
await expect(server.testExecuteTool('search_templates', { searchMode: 'by_nodes' }))
|
|
.rejects.toThrow('nodeTypes array is required for searchMode=by_nodes');
|
|
});
|
|
|
|
it('should succeed with valid nodeTypes array', async () => {
|
|
const result = await server.testExecuteTool('search_templates', {
|
|
searchMode: 'by_nodes',
|
|
nodeTypes: ['nodes-base.httpRequest', 'nodes-base.slack']
|
|
});
|
|
expect(result).toEqual({ templates: [] });
|
|
});
|
|
});
|
|
|
|
describe('get_template', () => {
|
|
it('should require templateId parameter', async () => {
|
|
await expect(server.testExecuteTool('get_template', {}))
|
|
.rejects.toThrow('Missing required parameters for get_template: templateId');
|
|
});
|
|
|
|
it('should succeed with valid templateId', async () => {
|
|
const result = await server.testExecuteTool('get_template', {
|
|
templateId: 123
|
|
});
|
|
expect(result).toEqual({ template: {} });
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Numeric Parameter Conversion', () => {
|
|
beforeEach(() => {
|
|
vi.spyOn(server as any, 'searchNodes').mockResolvedValue({ results: [] });
|
|
vi.spyOn(server as any, 'searchNodeProperties').mockResolvedValue({ properties: [] });
|
|
vi.spyOn(server as any, 'listNodeTemplates').mockResolvedValue({ templates: [] });
|
|
vi.spyOn(server as any, 'getTemplate').mockResolvedValue({ template: {} });
|
|
});
|
|
|
|
describe('limit parameter conversion', () => {
|
|
it('should reject string limit values', async () => {
|
|
await expect(server.testExecuteTool('search_nodes', {
|
|
query: 'test',
|
|
limit: '15'
|
|
})).rejects.toThrow('search_nodes: Validation failed:\n • limit: limit must be a number, got string');
|
|
});
|
|
|
|
it('should reject invalid string limit values', async () => {
|
|
await expect(server.testExecuteTool('search_nodes', {
|
|
query: 'test',
|
|
limit: 'invalid'
|
|
})).rejects.toThrow('search_nodes: Validation failed:\n • limit: limit must be a number, got string');
|
|
});
|
|
|
|
it('should use default when limit is undefined', async () => {
|
|
const mockSearchNodes = vi.spyOn(server as any, 'searchNodes');
|
|
|
|
await server.testExecuteTool('search_nodes', {
|
|
query: 'test'
|
|
});
|
|
|
|
expect(mockSearchNodes).toHaveBeenCalledWith('test', 20, { mode: undefined });
|
|
});
|
|
|
|
it('should reject zero as limit due to minimum constraint', async () => {
|
|
await expect(server.testExecuteTool('search_nodes', {
|
|
query: 'test',
|
|
limit: 0
|
|
})).rejects.toThrow('search_nodes: Validation failed:\n • limit: limit must be at least 1, got 0');
|
|
});
|
|
});
|
|
|
|
describe('maxPropertyResults parameter conversion (v2.26.0 consolidated)', () => {
|
|
it('should pass numeric maxPropertyResults to searchNodeProperties', async () => {
|
|
const mockSearchNodeProperties = vi.spyOn(server as any, 'searchNodeProperties');
|
|
|
|
// v2.26.0: search_node_properties consolidated into get_node with mode='search_properties'
|
|
await server.testExecuteTool('get_node', {
|
|
nodeType: 'nodes-base.httpRequest',
|
|
mode: 'search_properties',
|
|
propertyQuery: 'auth',
|
|
maxPropertyResults: 5
|
|
});
|
|
|
|
expect(mockSearchNodeProperties).toHaveBeenCalledWith('nodes-base.httpRequest', 'auth', 5);
|
|
});
|
|
|
|
it('should use default maxPropertyResults when not provided', async () => {
|
|
const mockSearchNodeProperties = vi.spyOn(server as any, 'searchNodeProperties');
|
|
|
|
// v2.26.0: search_node_properties consolidated into get_node with mode='search_properties'
|
|
await server.testExecuteTool('get_node', {
|
|
nodeType: 'nodes-base.httpRequest',
|
|
mode: 'search_properties',
|
|
propertyQuery: 'auth'
|
|
});
|
|
|
|
expect(mockSearchNodeProperties).toHaveBeenCalledWith('nodes-base.httpRequest', 'auth', 20);
|
|
});
|
|
});
|
|
|
|
describe('templateLimit parameter conversion (v2.26.0 consolidated)', () => {
|
|
it('should handle search_templates with by_nodes mode', async () => {
|
|
// search_templates now handles list_node_templates functionality via searchMode='by_nodes'
|
|
await expect(server.testExecuteTool('search_templates', {
|
|
searchMode: 'by_nodes',
|
|
nodeTypes: ['nodes-base.httpRequest'],
|
|
limit: 5
|
|
})).resolves.toEqual({ templates: [] });
|
|
});
|
|
});
|
|
|
|
describe('templateId parameter handling', () => {
|
|
it('should pass through numeric templateId', async () => {
|
|
const mockGetTemplate = vi.spyOn(server as any, 'getTemplate');
|
|
|
|
await server.testExecuteTool('get_template', {
|
|
templateId: 123
|
|
});
|
|
|
|
expect(mockGetTemplate).toHaveBeenCalledWith(123, 'full');
|
|
});
|
|
|
|
it('should convert string templateId to number', async () => {
|
|
const mockGetTemplate = vi.spyOn(server as any, 'getTemplate');
|
|
|
|
await server.testExecuteTool('get_template', {
|
|
templateId: '123'
|
|
});
|
|
|
|
expect(mockGetTemplate).toHaveBeenCalledWith(123, 'full');
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Tools with No Required Parameters', () => {
|
|
beforeEach(() => {
|
|
vi.spyOn(server as any, 'getToolsDocumentation').mockResolvedValue({ docs: 'test' });
|
|
vi.spyOn(server as any, 'listNodes').mockResolvedValue({ nodes: [] });
|
|
vi.spyOn(server as any, 'listAITools').mockResolvedValue({ tools: [] });
|
|
vi.spyOn(server as any, 'getDatabaseStatistics').mockResolvedValue({ stats: {} });
|
|
vi.spyOn(server as any, 'listTasks').mockResolvedValue({ tasks: [] });
|
|
});
|
|
|
|
it('should allow tools_documentation with no parameters', async () => {
|
|
const result = await server.testExecuteTool('tools_documentation', {});
|
|
expect(result).toEqual({ docs: 'test' });
|
|
});
|
|
|
|
it('should allow tools_documentation with no parameters', async () => {
|
|
const result = await server.testExecuteTool('tools_documentation', {});
|
|
expect(result).toBeDefined();
|
|
// tools_documentation returns an object with documentation content
|
|
expect(typeof result).toBe('object');
|
|
});
|
|
});
|
|
|
|
describe('Error Message Quality', () => {
|
|
it('should provide clear error messages with tool name', () => {
|
|
expect(() => {
|
|
server.testValidateToolParams('get_node', {}, ['nodeType']);
|
|
}).toThrow('Missing required parameters for get_node: nodeType. Please provide the required parameters to use this tool.');
|
|
});
|
|
|
|
it('should list all missing parameters', () => {
|
|
expect(() => {
|
|
server.testValidateToolParams('validate_node', { profile: 'strict' }, ['nodeType', 'config']);
|
|
}).toThrow('validate_node: Validation failed:\n • nodeType: nodeType is required\n • config: config is required');
|
|
});
|
|
|
|
it('should include helpful guidance', () => {
|
|
try {
|
|
server.testValidateToolParams('test_tool', {}, ['param1', 'param2']);
|
|
} catch (error: any) {
|
|
expect(error.message).toContain('Please provide the required parameters to use this tool');
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('MCP Error Response Handling', () => {
|
|
it('should convert validation errors to MCP error responses rather than throwing exceptions', async () => {
|
|
// This test simulates what happens at the MCP level when a tool validation fails
|
|
// The server should catch the validation error and return it as an MCP error response
|
|
|
|
// Directly test the executeTool method to ensure it throws appropriately
|
|
// The MCP server's request handler should catch these and convert to error responses
|
|
await expect(server.testExecuteTool('get_node', {}))
|
|
.rejects.toThrow('Missing required parameters for get_node: nodeType');
|
|
|
|
await expect(server.testExecuteTool('search_nodes', {}))
|
|
.rejects.toThrow('search_nodes: Validation failed:\n • query: query is required');
|
|
|
|
await expect(server.testExecuteTool('validate_node', { nodeType: 'test' }))
|
|
.rejects.toThrow('validate_node: Validation failed:\n • config: config is required');
|
|
});
|
|
|
|
it('should handle edge cases in parameter validation gracefully', async () => {
|
|
// Test with null args (should be handled by args = args || {})
|
|
await expect(server.testExecuteTool('get_node', null))
|
|
.rejects.toThrow('Missing required parameters');
|
|
|
|
// Test with undefined args
|
|
await expect(server.testExecuteTool('get_node', undefined))
|
|
.rejects.toThrow('Missing required parameters');
|
|
});
|
|
|
|
it('should provide consistent error format across all tools', async () => {
|
|
// Tools using legacy validation
|
|
const legacyValidationTools = [
|
|
{ name: 'get_node', args: {}, expected: 'Missing required parameters for get_node: nodeType' },
|
|
// v2.26.0: get_node_documentation consolidated into get_node with mode='docs'
|
|
// v2.26.0: search_node_properties consolidated into get_node with mode='search_properties'
|
|
// Note: get_node_for_task removed in v2.15.0
|
|
// Note: get_node_as_tool_info removed in v2.25.0
|
|
// v2.26.0: get_property_dependencies removed (low usage)
|
|
{ name: 'get_template', args: {}, expected: 'Missing required parameters for get_template: templateId' },
|
|
];
|
|
|
|
for (const tool of legacyValidationTools) {
|
|
await expect(server.testExecuteTool(tool.name, tool.args))
|
|
.rejects.toThrow(tool.expected);
|
|
}
|
|
|
|
// Tools using new schema validation
|
|
// Updated for v2.26.0 tool consolidation
|
|
const schemaValidationTools = [
|
|
{ name: 'search_nodes', args: {}, expected: 'search_nodes: Validation failed:\n • query: query is required' },
|
|
{ name: 'validate_node', args: {}, expected: 'validate_node: Validation failed:\n • nodeType: nodeType is required\n • config: config is required' },
|
|
// list_node_templates consolidated into search_templates with searchMode='by_nodes'
|
|
];
|
|
|
|
for (const tool of schemaValidationTools) {
|
|
await expect(server.testExecuteTool(tool.name, tool.args))
|
|
.rejects.toThrow(tool.expected);
|
|
}
|
|
});
|
|
|
|
it('should validate n8n management tools parameters', async () => {
|
|
// Mock the n8n handlers to avoid actual API calls
|
|
const mockHandlers = [
|
|
'handleCreateWorkflow',
|
|
'handleGetWorkflow',
|
|
'handleGetWorkflowDetails',
|
|
'handleGetWorkflowStructure',
|
|
'handleGetWorkflowMinimal',
|
|
'handleUpdateWorkflow',
|
|
'handleDeleteWorkflow',
|
|
'handleValidateWorkflow',
|
|
'handleTriggerWebhookWorkflow',
|
|
'handleGetExecution',
|
|
'handleDeleteExecution'
|
|
];
|
|
|
|
for (const handler of mockHandlers) {
|
|
vi.doMock('../../../src/mcp/handlers-n8n-manager', () => ({
|
|
[handler]: vi.fn().mockResolvedValue({ success: true })
|
|
}));
|
|
}
|
|
|
|
vi.doMock('../../../src/mcp/handlers-workflow-diff', () => ({
|
|
handleUpdatePartialWorkflow: vi.fn().mockResolvedValue({ success: true })
|
|
}));
|
|
|
|
// Updated for v2.26.0 tool consolidation:
|
|
// - n8n_get_workflow now supports mode parameter (full, details, structure, minimal)
|
|
// - n8n_executions now handles get/list/delete via action parameter
|
|
const n8nToolsWithRequiredParams = [
|
|
{ name: 'n8n_create_workflow', args: {}, expected: 'n8n_create_workflow: Validation failed:\n • name: name is required\n • nodes: nodes is required\n • connections: connections is required' },
|
|
{ name: 'n8n_get_workflow', args: {}, expected: 'n8n_get_workflow: Validation failed:\n • id: id is required' },
|
|
{ name: 'n8n_update_full_workflow', args: {}, expected: 'n8n_update_full_workflow: Validation failed:\n • id: id is required' },
|
|
{ name: 'n8n_delete_workflow', args: {}, expected: 'n8n_delete_workflow: Validation failed:\n • id: id is required' },
|
|
{ name: 'n8n_validate_workflow', args: {}, expected: 'n8n_validate_workflow: Validation failed:\n • id: id is required' },
|
|
];
|
|
|
|
// n8n_update_partial_workflow and n8n_test_workflow use legacy validation
|
|
await expect(server.testExecuteTool('n8n_update_partial_workflow', {}))
|
|
.rejects.toThrow('Missing required parameters for n8n_update_partial_workflow: id, operations');
|
|
|
|
await expect(server.testExecuteTool('n8n_test_workflow', {}))
|
|
.rejects.toThrow('Missing required parameters for n8n_test_workflow: workflowId');
|
|
|
|
for (const tool of n8nToolsWithRequiredParams) {
|
|
await expect(server.testExecuteTool(tool.name, tool.args))
|
|
.rejects.toThrow(tool.expected);
|
|
}
|
|
});
|
|
});
|
|
}); |