diff --git a/tests/integration/n8n-api/executions/delete-execution.test.ts b/tests/integration/n8n-api/executions/delete-execution.test.ts new file mode 100644 index 0000000..b20dcdb --- /dev/null +++ b/tests/integration/n8n-api/executions/delete-execution.test.ts @@ -0,0 +1,148 @@ +/** + * Integration Tests: handleDeleteExecution + * + * Tests execution deletion against a real n8n instance. + * Covers successful deletion, error handling, and cleanup verification. + */ + +import { describe, it, expect, beforeEach, beforeAll } from 'vitest'; +import { createMcpContext } from '../utils/mcp-context'; +import { InstanceContext } from '../../../../src/types/instance-context'; +import { handleDeleteExecution, handleTriggerWebhookWorkflow, handleGetExecution } from '../../../../src/mcp/handlers-n8n-manager'; +import { getN8nCredentials } from '../utils/credentials'; + +describe('Integration: handleDeleteExecution', () => { + let mcpContext: InstanceContext; + let webhookUrl: string; + + beforeEach(() => { + mcpContext = createMcpContext(); + }); + + beforeAll(() => { + const creds = getN8nCredentials(); + webhookUrl = creds.webhookUrls.get; + }); + + // ====================================================================== + // Successful Deletion + // ====================================================================== + + describe('Successful Deletion', () => { + it('should delete an execution successfully', async () => { + // First, create an execution to delete + const triggerResponse = await handleTriggerWebhookWorkflow( + { + webhookUrl, + httpMethod: 'GET', + waitForResponse: true + }, + mcpContext + ); + + // Try to extract execution ID + let executionId: string | undefined; + if (triggerResponse.success && triggerResponse.data) { + const responseData = triggerResponse.data as any; + executionId = responseData.executionId || + responseData.id || + responseData.execution?.id || + responseData.workflowData?.executionId; + } + + if (!executionId) { + console.warn('Could not extract execution ID for deletion test'); + return; + } + + // Delete the execution + const response = await handleDeleteExecution( + { id: executionId }, + mcpContext + ); + + expect(response.success).toBe(true); + expect(response.data).toBeDefined(); + }, 30000); + + it('should verify execution is actually deleted', async () => { + // Create an execution + const triggerResponse = await handleTriggerWebhookWorkflow( + { + webhookUrl, + httpMethod: 'GET', + waitForResponse: true + }, + mcpContext + ); + + let executionId: string | undefined; + if (triggerResponse.success && triggerResponse.data) { + const responseData = triggerResponse.data as any; + executionId = responseData.executionId || + responseData.id || + responseData.execution?.id || + responseData.workflowData?.executionId; + } + + if (!executionId) { + console.warn('Could not extract execution ID for deletion verification test'); + return; + } + + // Delete it + const deleteResponse = await handleDeleteExecution( + { id: executionId }, + mcpContext + ); + + expect(deleteResponse.success).toBe(true); + + // Try to fetch the deleted execution + const getResponse = await handleGetExecution( + { id: executionId }, + mcpContext + ); + + // Should fail to find the deleted execution + expect(getResponse.success).toBe(false); + expect(getResponse.error).toBeDefined(); + }, 30000); + }); + + // ====================================================================== + // Error Handling + // ====================================================================== + + describe('Error Handling', () => { + it('should handle non-existent execution ID', async () => { + const response = await handleDeleteExecution( + { id: '99999999' }, + mcpContext + ); + + expect(response.success).toBe(false); + expect(response.error).toBeDefined(); + }); + + it('should handle invalid execution ID format', async () => { + const response = await handleDeleteExecution( + { id: 'invalid-id-format' }, + mcpContext + ); + + expect(response.success).toBe(false); + expect(response.error).toBeDefined(); + }); + + it('should handle missing execution ID', async () => { + const response = await handleDeleteExecution( + {} as any, + mcpContext + ); + + expect(response.success).toBe(false); + expect(response.error).toBeDefined(); + }); + }); +}); diff --git a/tests/integration/n8n-api/executions/get-execution.test.ts b/tests/integration/n8n-api/executions/get-execution.test.ts new file mode 100644 index 0000000..eefdb9d --- /dev/null +++ b/tests/integration/n8n-api/executions/get-execution.test.ts @@ -0,0 +1,428 @@ +/** + * Integration Tests: handleGetExecution + * + * Tests execution retrieval against a real n8n instance. + * Covers all retrieval modes, filtering options, and error handling. + */ + +import { describe, it, expect, beforeAll } from 'vitest'; +import { createMcpContext } from '../utils/mcp-context'; +import { InstanceContext } from '../../../../src/types/instance-context'; +import { handleGetExecution, handleTriggerWebhookWorkflow } from '../../../../src/mcp/handlers-n8n-manager'; +import { getN8nCredentials } from '../utils/credentials'; + +describe('Integration: handleGetExecution', () => { + let mcpContext: InstanceContext; + let executionId: string; + let webhookUrl: string; + + beforeAll(async () => { + mcpContext = createMcpContext(); + const creds = getN8nCredentials(); + webhookUrl = creds.webhookUrls.get; + + // Trigger a webhook to create an execution for testing + const triggerResponse = await handleTriggerWebhookWorkflow( + { + webhookUrl, + httpMethod: 'GET', + waitForResponse: true + }, + mcpContext + ); + + // Extract execution ID from the response + if (triggerResponse.success && triggerResponse.data) { + const responseData = triggerResponse.data as any; + // Try to get execution ID from various possible locations + executionId = responseData.executionId || + responseData.id || + responseData.execution?.id || + responseData.workflowData?.executionId; + + if (!executionId) { + // If no execution ID in response, we'll use error handling tests + console.warn('Could not extract execution ID from webhook response'); + } + } + }, 30000); + + // ====================================================================== + // Preview Mode + // ====================================================================== + + describe('Preview Mode', () => { + it('should get execution in preview mode (structure only)', async () => { + if (!executionId) { + console.warn('Skipping test: No execution ID available'); + return; + } + + const response = await handleGetExecution( + { + id: executionId, + mode: 'preview' + }, + mcpContext + ); + + expect(response.success).toBe(true); + const data = response.data as any; + + // Preview mode should return structure and counts + expect(data).toBeDefined(); + expect(data.id).toBe(executionId); + + // Should have basic execution info + if (data.status) { + expect(['success', 'error', 'running', 'waiting']).toContain(data.status); + } + }); + }); + + // ====================================================================== + // Summary Mode (Default) + // ====================================================================== + + describe('Summary Mode', () => { + it('should get execution in summary mode (2 samples per node)', async () => { + if (!executionId) { + console.warn('Skipping test: No execution ID available'); + return; + } + + const response = await handleGetExecution( + { + id: executionId, + mode: 'summary' + }, + mcpContext + ); + + expect(response.success).toBe(true); + const data = response.data as any; + + expect(data).toBeDefined(); + expect(data.id).toBe(executionId); + }); + + it('should default to summary mode when mode not specified', async () => { + if (!executionId) { + console.warn('Skipping test: No execution ID available'); + return; + } + + const response = await handleGetExecution( + { + id: executionId + }, + mcpContext + ); + + expect(response.success).toBe(true); + const data = response.data as any; + + expect(data).toBeDefined(); + expect(data.id).toBe(executionId); + }); + }); + + // ====================================================================== + // Filtered Mode + // ====================================================================== + + describe('Filtered Mode', () => { + it('should get execution with custom items limit', async () => { + if (!executionId) { + console.warn('Skipping test: No execution ID available'); + return; + } + + const response = await handleGetExecution( + { + id: executionId, + mode: 'filtered', + itemsLimit: 5 + }, + mcpContext + ); + + expect(response.success).toBe(true); + const data = response.data as any; + + expect(data).toBeDefined(); + expect(data.id).toBe(executionId); + }); + + it('should get execution with itemsLimit 0 (structure only)', async () => { + if (!executionId) { + console.warn('Skipping test: No execution ID available'); + return; + } + + const response = await handleGetExecution( + { + id: executionId, + mode: 'filtered', + itemsLimit: 0 + }, + mcpContext + ); + + expect(response.success).toBe(true); + const data = response.data as any; + + expect(data).toBeDefined(); + expect(data.id).toBe(executionId); + }); + + it('should get execution with unlimited items (itemsLimit: -1)', async () => { + if (!executionId) { + console.warn('Skipping test: No execution ID available'); + return; + } + + const response = await handleGetExecution( + { + id: executionId, + mode: 'filtered', + itemsLimit: -1 + }, + mcpContext + ); + + expect(response.success).toBe(true); + const data = response.data as any; + + expect(data).toBeDefined(); + expect(data.id).toBe(executionId); + }); + + it('should get execution filtered by node names', async () => { + if (!executionId) { + console.warn('Skipping test: No execution ID available'); + return; + } + + const response = await handleGetExecution( + { + id: executionId, + mode: 'filtered', + nodeNames: ['Webhook'] + }, + mcpContext + ); + + expect(response.success).toBe(true); + const data = response.data as any; + + expect(data).toBeDefined(); + expect(data.id).toBe(executionId); + }); + }); + + // ====================================================================== + // Full Mode + // ====================================================================== + + describe('Full Mode', () => { + it('should get complete execution data', async () => { + if (!executionId) { + console.warn('Skipping test: No execution ID available'); + return; + } + + const response = await handleGetExecution( + { + id: executionId, + mode: 'full' + }, + mcpContext + ); + + expect(response.success).toBe(true); + const data = response.data as any; + + expect(data).toBeDefined(); + expect(data.id).toBe(executionId); + + // Full mode should include complete execution data + if (data.data) { + expect(typeof data.data).toBe('object'); + } + }); + }); + + // ====================================================================== + // Input Data Inclusion + // ====================================================================== + + describe('Input Data Inclusion', () => { + it('should include input data when requested', async () => { + if (!executionId) { + console.warn('Skipping test: No execution ID available'); + return; + } + + const response = await handleGetExecution( + { + id: executionId, + mode: 'summary', + includeInputData: true + }, + mcpContext + ); + + expect(response.success).toBe(true); + const data = response.data as any; + + expect(data).toBeDefined(); + expect(data.id).toBe(executionId); + }); + + it('should exclude input data by default', async () => { + if (!executionId) { + console.warn('Skipping test: No execution ID available'); + return; + } + + const response = await handleGetExecution( + { + id: executionId, + mode: 'summary', + includeInputData: false + }, + mcpContext + ); + + expect(response.success).toBe(true); + const data = response.data as any; + + expect(data).toBeDefined(); + expect(data.id).toBe(executionId); + }); + }); + + // ====================================================================== + // Legacy Parameter Compatibility + // ====================================================================== + + describe('Legacy Parameter Compatibility', () => { + it('should support legacy includeData parameter', async () => { + if (!executionId) { + console.warn('Skipping test: No execution ID available'); + return; + } + + const response = await handleGetExecution( + { + id: executionId, + includeData: true + }, + mcpContext + ); + + expect(response.success).toBe(true); + const data = response.data as any; + + expect(data).toBeDefined(); + expect(data.id).toBe(executionId); + }); + }); + + // ====================================================================== + // Error Handling + // ====================================================================== + + describe('Error Handling', () => { + it('should handle non-existent execution ID', async () => { + const response = await handleGetExecution( + { + id: '99999999' + }, + mcpContext + ); + + expect(response.success).toBe(false); + expect(response.error).toBeDefined(); + }); + + it('should handle invalid execution ID format', async () => { + const response = await handleGetExecution( + { + id: 'invalid-id-format' + }, + mcpContext + ); + + expect(response.success).toBe(false); + expect(response.error).toBeDefined(); + }); + + it('should handle missing execution ID', async () => { + const response = await handleGetExecution( + {} as any, + mcpContext + ); + + expect(response.success).toBe(false); + expect(response.error).toBeDefined(); + }); + + it('should handle invalid mode parameter', async () => { + if (!executionId) { + console.warn('Skipping test: No execution ID available'); + return; + } + + const response = await handleGetExecution( + { + id: executionId, + mode: 'invalid-mode' as any + }, + mcpContext + ); + + expect(response.success).toBe(false); + expect(response.error).toBeDefined(); + }); + }); + + // ====================================================================== + // Response Format Verification + // ====================================================================== + + describe('Response Format', () => { + it('should return complete execution response structure', async () => { + if (!executionId) { + console.warn('Skipping test: No execution ID available'); + return; + } + + const response = await handleGetExecution( + { + id: executionId, + mode: 'summary' + }, + mcpContext + ); + + expect(response.success).toBe(true); + expect(response.data).toBeDefined(); + + const data = response.data as any; + expect(data.id).toBeDefined(); + + // Should have execution metadata + if (data.status) { + expect(typeof data.status).toBe('string'); + } + if (data.mode) { + expect(typeof data.mode).toBe('string'); + } + if (data.startedAt) { + expect(typeof data.startedAt).toBe('string'); + } + }); + }); +}); diff --git a/tests/integration/n8n-api/executions/list-executions.test.ts b/tests/integration/n8n-api/executions/list-executions.test.ts new file mode 100644 index 0000000..56f7c7f --- /dev/null +++ b/tests/integration/n8n-api/executions/list-executions.test.ts @@ -0,0 +1,263 @@ +/** + * Integration Tests: handleListExecutions + * + * Tests execution listing against a real n8n instance. + * Covers filtering, pagination, and various list parameters. + */ + +import { describe, it, expect, beforeEach } from 'vitest'; +import { createMcpContext } from '../utils/mcp-context'; +import { InstanceContext } from '../../../../src/types/instance-context'; +import { handleListExecutions } from '../../../../src/mcp/handlers-n8n-manager'; + +describe('Integration: handleListExecutions', () => { + let mcpContext: InstanceContext; + + beforeEach(() => { + mcpContext = createMcpContext(); + }); + + // ====================================================================== + // No Filters + // ====================================================================== + + describe('No Filters', () => { + it('should list all executions without filters', async () => { + const response = await handleListExecutions({}, mcpContext); + + expect(response.success).toBe(true); + expect(response.data).toBeDefined(); + + const data = response.data as any; + expect(Array.isArray(data.executions)).toBe(true); + expect(data).toHaveProperty('returned'); + }); + }); + + // ====================================================================== + // Filter by Status + // ====================================================================== + + describe('Filter by Status', () => { + it('should filter executions by success status', async () => { + const response = await handleListExecutions( + { status: 'success' }, + mcpContext + ); + + expect(response.success).toBe(true); + const data = response.data as any; + + expect(Array.isArray(data.executions)).toBe(true); + // All returned executions should have success status + if (data.executions.length > 0) { + data.executions.forEach((exec: any) => { + expect(exec.status).toBe('success'); + }); + } + }); + + it('should filter executions by error status', async () => { + const response = await handleListExecutions( + { status: 'error' }, + mcpContext + ); + + expect(response.success).toBe(true); + const data = response.data as any; + + expect(Array.isArray(data.executions)).toBe(true); + // All returned executions should have error status + if (data.executions.length > 0) { + data.executions.forEach((exec: any) => { + expect(exec.status).toBe('error'); + }); + } + }); + + it('should filter executions by waiting status', async () => { + const response = await handleListExecutions( + { status: 'waiting' }, + mcpContext + ); + + expect(response.success).toBe(true); + const data = response.data as any; + + expect(Array.isArray(data.executions)).toBe(true); + }); + }); + + // ====================================================================== + // Pagination + // ====================================================================== + + describe('Pagination', () => { + it('should return first page with limit', async () => { + const response = await handleListExecutions( + { limit: 10 }, + mcpContext + ); + + expect(response.success).toBe(true); + const data = response.data as any; + + expect(Array.isArray(data.executions)).toBe(true); + expect(data.executions.length).toBeLessThanOrEqual(10); + }); + + it('should handle pagination with cursor', async () => { + // Get first page + const firstPage = await handleListExecutions( + { limit: 5 }, + mcpContext + ); + + expect(firstPage.success).toBe(true); + const firstData = firstPage.data as any; + + // If there's a next cursor, get second page + if (firstData.nextCursor) { + const secondPage = await handleListExecutions( + { limit: 5, cursor: firstData.nextCursor }, + mcpContext + ); + + expect(secondPage.success).toBe(true); + const secondData = secondPage.data as any; + + // Second page should have different executions + const firstIds = new Set(firstData.executions.map((e: any) => e.id)); + const secondIds = secondData.executions.map((e: any) => e.id); + + secondIds.forEach((id: string) => { + expect(firstIds.has(id)).toBe(false); + }); + } + }); + + it('should respect limit=1', async () => { + const response = await handleListExecutions( + { limit: 1 }, + mcpContext + ); + + expect(response.success).toBe(true); + const data = response.data as any; + + expect(data.executions.length).toBeLessThanOrEqual(1); + }); + + it('should respect limit=50', async () => { + const response = await handleListExecutions( + { limit: 50 }, + mcpContext + ); + + expect(response.success).toBe(true); + const data = response.data as any; + + expect(data.executions.length).toBeLessThanOrEqual(50); + }); + + it('should respect limit=100 (max)', async () => { + const response = await handleListExecutions( + { limit: 100 }, + mcpContext + ); + + expect(response.success).toBe(true); + const data = response.data as any; + + expect(data.executions.length).toBeLessThanOrEqual(100); + }); + }); + + // ====================================================================== + // Include Execution Data + // ====================================================================== + + describe('Include Execution Data', () => { + it('should exclude execution data by default', async () => { + const response = await handleListExecutions( + { limit: 5 }, + mcpContext + ); + + expect(response.success).toBe(true); + const data = response.data as any; + + expect(Array.isArray(data.executions)).toBe(true); + // By default, should not include full execution data + }); + + it('should include execution data when requested', async () => { + const response = await handleListExecutions( + { limit: 5, includeData: true }, + mcpContext + ); + + expect(response.success).toBe(true); + const data = response.data as any; + + expect(Array.isArray(data.executions)).toBe(true); + }); + }); + + // ====================================================================== + // Empty Results + // ====================================================================== + + describe('Empty Results', () => { + it('should return empty array when no executions match filters', async () => { + // Use a very restrictive workflowId that likely doesn't exist + const response = await handleListExecutions( + { workflowId: '99999999' }, + mcpContext + ); + + expect(response.success).toBe(true); + const data = response.data as any; + + expect(Array.isArray(data.executions)).toBe(true); + // May or may not be empty depending on actual data + }); + }); + + // ====================================================================== + // Response Format Verification + // ====================================================================== + + describe('Response Format', () => { + it('should return complete list response structure', async () => { + const response = await handleListExecutions( + { limit: 10 }, + mcpContext + ); + + expect(response.success).toBe(true); + const data = response.data as any; + + // Verify required fields + expect(data).toHaveProperty('executions'); + expect(Array.isArray(data.executions)).toBe(true); + expect(data).toHaveProperty('returned'); + expect(data).toHaveProperty('hasMore'); + + // Verify pagination fields when present + if (data.nextCursor) { + expect(typeof data.nextCursor).toBe('string'); + } + + // Verify execution structure if any executions returned + if (data.executions.length > 0) { + const execution = data.executions[0]; + expect(execution).toHaveProperty('id'); + + if (execution.status) { + expect(['success', 'error', 'running', 'waiting']).toContain(execution.status); + } + } + }); + }); +}); diff --git a/tests/integration/n8n-api/executions/trigger-webhook.test.ts b/tests/integration/n8n-api/executions/trigger-webhook.test.ts new file mode 100644 index 0000000..6c7e8d2 --- /dev/null +++ b/tests/integration/n8n-api/executions/trigger-webhook.test.ts @@ -0,0 +1,375 @@ +/** + * Integration Tests: handleTriggerWebhookWorkflow + * + * Tests webhook triggering against a real n8n instance. + * Covers all HTTP methods, request data, headers, and error handling. + */ + +import { describe, it, expect, beforeEach } from 'vitest'; +import { createMcpContext } from '../utils/mcp-context'; +import { InstanceContext } from '../../../../src/types/instance-context'; +import { handleTriggerWebhookWorkflow } from '../../../../src/mcp/handlers-n8n-manager'; +import { getN8nCredentials } from '../utils/credentials'; + +describe('Integration: handleTriggerWebhookWorkflow', () => { + let mcpContext: InstanceContext; + let webhookUrls: { + get: string; + post: string; + put: string; + delete: string; + }; + + beforeEach(() => { + mcpContext = createMcpContext(); + const creds = getN8nCredentials(); + webhookUrls = creds.webhookUrls; + }); + + // ====================================================================== + // GET Method Tests + // ====================================================================== + + describe('GET Method', () => { + it('should trigger GET webhook without data', async () => { + const response = await handleTriggerWebhookWorkflow( + { + webhookUrl: webhookUrls.get, + httpMethod: 'GET' + }, + mcpContext + ); + + expect(response.success).toBe(true); + expect(response.data).toBeDefined(); + expect(response.message).toContain('Webhook triggered successfully'); + }); + + it('should trigger GET webhook with query parameters', async () => { + // GET method uses query parameters in URL + const urlWithParams = `${webhookUrls.get}?testParam=value&number=42`; + + const response = await handleTriggerWebhookWorkflow( + { + webhookUrl: urlWithParams, + httpMethod: 'GET' + }, + mcpContext + ); + + expect(response.success).toBe(true); + expect(response.data).toBeDefined(); + }); + + it('should trigger GET webhook with custom headers', async () => { + const response = await handleTriggerWebhookWorkflow( + { + webhookUrl: webhookUrls.get, + httpMethod: 'GET', + headers: { + 'X-Custom-Header': 'test-value', + 'X-Request-Id': '12345' + } + }, + mcpContext + ); + + expect(response.success).toBe(true); + expect(response.data).toBeDefined(); + }); + + it('should trigger GET webhook and wait for response', async () => { + const response = await handleTriggerWebhookWorkflow( + { + webhookUrl: webhookUrls.get, + httpMethod: 'GET', + waitForResponse: true + }, + mcpContext + ); + + expect(response.success).toBe(true); + expect(response.data).toBeDefined(); + // Response should contain workflow execution data + }); + }); + + // ====================================================================== + // POST Method Tests + // ====================================================================== + + describe('POST Method', () => { + it('should trigger POST webhook with JSON data', async () => { + const response = await handleTriggerWebhookWorkflow( + { + webhookUrl: webhookUrls.post, + httpMethod: 'POST', + data: { + message: 'Test webhook trigger', + timestamp: Date.now(), + nested: { + value: 'nested data' + } + } + }, + mcpContext + ); + + expect(response.success).toBe(true); + expect(response.data).toBeDefined(); + }); + + it('should trigger POST webhook without data', async () => { + const response = await handleTriggerWebhookWorkflow( + { + webhookUrl: webhookUrls.post, + httpMethod: 'POST' + }, + mcpContext + ); + + expect(response.success).toBe(true); + expect(response.data).toBeDefined(); + }); + + it('should trigger POST webhook with custom headers', async () => { + const response = await handleTriggerWebhookWorkflow( + { + webhookUrl: webhookUrls.post, + httpMethod: 'POST', + data: { test: 'data' }, + headers: { + 'Content-Type': 'application/json', + 'X-Api-Key': 'test-key' + } + }, + mcpContext + ); + + expect(response.success).toBe(true); + expect(response.data).toBeDefined(); + }); + + it('should trigger POST webhook without waiting for response', async () => { + const response = await handleTriggerWebhookWorkflow( + { + webhookUrl: webhookUrls.post, + httpMethod: 'POST', + data: { async: true }, + waitForResponse: false + }, + mcpContext + ); + + expect(response.success).toBe(true); + // With waitForResponse: false, may return immediately + }); + }); + + // ====================================================================== + // PUT Method Tests + // ====================================================================== + + describe('PUT Method', () => { + it('should trigger PUT webhook with update data', async () => { + const response = await handleTriggerWebhookWorkflow( + { + webhookUrl: webhookUrls.put, + httpMethod: 'PUT', + data: { + id: '123', + updatedField: 'new value', + timestamp: Date.now() + } + }, + mcpContext + ); + + expect(response.success).toBe(true); + expect(response.data).toBeDefined(); + }); + + it('should trigger PUT webhook with custom headers', async () => { + const response = await handleTriggerWebhookWorkflow( + { + webhookUrl: webhookUrls.put, + httpMethod: 'PUT', + data: { update: true }, + headers: { + 'X-Update-Operation': 'modify', + 'If-Match': 'etag-value' + } + }, + mcpContext + ); + + expect(response.success).toBe(true); + expect(response.data).toBeDefined(); + }); + + it('should trigger PUT webhook without data', async () => { + const response = await handleTriggerWebhookWorkflow( + { + webhookUrl: webhookUrls.put, + httpMethod: 'PUT' + }, + mcpContext + ); + + expect(response.success).toBe(true); + expect(response.data).toBeDefined(); + }); + }); + + // ====================================================================== + // DELETE Method Tests + // ====================================================================== + + describe('DELETE Method', () => { + it('should trigger DELETE webhook with query parameters', async () => { + const urlWithParams = `${webhookUrls.delete}?id=123&reason=test`; + + const response = await handleTriggerWebhookWorkflow( + { + webhookUrl: urlWithParams, + httpMethod: 'DELETE' + }, + mcpContext + ); + + expect(response.success).toBe(true); + expect(response.data).toBeDefined(); + }); + + it('should trigger DELETE webhook with custom headers', async () => { + const response = await handleTriggerWebhookWorkflow( + { + webhookUrl: webhookUrls.delete, + httpMethod: 'DELETE', + headers: { + 'X-Delete-Reason': 'cleanup', + 'Authorization': 'Bearer token' + } + }, + mcpContext + ); + + expect(response.success).toBe(true); + expect(response.data).toBeDefined(); + }); + + it('should trigger DELETE webhook without parameters', async () => { + const response = await handleTriggerWebhookWorkflow( + { + webhookUrl: webhookUrls.delete, + httpMethod: 'DELETE' + }, + mcpContext + ); + + expect(response.success).toBe(true); + expect(response.data).toBeDefined(); + }); + }); + + // ====================================================================== + // Error Handling + // ====================================================================== + + describe('Error Handling', () => { + it('should handle invalid webhook URL', async () => { + const response = await handleTriggerWebhookWorkflow( + { + webhookUrl: 'https://invalid-url.example.com/webhook/nonexistent', + httpMethod: 'GET' + }, + mcpContext + ); + + expect(response.success).toBe(false); + expect(response.error).toBeDefined(); + }); + + it('should handle malformed webhook URL', async () => { + const response = await handleTriggerWebhookWorkflow( + { + webhookUrl: 'not-a-valid-url', + httpMethod: 'GET' + }, + mcpContext + ); + + expect(response.success).toBe(false); + expect(response.error).toBeDefined(); + }); + + it('should handle missing webhook URL', async () => { + const response = await handleTriggerWebhookWorkflow( + { + httpMethod: 'GET' + } as any, + mcpContext + ); + + expect(response.success).toBe(false); + expect(response.error).toBeDefined(); + }); + + it('should handle invalid HTTP method', async () => { + const response = await handleTriggerWebhookWorkflow( + { + webhookUrl: webhookUrls.get, + httpMethod: 'INVALID' as any + }, + mcpContext + ); + + expect(response.success).toBe(false); + expect(response.error).toBeDefined(); + }); + }); + + // ====================================================================== + // Default Method (POST) + // ====================================================================== + + describe('Default Method Behavior', () => { + it('should default to POST method when not specified', async () => { + const response = await handleTriggerWebhookWorkflow( + { + webhookUrl: webhookUrls.post, + data: { defaultMethod: true } + }, + mcpContext + ); + + expect(response.success).toBe(true); + expect(response.data).toBeDefined(); + }); + }); + + // ====================================================================== + // Response Format Verification + // ====================================================================== + + describe('Response Format', () => { + it('should return complete webhook response structure', async () => { + const response = await handleTriggerWebhookWorkflow( + { + webhookUrl: webhookUrls.get, + httpMethod: 'GET', + waitForResponse: true + }, + mcpContext + ); + + expect(response.success).toBe(true); + expect(response.data).toBeDefined(); + expect(response.message).toBeDefined(); + expect(response.message).toContain('Webhook triggered successfully'); + + // Response data should be defined (either workflow output or execution info) + expect(typeof response.data).not.toBe('undefined'); + }); + }); +});