mirror of
https://github.com/czlonkowski/n8n-mcp.git
synced 2026-02-06 13:33:11 +00:00
feat: implement integration testing foundation (Phase 1)
Complete implementation of Phase 1 foundation for n8n API integration tests. Establishes core utilities, fixtures, and infrastructure for testing all 17 n8n API handlers against real n8n instance. Changes: - Add integration test environment configuration to .env.example - Create comprehensive test utilities infrastructure: * credentials.ts: Environment-aware credential management (local .env vs CI secrets) * n8n-client.ts: Singleton API client wrapper with health checks * test-context.ts: Resource tracking and automatic cleanup * cleanup-helpers.ts: Multi-level cleanup strategies (orphaned, age-based, tag-based) * fixtures.ts: 6 pre-built workflow templates (webhook, HTTP, multi-node, error handling, AI, expressions) * factories.ts: Dynamic node/workflow builders with 15+ factory functions * webhook-workflows.ts: Webhook workflow configs and setup instructions - Add npm scripts: * test:integration:n8n: Run n8n API integration tests * test:cleanup:orphans: Clean up orphaned test resources - Create cleanup script for CI/manual use Documentation: - Add comprehensive integration testing plan (550 lines) - Add Phase 1 completion summary with lessons learned Key Features: - Automatic credential detection (CI vs local) - Multi-level cleanup (test, suite, CI, orphan) - 6 workflow fixtures covering common scenarios - 15+ factory functions for dynamic test data - Support for 4 HTTP methods (GET, POST, PUT, DELETE) via pre-activated webhook workflows - TypeScript-first with full type safety - Comprehensive error handling with helpful messages Total: ~1,520 lines of production-ready code + 650 lines of documentation Ready for Phase 2: Workflow creation tests 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
276
tests/integration/n8n-api/utils/cleanup-helpers.ts
Normal file
276
tests/integration/n8n-api/utils/cleanup-helpers.ts
Normal file
@@ -0,0 +1,276 @@
|
||||
/**
|
||||
* Cleanup Helpers for Integration Tests
|
||||
*
|
||||
* Provides multi-level cleanup strategies for test resources:
|
||||
* - Orphaned workflows (from failed test runs)
|
||||
* - Old executions (older than 24 hours)
|
||||
* - Bulk cleanup by tag or name prefix
|
||||
*/
|
||||
|
||||
import { getTestN8nClient } from './n8n-client';
|
||||
import { getN8nCredentials } from './credentials';
|
||||
import { Logger } from '../../../../src/utils/logger';
|
||||
|
||||
const logger = new Logger({ prefix: '[Cleanup]' });
|
||||
|
||||
/**
|
||||
* Clean up orphaned test workflows
|
||||
*
|
||||
* Finds and deletes all workflows tagged with the test tag or
|
||||
* prefixed with the test name prefix. Run this periodically in CI
|
||||
* to clean up failed test runs.
|
||||
*
|
||||
* @returns Array of deleted workflow IDs
|
||||
*/
|
||||
export async function cleanupOrphanedWorkflows(): Promise<string[]> {
|
||||
const creds = getN8nCredentials();
|
||||
const client = getTestN8nClient();
|
||||
const deleted: string[] = [];
|
||||
|
||||
logger.info('Searching for orphaned test workflows...');
|
||||
|
||||
let allWorkflows: any[] = [];
|
||||
let cursor: string | undefined;
|
||||
let pageCount = 0;
|
||||
|
||||
// Fetch all workflows with pagination
|
||||
try {
|
||||
do {
|
||||
pageCount++;
|
||||
logger.debug(`Fetching workflows page ${pageCount}...`);
|
||||
|
||||
const response = await client.listWorkflows({
|
||||
cursor,
|
||||
limit: 100,
|
||||
excludePinnedData: true
|
||||
});
|
||||
|
||||
allWorkflows.push(...response.data);
|
||||
cursor = response.nextCursor || undefined;
|
||||
} while (cursor);
|
||||
|
||||
logger.info(`Found ${allWorkflows.length} total workflows across ${pageCount} page(s)`);
|
||||
} catch (error) {
|
||||
logger.error('Failed to fetch workflows:', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Find test workflows
|
||||
const testWorkflows = allWorkflows.filter(w =>
|
||||
w.tags?.includes(creds.cleanup.tag) ||
|
||||
w.name?.startsWith(creds.cleanup.namePrefix)
|
||||
);
|
||||
|
||||
logger.info(`Found ${testWorkflows.length} orphaned test workflow(s)`);
|
||||
|
||||
if (testWorkflows.length === 0) {
|
||||
return deleted;
|
||||
}
|
||||
|
||||
// Delete them
|
||||
for (const workflow of testWorkflows) {
|
||||
try {
|
||||
await client.deleteWorkflow(workflow.id);
|
||||
deleted.push(workflow.id);
|
||||
logger.debug(`Deleted orphaned workflow: ${workflow.name} (${workflow.id})`);
|
||||
} catch (error) {
|
||||
logger.warn(`Failed to delete workflow ${workflow.id}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`Successfully deleted ${deleted.length} orphaned workflow(s)`);
|
||||
return deleted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up old executions
|
||||
*
|
||||
* Deletes executions older than the specified age.
|
||||
*
|
||||
* @param maxAgeMs - Maximum age in milliseconds (default: 24 hours)
|
||||
* @returns Array of deleted execution IDs
|
||||
*/
|
||||
export async function cleanupOldExecutions(
|
||||
maxAgeMs: number = 24 * 60 * 60 * 1000
|
||||
): Promise<string[]> {
|
||||
const client = getTestN8nClient();
|
||||
const deleted: string[] = [];
|
||||
|
||||
logger.info(`Searching for executions older than ${maxAgeMs}ms...`);
|
||||
|
||||
let allExecutions: any[] = [];
|
||||
let cursor: string | undefined;
|
||||
let pageCount = 0;
|
||||
|
||||
// Fetch all executions
|
||||
try {
|
||||
do {
|
||||
pageCount++;
|
||||
logger.debug(`Fetching executions page ${pageCount}...`);
|
||||
|
||||
const response = await client.listExecutions({
|
||||
cursor,
|
||||
limit: 100,
|
||||
includeData: false
|
||||
});
|
||||
|
||||
allExecutions.push(...response.data);
|
||||
cursor = response.nextCursor || undefined;
|
||||
} while (cursor);
|
||||
|
||||
logger.info(`Found ${allExecutions.length} total executions across ${pageCount} page(s)`);
|
||||
} catch (error) {
|
||||
logger.error('Failed to fetch executions:', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
const cutoffTime = Date.now() - maxAgeMs;
|
||||
const oldExecutions = allExecutions.filter(e => {
|
||||
const executionTime = new Date(e.startedAt).getTime();
|
||||
return executionTime < cutoffTime;
|
||||
});
|
||||
|
||||
logger.info(`Found ${oldExecutions.length} old execution(s)`);
|
||||
|
||||
if (oldExecutions.length === 0) {
|
||||
return deleted;
|
||||
}
|
||||
|
||||
for (const execution of oldExecutions) {
|
||||
try {
|
||||
await client.deleteExecution(execution.id);
|
||||
deleted.push(execution.id);
|
||||
logger.debug(`Deleted old execution: ${execution.id}`);
|
||||
} catch (error) {
|
||||
logger.warn(`Failed to delete execution ${execution.id}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`Successfully deleted ${deleted.length} old execution(s)`);
|
||||
return deleted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up all test resources
|
||||
*
|
||||
* Combines cleanupOrphanedWorkflows and cleanupOldExecutions.
|
||||
* Use this as a comprehensive cleanup in CI.
|
||||
*
|
||||
* @returns Object with counts of deleted resources
|
||||
*/
|
||||
export async function cleanupAllTestResources(): Promise<{
|
||||
workflows: number;
|
||||
executions: number;
|
||||
}> {
|
||||
logger.info('Starting comprehensive test resource cleanup...');
|
||||
|
||||
const [workflowIds, executionIds] = await Promise.all([
|
||||
cleanupOrphanedWorkflows(),
|
||||
cleanupOldExecutions()
|
||||
]);
|
||||
|
||||
logger.info(
|
||||
`Cleanup complete: ${workflowIds.length} workflows, ${executionIds.length} executions`
|
||||
);
|
||||
|
||||
return {
|
||||
workflows: workflowIds.length,
|
||||
executions: executionIds.length
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete workflows by tag
|
||||
*
|
||||
* Deletes all workflows with the specified tag.
|
||||
*
|
||||
* @param tag - Tag to match
|
||||
* @returns Array of deleted workflow IDs
|
||||
*/
|
||||
export async function cleanupWorkflowsByTag(tag: string): Promise<string[]> {
|
||||
const client = getTestN8nClient();
|
||||
const deleted: string[] = [];
|
||||
|
||||
logger.info(`Searching for workflows with tag: ${tag}`);
|
||||
|
||||
try {
|
||||
const response = await client.listWorkflows({
|
||||
tags: tag ? [tag] : undefined,
|
||||
limit: 100,
|
||||
excludePinnedData: true
|
||||
});
|
||||
|
||||
const workflows = response.data;
|
||||
logger.info(`Found ${workflows.length} workflow(s) with tag: ${tag}`);
|
||||
|
||||
for (const workflow of workflows) {
|
||||
if (!workflow.id) continue;
|
||||
|
||||
try {
|
||||
await client.deleteWorkflow(workflow.id);
|
||||
deleted.push(workflow.id);
|
||||
logger.debug(`Deleted workflow: ${workflow.name} (${workflow.id})`);
|
||||
} catch (error) {
|
||||
logger.warn(`Failed to delete workflow ${workflow.id}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`Successfully deleted ${deleted.length} workflow(s)`);
|
||||
return deleted;
|
||||
} catch (error) {
|
||||
logger.error(`Failed to cleanup workflows by tag: ${tag}`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete executions for a specific workflow
|
||||
*
|
||||
* @param workflowId - Workflow ID
|
||||
* @returns Array of deleted execution IDs
|
||||
*/
|
||||
export async function cleanupExecutionsByWorkflow(
|
||||
workflowId: string
|
||||
): Promise<string[]> {
|
||||
const client = getTestN8nClient();
|
||||
const deleted: string[] = [];
|
||||
|
||||
logger.info(`Searching for executions of workflow: ${workflowId}`);
|
||||
|
||||
let cursor: string | undefined;
|
||||
let totalCount = 0;
|
||||
|
||||
try {
|
||||
do {
|
||||
const response = await client.listExecutions({
|
||||
workflowId,
|
||||
cursor,
|
||||
limit: 100,
|
||||
includeData: false
|
||||
});
|
||||
|
||||
const executions = response.data;
|
||||
totalCount += executions.length;
|
||||
|
||||
for (const execution of executions) {
|
||||
try {
|
||||
await client.deleteExecution(execution.id);
|
||||
deleted.push(execution.id);
|
||||
logger.debug(`Deleted execution: ${execution.id}`);
|
||||
} catch (error) {
|
||||
logger.warn(`Failed to delete execution ${execution.id}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
cursor = response.nextCursor || undefined;
|
||||
} while (cursor);
|
||||
|
||||
logger.info(
|
||||
`Successfully deleted ${deleted.length}/${totalCount} execution(s) for workflow ${workflowId}`
|
||||
);
|
||||
return deleted;
|
||||
} catch (error) {
|
||||
logger.error(`Failed to cleanup executions for workflow: ${workflowId}`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
169
tests/integration/n8n-api/utils/credentials.ts
Normal file
169
tests/integration/n8n-api/utils/credentials.ts
Normal file
@@ -0,0 +1,169 @@
|
||||
/**
|
||||
* Integration Test Credentials Management
|
||||
*
|
||||
* Provides environment-aware credential loading for integration tests.
|
||||
* - Local development: Reads from .env file
|
||||
* - CI/GitHub Actions: Uses GitHub secrets from process.env
|
||||
*/
|
||||
|
||||
import dotenv from 'dotenv';
|
||||
import path from 'path';
|
||||
|
||||
// Load .env file for local development
|
||||
dotenv.config({ path: path.resolve(process.cwd(), '.env') });
|
||||
|
||||
export interface N8nTestCredentials {
|
||||
url: string;
|
||||
apiKey: string;
|
||||
webhookWorkflows: {
|
||||
get: string;
|
||||
post: string;
|
||||
put: string;
|
||||
delete: string;
|
||||
};
|
||||
cleanup: {
|
||||
enabled: boolean;
|
||||
tag: string;
|
||||
namePrefix: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get n8n credentials for integration tests
|
||||
*
|
||||
* Automatically detects environment (local vs CI) and loads
|
||||
* credentials from the appropriate source.
|
||||
*
|
||||
* @returns N8nTestCredentials
|
||||
* @throws Error if required credentials are missing
|
||||
*/
|
||||
export function getN8nCredentials(): N8nTestCredentials {
|
||||
if (process.env.CI) {
|
||||
// CI: Use GitHub secrets
|
||||
return {
|
||||
url: process.env.N8N_URL!,
|
||||
apiKey: process.env.N8N_API_KEY!,
|
||||
webhookWorkflows: {
|
||||
get: process.env.N8N_TEST_WEBHOOK_GET_ID!,
|
||||
post: process.env.N8N_TEST_WEBHOOK_POST_ID!,
|
||||
put: process.env.N8N_TEST_WEBHOOK_PUT_ID!,
|
||||
delete: process.env.N8N_TEST_WEBHOOK_DELETE_ID!
|
||||
},
|
||||
cleanup: {
|
||||
enabled: true,
|
||||
tag: 'mcp-integration-test',
|
||||
namePrefix: '[MCP-TEST]'
|
||||
}
|
||||
};
|
||||
} else {
|
||||
// Local: Use .env file
|
||||
return {
|
||||
url: process.env.N8N_API_URL!,
|
||||
apiKey: process.env.N8N_API_KEY!,
|
||||
webhookWorkflows: {
|
||||
get: process.env.N8N_TEST_WEBHOOK_GET_ID || '',
|
||||
post: process.env.N8N_TEST_WEBHOOK_POST_ID || '',
|
||||
put: process.env.N8N_TEST_WEBHOOK_PUT_ID || '',
|
||||
delete: process.env.N8N_TEST_WEBHOOK_DELETE_ID || ''
|
||||
},
|
||||
cleanup: {
|
||||
enabled: process.env.N8N_TEST_CLEANUP_ENABLED !== 'false',
|
||||
tag: process.env.N8N_TEST_TAG || 'mcp-integration-test',
|
||||
namePrefix: process.env.N8N_TEST_NAME_PREFIX || '[MCP-TEST]'
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that required credentials are present
|
||||
*
|
||||
* @param creds - Credentials to validate
|
||||
* @throws Error if required credentials are missing
|
||||
*/
|
||||
export function validateCredentials(creds: N8nTestCredentials): void {
|
||||
const missing: string[] = [];
|
||||
|
||||
if (!creds.url) {
|
||||
missing.push(process.env.CI ? 'N8N_URL' : 'N8N_API_URL');
|
||||
}
|
||||
if (!creds.apiKey) {
|
||||
missing.push('N8N_API_KEY');
|
||||
}
|
||||
|
||||
if (missing.length > 0) {
|
||||
throw new Error(
|
||||
`Missing required n8n credentials: ${missing.join(', ')}\n\n` +
|
||||
`Please set the following environment variables:\n` +
|
||||
missing.map(v => ` ${v}`).join('\n') + '\n\n' +
|
||||
`See .env.example for configuration details.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that webhook workflow IDs are configured
|
||||
*
|
||||
* @param creds - Credentials to validate
|
||||
* @throws Error with setup instructions if webhook workflows are missing
|
||||
*/
|
||||
export function validateWebhookWorkflows(creds: N8nTestCredentials): void {
|
||||
const missing: string[] = [];
|
||||
|
||||
if (!creds.webhookWorkflows.get) missing.push('GET');
|
||||
if (!creds.webhookWorkflows.post) missing.push('POST');
|
||||
if (!creds.webhookWorkflows.put) missing.push('PUT');
|
||||
if (!creds.webhookWorkflows.delete) missing.push('DELETE');
|
||||
|
||||
if (missing.length > 0) {
|
||||
const envVars = missing.map(m => `N8N_TEST_WEBHOOK_${m}_ID`);
|
||||
|
||||
throw new Error(
|
||||
`Missing webhook workflow IDs for HTTP methods: ${missing.join(', ')}\n\n` +
|
||||
`Webhook testing requires pre-activated workflows in n8n.\n` +
|
||||
`n8n API doesn't support workflow activation, so these must be created manually.\n\n` +
|
||||
`Setup Instructions:\n` +
|
||||
`1. Create ${missing.length} workflow(s) in your n8n instance\n` +
|
||||
`2. Each workflow should have a single Webhook node\n` +
|
||||
`3. Configure webhook paths:\n` +
|
||||
missing.map(m => ` - ${m}: mcp-test-${m.toLowerCase()}`).join('\n') + '\n' +
|
||||
`4. ACTIVATE each workflow in n8n UI\n` +
|
||||
`5. Set the following environment variables with workflow IDs:\n` +
|
||||
envVars.map(v => ` ${v}=<workflow-id>`).join('\n') + '\n\n' +
|
||||
`See docs/local/integration-testing-plan.md for detailed instructions.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if credentials are configured (non-throwing version)
|
||||
*
|
||||
* @returns true if basic credentials are available
|
||||
*/
|
||||
export function hasCredentials(): boolean {
|
||||
try {
|
||||
const creds = getN8nCredentials();
|
||||
return !!(creds.url && creds.apiKey);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if webhook workflows are configured (non-throwing version)
|
||||
*
|
||||
* @returns true if all webhook workflow IDs are available
|
||||
*/
|
||||
export function hasWebhookWorkflows(): boolean {
|
||||
try {
|
||||
const creds = getN8nCredentials();
|
||||
return !!(
|
||||
creds.webhookWorkflows.get &&
|
||||
creds.webhookWorkflows.post &&
|
||||
creds.webhookWorkflows.put &&
|
||||
creds.webhookWorkflows.delete
|
||||
);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
326
tests/integration/n8n-api/utils/factories.ts
Normal file
326
tests/integration/n8n-api/utils/factories.ts
Normal file
@@ -0,0 +1,326 @@
|
||||
/**
|
||||
* Test Data Factories
|
||||
*
|
||||
* Provides factory functions for generating test data dynamically.
|
||||
* Useful for creating variations of workflows, nodes, and parameters.
|
||||
*/
|
||||
|
||||
import { Workflow, WorkflowNode } from '../../../../src/types/n8n-api';
|
||||
import { createTestWorkflowName } from './test-context';
|
||||
|
||||
/**
|
||||
* Create a webhook node with custom parameters
|
||||
*
|
||||
* @param options - Node options
|
||||
* @returns WorkflowNode
|
||||
*/
|
||||
export function createWebhookNode(options: {
|
||||
id?: string;
|
||||
name?: string;
|
||||
httpMethod?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
|
||||
path?: string;
|
||||
position?: [number, number];
|
||||
responseMode?: 'onReceived' | 'lastNode';
|
||||
}): WorkflowNode {
|
||||
return {
|
||||
id: options.id || `webhook-${Date.now()}`,
|
||||
name: options.name || 'Webhook',
|
||||
type: 'n8n-nodes-base.webhook',
|
||||
typeVersion: 2,
|
||||
position: options.position || [250, 300],
|
||||
parameters: {
|
||||
httpMethod: options.httpMethod || 'GET',
|
||||
path: options.path || `test-${Date.now()}`,
|
||||
responseMode: options.responseMode || 'lastNode'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an HTTP Request node with custom parameters
|
||||
*
|
||||
* @param options - Node options
|
||||
* @returns WorkflowNode
|
||||
*/
|
||||
export function createHttpRequestNode(options: {
|
||||
id?: string;
|
||||
name?: string;
|
||||
url?: string;
|
||||
method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
|
||||
position?: [number, number];
|
||||
authentication?: string;
|
||||
}): WorkflowNode {
|
||||
return {
|
||||
id: options.id || `http-${Date.now()}`,
|
||||
name: options.name || 'HTTP Request',
|
||||
type: 'n8n-nodes-base.httpRequest',
|
||||
typeVersion: 4.2,
|
||||
position: options.position || [450, 300],
|
||||
parameters: {
|
||||
url: options.url || 'https://httpbin.org/get',
|
||||
method: options.method || 'GET',
|
||||
authentication: options.authentication || 'none'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Set node with custom assignments
|
||||
*
|
||||
* @param options - Node options
|
||||
* @returns WorkflowNode
|
||||
*/
|
||||
export function createSetNode(options: {
|
||||
id?: string;
|
||||
name?: string;
|
||||
position?: [number, number];
|
||||
assignments?: Array<{
|
||||
name: string;
|
||||
value: any;
|
||||
type?: 'string' | 'number' | 'boolean' | 'object' | 'array';
|
||||
}>;
|
||||
}): WorkflowNode {
|
||||
const assignments = options.assignments || [
|
||||
{ name: 'key', value: 'value', type: 'string' as const }
|
||||
];
|
||||
|
||||
return {
|
||||
id: options.id || `set-${Date.now()}`,
|
||||
name: options.name || 'Set',
|
||||
type: 'n8n-nodes-base.set',
|
||||
typeVersion: 3.4,
|
||||
position: options.position || [450, 300],
|
||||
parameters: {
|
||||
assignments: {
|
||||
assignments: assignments.map((a, idx) => ({
|
||||
id: `assign-${idx}`,
|
||||
name: a.name,
|
||||
value: a.value,
|
||||
type: a.type || 'string'
|
||||
}))
|
||||
},
|
||||
options: {}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Manual Trigger node
|
||||
*
|
||||
* @param options - Node options
|
||||
* @returns WorkflowNode
|
||||
*/
|
||||
export function createManualTriggerNode(options: {
|
||||
id?: string;
|
||||
name?: string;
|
||||
position?: [number, number];
|
||||
} = {}): WorkflowNode {
|
||||
return {
|
||||
id: options.id || `manual-${Date.now()}`,
|
||||
name: options.name || 'When clicking "Test workflow"',
|
||||
type: 'n8n-nodes-base.manualTrigger',
|
||||
typeVersion: 1,
|
||||
position: options.position || [250, 300],
|
||||
parameters: {}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a simple connection between two nodes
|
||||
*
|
||||
* @param from - Source node name
|
||||
* @param to - Target node name
|
||||
* @param options - Connection options
|
||||
* @returns Connection object
|
||||
*/
|
||||
export function createConnection(
|
||||
from: string,
|
||||
to: string,
|
||||
options: {
|
||||
sourceOutput?: string;
|
||||
targetInput?: string;
|
||||
sourceIndex?: number;
|
||||
targetIndex?: number;
|
||||
} = {}
|
||||
): Record<string, any> {
|
||||
const sourceOutput = options.sourceOutput || 'main';
|
||||
const targetInput = options.targetInput || 'main';
|
||||
const sourceIndex = options.sourceIndex || 0;
|
||||
const targetIndex = options.targetIndex || 0;
|
||||
|
||||
return {
|
||||
[from]: {
|
||||
[sourceOutput]: [
|
||||
[
|
||||
{
|
||||
node: to,
|
||||
type: targetInput,
|
||||
index: targetIndex
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a workflow from nodes with automatic connections
|
||||
*
|
||||
* Connects nodes in sequence: node1 -> node2 -> node3, etc.
|
||||
*
|
||||
* @param name - Workflow name
|
||||
* @param nodes - Array of nodes
|
||||
* @returns Partial workflow
|
||||
*/
|
||||
export function createSequentialWorkflow(
|
||||
name: string,
|
||||
nodes: WorkflowNode[]
|
||||
): Partial<Workflow> {
|
||||
const connections: Record<string, any> = {};
|
||||
|
||||
// Create connections between sequential nodes
|
||||
for (let i = 0; i < nodes.length - 1; i++) {
|
||||
const currentNode = nodes[i];
|
||||
const nextNode = nodes[i + 1];
|
||||
|
||||
connections[currentNode.name] = {
|
||||
main: [[{ node: nextNode.name, type: 'main', index: 0 }]]
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
name: createTestWorkflowName(name),
|
||||
nodes,
|
||||
connections,
|
||||
settings: {
|
||||
executionOrder: 'v1'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a workflow with parallel branches
|
||||
*
|
||||
* Creates a workflow with one trigger node that splits into multiple
|
||||
* parallel execution paths.
|
||||
*
|
||||
* @param name - Workflow name
|
||||
* @param trigger - Trigger node
|
||||
* @param branches - Array of branch nodes
|
||||
* @returns Partial workflow
|
||||
*/
|
||||
export function createParallelWorkflow(
|
||||
name: string,
|
||||
trigger: WorkflowNode,
|
||||
branches: WorkflowNode[]
|
||||
): Partial<Workflow> {
|
||||
const connections: Record<string, any> = {
|
||||
[trigger.name]: {
|
||||
main: [branches.map(node => ({ node: node.name, type: 'main', index: 0 }))]
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
name: createTestWorkflowName(name),
|
||||
nodes: [trigger, ...branches],
|
||||
connections,
|
||||
settings: {
|
||||
executionOrder: 'v1'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a random string for test data
|
||||
*
|
||||
* @param length - String length (default: 8)
|
||||
* @returns Random string
|
||||
*/
|
||||
export function randomString(length: number = 8): string {
|
||||
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
||||
let result = '';
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a unique ID for testing
|
||||
*
|
||||
* @param prefix - Optional prefix
|
||||
* @returns Unique ID
|
||||
*/
|
||||
export function uniqueId(prefix: string = 'test'): string {
|
||||
return `${prefix}-${Date.now()}-${randomString(4)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a workflow with error handling
|
||||
*
|
||||
* @param name - Workflow name
|
||||
* @param mainNode - Main processing node
|
||||
* @param errorNode - Error handling node
|
||||
* @returns Partial workflow with error handling configured
|
||||
*/
|
||||
export function createErrorHandlingWorkflow(
|
||||
name: string,
|
||||
mainNode: WorkflowNode,
|
||||
errorNode: WorkflowNode
|
||||
): Partial<Workflow> {
|
||||
const trigger = createWebhookNode({
|
||||
name: 'Trigger',
|
||||
position: [250, 300]
|
||||
});
|
||||
|
||||
// Configure main node for error handling
|
||||
const mainNodeWithError = {
|
||||
...mainNode,
|
||||
continueOnFail: true,
|
||||
onError: 'continueErrorOutput' as const
|
||||
};
|
||||
|
||||
const connections: Record<string, any> = {
|
||||
[trigger.name]: {
|
||||
main: [[{ node: mainNode.name, type: 'main', index: 0 }]]
|
||||
},
|
||||
[mainNode.name]: {
|
||||
error: [[{ node: errorNode.name, type: 'main', index: 0 }]]
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
name: createTestWorkflowName(name),
|
||||
nodes: [trigger, mainNodeWithError, errorNode],
|
||||
connections,
|
||||
settings: {
|
||||
executionOrder: 'v1'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create test workflow tags
|
||||
*
|
||||
* @param additional - Additional tags to include
|
||||
* @returns Array of tags for test workflows
|
||||
*/
|
||||
export function createTestTags(additional: string[] = []): string[] {
|
||||
return ['mcp-integration-test', ...additional];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create workflow settings with common test configurations
|
||||
*
|
||||
* @param overrides - Settings to override
|
||||
* @returns Workflow settings object
|
||||
*/
|
||||
export function createWorkflowSettings(overrides: Record<string, any> = {}): Record<string, any> {
|
||||
return {
|
||||
executionOrder: 'v1',
|
||||
saveDataErrorExecution: 'all',
|
||||
saveDataSuccessExecution: 'all',
|
||||
saveManualExecutions: true,
|
||||
...overrides
|
||||
};
|
||||
}
|
||||
374
tests/integration/n8n-api/utils/fixtures.ts
Normal file
374
tests/integration/n8n-api/utils/fixtures.ts
Normal file
@@ -0,0 +1,374 @@
|
||||
/**
|
||||
* Workflow Fixtures for Integration Tests
|
||||
*
|
||||
* Provides reusable workflow templates for testing.
|
||||
* All fixtures use FULL node type format (n8n-nodes-base.*)
|
||||
* as required by the n8n API.
|
||||
*/
|
||||
|
||||
import { Workflow, WorkflowNode } from '../../../../src/types/n8n-api';
|
||||
|
||||
/**
|
||||
* Simple webhook workflow with a single Webhook node
|
||||
*
|
||||
* Use this for basic workflow creation tests.
|
||||
*/
|
||||
export const SIMPLE_WEBHOOK_WORKFLOW: Partial<Workflow> = {
|
||||
nodes: [
|
||||
{
|
||||
id: 'webhook-1',
|
||||
name: 'Webhook',
|
||||
type: 'n8n-nodes-base.webhook',
|
||||
typeVersion: 2,
|
||||
position: [250, 300],
|
||||
parameters: {
|
||||
httpMethod: 'GET',
|
||||
path: 'test-webhook'
|
||||
}
|
||||
}
|
||||
],
|
||||
connections: {},
|
||||
settings: {
|
||||
executionOrder: 'v1'
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Simple HTTP request workflow
|
||||
*
|
||||
* Contains a Webhook trigger and an HTTP Request node.
|
||||
* Tests basic workflow connections.
|
||||
*/
|
||||
export const SIMPLE_HTTP_WORKFLOW: Partial<Workflow> = {
|
||||
nodes: [
|
||||
{
|
||||
id: 'webhook-1',
|
||||
name: 'Webhook',
|
||||
type: 'n8n-nodes-base.webhook',
|
||||
typeVersion: 2,
|
||||
position: [250, 300],
|
||||
parameters: {
|
||||
httpMethod: 'GET',
|
||||
path: 'trigger'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'http-1',
|
||||
name: 'HTTP Request',
|
||||
type: 'n8n-nodes-base.httpRequest',
|
||||
typeVersion: 4.2,
|
||||
position: [450, 300],
|
||||
parameters: {
|
||||
url: 'https://httpbin.org/get',
|
||||
method: 'GET'
|
||||
}
|
||||
}
|
||||
],
|
||||
connections: {
|
||||
Webhook: {
|
||||
main: [[{ node: 'HTTP Request', type: 'main', index: 0 }]]
|
||||
}
|
||||
},
|
||||
settings: {
|
||||
executionOrder: 'v1'
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Multi-node workflow with branching
|
||||
*
|
||||
* Tests complex connections and multiple execution paths.
|
||||
*/
|
||||
export const MULTI_NODE_WORKFLOW: Partial<Workflow> = {
|
||||
nodes: [
|
||||
{
|
||||
id: 'webhook-1',
|
||||
name: 'Webhook',
|
||||
type: 'n8n-nodes-base.webhook',
|
||||
typeVersion: 2,
|
||||
position: [250, 300],
|
||||
parameters: {
|
||||
httpMethod: 'POST',
|
||||
path: 'multi-node'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'set-1',
|
||||
name: 'Set 1',
|
||||
type: 'n8n-nodes-base.set',
|
||||
typeVersion: 3.4,
|
||||
position: [450, 200],
|
||||
parameters: {
|
||||
assignments: {
|
||||
assignments: [
|
||||
{
|
||||
id: 'assign-1',
|
||||
name: 'branch',
|
||||
value: 'top',
|
||||
type: 'string'
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'set-2',
|
||||
name: 'Set 2',
|
||||
type: 'n8n-nodes-base.set',
|
||||
typeVersion: 3.4,
|
||||
position: [450, 400],
|
||||
parameters: {
|
||||
assignments: {
|
||||
assignments: [
|
||||
{
|
||||
id: 'assign-2',
|
||||
name: 'branch',
|
||||
value: 'bottom',
|
||||
type: 'string'
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'merge-1',
|
||||
name: 'Merge',
|
||||
type: 'n8n-nodes-base.merge',
|
||||
typeVersion: 3,
|
||||
position: [650, 300],
|
||||
parameters: {
|
||||
mode: 'append',
|
||||
options: {}
|
||||
}
|
||||
}
|
||||
],
|
||||
connections: {
|
||||
Webhook: {
|
||||
main: [
|
||||
[
|
||||
{ node: 'Set 1', type: 'main', index: 0 },
|
||||
{ node: 'Set 2', type: 'main', index: 0 }
|
||||
]
|
||||
]
|
||||
},
|
||||
'Set 1': {
|
||||
main: [[{ node: 'Merge', type: 'main', index: 0 }]]
|
||||
},
|
||||
'Set 2': {
|
||||
main: [[{ node: 'Merge', type: 'main', index: 1 }]]
|
||||
}
|
||||
},
|
||||
settings: {
|
||||
executionOrder: 'v1'
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Workflow with error handling
|
||||
*
|
||||
* Tests error output configuration and error workflows.
|
||||
*/
|
||||
export const ERROR_HANDLING_WORKFLOW: Partial<Workflow> = {
|
||||
nodes: [
|
||||
{
|
||||
id: 'webhook-1',
|
||||
name: 'Webhook',
|
||||
type: 'n8n-nodes-base.webhook',
|
||||
typeVersion: 2,
|
||||
position: [250, 300],
|
||||
parameters: {
|
||||
httpMethod: 'GET',
|
||||
path: 'error-test'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'http-1',
|
||||
name: 'HTTP Request',
|
||||
type: 'n8n-nodes-base.httpRequest',
|
||||
typeVersion: 4.2,
|
||||
position: [450, 300],
|
||||
parameters: {
|
||||
url: 'https://httpbin.org/status/500',
|
||||
method: 'GET'
|
||||
},
|
||||
continueOnFail: true,
|
||||
onError: 'continueErrorOutput'
|
||||
},
|
||||
{
|
||||
id: 'set-error',
|
||||
name: 'Handle Error',
|
||||
type: 'n8n-nodes-base.set',
|
||||
typeVersion: 3.4,
|
||||
position: [650, 400],
|
||||
parameters: {
|
||||
assignments: {
|
||||
assignments: [
|
||||
{
|
||||
id: 'error-assign',
|
||||
name: 'error_handled',
|
||||
value: 'true',
|
||||
type: 'boolean'
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {}
|
||||
}
|
||||
}
|
||||
],
|
||||
connections: {
|
||||
Webhook: {
|
||||
main: [[{ node: 'HTTP Request', type: 'main', index: 0 }]]
|
||||
},
|
||||
'HTTP Request': {
|
||||
error: [[{ node: 'Handle Error', type: 'main', index: 0 }]]
|
||||
}
|
||||
},
|
||||
settings: {
|
||||
executionOrder: 'v1'
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* AI Agent workflow (langchain nodes)
|
||||
*
|
||||
* Tests langchain node support.
|
||||
*/
|
||||
export const AI_AGENT_WORKFLOW: Partial<Workflow> = {
|
||||
nodes: [
|
||||
{
|
||||
id: 'manual-1',
|
||||
name: 'When clicking "Test workflow"',
|
||||
type: 'n8n-nodes-base.manualTrigger',
|
||||
typeVersion: 1,
|
||||
position: [250, 300],
|
||||
parameters: {}
|
||||
},
|
||||
{
|
||||
id: 'agent-1',
|
||||
name: 'AI Agent',
|
||||
type: '@n8n/n8n-nodes-langchain.agent',
|
||||
typeVersion: 1.7,
|
||||
position: [450, 300],
|
||||
parameters: {
|
||||
promptType: 'define',
|
||||
text: '={{ $json.input }}',
|
||||
options: {}
|
||||
}
|
||||
}
|
||||
],
|
||||
connections: {
|
||||
'When clicking "Test workflow"': {
|
||||
main: [[{ node: 'AI Agent', type: 'main', index: 0 }]]
|
||||
}
|
||||
},
|
||||
settings: {
|
||||
executionOrder: 'v1'
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Workflow with n8n expressions
|
||||
*
|
||||
* Tests expression validation.
|
||||
*/
|
||||
export const EXPRESSION_WORKFLOW: Partial<Workflow> = {
|
||||
nodes: [
|
||||
{
|
||||
id: 'manual-1',
|
||||
name: 'Manual Trigger',
|
||||
type: 'n8n-nodes-base.manualTrigger',
|
||||
typeVersion: 1,
|
||||
position: [250, 300],
|
||||
parameters: {}
|
||||
},
|
||||
{
|
||||
id: 'set-1',
|
||||
name: 'Set Variables',
|
||||
type: 'n8n-nodes-base.set',
|
||||
typeVersion: 3.4,
|
||||
position: [450, 300],
|
||||
parameters: {
|
||||
assignments: {
|
||||
assignments: [
|
||||
{
|
||||
id: 'expr-1',
|
||||
name: 'timestamp',
|
||||
value: '={{ $now }}',
|
||||
type: 'string'
|
||||
},
|
||||
{
|
||||
id: 'expr-2',
|
||||
name: 'item_count',
|
||||
value: '={{ $json.items.length }}',
|
||||
type: 'number'
|
||||
},
|
||||
{
|
||||
id: 'expr-3',
|
||||
name: 'first_item',
|
||||
value: '={{ $node["Manual Trigger"].json }}',
|
||||
type: 'object'
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {}
|
||||
}
|
||||
}
|
||||
],
|
||||
connections: {
|
||||
'Manual Trigger': {
|
||||
main: [[{ node: 'Set Variables', type: 'main', index: 0 }]]
|
||||
}
|
||||
},
|
||||
settings: {
|
||||
executionOrder: 'v1'
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a fixture by name
|
||||
*
|
||||
* @param name - Fixture name
|
||||
* @returns Workflow fixture
|
||||
*/
|
||||
export function getFixture(
|
||||
name:
|
||||
| 'simple-webhook'
|
||||
| 'simple-http'
|
||||
| 'multi-node'
|
||||
| 'error-handling'
|
||||
| 'ai-agent'
|
||||
| 'expression'
|
||||
): Partial<Workflow> {
|
||||
const fixtures = {
|
||||
'simple-webhook': SIMPLE_WEBHOOK_WORKFLOW,
|
||||
'simple-http': SIMPLE_HTTP_WORKFLOW,
|
||||
'multi-node': MULTI_NODE_WORKFLOW,
|
||||
'error-handling': ERROR_HANDLING_WORKFLOW,
|
||||
'ai-agent': AI_AGENT_WORKFLOW,
|
||||
expression: EXPRESSION_WORKFLOW
|
||||
};
|
||||
|
||||
return JSON.parse(JSON.stringify(fixtures[name])); // Deep clone
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a minimal workflow with custom nodes
|
||||
*
|
||||
* @param nodes - Array of workflow nodes
|
||||
* @param connections - Optional connections object
|
||||
* @returns Workflow fixture
|
||||
*/
|
||||
export function createCustomWorkflow(
|
||||
nodes: WorkflowNode[],
|
||||
connections: Record<string, any> = {}
|
||||
): Partial<Workflow> {
|
||||
return {
|
||||
nodes,
|
||||
connections,
|
||||
settings: {
|
||||
executionOrder: 'v1'
|
||||
}
|
||||
};
|
||||
}
|
||||
63
tests/integration/n8n-api/utils/n8n-client.ts
Normal file
63
tests/integration/n8n-api/utils/n8n-client.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* Pre-configured n8n API Client for Integration Tests
|
||||
*
|
||||
* Provides a singleton API client instance configured with test credentials.
|
||||
* Automatically loads credentials from .env (local) or GitHub secrets (CI).
|
||||
*/
|
||||
|
||||
import { N8nApiClient } from '../../../../src/services/n8n-api-client';
|
||||
import { getN8nCredentials, validateCredentials } from './credentials';
|
||||
|
||||
let client: N8nApiClient | null = null;
|
||||
|
||||
/**
|
||||
* Get or create the test n8n API client
|
||||
*
|
||||
* Creates a singleton instance configured with credentials from
|
||||
* the environment. Validates that required credentials are present.
|
||||
*
|
||||
* @returns Configured N8nApiClient instance
|
||||
* @throws Error if credentials are missing or invalid
|
||||
*
|
||||
* @example
|
||||
* const client = getTestN8nClient();
|
||||
* const workflow = await client.createWorkflow({ ... });
|
||||
*/
|
||||
export function getTestN8nClient(): N8nApiClient {
|
||||
if (!client) {
|
||||
const creds = getN8nCredentials();
|
||||
validateCredentials(creds);
|
||||
client = new N8nApiClient({
|
||||
baseUrl: creds.url,
|
||||
apiKey: creds.apiKey
|
||||
});
|
||||
}
|
||||
return client;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the test client instance
|
||||
*
|
||||
* Forces recreation of the client on next call to getTestN8nClient().
|
||||
* Useful for testing or when credentials change.
|
||||
*/
|
||||
export function resetTestN8nClient(): void {
|
||||
client = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the n8n API is accessible
|
||||
*
|
||||
* Performs a health check to verify API connectivity.
|
||||
*
|
||||
* @returns true if API is accessible, false otherwise
|
||||
*/
|
||||
export async function isN8nApiAccessible(): Promise<boolean> {
|
||||
try {
|
||||
const client = getTestN8nClient();
|
||||
await client.healthCheck();
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
177
tests/integration/n8n-api/utils/test-context.ts
Normal file
177
tests/integration/n8n-api/utils/test-context.ts
Normal file
@@ -0,0 +1,177 @@
|
||||
/**
|
||||
* Test Context for Resource Tracking and Cleanup
|
||||
*
|
||||
* Tracks resources created during tests (workflows, executions) and
|
||||
* provides automatic cleanup functionality.
|
||||
*/
|
||||
|
||||
import { getTestN8nClient } from './n8n-client';
|
||||
import { getN8nCredentials } from './credentials';
|
||||
import { Logger } from '../../../../src/utils/logger';
|
||||
|
||||
const logger = new Logger({ prefix: '[TestContext]' });
|
||||
|
||||
export interface TestContext {
|
||||
/** Workflow IDs created during the test */
|
||||
workflowIds: string[];
|
||||
|
||||
/** Execution IDs created during the test */
|
||||
executionIds: string[];
|
||||
|
||||
/** Clean up all tracked resources */
|
||||
cleanup: () => Promise<void>;
|
||||
|
||||
/** Track a workflow for cleanup */
|
||||
trackWorkflow: (id: string) => void;
|
||||
|
||||
/** Track an execution for cleanup */
|
||||
trackExecution: (id: string) => void;
|
||||
|
||||
/** Remove a workflow from tracking (e.g., already deleted) */
|
||||
untrackWorkflow: (id: string) => void;
|
||||
|
||||
/** Remove an execution from tracking (e.g., already deleted) */
|
||||
untrackExecution: (id: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a test context for tracking and cleaning up resources
|
||||
*
|
||||
* Use this in test setup to create a context that tracks all
|
||||
* workflows and executions created during the test. Call cleanup()
|
||||
* in afterEach or afterAll to remove test resources.
|
||||
*
|
||||
* @returns TestContext
|
||||
*
|
||||
* @example
|
||||
* describe('Workflow tests', () => {
|
||||
* let context: TestContext;
|
||||
*
|
||||
* beforeEach(() => {
|
||||
* context = createTestContext();
|
||||
* });
|
||||
*
|
||||
* afterEach(async () => {
|
||||
* await context.cleanup();
|
||||
* });
|
||||
*
|
||||
* it('creates a workflow', async () => {
|
||||
* const workflow = await client.createWorkflow({ ... });
|
||||
* context.trackWorkflow(workflow.id);
|
||||
* // Test runs, then cleanup() automatically deletes the workflow
|
||||
* });
|
||||
* });
|
||||
*/
|
||||
export function createTestContext(): TestContext {
|
||||
const context: TestContext = {
|
||||
workflowIds: [],
|
||||
executionIds: [],
|
||||
|
||||
trackWorkflow(id: string) {
|
||||
if (!this.workflowIds.includes(id)) {
|
||||
this.workflowIds.push(id);
|
||||
logger.debug(`Tracking workflow for cleanup: ${id}`);
|
||||
}
|
||||
},
|
||||
|
||||
trackExecution(id: string) {
|
||||
if (!this.executionIds.includes(id)) {
|
||||
this.executionIds.push(id);
|
||||
logger.debug(`Tracking execution for cleanup: ${id}`);
|
||||
}
|
||||
},
|
||||
|
||||
untrackWorkflow(id: string) {
|
||||
const index = this.workflowIds.indexOf(id);
|
||||
if (index > -1) {
|
||||
this.workflowIds.splice(index, 1);
|
||||
logger.debug(`Untracked workflow: ${id}`);
|
||||
}
|
||||
},
|
||||
|
||||
untrackExecution(id: string) {
|
||||
const index = this.executionIds.indexOf(id);
|
||||
if (index > -1) {
|
||||
this.executionIds.splice(index, 1);
|
||||
logger.debug(`Untracked execution: ${id}`);
|
||||
}
|
||||
},
|
||||
|
||||
async cleanup() {
|
||||
const creds = getN8nCredentials();
|
||||
|
||||
// Skip cleanup if disabled
|
||||
if (!creds.cleanup.enabled) {
|
||||
logger.info('Cleanup disabled, skipping resource cleanup');
|
||||
return;
|
||||
}
|
||||
|
||||
const client = getTestN8nClient();
|
||||
|
||||
// Delete executions first (they reference workflows)
|
||||
if (this.executionIds.length > 0) {
|
||||
logger.info(`Cleaning up ${this.executionIds.length} execution(s)`);
|
||||
|
||||
for (const id of this.executionIds) {
|
||||
try {
|
||||
await client.deleteExecution(id);
|
||||
logger.debug(`Deleted execution: ${id}`);
|
||||
} catch (error) {
|
||||
// Log but don't fail - execution might already be deleted
|
||||
logger.warn(`Failed to delete execution ${id}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
this.executionIds = [];
|
||||
}
|
||||
|
||||
// Then delete workflows
|
||||
if (this.workflowIds.length > 0) {
|
||||
logger.info(`Cleaning up ${this.workflowIds.length} workflow(s)`);
|
||||
|
||||
for (const id of this.workflowIds) {
|
||||
try {
|
||||
await client.deleteWorkflow(id);
|
||||
logger.debug(`Deleted workflow: ${id}`);
|
||||
} catch (error) {
|
||||
// Log but don't fail - workflow might already be deleted
|
||||
logger.warn(`Failed to delete workflow ${id}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
this.workflowIds = [];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a test workflow name with prefix and timestamp
|
||||
*
|
||||
* Generates a unique workflow name for testing that follows
|
||||
* the configured naming convention.
|
||||
*
|
||||
* @param baseName - Base name for the workflow
|
||||
* @returns Prefixed workflow name with timestamp
|
||||
*
|
||||
* @example
|
||||
* const name = createTestWorkflowName('Simple HTTP Request');
|
||||
* // Returns: "[MCP-TEST] Simple HTTP Request 1704067200000"
|
||||
*/
|
||||
export function createTestWorkflowName(baseName: string): string {
|
||||
const creds = getN8nCredentials();
|
||||
const timestamp = Date.now();
|
||||
return `${creds.cleanup.namePrefix} ${baseName} ${timestamp}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the configured test tag
|
||||
*
|
||||
* @returns Tag to apply to test workflows
|
||||
*/
|
||||
export function getTestTag(): string {
|
||||
const creds = getN8nCredentials();
|
||||
return creds.cleanup.tag;
|
||||
}
|
||||
289
tests/integration/n8n-api/utils/webhook-workflows.ts
Normal file
289
tests/integration/n8n-api/utils/webhook-workflows.ts
Normal file
@@ -0,0 +1,289 @@
|
||||
/**
|
||||
* Webhook Workflow Configuration
|
||||
*
|
||||
* Provides configuration and setup instructions for webhook workflows
|
||||
* required for integration testing.
|
||||
*
|
||||
* These workflows must be created manually in n8n and activated because
|
||||
* the n8n API doesn't support workflow activation.
|
||||
*/
|
||||
|
||||
import { Workflow, WorkflowNode } from '../../../../src/types/n8n-api';
|
||||
|
||||
export interface WebhookWorkflowConfig {
|
||||
name: string;
|
||||
description: string;
|
||||
httpMethod: 'GET' | 'POST' | 'PUT' | 'DELETE';
|
||||
path: string;
|
||||
nodes: Array<Partial<WorkflowNode>>;
|
||||
connections: Record<string, any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration for required webhook workflows
|
||||
*/
|
||||
export const WEBHOOK_WORKFLOW_CONFIGS: Record<string, WebhookWorkflowConfig> = {
|
||||
GET: {
|
||||
name: '[MCP-TEST] Webhook GET',
|
||||
description: 'Pre-activated webhook for GET method testing',
|
||||
httpMethod: 'GET',
|
||||
path: 'mcp-test-get',
|
||||
nodes: [
|
||||
{
|
||||
name: 'Webhook',
|
||||
type: 'n8n-nodes-base.webhook',
|
||||
typeVersion: 2,
|
||||
position: [250, 300],
|
||||
parameters: {
|
||||
httpMethod: 'GET',
|
||||
path: 'mcp-test-get',
|
||||
responseMode: 'lastNode',
|
||||
options: {}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Respond to Webhook',
|
||||
type: 'n8n-nodes-base.respondToWebhook',
|
||||
typeVersion: 1.1,
|
||||
position: [450, 300],
|
||||
parameters: {
|
||||
options: {}
|
||||
}
|
||||
}
|
||||
],
|
||||
connections: {
|
||||
Webhook: {
|
||||
main: [[{ node: 'Respond to Webhook', type: 'main', index: 0 }]]
|
||||
}
|
||||
}
|
||||
},
|
||||
POST: {
|
||||
name: '[MCP-TEST] Webhook POST',
|
||||
description: 'Pre-activated webhook for POST method testing',
|
||||
httpMethod: 'POST',
|
||||
path: 'mcp-test-post',
|
||||
nodes: [
|
||||
{
|
||||
name: 'Webhook',
|
||||
type: 'n8n-nodes-base.webhook',
|
||||
typeVersion: 2,
|
||||
position: [250, 300],
|
||||
parameters: {
|
||||
httpMethod: 'POST',
|
||||
path: 'mcp-test-post',
|
||||
responseMode: 'lastNode',
|
||||
options: {}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Respond to Webhook',
|
||||
type: 'n8n-nodes-base.respondToWebhook',
|
||||
typeVersion: 1.1,
|
||||
position: [450, 300],
|
||||
parameters: {
|
||||
options: {}
|
||||
}
|
||||
}
|
||||
],
|
||||
connections: {
|
||||
Webhook: {
|
||||
main: [[{ node: 'Respond to Webhook', type: 'main', index: 0 }]]
|
||||
}
|
||||
}
|
||||
},
|
||||
PUT: {
|
||||
name: '[MCP-TEST] Webhook PUT',
|
||||
description: 'Pre-activated webhook for PUT method testing',
|
||||
httpMethod: 'PUT',
|
||||
path: 'mcp-test-put',
|
||||
nodes: [
|
||||
{
|
||||
name: 'Webhook',
|
||||
type: 'n8n-nodes-base.webhook',
|
||||
typeVersion: 2,
|
||||
position: [250, 300],
|
||||
parameters: {
|
||||
httpMethod: 'PUT',
|
||||
path: 'mcp-test-put',
|
||||
responseMode: 'lastNode',
|
||||
options: {}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Respond to Webhook',
|
||||
type: 'n8n-nodes-base.respondToWebhook',
|
||||
typeVersion: 1.1,
|
||||
position: [450, 300],
|
||||
parameters: {
|
||||
options: {}
|
||||
}
|
||||
}
|
||||
],
|
||||
connections: {
|
||||
Webhook: {
|
||||
main: [[{ node: 'Respond to Webhook', type: 'main', index: 0 }]]
|
||||
}
|
||||
}
|
||||
},
|
||||
DELETE: {
|
||||
name: '[MCP-TEST] Webhook DELETE',
|
||||
description: 'Pre-activated webhook for DELETE method testing',
|
||||
httpMethod: 'DELETE',
|
||||
path: 'mcp-test-delete',
|
||||
nodes: [
|
||||
{
|
||||
name: 'Webhook',
|
||||
type: 'n8n-nodes-base.webhook',
|
||||
typeVersion: 2,
|
||||
position: [250, 300],
|
||||
parameters: {
|
||||
httpMethod: 'DELETE',
|
||||
path: 'mcp-test-delete',
|
||||
responseMode: 'lastNode',
|
||||
options: {}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Respond to Webhook',
|
||||
type: 'n8n-nodes-base.respondToWebhook',
|
||||
typeVersion: 1.1,
|
||||
position: [450, 300],
|
||||
parameters: {
|
||||
options: {}
|
||||
}
|
||||
}
|
||||
],
|
||||
connections: {
|
||||
Webhook: {
|
||||
main: [[{ node: 'Respond to Webhook', type: 'main', index: 0 }]]
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Print setup instructions for webhook workflows
|
||||
*/
|
||||
export function printSetupInstructions(): void {
|
||||
console.log(`
|
||||
╔════════════════════════════════════════════════════════════════╗
|
||||
║ WEBHOOK WORKFLOW SETUP REQUIRED ║
|
||||
╠════════════════════════════════════════════════════════════════╣
|
||||
║ ║
|
||||
║ Integration tests require 4 pre-activated webhook workflows: ║
|
||||
║ ║
|
||||
║ 1. Create workflows manually in n8n UI ║
|
||||
║ 2. Use the configurations shown below ║
|
||||
║ 3. ACTIVATE each workflow in n8n UI ║
|
||||
║ 4. Copy workflow IDs to .env file ║
|
||||
║ ║
|
||||
╚════════════════════════════════════════════════════════════════╝
|
||||
|
||||
Required workflows:
|
||||
`);
|
||||
|
||||
Object.entries(WEBHOOK_WORKFLOW_CONFIGS).forEach(([method, config]) => {
|
||||
console.log(`
|
||||
${method} Method:
|
||||
Name: ${config.name}
|
||||
Path: ${config.path}
|
||||
.env variable: N8N_TEST_WEBHOOK_${method}_ID
|
||||
|
||||
Workflow Structure:
|
||||
1. Webhook node (${method} method, path: ${config.path})
|
||||
2. Respond to Webhook node
|
||||
|
||||
After creating:
|
||||
1. Save the workflow
|
||||
2. ACTIVATE the workflow (toggle in UI)
|
||||
3. Copy the workflow ID
|
||||
4. Add to .env: N8N_TEST_WEBHOOK_${method}_ID=<workflow-id>
|
||||
`);
|
||||
});
|
||||
|
||||
console.log(`
|
||||
See docs/local/integration-testing-plan.md for detailed instructions.
|
||||
`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate workflow JSON for a webhook workflow
|
||||
*
|
||||
* @param method - HTTP method
|
||||
* @returns Partial workflow ready to create
|
||||
*/
|
||||
export function generateWebhookWorkflowJson(
|
||||
method: 'GET' | 'POST' | 'PUT' | 'DELETE'
|
||||
): Partial<Workflow> {
|
||||
const config = WEBHOOK_WORKFLOW_CONFIGS[method];
|
||||
|
||||
return {
|
||||
name: config.name,
|
||||
nodes: config.nodes as any,
|
||||
connections: config.connections,
|
||||
active: false, // Will need to be activated manually
|
||||
settings: {
|
||||
executionOrder: 'v1'
|
||||
},
|
||||
tags: ['mcp-integration-test', 'webhook-test']
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Export all webhook workflow JSONs
|
||||
*
|
||||
* Returns an object with all 4 webhook workflow configurations
|
||||
* ready to be created in n8n.
|
||||
*
|
||||
* @returns Object with workflow configurations
|
||||
*/
|
||||
export function exportAllWebhookWorkflows(): Record<string, Partial<Workflow>> {
|
||||
return {
|
||||
GET: generateWebhookWorkflowJson('GET'),
|
||||
POST: generateWebhookWorkflowJson('POST'),
|
||||
PUT: generateWebhookWorkflowJson('PUT'),
|
||||
DELETE: generateWebhookWorkflowJson('DELETE')
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get webhook URL for a given n8n instance and HTTP method
|
||||
*
|
||||
* @param n8nUrl - n8n instance URL
|
||||
* @param method - HTTP method
|
||||
* @returns Webhook URL
|
||||
*/
|
||||
export function getWebhookUrl(
|
||||
n8nUrl: string,
|
||||
method: 'GET' | 'POST' | 'PUT' | 'DELETE'
|
||||
): string {
|
||||
const config = WEBHOOK_WORKFLOW_CONFIGS[method];
|
||||
const baseUrl = n8nUrl.replace(/\/$/, ''); // Remove trailing slash
|
||||
return `${baseUrl}/webhook/${config.path}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate webhook workflow structure
|
||||
*
|
||||
* Checks if a workflow matches the expected webhook workflow structure.
|
||||
*
|
||||
* @param workflow - Workflow to validate
|
||||
* @param method - Expected HTTP method
|
||||
* @returns true if valid
|
||||
*/
|
||||
export function isValidWebhookWorkflow(
|
||||
workflow: Partial<Workflow>,
|
||||
method: 'GET' | 'POST' | 'PUT' | 'DELETE'
|
||||
): boolean {
|
||||
if (!workflow.nodes || workflow.nodes.length < 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const webhookNode = workflow.nodes.find(n => n.type === 'n8n-nodes-base.webhook');
|
||||
if (!webhookNode) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const params = webhookNode.parameters as any;
|
||||
return params.httpMethod === method;
|
||||
}
|
||||
Reference in New Issue
Block a user