mirror of
https://github.com/czlonkowski/n8n-mcp.git
synced 2026-01-30 06:22:04 +00:00
* feat: add n8n_deploy_template tool for one-click template deployment (v2.27.0) Add new MCP tool that deploys n8n.io workflow templates directly to user's n8n instance in a single operation. Features: - Fetch template from local database - Extract and report required credentials - Optionally strip credentials (default: true) - Optionally auto-upgrade node typeVersions (default: true) - Optionally validate before deployment (default: true) - Return workflow ID, URL, and setup guidance Parameters: - templateId (required): Template ID from n8n.io - name (optional): Custom workflow name - autoUpgradeVersions (default: true) - validate (default: true) - stripCredentials (default: true) 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: address code review findings for n8n_deploy_template - Fix health check tool count (12 → 13) to include new tool - Add templateId validation (must be positive integer) - Use deep copy of workflow to prevent template mutation - Expand unit tests with negative/zero/decimal validation cases 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> * docs: update README with n8n_deploy_template tool - Update management tools count from 12 to 13 - Add n8n_deploy_template to the tools list 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: prevent workflow validator from mutating node types The validator was incorrectly mutating node types from full form (n8n-nodes-base.*) to short form (nodes-base.*) during validation. This caused deployed workflows to show "?" icons in n8n UI because the API requires full form node types. Also updated SplitInBatches detection to check both node type forms since workflows may contain either format. 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: update tool counts in handlers-n8n-manager test Update expected managementTools count from 12 to 13 and totalAvailable from 19 to 20 to account for the new n8n_deploy_template tool. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix: pin MCP SDK and Zod versions to prevent Zod v4 resolution Fixes #440, #444, #446, #447, #450 Root cause: package.json declared `"@modelcontextprotocol/sdk": "^1.20.1"` which allowed npm to resolve to SDK 1.23.0. That version accepts `"zod": "^3.25 || ^4.0"`, causing npm to deduplicate to Zod v4. SDK 1.23.0's `isZ4Schema()` function crashes when called with undefined, which happens for plain JSON Schema objects. Changes: - Pin SDK to exact version 1.20.1 (removes ^ prefix) - Pin Zod to exact version 3.24.1 (removes ^ prefix) - Add CI workflow to verify fresh installs get compatible versions The new CI workflow: - Packs and installs package fresh (without lockfile) - Verifies SDK is exactly 1.20.1 - Verifies Zod is NOT v4 (blocks 4.x.x) - Runs weekly to catch upstream dependency changes 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> * chore: improve dependency-check workflow based on code review - Add workflow_dispatch for manual triggering/debugging - Add explicit "not found" handling for version detection failures - Use regex pattern for Zod v4 check to catch pre-release versions - Add Zod error pattern detection in functionality test - Capture stderr output for comprehensive error checking 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> --------- Co-authored-by: Claude <noreply@anthropic.com>
1306 lines
41 KiB
TypeScript
1306 lines
41 KiB
TypeScript
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
import { N8nApiClient } from '@/services/n8n-api-client';
|
|
import { WorkflowValidator } from '@/services/workflow-validator';
|
|
import { NodeRepository } from '@/database/node-repository';
|
|
import {
|
|
N8nApiError,
|
|
N8nAuthenticationError,
|
|
N8nNotFoundError,
|
|
N8nValidationError,
|
|
N8nRateLimitError,
|
|
N8nServerError,
|
|
} from '@/utils/n8n-errors';
|
|
import { ExecutionStatus } from '@/types/n8n-api';
|
|
|
|
// Mock dependencies
|
|
vi.mock('@/services/n8n-api-client');
|
|
vi.mock('@/services/workflow-validator');
|
|
vi.mock('@/database/node-repository');
|
|
vi.mock('@/config/n8n-api', () => ({
|
|
getN8nApiConfig: vi.fn()
|
|
}));
|
|
vi.mock('@/services/n8n-validation', () => ({
|
|
validateWorkflowStructure: vi.fn(),
|
|
hasWebhookTrigger: vi.fn(),
|
|
getWebhookUrl: vi.fn(),
|
|
}));
|
|
vi.mock('@/utils/logger', () => ({
|
|
logger: {
|
|
info: vi.fn(),
|
|
error: vi.fn(),
|
|
debug: vi.fn(),
|
|
warn: vi.fn(),
|
|
},
|
|
Logger: vi.fn().mockImplementation(() => ({
|
|
info: vi.fn(),
|
|
error: vi.fn(),
|
|
debug: vi.fn(),
|
|
warn: vi.fn(),
|
|
})),
|
|
LogLevel: {
|
|
ERROR: 0,
|
|
WARN: 1,
|
|
INFO: 2,
|
|
DEBUG: 3,
|
|
}
|
|
}));
|
|
|
|
describe('handlers-n8n-manager', () => {
|
|
let mockApiClient: any;
|
|
let mockRepository: any;
|
|
let mockValidator: any;
|
|
let handlers: any;
|
|
let getN8nApiConfig: any;
|
|
let n8nValidation: any;
|
|
|
|
// Helper function to create test data
|
|
const createTestWorkflow = (overrides = {}) => ({
|
|
id: 'test-workflow-id',
|
|
name: 'Test Workflow',
|
|
active: true,
|
|
nodes: [
|
|
{
|
|
id: 'node1',
|
|
name: 'Start',
|
|
type: 'n8n-nodes-base.start',
|
|
typeVersion: 1,
|
|
position: [100, 100],
|
|
parameters: {},
|
|
},
|
|
],
|
|
connections: {},
|
|
createdAt: '2024-01-01T00:00:00Z',
|
|
updatedAt: '2024-01-01T00:00:00Z',
|
|
tags: [],
|
|
settings: {},
|
|
...overrides,
|
|
});
|
|
|
|
const createTestExecution = (overrides = {}) => ({
|
|
id: 'exec-123',
|
|
workflowId: 'test-workflow-id',
|
|
status: ExecutionStatus.SUCCESS,
|
|
startedAt: '2024-01-01T00:00:00Z',
|
|
stoppedAt: '2024-01-01T00:01:00Z',
|
|
...overrides,
|
|
});
|
|
|
|
beforeEach(async () => {
|
|
vi.clearAllMocks();
|
|
|
|
// Setup mock API client
|
|
mockApiClient = {
|
|
createWorkflow: vi.fn(),
|
|
getWorkflow: vi.fn(),
|
|
updateWorkflow: vi.fn(),
|
|
deleteWorkflow: vi.fn(),
|
|
listWorkflows: vi.fn(),
|
|
triggerWebhook: vi.fn(),
|
|
getExecution: vi.fn(),
|
|
listExecutions: vi.fn(),
|
|
deleteExecution: vi.fn(),
|
|
healthCheck: vi.fn(),
|
|
};
|
|
|
|
// Setup mock repository
|
|
mockRepository = {
|
|
getNodeByType: vi.fn(),
|
|
getAllNodes: vi.fn(),
|
|
};
|
|
|
|
// Setup mock validator
|
|
mockValidator = {
|
|
validateWorkflow: vi.fn(),
|
|
};
|
|
|
|
// Import mocked modules
|
|
getN8nApiConfig = (await import('@/config/n8n-api')).getN8nApiConfig;
|
|
n8nValidation = await import('@/services/n8n-validation');
|
|
|
|
// Mock the API config
|
|
vi.mocked(getN8nApiConfig).mockReturnValue({
|
|
baseUrl: 'https://n8n.test.com',
|
|
apiKey: 'test-key',
|
|
timeout: 30000,
|
|
maxRetries: 3,
|
|
});
|
|
|
|
// Mock validation functions
|
|
vi.mocked(n8nValidation.validateWorkflowStructure).mockReturnValue([]);
|
|
vi.mocked(n8nValidation.hasWebhookTrigger).mockReturnValue(false);
|
|
vi.mocked(n8nValidation.getWebhookUrl).mockReturnValue(null);
|
|
|
|
// Mock the N8nApiClient constructor
|
|
vi.mocked(N8nApiClient).mockImplementation(() => mockApiClient);
|
|
|
|
// Mock WorkflowValidator constructor
|
|
vi.mocked(WorkflowValidator).mockImplementation(() => mockValidator);
|
|
|
|
// Mock NodeRepository constructor
|
|
vi.mocked(NodeRepository).mockImplementation(() => mockRepository);
|
|
|
|
// Import handlers module after setting up mocks
|
|
handlers = await import('@/mcp/handlers-n8n-manager');
|
|
});
|
|
|
|
afterEach(() => {
|
|
// Clean up singleton state by accessing the module internals
|
|
if (handlers) {
|
|
// Access the module's internal state via the getN8nApiClient function
|
|
const clientGetter = handlers.getN8nApiClient;
|
|
if (clientGetter) {
|
|
// Force reset by setting config to null first
|
|
vi.mocked(getN8nApiConfig).mockReturnValue(null);
|
|
clientGetter();
|
|
}
|
|
}
|
|
});
|
|
|
|
describe('getN8nApiClient', () => {
|
|
it('should create new client when config is available', () => {
|
|
const client = handlers.getN8nApiClient();
|
|
expect(client).toBe(mockApiClient);
|
|
expect(N8nApiClient).toHaveBeenCalledWith({
|
|
baseUrl: 'https://n8n.test.com',
|
|
apiKey: 'test-key',
|
|
timeout: 30000,
|
|
maxRetries: 3,
|
|
});
|
|
});
|
|
|
|
it('should return null when config is not available', () => {
|
|
vi.mocked(getN8nApiConfig).mockReturnValue(null);
|
|
const client = handlers.getN8nApiClient();
|
|
expect(client).toBeNull();
|
|
});
|
|
|
|
it('should reuse existing client when config has not changed', () => {
|
|
// First call creates the client
|
|
const client1 = handlers.getN8nApiClient();
|
|
|
|
// Second call should reuse the same client
|
|
const client2 = handlers.getN8nApiClient();
|
|
|
|
expect(client1).toBe(client2);
|
|
expect(N8nApiClient).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('should create new client when config URL changes', () => {
|
|
// First call with initial config
|
|
const client1 = handlers.getN8nApiClient();
|
|
expect(N8nApiClient).toHaveBeenCalledTimes(1);
|
|
|
|
// Change the config URL
|
|
vi.mocked(getN8nApiConfig).mockReturnValue({
|
|
baseUrl: 'https://different.test.com',
|
|
apiKey: 'test-key',
|
|
timeout: 30000,
|
|
maxRetries: 3,
|
|
});
|
|
|
|
// Second call should create a new client
|
|
const client2 = handlers.getN8nApiClient();
|
|
expect(N8nApiClient).toHaveBeenCalledTimes(2);
|
|
|
|
// Verify the second call used the new config
|
|
expect(N8nApiClient).toHaveBeenNthCalledWith(2, {
|
|
baseUrl: 'https://different.test.com',
|
|
apiKey: 'test-key',
|
|
timeout: 30000,
|
|
maxRetries: 3,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('handleCreateWorkflow', () => {
|
|
it('should create workflow successfully', async () => {
|
|
const testWorkflow = createTestWorkflow();
|
|
const input = {
|
|
name: 'Test Workflow',
|
|
nodes: testWorkflow.nodes,
|
|
connections: testWorkflow.connections,
|
|
};
|
|
|
|
mockApiClient.createWorkflow.mockResolvedValue(testWorkflow);
|
|
|
|
const result = await handlers.handleCreateWorkflow(input);
|
|
|
|
expect(result).toEqual({
|
|
success: true,
|
|
data: testWorkflow,
|
|
message: 'Workflow "Test Workflow" created successfully with ID: test-workflow-id',
|
|
});
|
|
|
|
// Should send input as-is to API (n8n expects FULL form: n8n-nodes-base.*)
|
|
expect(mockApiClient.createWorkflow).toHaveBeenCalledWith(input);
|
|
expect(n8nValidation.validateWorkflowStructure).toHaveBeenCalledWith(input);
|
|
});
|
|
|
|
it('should handle validation errors', async () => {
|
|
const input = { invalid: 'data' };
|
|
|
|
const result = await handlers.handleCreateWorkflow(input);
|
|
|
|
expect(result.success).toBe(false);
|
|
expect(result.error).toBe('Invalid input');
|
|
expect(result.details).toHaveProperty('errors');
|
|
});
|
|
|
|
it('should handle workflow structure validation failures', async () => {
|
|
const input = {
|
|
name: 'Test Workflow',
|
|
nodes: [],
|
|
connections: {},
|
|
};
|
|
|
|
vi.mocked(n8nValidation.validateWorkflowStructure).mockReturnValue([
|
|
'Workflow must have at least one node',
|
|
]);
|
|
|
|
const result = await handlers.handleCreateWorkflow(input);
|
|
|
|
expect(result).toEqual({
|
|
success: false,
|
|
error: 'Workflow validation failed',
|
|
details: { errors: ['Workflow must have at least one node'] },
|
|
});
|
|
});
|
|
|
|
it('should handle API errors', async () => {
|
|
const input = {
|
|
name: 'Test Workflow',
|
|
nodes: [{
|
|
id: 'node1',
|
|
name: 'Start',
|
|
type: 'n8n-nodes-base.start',
|
|
typeVersion: 1,
|
|
position: [100, 100],
|
|
parameters: {}
|
|
}],
|
|
connections: {},
|
|
};
|
|
|
|
const apiError = new N8nValidationError('Invalid workflow data', {
|
|
field: 'nodes',
|
|
message: 'Node configuration invalid',
|
|
});
|
|
mockApiClient.createWorkflow.mockRejectedValue(apiError);
|
|
|
|
const result = await handlers.handleCreateWorkflow(input);
|
|
|
|
expect(result).toEqual({
|
|
success: false,
|
|
error: 'Invalid request: Invalid workflow data',
|
|
code: 'VALIDATION_ERROR',
|
|
details: { field: 'nodes', message: 'Node configuration invalid' },
|
|
});
|
|
});
|
|
|
|
it('should handle API not configured error', async () => {
|
|
vi.mocked(getN8nApiConfig).mockReturnValue(null);
|
|
|
|
const result = await handlers.handleCreateWorkflow({ name: 'Test', nodes: [], connections: {} });
|
|
|
|
expect(result).toEqual({
|
|
success: false,
|
|
error: 'n8n API not configured. Please set N8N_API_URL and N8N_API_KEY environment variables.',
|
|
});
|
|
});
|
|
|
|
describe('SHORT form detection', () => {
|
|
it('should detect and reject nodes-base.* SHORT form', async () => {
|
|
const input = {
|
|
name: 'Test Workflow',
|
|
nodes: [{
|
|
id: 'node1',
|
|
name: 'Webhook',
|
|
type: 'nodes-base.webhook',
|
|
typeVersion: 1,
|
|
position: [100, 100],
|
|
parameters: {}
|
|
}],
|
|
connections: {}
|
|
};
|
|
|
|
const result = await handlers.handleCreateWorkflow(input);
|
|
|
|
expect(result.success).toBe(false);
|
|
expect(result.error).toBe('Node type format error: n8n API requires FULL form node types');
|
|
expect(result.details.errors).toHaveLength(1);
|
|
expect(result.details.errors[0]).toContain('Node 0');
|
|
expect(result.details.errors[0]).toContain('Webhook');
|
|
expect(result.details.errors[0]).toContain('nodes-base.webhook');
|
|
expect(result.details.errors[0]).toContain('n8n-nodes-base.webhook');
|
|
expect(result.details.errors[0]).toContain('SHORT form');
|
|
expect(result.details.errors[0]).toContain('FULL form');
|
|
expect(result.details.hint).toBe('Use n8n-nodes-base.* instead of nodes-base.* for standard nodes');
|
|
});
|
|
|
|
it('should detect and reject nodes-langchain.* SHORT form', async () => {
|
|
const input = {
|
|
name: 'AI Workflow',
|
|
nodes: [{
|
|
id: 'ai1',
|
|
name: 'AI Agent',
|
|
type: 'nodes-langchain.agent',
|
|
typeVersion: 1,
|
|
position: [100, 100],
|
|
parameters: {}
|
|
}],
|
|
connections: {}
|
|
};
|
|
|
|
const result = await handlers.handleCreateWorkflow(input);
|
|
|
|
expect(result.success).toBe(false);
|
|
expect(result.error).toBe('Node type format error: n8n API requires FULL form node types');
|
|
expect(result.details.errors).toHaveLength(1);
|
|
expect(result.details.errors[0]).toContain('Node 0');
|
|
expect(result.details.errors[0]).toContain('AI Agent');
|
|
expect(result.details.errors[0]).toContain('nodes-langchain.agent');
|
|
expect(result.details.errors[0]).toContain('@n8n/n8n-nodes-langchain.agent');
|
|
expect(result.details.errors[0]).toContain('SHORT form');
|
|
expect(result.details.errors[0]).toContain('FULL form');
|
|
expect(result.details.hint).toBe('Use n8n-nodes-base.* instead of nodes-base.* for standard nodes');
|
|
});
|
|
|
|
it('should detect multiple SHORT form nodes', async () => {
|
|
const input = {
|
|
name: 'Test Workflow',
|
|
nodes: [
|
|
{
|
|
id: 'node1',
|
|
name: 'Webhook',
|
|
type: 'nodes-base.webhook',
|
|
typeVersion: 1,
|
|
position: [100, 100],
|
|
parameters: {}
|
|
},
|
|
{
|
|
id: 'node2',
|
|
name: 'HTTP Request',
|
|
type: 'nodes-base.httpRequest',
|
|
typeVersion: 1,
|
|
position: [200, 100],
|
|
parameters: {}
|
|
},
|
|
{
|
|
id: 'node3',
|
|
name: 'AI Agent',
|
|
type: 'nodes-langchain.agent',
|
|
typeVersion: 1,
|
|
position: [300, 100],
|
|
parameters: {}
|
|
}
|
|
],
|
|
connections: {}
|
|
};
|
|
|
|
const result = await handlers.handleCreateWorkflow(input);
|
|
|
|
expect(result.success).toBe(false);
|
|
expect(result.error).toBe('Node type format error: n8n API requires FULL form node types');
|
|
expect(result.details.errors).toHaveLength(3);
|
|
expect(result.details.errors[0]).toContain('Node 0');
|
|
expect(result.details.errors[0]).toContain('Webhook');
|
|
expect(result.details.errors[0]).toContain('n8n-nodes-base.webhook');
|
|
expect(result.details.errors[1]).toContain('Node 1');
|
|
expect(result.details.errors[1]).toContain('HTTP Request');
|
|
expect(result.details.errors[1]).toContain('n8n-nodes-base.httpRequest');
|
|
expect(result.details.errors[2]).toContain('Node 2');
|
|
expect(result.details.errors[2]).toContain('AI Agent');
|
|
expect(result.details.errors[2]).toContain('@n8n/n8n-nodes-langchain.agent');
|
|
});
|
|
|
|
it('should allow FULL form n8n-nodes-base.* without error', async () => {
|
|
const testWorkflow = createTestWorkflow({
|
|
nodes: [{
|
|
id: 'node1',
|
|
name: 'Webhook',
|
|
type: 'n8n-nodes-base.webhook',
|
|
typeVersion: 1,
|
|
position: [100, 100],
|
|
parameters: {}
|
|
}]
|
|
});
|
|
|
|
const input = {
|
|
name: 'Test Workflow',
|
|
nodes: testWorkflow.nodes,
|
|
connections: {}
|
|
};
|
|
|
|
mockApiClient.createWorkflow.mockResolvedValue(testWorkflow);
|
|
|
|
const result = await handlers.handleCreateWorkflow(input);
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(mockApiClient.createWorkflow).toHaveBeenCalledWith(input);
|
|
});
|
|
|
|
it('should allow FULL form @n8n/n8n-nodes-langchain.* without error', async () => {
|
|
const testWorkflow = createTestWorkflow({
|
|
nodes: [{
|
|
id: 'ai1',
|
|
name: 'AI Agent',
|
|
type: '@n8n/n8n-nodes-langchain.agent',
|
|
typeVersion: 1,
|
|
position: [100, 100],
|
|
parameters: {}
|
|
}]
|
|
});
|
|
|
|
const input = {
|
|
name: 'AI Workflow',
|
|
nodes: testWorkflow.nodes,
|
|
connections: {}
|
|
};
|
|
|
|
mockApiClient.createWorkflow.mockResolvedValue(testWorkflow);
|
|
|
|
const result = await handlers.handleCreateWorkflow(input);
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(mockApiClient.createWorkflow).toHaveBeenCalledWith(input);
|
|
});
|
|
|
|
it('should detect SHORT form in mixed FULL/SHORT workflow', async () => {
|
|
const input = {
|
|
name: 'Mixed Workflow',
|
|
nodes: [
|
|
{
|
|
id: 'node1',
|
|
name: 'Start',
|
|
type: 'n8n-nodes-base.start', // FULL form - correct
|
|
typeVersion: 1,
|
|
position: [100, 100],
|
|
parameters: {}
|
|
},
|
|
{
|
|
id: 'node2',
|
|
name: 'Webhook',
|
|
type: 'nodes-base.webhook', // SHORT form - error
|
|
typeVersion: 1,
|
|
position: [200, 100],
|
|
parameters: {}
|
|
}
|
|
],
|
|
connections: {}
|
|
};
|
|
|
|
const result = await handlers.handleCreateWorkflow(input);
|
|
|
|
expect(result.success).toBe(false);
|
|
expect(result.error).toBe('Node type format error: n8n API requires FULL form node types');
|
|
expect(result.details.errors).toHaveLength(1);
|
|
expect(result.details.errors[0]).toContain('Node 1');
|
|
expect(result.details.errors[0]).toContain('Webhook');
|
|
expect(result.details.errors[0]).toContain('nodes-base.webhook');
|
|
});
|
|
|
|
it('should handle nodes with null type gracefully', async () => {
|
|
const input = {
|
|
name: 'Test Workflow',
|
|
nodes: [{
|
|
id: 'node1',
|
|
name: 'Unknown',
|
|
type: null,
|
|
typeVersion: 1,
|
|
position: [100, 100],
|
|
parameters: {}
|
|
}],
|
|
connections: {}
|
|
};
|
|
|
|
// Should pass SHORT form detection (null doesn't start with 'nodes-base.')
|
|
// Will fail at structure validation or API call
|
|
vi.mocked(n8nValidation.validateWorkflowStructure).mockReturnValue([
|
|
'Node type is required'
|
|
]);
|
|
|
|
const result = await handlers.handleCreateWorkflow(input);
|
|
|
|
// Should fail at validation, not SHORT form detection
|
|
expect(result.success).toBe(false);
|
|
expect(result.error).toBe('Workflow validation failed');
|
|
});
|
|
|
|
it('should handle nodes with undefined type gracefully', async () => {
|
|
const input = {
|
|
name: 'Test Workflow',
|
|
nodes: [{
|
|
id: 'node1',
|
|
name: 'Unknown',
|
|
// type is undefined
|
|
typeVersion: 1,
|
|
position: [100, 100],
|
|
parameters: {}
|
|
}],
|
|
connections: {}
|
|
};
|
|
|
|
// Should pass SHORT form detection (undefined doesn't start with 'nodes-base.')
|
|
// Will fail at structure validation or API call
|
|
vi.mocked(n8nValidation.validateWorkflowStructure).mockReturnValue([
|
|
'Node type is required'
|
|
]);
|
|
|
|
const result = await handlers.handleCreateWorkflow(input);
|
|
|
|
// Should fail at validation, not SHORT form detection
|
|
expect(result.success).toBe(false);
|
|
expect(result.error).toBe('Workflow validation failed');
|
|
});
|
|
|
|
it('should handle empty nodes array gracefully', async () => {
|
|
const input = {
|
|
name: 'Empty Workflow',
|
|
nodes: [],
|
|
connections: {}
|
|
};
|
|
|
|
// Should pass SHORT form detection (no nodes to check)
|
|
vi.mocked(n8nValidation.validateWorkflowStructure).mockReturnValue([
|
|
'Workflow must have at least one node'
|
|
]);
|
|
|
|
const result = await handlers.handleCreateWorkflow(input);
|
|
|
|
// Should fail at validation, not SHORT form detection
|
|
expect(result.success).toBe(false);
|
|
expect(result.error).toBe('Workflow validation failed');
|
|
});
|
|
|
|
it('should handle nodes array with undefined nodes gracefully', async () => {
|
|
const input = {
|
|
name: 'Test Workflow',
|
|
nodes: undefined,
|
|
connections: {}
|
|
};
|
|
|
|
const result = await handlers.handleCreateWorkflow(input);
|
|
|
|
// Should fail at Zod validation (nodes is required in schema)
|
|
expect(result.success).toBe(false);
|
|
expect(result.error).toBe('Invalid input');
|
|
expect(result.details).toHaveProperty('errors');
|
|
});
|
|
|
|
it('should provide correct index in error message for multiple nodes', async () => {
|
|
const input = {
|
|
name: 'Test Workflow',
|
|
nodes: [
|
|
{
|
|
id: 'node1',
|
|
name: 'Start',
|
|
type: 'n8n-nodes-base.start', // FULL form - OK
|
|
typeVersion: 1,
|
|
position: [100, 100],
|
|
parameters: {}
|
|
},
|
|
{
|
|
id: 'node2',
|
|
name: 'Process',
|
|
type: 'n8n-nodes-base.set', // FULL form - OK
|
|
typeVersion: 1,
|
|
position: [200, 100],
|
|
parameters: {}
|
|
},
|
|
{
|
|
id: 'node3',
|
|
name: 'Webhook',
|
|
type: 'nodes-base.webhook', // SHORT form - index 2
|
|
typeVersion: 1,
|
|
position: [300, 100],
|
|
parameters: {}
|
|
}
|
|
],
|
|
connections: {}
|
|
};
|
|
|
|
const result = await handlers.handleCreateWorkflow(input);
|
|
|
|
expect(result.success).toBe(false);
|
|
expect(result.details.errors).toHaveLength(1);
|
|
expect(result.details.errors[0]).toContain('Node 2'); // Zero-indexed
|
|
expect(result.details.errors[0]).toContain('Webhook');
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('handleGetWorkflow', () => {
|
|
it('should get workflow successfully', async () => {
|
|
const testWorkflow = createTestWorkflow();
|
|
mockApiClient.getWorkflow.mockResolvedValue(testWorkflow);
|
|
|
|
const result = await handlers.handleGetWorkflow({ id: 'test-workflow-id' });
|
|
|
|
expect(result).toEqual({
|
|
success: true,
|
|
data: testWorkflow,
|
|
});
|
|
expect(mockApiClient.getWorkflow).toHaveBeenCalledWith('test-workflow-id');
|
|
});
|
|
|
|
it('should handle not found error', async () => {
|
|
const notFoundError = new N8nNotFoundError('Workflow', 'non-existent');
|
|
mockApiClient.getWorkflow.mockRejectedValue(notFoundError);
|
|
|
|
const result = await handlers.handleGetWorkflow({ id: 'non-existent' });
|
|
|
|
expect(result).toEqual({
|
|
success: false,
|
|
error: 'Workflow with ID non-existent not found',
|
|
code: 'NOT_FOUND',
|
|
});
|
|
});
|
|
|
|
it('should handle invalid input', async () => {
|
|
const result = await handlers.handleGetWorkflow({ notId: 'test' });
|
|
|
|
expect(result.success).toBe(false);
|
|
expect(result.error).toBe('Invalid input');
|
|
});
|
|
});
|
|
|
|
describe('handleGetWorkflowDetails', () => {
|
|
it('should get workflow details with execution stats', async () => {
|
|
const testWorkflow = createTestWorkflow();
|
|
const testExecutions = [
|
|
createTestExecution({ status: ExecutionStatus.SUCCESS }),
|
|
createTestExecution({ status: ExecutionStatus.ERROR }),
|
|
createTestExecution({ status: ExecutionStatus.SUCCESS }),
|
|
];
|
|
|
|
mockApiClient.getWorkflow.mockResolvedValue(testWorkflow);
|
|
mockApiClient.listExecutions.mockResolvedValue({
|
|
data: testExecutions,
|
|
nextCursor: null,
|
|
});
|
|
|
|
const result = await handlers.handleGetWorkflowDetails({ id: 'test-workflow-id' });
|
|
|
|
expect(result).toEqual({
|
|
success: true,
|
|
data: {
|
|
workflow: testWorkflow,
|
|
executionStats: {
|
|
totalExecutions: 3,
|
|
successCount: 2,
|
|
errorCount: 1,
|
|
lastExecutionTime: '2024-01-01T00:00:00Z',
|
|
},
|
|
hasWebhookTrigger: false,
|
|
webhookPath: null,
|
|
},
|
|
});
|
|
});
|
|
|
|
it('should handle workflow with webhook trigger', async () => {
|
|
const testWorkflow = createTestWorkflow({
|
|
nodes: [
|
|
{
|
|
id: 'webhook1',
|
|
name: 'Webhook',
|
|
type: 'n8n-nodes-base.webhook',
|
|
typeVersion: 1,
|
|
position: [100, 100],
|
|
parameters: { path: 'test-webhook' },
|
|
},
|
|
],
|
|
});
|
|
|
|
mockApiClient.getWorkflow.mockResolvedValue(testWorkflow);
|
|
mockApiClient.listExecutions.mockResolvedValue({ data: [], nextCursor: null });
|
|
vi.mocked(n8nValidation.hasWebhookTrigger).mockReturnValue(true);
|
|
vi.mocked(n8nValidation.getWebhookUrl).mockReturnValue('/webhook/test-webhook');
|
|
|
|
const result = await handlers.handleGetWorkflowDetails({ id: 'test-workflow-id' });
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.data).toHaveProperty('hasWebhookTrigger', true);
|
|
expect(result.data).toHaveProperty('webhookPath', '/webhook/test-webhook');
|
|
});
|
|
});
|
|
|
|
describe('handleDeleteWorkflow', () => {
|
|
it('should delete workflow successfully', async () => {
|
|
const testWorkflow = createTestWorkflow();
|
|
mockApiClient.deleteWorkflow.mockResolvedValue(testWorkflow);
|
|
|
|
const result = await handlers.handleDeleteWorkflow({ id: 'test-workflow-id' });
|
|
|
|
expect(result).toEqual({
|
|
success: true,
|
|
data: testWorkflow,
|
|
message: 'Workflow test-workflow-id deleted successfully',
|
|
});
|
|
expect(mockApiClient.deleteWorkflow).toHaveBeenCalledWith('test-workflow-id');
|
|
});
|
|
|
|
it('should handle invalid input', async () => {
|
|
const result = await handlers.handleDeleteWorkflow({ notId: 'test' });
|
|
|
|
expect(result.success).toBe(false);
|
|
expect(result.error).toBe('Invalid input');
|
|
expect(result.details).toHaveProperty('errors');
|
|
});
|
|
|
|
it('should handle N8nApiError', async () => {
|
|
const apiError = new N8nNotFoundError('Workflow', 'non-existent-id');
|
|
mockApiClient.deleteWorkflow.mockRejectedValue(apiError);
|
|
|
|
const result = await handlers.handleDeleteWorkflow({ id: 'non-existent-id' });
|
|
|
|
expect(result).toEqual({
|
|
success: false,
|
|
error: 'Workflow with ID non-existent-id not found',
|
|
code: 'NOT_FOUND',
|
|
});
|
|
});
|
|
|
|
it('should handle generic errors', async () => {
|
|
const genericError = new Error('Database connection failed');
|
|
mockApiClient.deleteWorkflow.mockRejectedValue(genericError);
|
|
|
|
const result = await handlers.handleDeleteWorkflow({ id: 'test-workflow-id' });
|
|
|
|
expect(result).toEqual({
|
|
success: false,
|
|
error: 'Database connection failed',
|
|
});
|
|
});
|
|
|
|
it('should handle API not configured error', async () => {
|
|
vi.mocked(getN8nApiConfig).mockReturnValue(null);
|
|
|
|
const result = await handlers.handleDeleteWorkflow({ id: 'test-workflow-id' });
|
|
|
|
expect(result).toEqual({
|
|
success: false,
|
|
error: 'n8n API not configured. Please set N8N_API_URL and N8N_API_KEY environment variables.',
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('handleListWorkflows', () => {
|
|
it('should list workflows with minimal data', async () => {
|
|
const workflows = [
|
|
createTestWorkflow({ id: 'wf1', name: 'Workflow 1', nodes: [{}, {}] }),
|
|
createTestWorkflow({ id: 'wf2', name: 'Workflow 2', active: false, nodes: [{}, {}, {}] }),
|
|
];
|
|
|
|
mockApiClient.listWorkflows.mockResolvedValue({
|
|
data: workflows,
|
|
nextCursor: 'next-page-cursor',
|
|
});
|
|
|
|
const result = await handlers.handleListWorkflows({
|
|
limit: 50,
|
|
active: true,
|
|
});
|
|
|
|
expect(result).toEqual({
|
|
success: true,
|
|
data: {
|
|
workflows: [
|
|
{
|
|
id: 'wf1',
|
|
name: 'Workflow 1',
|
|
active: true,
|
|
createdAt: '2024-01-01T00:00:00Z',
|
|
updatedAt: '2024-01-01T00:00:00Z',
|
|
tags: [],
|
|
nodeCount: 2,
|
|
},
|
|
{
|
|
id: 'wf2',
|
|
name: 'Workflow 2',
|
|
active: false,
|
|
createdAt: '2024-01-01T00:00:00Z',
|
|
updatedAt: '2024-01-01T00:00:00Z',
|
|
tags: [],
|
|
nodeCount: 3,
|
|
},
|
|
],
|
|
returned: 2,
|
|
nextCursor: 'next-page-cursor',
|
|
hasMore: true,
|
|
_note: 'More workflows available. Use cursor to get next page.',
|
|
},
|
|
});
|
|
});
|
|
|
|
it('should handle invalid input with ZodError', async () => {
|
|
const result = await handlers.handleListWorkflows({
|
|
limit: 'invalid', // Should be a number
|
|
});
|
|
|
|
expect(result.success).toBe(false);
|
|
expect(result.error).toBe('Invalid input');
|
|
expect(result.details).toHaveProperty('errors');
|
|
});
|
|
|
|
it('should handle N8nApiError', async () => {
|
|
const apiError = new N8nAuthenticationError('Invalid API key');
|
|
mockApiClient.listWorkflows.mockRejectedValue(apiError);
|
|
|
|
const result = await handlers.handleListWorkflows({});
|
|
|
|
expect(result).toEqual({
|
|
success: false,
|
|
error: 'Failed to authenticate with n8n. Please check your API key.',
|
|
code: 'AUTHENTICATION_ERROR',
|
|
});
|
|
});
|
|
|
|
it('should handle generic errors', async () => {
|
|
const genericError = new Error('Network timeout');
|
|
mockApiClient.listWorkflows.mockRejectedValue(genericError);
|
|
|
|
const result = await handlers.handleListWorkflows({});
|
|
|
|
expect(result).toEqual({
|
|
success: false,
|
|
error: 'Network timeout',
|
|
});
|
|
});
|
|
|
|
it('should handle workflows without isArchived field gracefully', async () => {
|
|
const workflows = [
|
|
createTestWorkflow({ id: 'wf1', name: 'Workflow 1' }),
|
|
];
|
|
// Remove isArchived field to test undefined handling
|
|
delete (workflows[0] as any).isArchived;
|
|
|
|
mockApiClient.listWorkflows.mockResolvedValue({
|
|
data: workflows,
|
|
nextCursor: null,
|
|
});
|
|
|
|
const result = await handlers.handleListWorkflows({});
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.data.workflows[0]).toHaveProperty('isArchived');
|
|
});
|
|
|
|
it('should convert tags array to comma-separated string', async () => {
|
|
const workflows = [
|
|
createTestWorkflow({ id: 'wf1', name: 'Workflow 1', tags: ['tag1', 'tag2'] }),
|
|
];
|
|
|
|
mockApiClient.listWorkflows.mockResolvedValue({
|
|
data: workflows,
|
|
nextCursor: null,
|
|
});
|
|
|
|
const result = await handlers.handleListWorkflows({
|
|
tags: ['production', 'active'],
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(mockApiClient.listWorkflows).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
tags: 'production,active',
|
|
})
|
|
);
|
|
});
|
|
|
|
it('should handle empty tags array', async () => {
|
|
const workflows = [
|
|
createTestWorkflow({ id: 'wf1', name: 'Workflow 1' }),
|
|
];
|
|
|
|
mockApiClient.listWorkflows.mockResolvedValue({
|
|
data: workflows,
|
|
nextCursor: null,
|
|
});
|
|
|
|
const result = await handlers.handleListWorkflows({
|
|
tags: [],
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(mockApiClient.listWorkflows).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
tags: undefined,
|
|
})
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('handleValidateWorkflow', () => {
|
|
it('should validate workflow from n8n instance', async () => {
|
|
const testWorkflow = createTestWorkflow();
|
|
const mockNodeRepository = {} as any; // Mock repository
|
|
|
|
mockApiClient.getWorkflow.mockResolvedValue(testWorkflow);
|
|
mockValidator.validateWorkflow.mockResolvedValue({
|
|
valid: true,
|
|
errors: [],
|
|
warnings: [
|
|
{
|
|
nodeName: 'node1',
|
|
message: 'Consider using newer version',
|
|
details: { currentVersion: 1, latestVersion: 2 },
|
|
},
|
|
],
|
|
suggestions: ['Add error handling to workflow'],
|
|
statistics: {
|
|
totalNodes: 1,
|
|
enabledNodes: 1,
|
|
triggerNodes: 1,
|
|
validConnections: 0,
|
|
invalidConnections: 0,
|
|
expressionsValidated: 0,
|
|
},
|
|
});
|
|
|
|
const result = await handlers.handleValidateWorkflow(
|
|
{ id: 'test-workflow-id', options: { validateNodes: true } },
|
|
mockNodeRepository
|
|
);
|
|
|
|
expect(result).toEqual({
|
|
success: true,
|
|
data: {
|
|
valid: true,
|
|
workflowId: 'test-workflow-id',
|
|
workflowName: 'Test Workflow',
|
|
summary: {
|
|
totalNodes: 1,
|
|
enabledNodes: 1,
|
|
triggerNodes: 1,
|
|
validConnections: 0,
|
|
invalidConnections: 0,
|
|
expressionsValidated: 0,
|
|
errorCount: 0,
|
|
warningCount: 1,
|
|
},
|
|
warnings: [
|
|
{
|
|
node: 'node1',
|
|
nodeName: 'node1',
|
|
message: 'Consider using newer version',
|
|
details: { currentVersion: 1, latestVersion: 2 },
|
|
},
|
|
],
|
|
suggestions: ['Add error handling to workflow'],
|
|
},
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('handleHealthCheck', () => {
|
|
it('should check health successfully', async () => {
|
|
const healthData = {
|
|
status: 'ok',
|
|
instanceId: 'n8n-instance-123',
|
|
n8nVersion: '1.0.0',
|
|
features: ['webhooks', 'api'],
|
|
};
|
|
|
|
mockApiClient.healthCheck.mockResolvedValue(healthData);
|
|
|
|
const result = await handlers.handleHealthCheck();
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.data).toMatchObject({
|
|
status: 'ok',
|
|
instanceId: 'n8n-instance-123',
|
|
n8nVersion: '1.0.0',
|
|
features: ['webhooks', 'api'],
|
|
apiUrl: 'https://n8n.test.com',
|
|
});
|
|
});
|
|
|
|
it('should handle API errors', async () => {
|
|
const apiError = new N8nServerError('Service unavailable');
|
|
mockApiClient.healthCheck.mockRejectedValue(apiError);
|
|
|
|
const result = await handlers.handleHealthCheck();
|
|
|
|
expect(result).toEqual({
|
|
success: false,
|
|
error: 'Service unavailable',
|
|
code: 'SERVER_ERROR',
|
|
details: {
|
|
apiUrl: 'https://n8n.test.com',
|
|
hint: 'Check if n8n is running and API is enabled',
|
|
troubleshooting: [
|
|
'1. Verify n8n instance is running',
|
|
'2. Check N8N_API_URL is correct',
|
|
'3. Verify N8N_API_KEY has proper permissions',
|
|
'4. Run n8n_health_check with mode="diagnostic" for detailed analysis',
|
|
],
|
|
},
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('handleDiagnostic', () => {
|
|
it('should provide diagnostic information', async () => {
|
|
const healthData = {
|
|
status: 'ok',
|
|
n8nVersion: '1.0.0',
|
|
};
|
|
mockApiClient.healthCheck.mockResolvedValue(healthData);
|
|
|
|
// Set environment variables for the test
|
|
process.env.N8N_API_URL = 'https://n8n.test.com';
|
|
process.env.N8N_API_KEY = 'test-key';
|
|
|
|
const result = await handlers.handleDiagnostic({ params: { arguments: {} } });
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.data).toMatchObject({
|
|
environment: {
|
|
N8N_API_URL: 'https://n8n.test.com',
|
|
N8N_API_KEY: '***configured***',
|
|
},
|
|
apiConfiguration: {
|
|
configured: true,
|
|
status: {
|
|
configured: true,
|
|
connected: true,
|
|
version: '1.0.0',
|
|
},
|
|
},
|
|
toolsAvailability: {
|
|
documentationTools: {
|
|
count: 7,
|
|
enabled: true,
|
|
},
|
|
managementTools: {
|
|
count: 13,
|
|
enabled: true,
|
|
},
|
|
totalAvailable: 20,
|
|
},
|
|
});
|
|
|
|
// Clean up env vars
|
|
process.env.N8N_API_URL = undefined as any;
|
|
process.env.N8N_API_KEY = undefined as any;
|
|
});
|
|
});
|
|
|
|
describe('Error handling', () => {
|
|
it('should handle authentication errors', async () => {
|
|
const authError = new N8nAuthenticationError('Invalid API key');
|
|
mockApiClient.getWorkflow.mockRejectedValue(authError);
|
|
|
|
const result = await handlers.handleGetWorkflow({ id: 'test-id' });
|
|
|
|
expect(result).toEqual({
|
|
success: false,
|
|
error: 'Failed to authenticate with n8n. Please check your API key.',
|
|
code: 'AUTHENTICATION_ERROR',
|
|
});
|
|
});
|
|
|
|
it('should handle rate limit errors', async () => {
|
|
const rateLimitError = new N8nRateLimitError(60);
|
|
mockApiClient.listWorkflows.mockRejectedValue(rateLimitError);
|
|
|
|
const result = await handlers.handleListWorkflows({});
|
|
|
|
expect(result).toEqual({
|
|
success: false,
|
|
error: 'Too many requests. Please wait a moment and try again.',
|
|
code: 'RATE_LIMIT_ERROR',
|
|
});
|
|
});
|
|
|
|
it('should handle generic errors', async () => {
|
|
const genericError = new Error('Something went wrong');
|
|
mockApiClient.createWorkflow.mockRejectedValue(genericError);
|
|
|
|
const result = await handlers.handleCreateWorkflow({
|
|
name: 'Test',
|
|
nodes: [],
|
|
connections: {},
|
|
});
|
|
|
|
expect(result).toEqual({
|
|
success: false,
|
|
error: 'Something went wrong',
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('handleTriggerWebhookWorkflow', () => {
|
|
it('should trigger webhook successfully', async () => {
|
|
const webhookResponse = {
|
|
status: 200,
|
|
statusText: 'OK',
|
|
data: { result: 'success' },
|
|
headers: {}
|
|
};
|
|
|
|
mockApiClient.triggerWebhook.mockResolvedValue(webhookResponse);
|
|
|
|
const result = await handlers.handleTriggerWebhookWorkflow({
|
|
webhookUrl: 'https://n8n.test.com/webhook/test-123',
|
|
httpMethod: 'POST',
|
|
data: { test: 'data' }
|
|
});
|
|
|
|
expect(result).toEqual({
|
|
success: true,
|
|
data: webhookResponse,
|
|
message: 'Webhook triggered successfully'
|
|
});
|
|
});
|
|
|
|
it('should extract execution ID from webhook error response', async () => {
|
|
const apiError = new N8nServerError('Workflow execution failed');
|
|
apiError.details = {
|
|
executionId: 'exec_abc123',
|
|
workflowId: 'wf_xyz789'
|
|
};
|
|
|
|
mockApiClient.triggerWebhook.mockRejectedValue(apiError);
|
|
|
|
const result = await handlers.handleTriggerWebhookWorkflow({
|
|
webhookUrl: 'https://n8n.test.com/webhook/test-123',
|
|
httpMethod: 'POST'
|
|
});
|
|
|
|
expect(result.success).toBe(false);
|
|
expect(result.error).toContain('Workflow wf_xyz789 execution exec_abc123 failed');
|
|
expect(result.error).toContain('n8n_get_execution');
|
|
expect(result.error).toContain("mode: 'preview'");
|
|
expect(result.executionId).toBe('exec_abc123');
|
|
expect(result.workflowId).toBe('wf_xyz789');
|
|
});
|
|
|
|
it('should extract execution ID without workflow ID', async () => {
|
|
const apiError = new N8nServerError('Execution failed');
|
|
apiError.details = {
|
|
executionId: 'exec_only_123'
|
|
};
|
|
|
|
mockApiClient.triggerWebhook.mockRejectedValue(apiError);
|
|
|
|
const result = await handlers.handleTriggerWebhookWorkflow({
|
|
webhookUrl: 'https://n8n.test.com/webhook/test-123',
|
|
httpMethod: 'GET'
|
|
});
|
|
|
|
expect(result.success).toBe(false);
|
|
expect(result.error).toContain('Execution exec_only_123 failed');
|
|
expect(result.error).toContain('n8n_get_execution');
|
|
expect(result.error).toContain("mode: 'preview'");
|
|
expect(result.executionId).toBe('exec_only_123');
|
|
expect(result.workflowId).toBeUndefined();
|
|
});
|
|
|
|
it('should handle execution ID as "id" field', async () => {
|
|
const apiError = new N8nServerError('Error');
|
|
apiError.details = {
|
|
id: 'exec_from_id_field',
|
|
workflowId: 'wf_test'
|
|
};
|
|
|
|
mockApiClient.triggerWebhook.mockRejectedValue(apiError);
|
|
|
|
const result = await handlers.handleTriggerWebhookWorkflow({
|
|
webhookUrl: 'https://n8n.test.com/webhook/test',
|
|
httpMethod: 'POST'
|
|
});
|
|
|
|
expect(result.error).toContain('exec_from_id_field');
|
|
expect(result.executionId).toBe('exec_from_id_field');
|
|
});
|
|
|
|
it('should provide generic guidance when no execution ID is available', async () => {
|
|
const apiError = new N8nServerError('Server error without execution context');
|
|
apiError.details = {}; // No execution ID
|
|
|
|
mockApiClient.triggerWebhook.mockRejectedValue(apiError);
|
|
|
|
const result = await handlers.handleTriggerWebhookWorkflow({
|
|
webhookUrl: 'https://n8n.test.com/webhook/test',
|
|
httpMethod: 'POST'
|
|
});
|
|
|
|
expect(result.success).toBe(false);
|
|
expect(result.error).toContain('Workflow failed to execute');
|
|
expect(result.error).toContain('n8n_list_executions');
|
|
expect(result.error).toContain('n8n_get_execution');
|
|
expect(result.error).toContain("mode='preview'");
|
|
expect(result.executionId).toBeUndefined();
|
|
});
|
|
|
|
it('should use standard error message for authentication errors', async () => {
|
|
const authError = new N8nAuthenticationError('Invalid API key');
|
|
mockApiClient.triggerWebhook.mockRejectedValue(authError);
|
|
|
|
const result = await handlers.handleTriggerWebhookWorkflow({
|
|
webhookUrl: 'https://n8n.test.com/webhook/test',
|
|
httpMethod: 'POST'
|
|
});
|
|
|
|
expect(result).toEqual({
|
|
success: false,
|
|
error: 'Failed to authenticate with n8n. Please check your API key.',
|
|
code: 'AUTHENTICATION_ERROR',
|
|
details: undefined
|
|
});
|
|
});
|
|
|
|
it('should use standard error message for validation errors', async () => {
|
|
const validationError = new N8nValidationError('Invalid webhook URL');
|
|
mockApiClient.triggerWebhook.mockRejectedValue(validationError);
|
|
|
|
const result = await handlers.handleTriggerWebhookWorkflow({
|
|
webhookUrl: 'https://n8n.test.com/webhook/test',
|
|
httpMethod: 'POST'
|
|
});
|
|
|
|
expect(result.error).toBe('Invalid request: Invalid webhook URL');
|
|
expect(result.code).toBe('VALIDATION_ERROR');
|
|
});
|
|
|
|
it('should handle invalid input with Zod validation error', async () => {
|
|
const result = await handlers.handleTriggerWebhookWorkflow({
|
|
webhookUrl: 'not-a-url',
|
|
httpMethod: 'INVALID_METHOD'
|
|
});
|
|
|
|
expect(result.success).toBe(false);
|
|
expect(result.error).toBe('Invalid input');
|
|
expect(result.details).toHaveProperty('errors');
|
|
});
|
|
|
|
it('should not include "contact support" in error messages', async () => {
|
|
const apiError = new N8nServerError('Test error');
|
|
apiError.details = { executionId: 'test_exec' };
|
|
|
|
mockApiClient.triggerWebhook.mockRejectedValue(apiError);
|
|
|
|
const result = await handlers.handleTriggerWebhookWorkflow({
|
|
webhookUrl: 'https://n8n.test.com/webhook/test',
|
|
httpMethod: 'POST'
|
|
});
|
|
|
|
expect(result.error?.toLowerCase()).not.toContain('contact support');
|
|
expect(result.error?.toLowerCase()).not.toContain('try again later');
|
|
});
|
|
|
|
it('should always recommend preview mode in error messages', async () => {
|
|
const apiError = new N8nServerError('Error');
|
|
apiError.details = { executionId: 'test_123' };
|
|
|
|
mockApiClient.triggerWebhook.mockRejectedValue(apiError);
|
|
|
|
const result = await handlers.handleTriggerWebhookWorkflow({
|
|
webhookUrl: 'https://n8n.test.com/webhook/test',
|
|
httpMethod: 'POST'
|
|
});
|
|
|
|
expect(result.error).toMatch(/mode:\s*'preview'/);
|
|
});
|
|
});
|
|
}); |