mirror of
https://github.com/czlonkowski/n8n-mcp.git
synced 2026-02-07 05:53:07 +00:00
feat: rename n8n_trigger_webhook_workflow to n8n_test_workflow with multi-trigger support (#460)
* 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>
This commit is contained in:
committed by
GitHub
parent
ddf9556759
commit
33690c5650
335
tests/unit/triggers/handlers/base-handler.test.ts
Normal file
335
tests/unit/triggers/handlers/base-handler.test.ts
Normal file
@@ -0,0 +1,335 @@
|
||||
/**
|
||||
* Unit tests for BaseTriggerHandler
|
||||
*/
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { BaseTriggerHandler } from '../../../../src/triggers/handlers/base-handler';
|
||||
import { N8nApiClient } from '../../../../src/services/n8n-api-client';
|
||||
import { InstanceContext } from '../../../../src/types/instance-context';
|
||||
import { Workflow } from '../../../../src/types/n8n-api';
|
||||
import { TriggerType, TriggerResponse, TriggerHandlerCapabilities, BaseTriggerInput } from '../../../../src/triggers/types';
|
||||
import { z } from 'zod';
|
||||
|
||||
// Mock getN8nApiConfig
|
||||
vi.mock('../../../../src/config/n8n-api', () => ({
|
||||
getN8nApiConfig: vi.fn(() => ({
|
||||
baseUrl: 'https://env-n8n.example.com/api/v1',
|
||||
apiKey: 'env-api-key',
|
||||
})),
|
||||
}));
|
||||
|
||||
// Create a concrete implementation for testing
|
||||
class TestHandler extends BaseTriggerHandler {
|
||||
readonly triggerType: TriggerType = 'webhook';
|
||||
readonly capabilities: TriggerHandlerCapabilities = {
|
||||
requiresActiveWorkflow: true,
|
||||
canPassInputData: true,
|
||||
};
|
||||
readonly inputSchema = z.object({
|
||||
workflowId: z.string(),
|
||||
triggerType: z.literal('webhook'),
|
||||
});
|
||||
|
||||
async execute(
|
||||
input: BaseTriggerInput,
|
||||
workflow: Workflow
|
||||
): Promise<TriggerResponse> {
|
||||
return {
|
||||
success: true,
|
||||
triggerType: this.triggerType,
|
||||
workflowId: input.workflowId,
|
||||
data: { test: 'data' },
|
||||
metadata: { duration: 100 },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Create mock client
|
||||
const createMockClient = (): N8nApiClient => ({
|
||||
getWorkflow: vi.fn(),
|
||||
listWorkflows: vi.fn(),
|
||||
createWorkflow: vi.fn(),
|
||||
updateWorkflow: vi.fn(),
|
||||
deleteWorkflow: vi.fn(),
|
||||
triggerWebhook: vi.fn(),
|
||||
getExecution: vi.fn(),
|
||||
listExecutions: vi.fn(),
|
||||
deleteExecution: vi.fn(),
|
||||
} as unknown as N8nApiClient);
|
||||
|
||||
describe('BaseTriggerHandler', () => {
|
||||
let mockClient: N8nApiClient;
|
||||
|
||||
beforeEach(() => {
|
||||
mockClient = createMockClient();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('constructor', () => {
|
||||
it('should initialize with client only', () => {
|
||||
const handler = new TestHandler(mockClient);
|
||||
expect(handler).toBeDefined();
|
||||
expect(handler.triggerType).toBe('webhook');
|
||||
});
|
||||
|
||||
it('should initialize with client and context', () => {
|
||||
const context: InstanceContext = {
|
||||
n8nApiUrl: 'https://test.n8n.com/api/v1',
|
||||
n8nApiKey: 'test-key',
|
||||
sessionId: 'test-session',
|
||||
};
|
||||
const handler = new TestHandler(mockClient, context);
|
||||
expect(handler).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('validate', () => {
|
||||
it('should validate correct input', () => {
|
||||
const handler = new TestHandler(mockClient);
|
||||
const input = {
|
||||
workflowId: 'workflow-123',
|
||||
triggerType: 'webhook',
|
||||
};
|
||||
|
||||
const result = handler.validate(input);
|
||||
expect(result).toEqual(input);
|
||||
});
|
||||
|
||||
it('should throw ZodError for invalid input', () => {
|
||||
const handler = new TestHandler(mockClient);
|
||||
const input = {
|
||||
workflowId: 123, // Wrong type
|
||||
triggerType: 'webhook',
|
||||
};
|
||||
|
||||
expect(() => handler.validate(input)).toThrow();
|
||||
});
|
||||
|
||||
it('should throw ZodError for missing required fields', () => {
|
||||
const handler = new TestHandler(mockClient);
|
||||
const input = {
|
||||
triggerType: 'webhook',
|
||||
// Missing workflowId
|
||||
};
|
||||
|
||||
expect(() => handler.validate(input)).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getBaseUrl', () => {
|
||||
it('should return base URL from context', () => {
|
||||
const context: InstanceContext = {
|
||||
n8nApiUrl: 'https://context.n8n.com/api/v1',
|
||||
n8nApiKey: 'context-key',
|
||||
sessionId: 'test-session',
|
||||
};
|
||||
const handler = new TestHandler(mockClient, context);
|
||||
|
||||
const baseUrl = (handler as any).getBaseUrl();
|
||||
expect(baseUrl).toBe('https://context.n8n.com');
|
||||
});
|
||||
|
||||
it('should strip trailing slash and /api/v1 from context URL', () => {
|
||||
const context: InstanceContext = {
|
||||
n8nApiUrl: 'https://context.n8n.com/api/v1/',
|
||||
n8nApiKey: 'context-key',
|
||||
sessionId: 'test-session',
|
||||
};
|
||||
const handler = new TestHandler(mockClient, context);
|
||||
|
||||
const baseUrl = (handler as any).getBaseUrl();
|
||||
expect(baseUrl).toBe('https://context.n8n.com');
|
||||
});
|
||||
|
||||
it('should return base URL from environment config when no context', () => {
|
||||
const handler = new TestHandler(mockClient);
|
||||
|
||||
const baseUrl = (handler as any).getBaseUrl();
|
||||
expect(baseUrl).toBe('https://env-n8n.example.com');
|
||||
});
|
||||
|
||||
it('should prefer context over environment config', () => {
|
||||
const context: InstanceContext = {
|
||||
n8nApiUrl: 'https://context.n8n.com/api/v1',
|
||||
n8nApiKey: 'context-key',
|
||||
sessionId: 'test-session',
|
||||
};
|
||||
const handler = new TestHandler(mockClient, context);
|
||||
|
||||
const baseUrl = (handler as any).getBaseUrl();
|
||||
expect(baseUrl).toBe('https://context.n8n.com');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getApiKey', () => {
|
||||
it('should return API key from context', () => {
|
||||
const context: InstanceContext = {
|
||||
n8nApiUrl: 'https://context.n8n.com/api/v1',
|
||||
n8nApiKey: 'context-api-key',
|
||||
sessionId: 'test-session',
|
||||
};
|
||||
const handler = new TestHandler(mockClient, context);
|
||||
|
||||
const apiKey = (handler as any).getApiKey();
|
||||
expect(apiKey).toBe('context-api-key');
|
||||
});
|
||||
|
||||
it('should return API key from environment config when no context', () => {
|
||||
const handler = new TestHandler(mockClient);
|
||||
|
||||
const apiKey = (handler as any).getApiKey();
|
||||
expect(apiKey).toBe('env-api-key');
|
||||
});
|
||||
|
||||
it('should prefer context over environment config', () => {
|
||||
const context: InstanceContext = {
|
||||
n8nApiUrl: 'https://context.n8n.com/api/v1',
|
||||
n8nApiKey: 'context-key',
|
||||
sessionId: 'test-session',
|
||||
};
|
||||
const handler = new TestHandler(mockClient, context);
|
||||
|
||||
const apiKey = (handler as any).getApiKey();
|
||||
expect(apiKey).toBe('context-key');
|
||||
});
|
||||
});
|
||||
|
||||
describe('normalizeResponse', () => {
|
||||
it('should create normalized success response', () => {
|
||||
const handler = new TestHandler(mockClient);
|
||||
const input: BaseTriggerInput = {
|
||||
workflowId: 'workflow-123',
|
||||
triggerType: 'webhook',
|
||||
};
|
||||
const startTime = Date.now() - 150;
|
||||
const result = { data: 'test-result' };
|
||||
|
||||
const response = (handler as any).normalizeResponse(result, input, startTime);
|
||||
|
||||
expect(response.success).toBe(true);
|
||||
expect(response.triggerType).toBe('webhook');
|
||||
expect(response.workflowId).toBe('workflow-123');
|
||||
expect(response.data).toEqual(result);
|
||||
expect(response.metadata.duration).toBeGreaterThanOrEqual(150);
|
||||
});
|
||||
|
||||
it('should merge extra fields into response', () => {
|
||||
const handler = new TestHandler(mockClient);
|
||||
const input: BaseTriggerInput = {
|
||||
workflowId: 'workflow-123',
|
||||
triggerType: 'webhook',
|
||||
};
|
||||
const startTime = Date.now();
|
||||
const result = { data: 'test' };
|
||||
const extra = {
|
||||
executionId: 'exec-123',
|
||||
status: 200,
|
||||
};
|
||||
|
||||
const response = (handler as any).normalizeResponse(result, input, startTime, extra);
|
||||
|
||||
expect(response.executionId).toBe('exec-123');
|
||||
expect(response.status).toBe(200);
|
||||
});
|
||||
|
||||
it('should calculate duration correctly', () => {
|
||||
const handler = new TestHandler(mockClient);
|
||||
const input: BaseTriggerInput = {
|
||||
workflowId: 'workflow-123',
|
||||
triggerType: 'webhook',
|
||||
};
|
||||
const startTime = Date.now() - 500;
|
||||
|
||||
const response = (handler as any).normalizeResponse({}, input, startTime);
|
||||
|
||||
expect(response.metadata.duration).toBeGreaterThanOrEqual(500);
|
||||
expect(response.metadata.duration).toBeLessThan(1000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('errorResponse', () => {
|
||||
it('should create error response', () => {
|
||||
const handler = new TestHandler(mockClient);
|
||||
const input: BaseTriggerInput = {
|
||||
workflowId: 'workflow-123',
|
||||
triggerType: 'webhook',
|
||||
};
|
||||
const startTime = Date.now() - 200;
|
||||
|
||||
const response = (handler as any).errorResponse(
|
||||
input,
|
||||
'Test error message',
|
||||
startTime
|
||||
);
|
||||
|
||||
expect(response.success).toBe(false);
|
||||
expect(response.triggerType).toBe('webhook');
|
||||
expect(response.workflowId).toBe('workflow-123');
|
||||
expect(response.error).toBe('Test error message');
|
||||
expect(response.metadata.duration).toBeGreaterThanOrEqual(200);
|
||||
});
|
||||
|
||||
it('should merge extra error details', () => {
|
||||
const handler = new TestHandler(mockClient);
|
||||
const input: BaseTriggerInput = {
|
||||
workflowId: 'workflow-123',
|
||||
triggerType: 'webhook',
|
||||
};
|
||||
const startTime = Date.now();
|
||||
const extra = {
|
||||
code: 'ERR_TEST',
|
||||
details: { reason: 'test reason' },
|
||||
};
|
||||
|
||||
const response = (handler as any).errorResponse(
|
||||
input,
|
||||
'Error',
|
||||
startTime,
|
||||
extra
|
||||
);
|
||||
|
||||
expect(response.code).toBe('ERR_TEST');
|
||||
expect(response.details).toEqual({ reason: 'test reason' });
|
||||
});
|
||||
|
||||
it('should calculate error duration correctly', () => {
|
||||
const handler = new TestHandler(mockClient);
|
||||
const input: BaseTriggerInput = {
|
||||
workflowId: 'workflow-123',
|
||||
triggerType: 'webhook',
|
||||
};
|
||||
const startTime = Date.now() - 750;
|
||||
|
||||
const response = (handler as any).errorResponse(input, 'Error', startTime);
|
||||
|
||||
expect(response.metadata.duration).toBeGreaterThanOrEqual(750);
|
||||
expect(response.metadata.duration).toBeLessThan(1500);
|
||||
});
|
||||
});
|
||||
|
||||
describe('execute', () => {
|
||||
it('should execute successfully', async () => {
|
||||
const handler = new TestHandler(mockClient);
|
||||
const input: BaseTriggerInput = {
|
||||
workflowId: 'workflow-123',
|
||||
triggerType: 'webhook',
|
||||
};
|
||||
const workflow: Workflow = {
|
||||
id: 'workflow-123',
|
||||
name: 'Test Workflow',
|
||||
active: true,
|
||||
nodes: [],
|
||||
connections: {},
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
settings: {},
|
||||
staticData: undefined,
|
||||
} as Workflow;
|
||||
|
||||
const response = await handler.execute(input, workflow);
|
||||
|
||||
expect(response.success).toBe(true);
|
||||
expect(response.workflowId).toBe('workflow-123');
|
||||
expect(response.data).toEqual({ test: 'data' });
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user