mirror of
https://github.com/czlonkowski/n8n-mcp.git
synced 2026-02-06 21:43: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
330
tests/unit/triggers/trigger-detector.test.ts
Normal file
330
tests/unit/triggers/trigger-detector.test.ts
Normal file
@@ -0,0 +1,330 @@
|
||||
/**
|
||||
* Unit tests for trigger detection
|
||||
*/
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { detectTriggerFromWorkflow, buildTriggerUrl, describeTrigger } from '../../../src/triggers/trigger-detector';
|
||||
import type { Workflow } from '../../../src/types/n8n-api';
|
||||
|
||||
// Helper to create a workflow with a specific trigger node
|
||||
function createWorkflowWithTrigger(triggerType: string, params: Record<string, unknown> = {}): Workflow {
|
||||
return {
|
||||
id: 'test-workflow',
|
||||
name: 'Test Workflow',
|
||||
active: true,
|
||||
nodes: [
|
||||
{
|
||||
id: 'trigger-node',
|
||||
name: 'Trigger',
|
||||
type: triggerType,
|
||||
typeVersion: 1,
|
||||
position: [0, 0],
|
||||
parameters: params,
|
||||
},
|
||||
{
|
||||
id: 'action-node',
|
||||
name: 'Action',
|
||||
type: 'n8n-nodes-base.noOp',
|
||||
typeVersion: 1,
|
||||
position: [200, 0],
|
||||
parameters: {},
|
||||
},
|
||||
],
|
||||
connections: {},
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
settings: {},
|
||||
staticData: undefined,
|
||||
} as Workflow;
|
||||
}
|
||||
|
||||
describe('Trigger Detector', () => {
|
||||
describe('detectTriggerFromWorkflow', () => {
|
||||
describe('webhook detection', () => {
|
||||
it('should detect n8n-nodes-base.webhook as webhook trigger', () => {
|
||||
const workflow = createWorkflowWithTrigger('n8n-nodes-base.webhook', {
|
||||
path: 'my-webhook',
|
||||
httpMethod: 'POST',
|
||||
});
|
||||
|
||||
const result = detectTriggerFromWorkflow(workflow);
|
||||
|
||||
expect(result.detected).toBe(true);
|
||||
expect(result.trigger?.type).toBe('webhook');
|
||||
expect(result.trigger?.webhookPath).toBe('my-webhook');
|
||||
expect(result.trigger?.httpMethod).toBe('POST');
|
||||
});
|
||||
|
||||
it('should detect webhook node with httpMethod from parameters', () => {
|
||||
const workflow = createWorkflowWithTrigger('n8n-nodes-base.webhook', {
|
||||
path: 'get-data',
|
||||
httpMethod: 'GET',
|
||||
});
|
||||
|
||||
const result = detectTriggerFromWorkflow(workflow);
|
||||
|
||||
expect(result.detected).toBe(true);
|
||||
expect(result.trigger?.type).toBe('webhook');
|
||||
expect(result.trigger?.httpMethod).toBe('GET');
|
||||
});
|
||||
|
||||
it('should default httpMethod to POST when not specified', () => {
|
||||
const workflow = createWorkflowWithTrigger('n8n-nodes-base.webhook', {
|
||||
path: 'test-path',
|
||||
});
|
||||
|
||||
const result = detectTriggerFromWorkflow(workflow);
|
||||
|
||||
expect(result.detected).toBe(true);
|
||||
expect(result.trigger?.type).toBe('webhook');
|
||||
// Default is POST when not specified
|
||||
expect(result.trigger?.httpMethod).toBe('POST');
|
||||
});
|
||||
});
|
||||
|
||||
describe('form detection', () => {
|
||||
it('should detect n8n-nodes-base.formTrigger as form trigger', () => {
|
||||
const workflow = createWorkflowWithTrigger('n8n-nodes-base.formTrigger', {
|
||||
path: 'my-form',
|
||||
});
|
||||
|
||||
const result = detectTriggerFromWorkflow(workflow);
|
||||
|
||||
expect(result.detected).toBe(true);
|
||||
expect(result.trigger?.type).toBe('form');
|
||||
expect(result.trigger?.node?.parameters?.path).toBe('my-form');
|
||||
});
|
||||
});
|
||||
|
||||
describe('chat detection', () => {
|
||||
it('should detect @n8n/n8n-nodes-langchain.chatTrigger as chat trigger', () => {
|
||||
const workflow = createWorkflowWithTrigger('@n8n/n8n-nodes-langchain.chatTrigger', {
|
||||
path: 'chat-endpoint',
|
||||
});
|
||||
|
||||
const result = detectTriggerFromWorkflow(workflow);
|
||||
|
||||
expect(result.detected).toBe(true);
|
||||
expect(result.trigger?.type).toBe('chat');
|
||||
});
|
||||
|
||||
it('should detect n8n-nodes-langchain.chatTrigger as chat trigger', () => {
|
||||
const workflow = createWorkflowWithTrigger('n8n-nodes-langchain.chatTrigger', {
|
||||
webhookPath: 'ai-chat',
|
||||
});
|
||||
|
||||
const result = detectTriggerFromWorkflow(workflow);
|
||||
|
||||
expect(result.detected).toBe(true);
|
||||
expect(result.trigger?.type).toBe('chat');
|
||||
});
|
||||
});
|
||||
|
||||
describe('non-triggerable workflows', () => {
|
||||
it('should return not detected for schedule trigger', () => {
|
||||
const workflow = createWorkflowWithTrigger('n8n-nodes-base.scheduleTrigger', {
|
||||
rule: { interval: [{ field: 'hours', value: 1 }] },
|
||||
});
|
||||
|
||||
const result = detectTriggerFromWorkflow(workflow);
|
||||
|
||||
expect(result.detected).toBe(false);
|
||||
// Fallback reason may be undefined for non-input triggers
|
||||
});
|
||||
|
||||
it('should return not detected for manual trigger', () => {
|
||||
const workflow = createWorkflowWithTrigger('n8n-nodes-base.manualTrigger', {});
|
||||
|
||||
const result = detectTriggerFromWorkflow(workflow);
|
||||
|
||||
expect(result.detected).toBe(false);
|
||||
});
|
||||
|
||||
it('should return not detected for email trigger', () => {
|
||||
const workflow = createWorkflowWithTrigger('n8n-nodes-base.emailReadImap', {
|
||||
mailbox: 'INBOX',
|
||||
});
|
||||
|
||||
const result = detectTriggerFromWorkflow(workflow);
|
||||
|
||||
expect(result.detected).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('workflows without triggers', () => {
|
||||
it('should return not detected for workflow with no trigger node', () => {
|
||||
const workflow: Workflow = {
|
||||
id: 'test-workflow',
|
||||
name: 'Test Workflow',
|
||||
active: true,
|
||||
nodes: [
|
||||
{
|
||||
id: 'action-node',
|
||||
name: 'Action',
|
||||
type: 'n8n-nodes-base.noOp',
|
||||
typeVersion: 1,
|
||||
position: [0, 0],
|
||||
parameters: {},
|
||||
},
|
||||
],
|
||||
connections: {},
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
settings: {},
|
||||
staticData: undefined,
|
||||
} as Workflow;
|
||||
|
||||
const result = detectTriggerFromWorkflow(workflow);
|
||||
|
||||
expect(result.detected).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildTriggerUrl', () => {
|
||||
it('should build webhook URL correctly', () => {
|
||||
const baseUrl = 'https://n8n.example.com';
|
||||
const trigger = {
|
||||
type: 'webhook' as const,
|
||||
node: {
|
||||
id: 'trigger',
|
||||
name: 'Webhook',
|
||||
type: 'n8n-nodes-base.webhook',
|
||||
typeVersion: 1,
|
||||
position: [0, 0] as [number, number],
|
||||
parameters: { path: 'my-webhook' },
|
||||
},
|
||||
webhookPath: 'my-webhook',
|
||||
};
|
||||
|
||||
const url = buildTriggerUrl(baseUrl, trigger, 'production');
|
||||
|
||||
expect(url).toBe('https://n8n.example.com/webhook/my-webhook');
|
||||
});
|
||||
|
||||
it('should build test webhook URL correctly', () => {
|
||||
const baseUrl = 'https://n8n.example.com/';
|
||||
const trigger = {
|
||||
type: 'webhook' as const,
|
||||
node: {
|
||||
id: 'trigger',
|
||||
name: 'Webhook',
|
||||
type: 'n8n-nodes-base.webhook',
|
||||
typeVersion: 1,
|
||||
position: [0, 0] as [number, number],
|
||||
parameters: { path: 'test-path' },
|
||||
},
|
||||
webhookPath: 'test-path',
|
||||
};
|
||||
|
||||
const url = buildTriggerUrl(baseUrl, trigger, 'test');
|
||||
|
||||
expect(url).toBe('https://n8n.example.com/webhook-test/test-path');
|
||||
});
|
||||
|
||||
it('should build form URL with node ID when webhookPath not set', () => {
|
||||
const baseUrl = 'https://n8n.example.com';
|
||||
const trigger = {
|
||||
type: 'form' as const,
|
||||
node: {
|
||||
id: 'trigger',
|
||||
name: 'Form',
|
||||
type: 'n8n-nodes-base.formTrigger',
|
||||
typeVersion: 1,
|
||||
position: [0, 0] as [number, number],
|
||||
parameters: { path: 'my-form' },
|
||||
},
|
||||
// webhookPath is undefined - should use node.id
|
||||
};
|
||||
|
||||
const url = buildTriggerUrl(baseUrl, trigger, 'production');
|
||||
|
||||
// When webhookPath is not set, uses node.id as fallback
|
||||
expect(url).toContain('/form/');
|
||||
});
|
||||
|
||||
it('should build chat URL correctly', () => {
|
||||
const baseUrl = 'https://n8n.example.com';
|
||||
const trigger = {
|
||||
type: 'chat' as const,
|
||||
node: {
|
||||
id: 'trigger',
|
||||
name: 'Chat',
|
||||
type: '@n8n/n8n-nodes-langchain.chatTrigger',
|
||||
typeVersion: 1,
|
||||
position: [0, 0] as [number, number],
|
||||
parameters: { path: 'ai-chat' },
|
||||
},
|
||||
webhookPath: 'ai-chat',
|
||||
};
|
||||
|
||||
const url = buildTriggerUrl(baseUrl, trigger, 'production');
|
||||
|
||||
expect(url).toBe('https://n8n.example.com/webhook/ai-chat');
|
||||
});
|
||||
});
|
||||
|
||||
describe('describeTrigger', () => {
|
||||
it('should describe webhook trigger', () => {
|
||||
const trigger = {
|
||||
type: 'webhook' as const,
|
||||
node: {
|
||||
id: 'trigger',
|
||||
name: 'My Webhook',
|
||||
type: 'n8n-nodes-base.webhook',
|
||||
typeVersion: 1,
|
||||
position: [0, 0] as [number, number],
|
||||
parameters: { path: 'my-webhook' },
|
||||
},
|
||||
webhookPath: 'my-webhook',
|
||||
httpMethod: 'POST' as const,
|
||||
};
|
||||
|
||||
const description = describeTrigger(trigger);
|
||||
|
||||
// Case-insensitive check
|
||||
expect(description.toLowerCase()).toContain('webhook');
|
||||
expect(description).toContain('POST');
|
||||
expect(description).toContain('my-webhook');
|
||||
});
|
||||
|
||||
it('should describe form trigger', () => {
|
||||
const trigger = {
|
||||
type: 'form' as const,
|
||||
node: {
|
||||
id: 'trigger',
|
||||
name: 'Contact Form',
|
||||
type: 'n8n-nodes-base.formTrigger',
|
||||
typeVersion: 1,
|
||||
position: [0, 0] as [number, number],
|
||||
parameters: { path: 'contact' },
|
||||
},
|
||||
webhookPath: 'contact',
|
||||
};
|
||||
|
||||
const description = describeTrigger(trigger);
|
||||
|
||||
// Case-insensitive check
|
||||
expect(description.toLowerCase()).toContain('form');
|
||||
});
|
||||
|
||||
it('should describe chat trigger', () => {
|
||||
const trigger = {
|
||||
type: 'chat' as const,
|
||||
node: {
|
||||
id: 'trigger',
|
||||
name: 'AI Chat',
|
||||
type: '@n8n/n8n-nodes-langchain.chatTrigger',
|
||||
typeVersion: 1,
|
||||
position: [0, 0] as [number, number],
|
||||
parameters: {},
|
||||
},
|
||||
};
|
||||
|
||||
const description = describeTrigger(trigger);
|
||||
|
||||
// Case-insensitive check
|
||||
expect(description.toLowerCase()).toContain('chat');
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user