diff --git a/.gitignore b/.gitignore index 5f7bba9..d688272 100644 --- a/.gitignore +++ b/.gitignore @@ -17,7 +17,6 @@ package-lock.json # Claude MCP files cline_mcp_settings.json -CLAUDE.md AGENTS.md .claude/ .agents/ @@ -53,5 +52,19 @@ Thumbs.db # Documentation (MkDocs) site/ .cache/ + +# Test scripts (may load local secrets) +test-*.js +cleanup-*.js +debug-*.js server.log -test-new-api-key.js + +# Additional secret patterns +*.pem +*.key +*.p12 +*.pfx +secrets.json +credentials.json +**/secrets/ +**/credentials/ diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..6a2e7a9 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,123 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +This is an MCP (Model Context Protocol) server that enables AI-powered management of n8n workflows. It allows Claude AI and Cursor IDE to create, manage, and monitor n8n workflows through natural language via the MCP protocol. + +## Common Commands + +```bash +# Build the TypeScript project +npm run build + +# Run tests +npm test + +# Run a single test file +npx jest src/services/__tests__/environmentManager.test.ts + +# Run tests in watch mode +npm run test:watch + +# Run tests with coverage +npm run test:coverage + +# Start the MCP server (after build) +npm start + +# Development mode (watch for TypeScript changes) +npm run dev + +# Build and serve documentation locally +npm run docs:dev +``` + +## Architecture + +### Core Components + +**Entry Point (`src/index.ts`)** +- `N8NWorkflowServer` class implements the MCP server +- Handles tool calls, resource requests, prompts, and notifications +- Can run in two modes: MCP subprocess (stdin/stdout) or standalone HTTP mode +- Registers 23 MCP tools for workflow, execution, tag, and credential management + +**Configuration Layer (`src/config/`)** +- `ConfigLoader` - Singleton that loads config from `.config.json` (multi-instance) or `.env` (single instance fallback) +- Supports multi-instance n8n environments (production, staging, development) + +**Environment/API Layer (`src/services/`)** +- `EnvironmentManager` - Singleton managing axios instances per n8n environment with URL normalization +- `N8NApiWrapper` - High-level API wrapper with instance routing for all n8n operations +- `n8nApi.ts` - Lower-level API functions (deprecated, use wrapper instead) + +**Workflow Processing** +- `WorkflowBuilder` - Constructs n8n-compatible workflow specifications +- `validation.ts` - Validates and transforms workflow input (connection format conversion, node positioning) + +### Data Flow + +``` +MCP Client (Claude/Cursor) + ↓ +N8NWorkflowServer (index.ts) + ↓ +N8NApiWrapper (services/n8nApiWrapper.ts) + ↓ +EnvironmentManager (services/environmentManager.ts) + ↓ +ConfigLoader (config/configLoader.ts) + ↓ +n8n REST API +``` + +### Multi-Instance Support + +Every tool accepts an optional `instance` parameter to target a specific n8n environment. Without it, the default environment is used. + +Configuration format (`.config.json`): +```json +{ + "environments": { + "production": { "n8n_host": "...", "n8n_api_key": "..." }, + "development": { "n8n_host": "...", "n8n_api_key": "..." } + }, + "defaultEnv": "development" +} +``` + +### Key Types (`src/types/`) + +- `WorkflowInput` - Input format for creating/updating workflows (uses `LegacyWorkflowConnection[]` for connections) +- `WorkflowSpec` - n8n API format (uses `ConnectionMap` object format) +- `N8NWorkflowResponse`, `N8NExecutionResponse`, etc. - API response types + +### Connection Format Transformation + +The server accepts a simplified connection format: +```typescript +{ source: "Node1", target: "Node2", sourceOutput: 0, targetInput: 0 } +``` + +And transforms it to n8n's native format: +```typescript +{ "Node1": { main: [[{ node: "Node2", type: "main", index: 0 }]] } } +``` + +## n8n API Limitations + +Several n8n REST API operations are intentionally blocked or limited: + +- **Workflow activation/deactivation** - Returns guidance; activate/deactivate via n8n web interface +- **Workflow execution** - Manual Trigger workflows cannot be executed via API; use webhook triggers instead +- **PATCH workflows** - Not supported (405); use full PUT update instead +- **Credential read/update** - Blocked for security; credentials are write-only via API +- **Credential listing** - Limited to metadata only; no sensitive data exposed + +## Testing + +Tests are located alongside source files in `__tests__` directories. The project uses Jest with ts-jest for TypeScript support. + +Coverage thresholds are set to 80% for branches, functions, lines, and statements. diff --git a/cleanup-test-tags.js b/cleanup-test-tags.js deleted file mode 100644 index 7471da9..0000000 --- a/cleanup-test-tags.js +++ /dev/null @@ -1,74 +0,0 @@ -#!/usr/bin/env node - -/** - * Cleanup script for test tags - * Removes all tags with prefix "TagTest_" - */ - -const axios = require('axios'); - -const mcpServerUrl = 'http://localhost:3456/mcp'; -let requestId = 1; - -async function sendMcpRequest(method, params = {}) { - const response = await axios.post(mcpServerUrl, { - jsonrpc: '2.0', - id: requestId++, - method, - params - }); - return response.data.result; -} - -async function callTool(name, args = {}) { - const result = await sendMcpRequest('tools/call', { name, arguments: args }); - if (result.isError) { - const errorMessage = result.content && result.content[0] && result.content[0].text - ? result.content[0].text - : 'Unknown error'; - throw new Error(errorMessage); - } - return result; -} - -async function cleanup() { - try { - console.error('[INFO] Fetching all tags...'); - - // Get all tags - const result = await callTool('get_tags', { limit: 100 }); - const data = JSON.parse(result.content[0].text); - const tags = data.data || []; - - console.error(`[INFO] Found ${tags.length} total tags`); - - // Filter test tags - const testTags = tags.filter(tag => tag.name.startsWith('TagTest_')); - console.error(`[INFO] Found ${testTags.length} test tags to delete`); - - if (testTags.length === 0) { - console.error('[INFO] No test tags to clean up'); - return; - } - - // Delete each test tag - let deleted = 0; - for (const tag of testTags) { - try { - await callTool('delete_tag', { id: tag.id }); - deleted++; - console.error(`[SUCCESS] Deleted tag: ${tag.name} (${tag.id})`); - } catch (error) { - console.error(`[ERROR] Failed to delete tag ${tag.name}: ${error.message}`); - } - } - - console.error(`[SUCCESS] Cleaned up ${deleted}/${testTags.length} test tags`); - - } catch (error) { - console.error(`[ERROR] Cleanup failed: ${error.message}`); - process.exit(1); - } -} - -cleanup(); diff --git a/cleanup-validtest-tags.js b/cleanup-validtest-tags.js deleted file mode 100644 index 6707184..0000000 --- a/cleanup-validtest-tags.js +++ /dev/null @@ -1,65 +0,0 @@ -#!/usr/bin/env node - -const axios = require('axios'); - -const mcpServerUrl = 'http://localhost:3456/mcp'; -let requestId = 1; - -async function sendMcpRequest(method, params = {}) { - const response = await axios.post(mcpServerUrl, { - jsonrpc: '2.0', - id: requestId++, - method, - params - }); - return response.data.result; -} - -async function callTool(name, args = {}) { - const result = await sendMcpRequest('tools/call', { name, arguments: args }); - if (result.isError) { - const errorMessage = result.content && result.content[0] && result.content[0].text - ? result.content[0].text - : 'Unknown error'; - throw new Error(errorMessage); - } - return result; -} - -async function cleanup() { - try { - console.error('[INFO] Fetching all tags...'); - const result = await callTool('get_tags', { limit: 100 }); - const data = JSON.parse(result.content[0].text); - const tags = data.data || []; - - console.error(`[INFO] Found ${tags.length} total tags`); - - const validTestTags = tags.filter(tag => tag.name.startsWith('ValidTest')); - console.error(`[INFO] Found ${validTestTags.length} ValidTest tags to delete`); - - if (validTestTags.length === 0) { - console.error('[INFO] No ValidTest tags to clean up'); - return; - } - - let deleted = 0; - for (const tag of validTestTags) { - try { - await callTool('delete_tag', { id: tag.id }); - deleted++; - console.error(`[SUCCESS] Deleted tag: ${tag.name} (${tag.id})`); - } catch (error) { - console.error(`[ERROR] Failed to delete tag ${tag.name}: ${error.message}`); - } - } - - console.error(`[SUCCESS] Cleaned up ${deleted}/${validTestTags.length} ValidTest tags`); - - } catch (error) { - console.error(`[ERROR] Cleanup failed: ${error.message}`); - process.exit(1); - } -} - -cleanup(); diff --git a/debug-tags.js b/debug-tags.js deleted file mode 100644 index 0f212a4..0000000 --- a/debug-tags.js +++ /dev/null @@ -1,66 +0,0 @@ -#!/usr/bin/env node - -const axios = require('axios'); - -const mcpServerUrl = 'http://localhost:3456/mcp'; -let requestId = 1; - -async function sendMcpRequest(method, params = {}) { - const response = await axios.post(mcpServerUrl, { - jsonrpc: '2.0', - id: requestId++, - method, - params - }); - return response.data.result; -} - -async function callTool(name, args = {}) { - const result = await sendMcpRequest('tools/call', { name, arguments: args }); - if (result.isError) { - const errorMessage = result.content && result.content[0] && result.content[0].text - ? result.content[0].text - : 'Unknown error'; - throw new Error(errorMessage); - } - return result; -} - -async function debug() { - try { - // List all tags - console.error('[INFO] Fetching all tags...'); - const result = await callTool('get_tags', { limit: 100 }); - const data = JSON.parse(result.content[0].text); - - console.error('\n[INFO] All tags in system:'); - console.error(JSON.stringify(data, null, 2)); - - // Try to create a test tag - console.error('\n[INFO] Attempting to create a test tag...'); - const tagName = `TagTest_${Date.now()}_${Math.random().toString(36).substring(7)}`; - console.error(`[INFO] Tag name: ${tagName}`); - - try { - const createResult = await callTool('create_tag', { name: tagName }); - const tag = JSON.parse(createResult.content[0].text); - console.error('[SUCCESS] Tag created successfully!'); - console.error(JSON.stringify(tag, null, 2)); - - // Clean up - console.error('\n[INFO] Cleaning up test tag...'); - await callTool('delete_tag', { id: tag.id }); - console.error('[SUCCESS] Test tag deleted'); - } catch (error) { - console.error('[ERROR] Failed to create tag:'); - console.error(error.message); - console.error('\nFull error:', error); - } - - } catch (error) { - console.error(`[ERROR] Debug failed: ${error.message}`); - process.exit(1); - } -} - -debug(); diff --git a/test-activate-methods.js b/test-activate-methods.js deleted file mode 100644 index 99b780a..0000000 --- a/test-activate-methods.js +++ /dev/null @@ -1,186 +0,0 @@ -#!/usr/bin/env node - -/** - * Test script to determine which activation method works with n8n API - * Tests both PATCH and PUT methods for workflow activation - */ - -const axios = require('axios'); -const fs = require('fs'); -const path = require('path'); - -// Load configuration -const configPath = path.join(__dirname, '.config.json'); -const config = JSON.parse(fs.readFileSync(configPath, 'utf8')); -const env = config.environments[config.defaultEnv]; - -const API_BASE = env.n8n_host; -const API_KEY = env.n8n_api_key; - -console.log('🧪 Testing n8n Workflow Activation Methods'); -console.log(`📡 Server: ${API_BASE}`); -console.log(''); - -// Create axios instance -const api = axios.create({ - baseURL: `${API_BASE}/api/v1`, - headers: { - 'X-N8N-API-KEY': API_KEY, - 'Content-Type': 'application/json', - 'Accept': 'application/json' - } -}); - -async function createTestWorkflow() { - console.log('1️⃣ Creating test workflow...'); - - const workflow = { - name: `Test Activation Methods ${Date.now()}`, - nodes: [ - { - name: 'Schedule Trigger', - type: 'n8n-nodes-base.scheduleTrigger', - position: [250, 300], - parameters: { - rule: { - interval: [{ field: 'hours', hoursInterval: 1 }] - } - }, - typeVersion: 1.1 - } - ], - connections: {}, - settings: { - executionOrder: 'v1' - } - }; - - try { - const response = await api.post('/workflows', workflow); - console.log(`✅ Workflow created: ID=${response.data.id}, Name="${response.data.name}"`); - console.log(` Active status: ${response.data.active}`); - console.log(''); - return response.data.id; - } catch (error) { - console.error('❌ Failed to create workflow:', error.response?.data || error.message); - process.exit(1); - } -} - -async function testMethodPUT(workflowId) { - console.log('2️⃣ Testing Method 1: PUT /workflows/{id}/activate'); - - try { - const response = await api.put(`/workflows/${workflowId}/activate`, {}); - console.log('✅ PUT /activate method WORKS!'); - console.log(` Response: active=${response.data.active}`); - console.log(''); - return true; - } catch (error) { - console.log('❌ PUT /activate method FAILED'); - console.log(` Status: ${error.response?.status || 'N/A'}`); - console.log(` Error: ${error.response?.data?.message || error.message}`); - console.log(''); - return false; - } -} - -async function testMethodPATCH(workflowId) { - console.log('3️⃣ Testing Method 2: PATCH /workflows/{id} with {active: true}'); - - try { - const response = await api.patch(`/workflows/${workflowId}`, { - active: true, - settings: {}, - staticData: null, - tags: [] - }); - console.log('✅ PATCH method WORKS!'); - console.log(` Response: active=${response.data.active}`); - console.log(''); - return true; - } catch (error) { - console.log('❌ PATCH method FAILED'); - console.log(` Status: ${error.response?.status || 'N/A'}`); - console.log(` Error: ${error.response?.data?.message || error.message}`); - console.log(''); - return false; - } -} - -async function deactivateWorkflow(workflowId) { - console.log('4️⃣ Deactivating workflow for next test...'); - - try { - // Try PUT method first - const response = await api.put(`/workflows/${workflowId}/deactivate`, {}); - console.log('✅ Deactivated using PUT /deactivate'); - console.log(''); - } catch (error) { - // Fallback to PATCH - try { - await api.patch(`/workflows/${workflowId}`, { active: false }); - console.log('✅ Deactivated using PATCH with {active: false}'); - console.log(''); - } catch (err) { - console.log('⚠️ Could not deactivate workflow'); - console.log(''); - } - } -} - -async function cleanupWorkflow(workflowId) { - console.log('🧹 Cleaning up test workflow...'); - - try { - await api.delete(`/workflows/${workflowId}`); - console.log('✅ Test workflow deleted'); - } catch (error) { - console.log('⚠️ Could not delete workflow:', error.message); - } -} - -async function main() { - let workflowId = null; - - try { - // Create test workflow - workflowId = await createTestWorkflow(); - - // Test PUT method - const putWorks = await testMethodPUT(workflowId); - - if (putWorks) { - // Deactivate before testing PATCH - await deactivateWorkflow(workflowId); - } - - // Test PATCH method - const patchWorks = await testMethodPATCH(workflowId); - - // Summary - console.log('📊 Test Results Summary:'); - console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); - console.log(`PUT /workflows/{id}/activate: ${putWorks ? '✅ WORKS' : '❌ FAILS'}`); - console.log(`PATCH /workflows/{id} {active}: ${patchWorks ? '✅ WORKS' : '❌ FAILS'}`); - console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); - console.log(''); - - if (putWorks) { - console.log('💡 Recommendation: Use PUT /workflows/{id}/activate (dedicated endpoint)'); - } else if (patchWorks) { - console.log('💡 Recommendation: Use PATCH /workflows/{id} with {active: true}'); - } else { - console.log('⚠️ Neither method works! API may not support activation.'); - } - - } catch (error) { - console.error('💥 Test failed:', error.message); - } finally { - if (workflowId) { - await cleanupWorkflow(workflowId); - } - } -} - -main(); diff --git a/test-check-tags.js b/test-check-tags.js deleted file mode 100644 index 4bee3fe..0000000 --- a/test-check-tags.js +++ /dev/null @@ -1,51 +0,0 @@ -const axios = require('axios'); - -async function mcpRequest(method, params = {}) { - const response = await axios.post('http://localhost:3456/mcp', { - jsonrpc: '2.0', - id: Date.now(), - method, - params - }); - return response.data.result; -} - -async function callTool(toolName, args) { - const result = await mcpRequest('tools/call', { - name: toolName, - arguments: args - }); - if (result.content && result.content[0]) { - try { - return JSON.parse(result.content[0].text); - } catch (e) { - return result.content[0].text; - } - } - return result; -} - -async function test() { - const tagsResponse = await callTool('get_tags', {}); - const tags = tagsResponse.data || tagsResponse; - - console.log('\nExisting tags:'); - tags.forEach(tag => { - console.log(` - ${tag.name} (${tag.id})`); - }); - - // Delete E2E test tags - console.log('\nDeleting E2E test tags...'); - for (const tag of tags) { - if (tag.name.includes('E2E Test')) { - try { - await callTool('delete_tag', { id: tag.id }); - console.log(` ✅ Deleted: ${tag.name}`); - } catch (error) { - console.log(` ❌ Failed to delete: ${tag.name}`); - } - } - } -} - -test().catch(console.error); diff --git a/test-comprehensive.js b/test-comprehensive.js deleted file mode 100644 index 7ac3ca7..0000000 --- a/test-comprehensive.js +++ /dev/null @@ -1,468 +0,0 @@ -#!/usr/bin/env node - -/** - * Comprehensive Integration Test for MCP n8n Workflow Builder - * Tests all MCP functionality to ensure nothing broke with notification handler changes - */ - -const http = require('http'); - -const PORT = process.env.MCP_PORT || 3456; -const HOST = 'localhost'; - -let testsPassed = 0; -let testsFailed = 0; - -// Helper function to send JSON-RPC request -function sendRequest(data) { - return new Promise((resolve, reject) => { - const postData = JSON.stringify(data); - - const options = { - hostname: HOST, - port: PORT, - path: '/mcp', - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Content-Length': Buffer.byteLength(postData) - } - }; - - const req = http.request(options, (res) => { - let body = ''; - - res.on('data', (chunk) => { - body += chunk; - }); - - res.on('end', () => { - resolve({ - statusCode: res.statusCode, - statusMessage: res.statusMessage, - headers: res.headers, - body: body ? (body.length > 0 ? JSON.parse(body) : null) : null - }); - }); - }); - - req.on('error', reject); - req.write(postData); - req.end(); - }); -} - -function logTest(name, passed, details = '') { - if (passed) { - console.log(` ✅ PASS: ${name}`); - testsPassed++; - } else { - console.log(` ❌ FAIL: ${name}`); - if (details) console.log(` ${details}`); - testsFailed++; - } -} - -// Test cases -async function runTests() { - console.log('\n╔══════════════════════════════════════════════════════════════╗'); - console.log('║ MCP n8n Workflow Builder - Comprehensive Integration Test ║'); - console.log('╚══════════════════════════════════════════════════════════════╝\n'); - - try { - // ========================================== - // 1. BASIC CONNECTIVITY TESTS - // ========================================== - console.log('📡 1. Basic Connectivity Tests'); - console.log('─────────────────────────────────────────────────────────────'); - - // Test 1.1: Health Check - const healthResult = await new Promise((resolve, reject) => { - http.get(`http://${HOST}:${PORT}/health`, (res) => { - let body = ''; - res.on('data', chunk => body += chunk); - res.on('end', () => resolve({ - statusCode: res.statusCode, - body: JSON.parse(body) - })); - }).on('error', reject); - }); - - logTest( - 'Health check endpoint', - healthResult.statusCode === 200 && healthResult.body.status === 'ok', - JSON.stringify(healthResult.body) - ); - - // ========================================== - // 2. NOTIFICATION HANDLING TESTS (NEW) - // ========================================== - console.log('\n🔔 2. Notification Handling Tests (New Functionality)'); - console.log('─────────────────────────────────────────────────────────────'); - - // Test 2.1: notifications/initialized - const initNotification = await sendRequest({ - jsonrpc: '2.0', - method: 'notifications/initialized', - params: {} - }); - logTest( - 'notifications/initialized → 204 No Content', - initNotification.statusCode === 204 && initNotification.body === null - ); - - // Test 2.2: notifications/cancelled - const cancelNotification = await sendRequest({ - jsonrpc: '2.0', - method: 'notifications/cancelled', - params: { requestId: 123 } - }); - logTest( - 'notifications/cancelled → 204 No Content', - cancelNotification.statusCode === 204 && cancelNotification.body === null - ); - - // Test 2.3: notifications/progress - const progressNotification = await sendRequest({ - jsonrpc: '2.0', - method: 'notifications/progress', - params: { progress: 50, total: 100 } - }); - logTest( - 'notifications/progress → 204 No Content', - progressNotification.statusCode === 204 && progressNotification.body === null - ); - - // ========================================== - // 3. MCP TOOLS TESTS - // ========================================== - console.log('\n🛠️ 3. MCP Tools Tests (Core Functionality)'); - console.log('─────────────────────────────────────────────────────────────'); - - // Test 3.1: List Tools - const listToolsResult = await sendRequest({ - jsonrpc: '2.0', - method: 'tools/list', - params: {}, - id: 1 - }); - logTest( - 'tools/list - Returns list of available tools', - listToolsResult.statusCode === 200 && - listToolsResult.body.result && - Array.isArray(listToolsResult.body.result.tools) && - listToolsResult.body.result.tools.length > 0, - `Found ${listToolsResult.body.result?.tools?.length || 0} tools` - ); - - // Test 3.2: Call Tool - list_workflows (may fail without n8n connection) - const listWorkflowsResult = await sendRequest({ - jsonrpc: '2.0', - method: 'tools/call', - params: { - name: 'list_workflows', - arguments: {} - }, - id: 2 - }); - - // This test is expected to fail if n8n is not configured, but should return proper error structure - const hasProperStructure = listWorkflowsResult.statusCode === 200 && - listWorkflowsResult.body.jsonrpc === '2.0' && - listWorkflowsResult.body.id === 2; - - logTest( - 'tools/call - list_workflows (structure check)', - hasProperStructure, - listWorkflowsResult.body.error ? - `Expected error (no n8n): ${listWorkflowsResult.body.error.message}` : - 'Success' - ); - - // Test 3.3: Call Tool - list_executions - const listExecutionsResult = await sendRequest({ - jsonrpc: '2.0', - method: 'tools/call', - params: { - name: 'list_executions', - arguments: {} - }, - id: 3 - }); - - const hasProperExecStructure = listExecutionsResult.statusCode === 200 && - listExecutionsResult.body.jsonrpc === '2.0' && - listExecutionsResult.body.id === 3; - - logTest( - 'tools/call - list_executions (structure check)', - hasProperExecStructure, - listExecutionsResult.body.error ? - `Expected error (no n8n): ${listExecutionsResult.body.error.message}` : - 'Success' - ); - - // ========================================== - // 4. MCP RESOURCES TESTS - // ========================================== - console.log('\n📦 4. MCP Resources Tests'); - console.log('─────────────────────────────────────────────────────────────'); - - // Test 4.1: List Resources - const listResourcesResult = await sendRequest({ - jsonrpc: '2.0', - method: 'resources/list', - params: {}, - id: 4 - }); - - logTest( - 'resources/list - Returns available resources', - listResourcesResult.statusCode === 200 && - listResourcesResult.body.result && - Array.isArray(listResourcesResult.body.result.resources), - `Found ${listResourcesResult.body.result?.resources?.length || 0} resources` - ); - - // Test 4.2: List Resource Templates - const listTemplatesResult = await sendRequest({ - jsonrpc: '2.0', - method: 'resources/templates/list', - params: {}, - id: 5 - }); - - logTest( - 'resources/templates/list - Returns resource templates', - listTemplatesResult.statusCode === 200 && - listTemplatesResult.body.result && - Array.isArray(listTemplatesResult.body.result.resourceTemplates), - `Found ${listTemplatesResult.body.result?.resourceTemplates?.length || 0} templates` - ); - - // Test 4.3: Read Resource - workflows - const readResourceResult = await sendRequest({ - jsonrpc: '2.0', - method: 'resources/read', - params: { - uri: 'n8n://workflows' - }, - id: 6 - }); - - const hasResourceStructure = readResourceResult.statusCode === 200 && - readResourceResult.body.jsonrpc === '2.0' && - readResourceResult.body.id === 6; - - logTest( - 'resources/read - Read workflows resource', - hasResourceStructure, - readResourceResult.body.error ? - `Expected error (no n8n): ${readResourceResult.body.error.message}` : - 'Success' - ); - - // ========================================== - // 5. MCP PROMPTS TESTS - // ========================================== - console.log('\n📝 5. MCP Prompts Tests'); - console.log('─────────────────────────────────────────────────────────────'); - - // Test 5.1: List Prompts - const listPromptsResult = await sendRequest({ - jsonrpc: '2.0', - method: 'prompts/list', - params: {}, - id: 7 - }); - - logTest( - 'prompts/list - Returns available prompts', - listPromptsResult.statusCode === 200 && - listPromptsResult.body.result && - Array.isArray(listPromptsResult.body.result.prompts), - `Found ${listPromptsResult.body.result?.prompts?.length || 0} prompts` - ); - - // ========================================== - // 6. JSON-RPC 2.0 COMPLIANCE TESTS - // ========================================== - console.log('\n⚙️ 6. JSON-RPC 2.0 Compliance Tests'); - console.log('─────────────────────────────────────────────────────────────'); - - // Test 6.1: Request with ID returns proper response structure - const validRequest = await sendRequest({ - jsonrpc: '2.0', - method: 'tools/list', - params: {}, - id: 100 - }); - - logTest( - 'Request with ID returns response with same ID', - validRequest.body.id === 100 && - validRequest.body.jsonrpc === '2.0' && - validRequest.body.result !== undefined - ); - - // Test 6.2: Invalid method returns proper error - const invalidMethod = await sendRequest({ - jsonrpc: '2.0', - method: 'invalid/method', - params: {}, - id: 101 - }); - - logTest( - 'Invalid method returns JSON-RPC error', - invalidMethod.body.error && - invalidMethod.body.error.code === -32601 && - invalidMethod.body.id === 101 - ); - - // Test 6.3: Notification with unknown method is ignored gracefully - const unknownNotification = await sendRequest({ - jsonrpc: '2.0', - method: 'notifications/unknown', - params: {} - }); - - logTest( - 'Unknown notification returns 204 (ignored gracefully)', - unknownNotification.statusCode === 204 - ); - - // ========================================== - // 7. BACKWARD COMPATIBILITY TESTS - // ========================================== - console.log('\n🔄 7. Backward Compatibility Tests'); - console.log('─────────────────────────────────────────────────────────────'); - - // Test 7.1: Multiple sequential requests work correctly - const seq1 = await sendRequest({ - jsonrpc: '2.0', - method: 'tools/list', - params: {}, - id: 201 - }); - - const seq2 = await sendRequest({ - jsonrpc: '2.0', - method: 'resources/list', - params: {}, - id: 202 - }); - - const seq3 = await sendRequest({ - jsonrpc: '2.0', - method: 'prompts/list', - params: {}, - id: 203 - }); - - logTest( - 'Sequential requests maintain proper ID mapping', - seq1.body.id === 201 && - seq2.body.id === 202 && - seq3.body.id === 203 - ); - - // Test 7.2: Mixed notifications and requests - const mixedSeq1 = await sendRequest({ - jsonrpc: '2.0', - method: 'notifications/initialized', - params: {} - }); - - const mixedSeq2 = await sendRequest({ - jsonrpc: '2.0', - method: 'tools/list', - params: {}, - id: 301 - }); - - const mixedSeq3 = await sendRequest({ - jsonrpc: '2.0', - method: 'notifications/progress', - params: { progress: 75 } - }); - - logTest( - 'Mixed notifications and requests work correctly', - mixedSeq1.statusCode === 204 && - mixedSeq2.body.id === 301 && - mixedSeq3.statusCode === 204 - ); - - // ========================================== - // 8. ERROR HANDLING TESTS - // ========================================== - console.log('\n🚨 8. Error Handling Tests'); - console.log('─────────────────────────────────────────────────────────────'); - - // Test 8.1: Malformed JSON-RPC request - const malformedRequest = await sendRequest({ - method: 'tools/list', - // Missing jsonrpc field - id: 401 - }); - - logTest( - 'Malformed request handled gracefully', - malformedRequest.statusCode === 200 || malformedRequest.statusCode === 500, - 'Server responds without crashing' - ); - - // Test 8.2: Tool call with missing arguments - const missingArgs = await sendRequest({ - jsonrpc: '2.0', - method: 'tools/call', - params: { - name: 'create_workflow' - // Missing required arguments - }, - id: 402 - }); - - logTest( - 'Tool call with missing arguments returns error', - missingArgs.body.error !== undefined, - `Error: ${missingArgs.body.error?.message || 'Unknown'}` - ); - - // ========================================== - // FINAL SUMMARY - // ========================================== - console.log('\n╔══════════════════════════════════════════════════════════════╗'); - console.log('║ TEST SUMMARY ║'); - console.log('╚══════════════════════════════════════════════════════════════╝\n'); - - const totalTests = testsPassed + testsFailed; - const successRate = ((testsPassed / totalTests) * 100).toFixed(1); - - console.log(` Total Tests: ${totalTests}`); - console.log(` ✅ Passed: ${testsPassed}`); - console.log(` ❌ Failed: ${testsFailed}`); - console.log(` Success Rate: ${successRate}%`); - - console.log('\n─────────────────────────────────────────────────────────────'); - - if (testsFailed === 0) { - console.log('\n 🎉 All tests passed! Server functionality is intact.\n'); - process.exit(0); - } else { - console.log('\n ⚠️ Some tests failed. Review the output above.\n'); - process.exit(1); - } - - } catch (error) { - console.error('\n❌ Test suite failed with error:', error.message); - console.error('\nMake sure the MCP server is running with:'); - console.error(' MCP_STANDALONE=true npm start\n'); - process.exit(1); - } -} - -// Run tests -console.log('\nStarting comprehensive integration tests...'); -setTimeout(runTests, 1000); // Wait 1 second for any startup delays diff --git a/test-create-tag-simple.js b/test-create-tag-simple.js deleted file mode 100644 index 6c1e94f..0000000 --- a/test-create-tag-simple.js +++ /dev/null @@ -1,45 +0,0 @@ -const axios = require('axios'); - -async function mcpRequest(method, params = {}) { - const response = await axios.post('http://localhost:3456/mcp', { - jsonrpc: '2.0', - id: Date.now(), - method, - params - }); - return response.data.result; -} - -async function callTool(toolName, args) { - const result = await mcpRequest('tools/call', { - name: toolName, - arguments: args - }); - if (result.content && result.content[0]) { - try { - return JSON.parse(result.content[0].text); - } catch (e) { - return result.content[0].text; - } - } - return result; -} - -async function test() { - const uniqueName = 'E2E-Test-' + Math.random().toString(36).substring(7); - - console.log('Creating tag:', uniqueName); - try { - const tag = await callTool('create_tag', { name: uniqueName }); - console.log('Success:', JSON.stringify(tag, null, 2)); - - if (tag && tag.id) { - await callTool('delete_tag', { id: tag.id }); - console.log('Deleted:', tag.id); - } - } catch (error) { - console.log('Error:', error.message); - } -} - -test().catch(console.error); diff --git a/test-credentials-all-methods-direct.js b/test-credentials-all-methods-direct.js deleted file mode 100644 index 5ea8a5a..0000000 --- a/test-credentials-all-methods-direct.js +++ /dev/null @@ -1,174 +0,0 @@ -#!/usr/bin/env node - -/** - * Direct API test for ALL Credentials endpoints - * Tests POST, PUT, DELETE methods to confirm entire API is restricted - */ - -const axios = require('axios'); -const fs = require('fs'); - -// Load configuration -let config; -if (fs.existsSync('.config.json')) { - const rawConfig = JSON.parse(fs.readFileSync('.config.json', 'utf8')); - const defaultEnv = rawConfig.defaultEnv || Object.keys(rawConfig.environments)[0]; - const envConfig = rawConfig.environments[defaultEnv]; - config = { - n8nHost: envConfig.n8n_host, - n8nApiKey: envConfig.n8n_api_key - }; -} else { - require('dotenv').config(); - config = { - n8nHost: process.env.N8N_HOST, - n8nApiKey: process.env.N8N_API_KEY - }; -} - -const testId = '1'; - -async function testEndpoint(method, endpoint, data = null) { - try { - const options = { - method: method, - url: `${config.n8nHost}/api/v1${endpoint}`, - headers: { - 'X-N8N-API-KEY': config.n8nApiKey, - 'Accept': 'application/json', - 'Content-Type': 'application/json' - } - }; - - if (data) { - options.data = data; - } - - const response = await axios(options); - return { success: true, status: response.status, data: response.data }; - } catch (error) { - if (error.response) { - return { - success: false, - status: error.response.status, - message: error.response.data - }; - } - return { success: false, error: error.message }; - } -} - -async function main() { - console.log('\n=== Comprehensive Credentials API Test ===\n'); - console.log(`Testing against: ${config.n8nHost}\n`); - - const tests = [ - { - name: 'GET /credentials', - method: 'GET', - endpoint: '/credentials', - story: '2.6.1' - }, - { - name: 'GET /credentials/{id}', - method: 'GET', - endpoint: `/credentials/${testId}`, - story: '2.6.2' - }, - { - name: 'POST /credentials', - method: 'POST', - endpoint: '/credentials', - data: { - name: 'Test Credential', - type: 'httpBasicAuth', - data: { - user: 'test', - password: 'test' - } - }, - story: '2.6.3' - }, - { - name: 'PUT /credentials/{id}', - method: 'PUT', - endpoint: `/credentials/${testId}`, - data: { - name: 'Updated Credential', - type: 'httpBasicAuth', - data: { - user: 'test', - password: 'test' - } - }, - story: '2.6.4' - }, - { - name: 'DELETE /credentials/{id}', - method: 'DELETE', - endpoint: `/credentials/${testId}`, - story: '2.6.5' - }, - { - name: 'GET /credentials/schema/{typeName}', - method: 'GET', - endpoint: '/credentials/schema/httpBasicAuth', - story: '2.6.6' - } - ]; - - const results = []; - - for (const test of tests) { - console.log(`Testing: ${test.name} (Story ${test.story})`); - const result = await testEndpoint(test.method, test.endpoint, test.data); - - results.push({ - ...test, - result - }); - - if (result.success) { - console.log(` ✅ Status ${result.status} - SUPPORTED`); - } else if (result.status === 405) { - console.log(` ❌ Status 405 - NOT SUPPORTED (Method Not Allowed)`); - } else if (result.status === 404) { - console.log(` ⚠️ Status 404 - Might be supported but resource not found`); - } else if (result.status === 401) { - console.log(` ❌ Status 401 - Authentication error`); - } else if (result.status === 403) { - console.log(` ❌ Status 403 - Permission denied`); - } else if (result.status) { - console.log(` ⚠️ Status ${result.status} - ${JSON.stringify(result.message)}`); - } else { - console.log(` ❌ Error: ${result.error}`); - } - } - - // Summary - console.log('\n=== Summary ===\n'); - - const supported = results.filter(r => r.result.success); - const notAllowed = results.filter(r => r.result.status === 405); - const other = results.filter(r => !r.result.success && r.result.status !== 405); - - console.log(`Total endpoints tested: ${results.length}`); - console.log(`✅ Supported: ${supported.length}`); - console.log(`❌ Not Allowed (405): ${notAllowed.length}`); - console.log(`⚠️ Other errors: ${other.length}`); - - if (notAllowed.length === results.length) { - console.log('\n🔒 CONCLUSION: Entire Credentials API is RESTRICTED (all 405)'); - console.log(' This is a security-based design decision by n8n'); - console.log(' Stories 2.6.1 - 2.6.6 should return informative messages'); - } else if (notAllowed.length > 0) { - console.log(`\n⚠️ CONCLUSION: ${notAllowed.length}/${results.length} endpoints restricted`); - console.log(' Partial API availability - investigate further'); - } else { - console.log('\n✅ CONCLUSION: Credentials API appears to be available'); - } - - console.log('\n'); -} - -main(); diff --git a/test-credentials-api-direct.js b/test-credentials-api-direct.js deleted file mode 100644 index 20883d6..0000000 --- a/test-credentials-api-direct.js +++ /dev/null @@ -1,84 +0,0 @@ -#!/usr/bin/env node - -/** - * Direct API test for GET /credentials endpoint - * Tests if n8n API supports credentials listing - */ - -const axios = require('axios'); -const fs = require('fs'); - -// Load configuration -let config; -if (fs.existsSync('.config.json')) { - const rawConfig = JSON.parse(fs.readFileSync('.config.json', 'utf8')); - const defaultEnv = rawConfig.defaultEnv || Object.keys(rawConfig.environments)[0]; - const envConfig = rawConfig.environments[defaultEnv]; - config = { - n8nHost: envConfig.n8n_host, - n8nApiKey: envConfig.n8n_api_key - }; -} else { - require('dotenv').config(); - config = { - n8nHost: process.env.N8N_HOST, - n8nApiKey: process.env.N8N_API_KEY - }; -} - -async function testCredentialsEndpoint() { - console.log('\n=== Direct n8n API Test: GET /credentials ===\n'); - console.log(`Testing against: ${config.n8nHost}`); - - try { - const response = await axios.get(`${config.n8nHost}/api/v1/credentials`, { - headers: { - 'X-N8N-API-KEY': config.n8nApiKey, - 'Accept': 'application/json' - } - }); - - console.log(`\n✅ SUCCESS: Status ${response.status}`); - console.log(`\nResponse structure:`); - console.log(`- Data array: ${response.data.data ? response.data.data.length : 0} credentials`); - console.log(`- Has nextCursor: ${!!response.data.nextCursor}`); - - if (response.data.data && response.data.data.length > 0) { - console.log(`\nFirst credential structure:`); - const first = response.data.data[0]; - console.log(`- ID: ${first.id}`); - console.log(`- Name: ${first.name}`); - console.log(`- Type: ${first.type}`); - console.log(`- Has 'data' field (sensitive): ${!!first.data}`); - console.log(`- Has nodesAccess: ${!!first.nodesAccess}`); - console.log(`- Created: ${first.createdAt}`); - console.log(`- Updated: ${first.updatedAt}`); - } - - console.log('\n✅ Credentials API is SUPPORTED\n'); - - } catch (error) { - if (error.response) { - console.log(`\n❌ ERROR: Status ${error.response.status}`); - console.log(`Message: ${JSON.stringify(error.response.data)}`); - - if (error.response.status === 405) { - console.log('\n❌ GET /credentials endpoint is NOT SUPPORTED by this n8n version'); - console.log(' This is similar to the PATCH /workflows limitation'); - } else if (error.response.status === 401) { - console.log('\n❌ Authentication error - check API key'); - } else if (error.response.status === 403) { - console.log('\n❌ Permission denied - API key lacks credentials access'); - } - } else { - console.log(`\n❌ Request failed: ${error.message}`); - } - - console.log('\n'); - return false; - } - - return true; -} - -testCredentialsEndpoint(); diff --git a/test-credentials-create.js b/test-credentials-create.js deleted file mode 100644 index 5a60d65..0000000 --- a/test-credentials-create.js +++ /dev/null @@ -1,320 +0,0 @@ -#!/usr/bin/env node - -/** - * Story 2.6.3: POST /credentials - Create Credential Test - * - * Tests the create_credential MCP tool with schema-driven workflow: - * 1. Get credential schema for type - * 2. Create credential with validated data - * 3. Verify creation success - * 4. Clean up test credentials - * - * Tests multiple credential types: - * - httpBasicAuth (user/password) - * - httpHeaderAuth (name/value) - * - oAuth2Api (complex OAuth2 setup) - */ - -const axios = require('axios'); - -// Configuration -const config = { - mcpServerUrl: 'http://localhost:3456/mcp', - healthCheckUrl: 'http://localhost:3456/health', - testFlags: { - runHttpBasicAuth: true, - runHttpHeaderAuth: true, - runOAuth2: false, // OAuth2 is complex - skip for basic validation - runCleanup: true - } -}; - -// Storage for created credential IDs -const createdCredentials = []; - -// Utility: MCP JSON-RPC request -async function mcpRequest(method, params = {}) { - try { - const response = await axios.post(config.mcpServerUrl, { - jsonrpc: '2.0', - id: Date.now(), - method, - params - }); - - if (response.data.error) { - throw new Error(`MCP Error: ${response.data.error.message}`); - } - - return response.data.result; - } catch (error) { - if (error.response) { - throw new Error(`HTTP ${error.response.status}: ${JSON.stringify(error.response.data)}`); - } - throw error; - } -} - -// Utility: Call MCP tool -async function callTool(toolName, args) { - const result = await mcpRequest('tools/call', { - name: toolName, - arguments: args - }); - - if (result.content && result.content[0]) { - return JSON.parse(result.content[0].text); - } - - return result; -} - -// Health check -async function healthCheck() { - try { - const response = await axios.get(config.healthCheckUrl); - console.log('✅ Server health check passed'); - console.log(` Server version: ${response.data.version}`); - return true; - } catch (error) { - console.error('❌ Health check failed:', error.message); - return false; - } -} - -// Test 1: httpBasicAuth credential -async function testHttpBasicAuth() { - console.log('\n=== Test 1: httpBasicAuth Credential ===\n'); - - try { - // Step 1: Get schema for httpBasicAuth - console.log('Step 1: Getting schema for httpBasicAuth...'); - const schema = await callTool('get_credential_schema', { - typeName: 'httpBasicAuth' - }); - - console.log(` ✅ Schema retrieved successfully`); - console.log(` ✓ Schema type: ${schema.type}`); - console.log(` ✓ Properties: ${Object.keys(schema.properties || {}).join(', ')}`); - - // Step 2: Create credential using schema structure - console.log('\nStep 2: Creating httpBasicAuth credential...'); - const timestamp = Date.now(); - const credential = await callTool('create_credential', { - name: `Test HTTP Basic Auth ${timestamp}`, - type: 'httpBasicAuth', - data: { - user: 'testuser', - password: 'testpassword123' - } - }); - - console.log(` ✅ Credential created successfully`); - console.log(` ✓ ID: ${credential.id}`); - console.log(` ✓ Name: ${credential.name}`); - console.log(` ✓ Type: ${credential.type}`); - console.log(` ✓ Created: ${credential.createdAt}`); - - // Store for cleanup - createdCredentials.push({ id: credential.id, name: credential.name, type: credential.type }); - - console.log('\n✅ Test 1 PASSED'); - return true; - } catch (error) { - console.error('\n❌ Test 1 FAILED:', error.message); - return false; - } -} - -// Test 2: httpHeaderAuth credential -async function testHttpHeaderAuth() { - console.log('\n=== Test 2: httpHeaderAuth Credential ===\n'); - - try { - // Step 1: Get schema for httpHeaderAuth - console.log('Step 1: Getting schema for httpHeaderAuth...'); - const schema = await callTool('get_credential_schema', { - typeName: 'httpHeaderAuth' - }); - - console.log(` ✅ Schema retrieved successfully`); - console.log(` ✓ Schema type: ${schema.type}`); - console.log(` ✓ Properties: ${Object.keys(schema.properties || {}).join(', ')}`); - - // Step 2: Create credential using schema structure - console.log('\nStep 2: Creating httpHeaderAuth credential...'); - const timestamp = Date.now(); - const credential = await callTool('create_credential', { - name: `Test HTTP Header Auth ${timestamp}`, - type: 'httpHeaderAuth', - data: { - name: 'Authorization', - value: 'Bearer test-token-12345' - } - }); - - console.log(` ✅ Credential created successfully`); - console.log(` ✓ ID: ${credential.id}`); - console.log(` ✓ Name: ${credential.name}`); - console.log(` ✓ Type: ${credential.type}`); - console.log(` ✓ Created: ${credential.createdAt}`); - - // Store for cleanup - createdCredentials.push({ id: credential.id, name: credential.name, type: credential.type }); - - console.log('\n✅ Test 2 PASSED'); - return true; - } catch (error) { - console.error('\n❌ Test 2 FAILED:', error.message); - return false; - } -} - -// Test 3: oAuth2Api credential (complex structure) -async function testOAuth2() { - console.log('\n=== Test 3: oAuth2Api Credential (Complex) ===\n'); - - try { - // Step 1: Get schema for oAuth2Api - console.log('Step 1: Getting schema for oAuth2Api...'); - const schema = await callTool('get_credential_schema', { - typeName: 'oAuth2Api' - }); - - console.log(` ✅ Schema retrieved successfully`); - console.log(` ✓ Schema type: ${schema.type}`); - console.log(` ✓ Properties count: ${Object.keys(schema.properties || {}).length}`); - console.log(` ✓ Sample properties: ${Object.keys(schema.properties || {}).slice(0, 5).join(', ')}`); - - // Step 2: Create credential with minimal OAuth2 configuration - console.log('\nStep 2: Creating oAuth2Api credential...'); - const timestamp = Date.now(); - const credential = await callTool('create_credential', { - name: `Test OAuth2 ${timestamp}`, - type: 'oAuth2Api', - data: { - grantType: 'authorizationCode', - authUrl: 'https://example.com/oauth/authorize', - accessTokenUrl: 'https://example.com/oauth/token', - clientId: 'test-client-id', - clientSecret: 'test-client-secret', - scope: 'read write', - authQueryParameters: '', - authentication: 'body' - } - }); - - console.log(` ✅ Credential created successfully`); - console.log(` ✓ ID: ${credential.id}`); - console.log(` ✓ Name: ${credential.name}`); - console.log(` ✓ Type: ${credential.type}`); - console.log(` ✓ Created: ${credential.createdAt}`); - - // Store for cleanup - createdCredentials.push({ id: credential.id, name: credential.name, type: credential.type }); - - console.log('\n✅ Test 3 PASSED'); - return true; - } catch (error) { - console.error('\n❌ Test 3 FAILED:', error.message); - return false; - } -} - -// Cleanup: Delete all test credentials -async function cleanup() { - console.log('\n=== Cleanup: Deleting Test Credentials ===\n'); - - let successCount = 0; - let failCount = 0; - - for (const cred of createdCredentials) { - try { - console.log(`Deleting credential: ${cred.name} (${cred.id})...`); - await callTool('delete_credential', { id: cred.id }); - console.log(` ✅ Deleted successfully`); - successCount++; - } catch (error) { - console.error(` ❌ Failed to delete: ${error.message}`); - failCount++; - } - } - - console.log(`\n📊 Cleanup Summary:`); - console.log(` ✅ Deleted: ${successCount}`); - if (failCount > 0) { - console.log(` ❌ Failed: ${failCount}`); - } - - return failCount === 0; -} - -// Main test runner -async function runTests() { - console.log('╔════════════════════════════════════════════════════════════╗'); - console.log('║ Story 2.6.3: POST /credentials - Create Credential Test ║'); - console.log('╚════════════════════════════════════════════════════════════╝\n'); - - // Health check - const healthy = await healthCheck(); - if (!healthy) { - console.error('\n❌ Server not healthy, aborting tests'); - process.exit(1); - } - - const results = { - total: 0, - passed: 0, - failed: 0 - }; - - // Run tests - if (config.testFlags.runHttpBasicAuth) { - results.total++; - if (await testHttpBasicAuth()) results.passed++; - else results.failed++; - } - - if (config.testFlags.runHttpHeaderAuth) { - results.total++; - if (await testHttpHeaderAuth()) results.passed++; - else results.failed++; - } - - if (config.testFlags.runOAuth2) { - results.total++; - if (await testOAuth2()) results.passed++; - else results.failed++; - } - - // Cleanup - if (config.testFlags.runCleanup && createdCredentials.length > 0) { - await cleanup(); - } else if (createdCredentials.length > 0) { - console.log(`\n⚠️ Cleanup disabled. Created ${createdCredentials.length} credentials that need manual deletion:`); - createdCredentials.forEach(cred => { - console.log(` - ${cred.name} (${cred.id})`); - }); - } - - // Final summary - console.log('\n╔════════════════════════════════════════════════════════════╗'); - console.log('║ TEST SUMMARY ║'); - console.log('╚════════════════════════════════════════════════════════════╝\n'); - console.log(`Total tests: ${results.total}`); - console.log(`✅ Passed: ${results.passed}`); - if (results.failed > 0) { - console.log(`❌ Failed: ${results.failed}`); - } - - console.log(`\n📊 Success Rate: ${((results.passed / results.total) * 100).toFixed(1)}%`); - - // Exit with appropriate code - process.exit(results.failed > 0 ? 1 : 0); -} - -// Run tests -runTests().catch(error => { - console.error('Unhandled error:', error); - process.exit(1); -}); diff --git a/test-credentials-delete-and-schema.js b/test-credentials-delete-and-schema.js deleted file mode 100644 index 94dae04..0000000 --- a/test-credentials-delete-and-schema.js +++ /dev/null @@ -1,215 +0,0 @@ -#!/usr/bin/env node - -/** - * Test: delete_credential and get_credential_schema - * Stories 2.6.5 and 2.6.6 - Full implementation tests - */ - -const axios = require('axios'); - -const config = { - mcpServerUrl: 'http://localhost:3456/mcp' -}; - -let requestId = 1; - -async function sendMcpRequest(method, params = {}) { - const response = await axios.post(config.mcpServerUrl, { - jsonrpc: '2.0', - id: requestId++, - method, - params - }); - return response.data.result; -} - -async function callTool(name, args = {}) { - const result = await sendMcpRequest('tools/call', { name, arguments: args }); - if (result.isError) { - const errorMessage = result.content && result.content[0] && result.content[0].text - ? result.content[0].text - : 'Unknown error'; - throw new Error(errorMessage); - } - return result; -} - -async function testGetCredentialSchema() { - console.log('\n=== Test 1: get_credential_schema (Story 2.6.6) ===\n'); - - const testTypes = [ - { name: 'httpBasicAuth', desc: 'HTTP Basic Authentication' }, - { name: 'httpHeaderAuth', desc: 'HTTP Header Authentication' }, - { name: 'oAuth2Api', desc: 'OAuth2 API' } - ]; - - let passedCount = 0; - - for (const testType of testTypes) { - try { - console.log(`\n📋 Testing schema for: ${testType.name} (${testType.desc})`); - - const result = await callTool('get_credential_schema', { - typeName: testType.name - }); - - const schema = JSON.parse(result.content[0].text); - - console.log(` ✅ Schema retrieved successfully`); - - // Validate schema structure - if (schema.properties || schema.type) { - console.log(` ✓ Has schema structure`); - } - - if (schema.properties) { - const fieldCount = Object.keys(schema.properties).length; - console.log(` ✓ Fields defined: ${fieldCount}`); - - // Show first few fields - const fields = Object.keys(schema.properties).slice(0, 3); - console.log(` ✓ Sample fields: ${fields.join(', ')}`); - } - - if (schema.required) { - console.log(` ✓ Required fields: ${schema.required.length}`); - } - - passedCount++; - - } catch (error) { - console.error(` ❌ Failed: ${error.message}`); - } - } - - console.log(`\n📊 Schema tests: ${passedCount}/${testTypes.length} passed`); - return passedCount === testTypes.length; -} - -async function testDeleteCredential() { - console.log('\n\n=== Test 2: delete_credential (Story 2.6.5) ===\n'); - - let createdId = null; - - try { - // First, we need to create a credential to delete - // Since create_credential is not implemented yet in Story 2.6.3, - // we'll use the direct API approach - - console.log('Step 1: Creating test credential via direct API...'); - - // Load config for direct API call - const fs = require('fs'); - let apiConfig; - if (fs.existsSync('.config.json')) { - const rawConfig = JSON.parse(fs.readFileSync('.config.json', 'utf8')); - const defaultEnv = rawConfig.defaultEnv || Object.keys(rawConfig.environments)[0]; - const envConfig = rawConfig.environments[defaultEnv]; - apiConfig = { - n8nHost: envConfig.n8n_host, - n8nApiKey: envConfig.n8n_api_key - }; - } else { - require('dotenv').config(); - apiConfig = { - n8nHost: process.env.N8N_HOST, - n8nApiKey: process.env.N8N_API_KEY - }; - } - - const createResponse = await axios.post( - `${apiConfig.n8nHost}/api/v1/credentials`, - { - name: `Test Credential for Delete ${Date.now()}`, - type: 'httpBasicAuth', - data: { - user: 'testuser', - password: 'testpass' - } - }, - { - headers: { - 'X-N8N-API-KEY': apiConfig.n8nApiKey, - 'Accept': 'application/json', - 'Content-Type': 'application/json' - } - } - ); - - createdId = createResponse.data.id; - console.log(` ✅ Created test credential: ${createdId}`); - - // Now test delete via MCP tool - console.log(`\nStep 2: Deleting credential via delete_credential tool...`); - - const result = await callTool('delete_credential', { id: createdId }); - const deletedCred = JSON.parse(result.content[0].text); - - console.log(` ✅ Credential deleted successfully`); - console.log(` ✓ Deleted credential name: ${deletedCred.name}`); - console.log(` ✓ Deleted credential type: ${deletedCred.type}`); - - // Verify deletion by trying to delete again (should fail with 404) - console.log(`\nStep 3: Verifying deletion (should fail with 404)...`); - - try { - await callTool('delete_credential', { id: createdId }); - console.log(` ❌ ERROR: Should have failed but didn't`); - return false; - } catch (error) { - if (error.message.includes('404') || error.message.includes('not found')) { - console.log(` ✅ Correctly returns 404 for already deleted credential`); - } else { - console.log(` ✓ Delete failed as expected: ${error.message}`); - } - } - - console.log('\n✅ Test 2 PASSED'); - return true; - - } catch (error) { - console.error(`\n❌ Test 2 FAILED: ${error.message}`); - - // Cleanup if credential was created - if (createdId) { - console.log(`\nAttempting cleanup of credential ${createdId}...`); - try { - await callTool('delete_credential', { id: createdId }); - console.log(' ✅ Cleanup successful'); - } catch (cleanupError) { - console.log(` ⚠️ Cleanup failed (may already be deleted): ${cleanupError.message}`); - } - } - - return false; - } -} - -async function main() { - console.log('\n=== Credentials API Full Implementation Test Suite ==='); - console.log('Testing Stories 2.6.5 (delete_credential) and 2.6.6 (get_credential_schema)\n'); - - const results = []; - - // Test Story 2.6.6 first (doesn't require setup) - results.push(await testGetCredentialSchema()); - - // Test Story 2.6.5 (requires credential creation) - results.push(await testDeleteCredential()); - - const passed = results.filter(r => r).length; - const total = results.length; - - console.log('\n\n=== Summary ==='); - console.log(`Tests passed: ${passed}/${total}`); - - if (passed === total) { - console.log('✅ All tests PASSED!\n'); - console.log('Stories 2.6.5 and 2.6.6 are COMPLETE\n'); - } else { - console.log('❌ Some tests failed\n'); - process.exit(1); - } -} - -main(); diff --git a/test-credentials-delete-validation.js b/test-credentials-delete-validation.js deleted file mode 100644 index c0e4528..0000000 --- a/test-credentials-delete-validation.js +++ /dev/null @@ -1,97 +0,0 @@ -#!/usr/bin/env node - -/** - * Test DELETE /credentials/{id} with real credential - * Creates credential via POST, then tries to DELETE it - */ - -const axios = require('axios'); -const fs = require('fs'); - -// Load configuration -let config; -if (fs.existsSync('.config.json')) { - const rawConfig = JSON.parse(fs.readFileSync('.config.json', 'utf8')); - const defaultEnv = rawConfig.defaultEnv || Object.keys(rawConfig.environments)[0]; - const envConfig = rawConfig.environments[defaultEnv]; - config = { - n8nHost: envConfig.n8n_host, - n8nApiKey: envConfig.n8n_api_key - }; -} else { - require('dotenv').config(); - config = { - n8nHost: process.env.N8N_HOST, - n8nApiKey: process.env.N8N_API_KEY - }; -} - -async function testDeleteCredential() { - console.log('\n=== DELETE /credentials/{id} Validation Test ===\n'); - console.log(`Testing against: ${config.n8nHost}\n`); - - let createdId = null; - - try { - // Step 1: Create a test credential - console.log('Step 1: Creating test credential via POST...'); - const createResponse = await axios.post( - `${config.n8nHost}/api/v1/credentials`, - { - name: `Test Delete Credential ${Date.now()}`, - type: 'httpBasicAuth', - data: { - user: 'testuser', - password: 'testpass' - } - }, - { - headers: { - 'X-N8N-API-KEY': config.n8nApiKey, - 'Accept': 'application/json', - 'Content-Type': 'application/json' - } - } - ); - - createdId = createResponse.data.id; - console.log(` ✅ Created credential with ID: ${createdId}`); - - // Step 2: Try to DELETE it - console.log(`\nStep 2: Attempting to DELETE credential ${createdId}...`); - const deleteResponse = await axios.delete( - `${config.n8nHost}/api/v1/credentials/${createdId}`, - { - headers: { - 'X-N8N-API-KEY': config.n8nApiKey, - 'Accept': 'application/json' - } - } - ); - - console.log(` ✅ DELETE successful! Status: ${deleteResponse.status}`); - console.log(` Response: ${JSON.stringify(deleteResponse.data)}`); - console.log('\n✅ CONCLUSION: DELETE /credentials/{id} is SUPPORTED\n'); - - } catch (error) { - if (error.response) { - console.log(` ❌ DELETE failed! Status: ${error.response.status}`); - console.log(` Message: ${JSON.stringify(error.response.data)}`); - - if (error.response.status === 405) { - console.log('\n❌ CONCLUSION: DELETE /credentials/{id} is NOT SUPPORTED (405)\n'); - } else { - console.log(`\n⚠️ CONCLUSION: DELETE returned ${error.response.status} - investigate further\n`); - } - - // Cleanup attempt if credential was created - if (createdId) { - console.log(`Note: Test credential ${createdId} may still exist in n8n`); - } - } else { - console.log(`\n❌ Request error: ${error.message}\n`); - } - } -} - -testDeleteCredential(); diff --git a/test-credentials-get-by-id-direct.js b/test-credentials-get-by-id-direct.js deleted file mode 100644 index 6160ec7..0000000 --- a/test-credentials-get-by-id-direct.js +++ /dev/null @@ -1,76 +0,0 @@ -#!/usr/bin/env node - -/** - * Direct API test for GET /credentials/{id} endpoint - * Tests if n8n API supports getting single credential - */ - -const axios = require('axios'); -const fs = require('fs'); - -// Load configuration -let config; -if (fs.existsSync('.config.json')) { - const rawConfig = JSON.parse(fs.readFileSync('.config.json', 'utf8')); - const defaultEnv = rawConfig.defaultEnv || Object.keys(rawConfig.environments)[0]; - const envConfig = rawConfig.environments[defaultEnv]; - config = { - n8nHost: envConfig.n8n_host, - n8nApiKey: envConfig.n8n_api_key - }; -} else { - require('dotenv').config(); - config = { - n8nHost: process.env.N8N_HOST, - n8nApiKey: process.env.N8N_API_KEY - }; -} - -async function testGetCredentialById() { - console.log('\n=== Direct n8n API Test: GET /credentials/{id} ===\n'); - console.log(`Testing against: ${config.n8nHost}`); - - // Try with a dummy ID (we expect 405 regardless of ID validity) - const testId = '1'; - - try { - const response = await axios.get(`${config.n8nHost}/api/v1/credentials/${testId}`, { - headers: { - 'X-N8N-API-KEY': config.n8nApiKey, - 'Accept': 'application/json' - } - }); - - console.log(`\n✅ SUCCESS: Status ${response.status}`); - console.log(`\nResponse structure:`); - console.log(JSON.stringify(response.data, null, 2)); - console.log('\n✅ GET /credentials/{id} endpoint is SUPPORTED\n'); - - } catch (error) { - if (error.response) { - console.log(`\n❌ ERROR: Status ${error.response.status}`); - console.log(`Message: ${JSON.stringify(error.response.data)}`); - - if (error.response.status === 405) { - console.log('\n❌ GET /credentials/{id} endpoint is NOT SUPPORTED'); - console.log(' This confirms the entire Credentials API is restricted'); - } else if (error.response.status === 404) { - console.log('\n🤔 Got 404 - endpoint might exist but credential not found'); - console.log(' This could mean the API is available but requires valid ID'); - } else if (error.response.status === 401) { - console.log('\n❌ Authentication error - check API key'); - } else if (error.response.status === 403) { - console.log('\n❌ Permission denied - API key lacks credentials access'); - } - } else { - console.log(`\n❌ Request failed: ${error.message}`); - } - - console.log('\n'); - return false; - } - - return true; -} - -testGetCredentialById(); diff --git a/test-credentials-informative-messages.js b/test-credentials-informative-messages.js deleted file mode 100644 index 8b39ee2..0000000 --- a/test-credentials-informative-messages.js +++ /dev/null @@ -1,135 +0,0 @@ -#!/usr/bin/env node - -/** - * Test: get_credential and update_credential informative messages - * Verifies Stories 2.6.2 and 2.6.4 return helpful guidance - */ - -const axios = require('axios'); - -const config = { - mcpServerUrl: 'http://localhost:3456/mcp' -}; - -let requestId = 1; - -async function sendMcpRequest(method, params = {}) { - const response = await axios.post(config.mcpServerUrl, { - jsonrpc: '2.0', - id: requestId++, - method, - params - }); - return response.data.result; -} - -async function callTool(name, args = {}) { - const result = await sendMcpRequest('tools/call', { name, arguments: args }); - if (result.isError) { - const errorMessage = result.content && result.content[0] && result.content[0].text - ? result.content[0].text - : 'Unknown error'; - throw new Error(errorMessage); - } - return result; -} - -async function testGetCredential() { - console.log('\n=== Test 1: get_credential (Story 2.6.2) ===\n'); - - try { - const result = await callTool('get_credential', { id: 'test-cred-id' }); - const response = JSON.parse(result.content[0].text); - - console.log('✅ Получен информационный ответ\n'); - - console.log('📋 Проверка структуры:'); - console.log(` ✓ success: ${response.success === false ? 'false (ожидаемо)' : response.success}`); - console.log(` ✓ method: ${response.method}`); - console.log(` ✓ endpoint: ${response.endpoint}`); - console.log(` ✓ credentialId: ${response.credentialId}`); - console.log(` ✓ message: ${response.message}`); - console.log(` ✓ securityReason: ${response.securityReason ? 'присутствует' : 'отсутствует'}`); - console.log(` ✓ alternativeApproaches: ${response.alternativeApproaches ? 'присутствует' : 'отсутствует'}`); - console.log(` ✓ availableOperations: ${response.availableOperations ? 'присутствует' : 'отсутствует'}`); - - console.log(`\n💡 Recommendation: ${response.recommendation}`); - console.log(`\n🔒 Security: ${response.securityReason}`); - - console.log('\n✅ Test 1 PASSED\n'); - return true; - } catch (error) { - console.error(`\n❌ Test 1 FAILED: ${error.message}\n`); - return false; - } -} - -async function testUpdateCredential() { - console.log('\n=== Test 2: update_credential (Story 2.6.4) ===\n'); - - try { - const result = await callTool('update_credential', { - id: 'test-cred-id', - name: 'Updated Credential', - type: 'httpBasicAuth', - data: { user: 'test', password: 'test' } - }); - const response = JSON.parse(result.content[0].text); - - console.log('✅ Получен информационный ответ\n'); - - console.log('📋 Проверка структуры:'); - console.log(` ✓ success: ${response.success === false ? 'false (ожидаемо)' : response.success}`); - console.log(` ✓ method: ${response.method}`); - console.log(` ✓ endpoint: ${response.endpoint}`); - console.log(` ✓ credentialId: ${response.credentialId}`); - console.log(` ✓ message: ${response.message}`); - console.log(` ✓ securityReason: ${response.securityReason ? 'присутствует' : 'отсутствует'}`); - console.log(` ✓ workaround: ${response.workaround ? 'присутствует' : 'отсутствует'}`); - console.log(` ✓ alternativeApproaches: ${response.alternativeApproaches ? 'присутствует' : 'отсутствует'}`); - - console.log(`\n💡 Recommendation: ${response.recommendation}`); - console.log(`\n🔒 Security: ${response.securityReason}`); - - if (response.workaround) { - console.log(`\n🛠️ Workaround:`); - console.log(` ${response.workaround.description}`); - console.log(`\n Steps:`); - response.workaround.steps.forEach(step => { - console.log(` ${step}`); - }); - } - - console.log('\n✅ Test 2 PASSED\n'); - return true; - } catch (error) { - console.error(`\n❌ Test 2 FAILED: ${error.message}\n`); - return false; - } -} - -async function main() { - console.log('\n=== Credentials API Informative Messages Test Suite ==='); - console.log('Testing Stories 2.6.2 and 2.6.4\n'); - - const results = []; - - results.push(await testGetCredential()); - results.push(await testUpdateCredential()); - - const passed = results.filter(r => r).length; - const total = results.length; - - console.log('\n=== Summary ==='); - console.log(`Tests passed: ${passed}/${total}`); - - if (passed === total) { - console.log('✅ All tests PASSED!\n'); - console.log('Stories 2.6.2 and 2.6.4 are COMPLETE\n'); - } else { - console.log('❌ Some tests failed\n'); - process.exit(1); - } -} - -main(); diff --git a/test-credentials-message.js b/test-credentials-message.js deleted file mode 100644 index 89bf223..0000000 --- a/test-credentials-message.js +++ /dev/null @@ -1,79 +0,0 @@ -#!/usr/bin/env node - -/** - * Test: list_credentials informative message - * Verifies the tool returns helpful guidance about API limitation - */ - -const axios = require('axios'); - -const config = { - mcpServerUrl: 'http://localhost:3456/mcp' -}; - -let requestId = 1; - -async function sendMcpRequest(method, params = {}) { - const response = await axios.post(config.mcpServerUrl, { - jsonrpc: '2.0', - id: requestId++, - method, - params - }); - return response.data.result; -} - -async function callTool(name, args = {}) { - const result = await sendMcpRequest('tools/call', { name, arguments: args }); - if (result.isError) { - const errorMessage = result.content && result.content[0] && result.content[0].text - ? result.content[0].text - : 'Unknown error'; - throw new Error(errorMessage); - } - return result; -} - -async function main() { - console.log('\n=== Test: list_credentials informative message ===\n'); - - try { - // Call list_credentials - const result = await callTool('list_credentials'); - const response = JSON.parse(result.content[0].text); - - console.log('✅ УСПЕХ! Получен информационный ответ\n'); - - // Verify response structure - console.log('📋 Проверка структуры ответа:'); - console.log(` ✓ success: ${response.success === false ? 'false (ожидаемо)' : response.success}`); - console.log(` ✓ method: ${response.method}`); - console.log(` ✓ endpoint: ${response.endpoint}`); - console.log(` ✓ message: ${response.message}`); - console.log(` ✓ recommendation: ${response.recommendation ? 'присутствует' : 'отсутствует'}`); - console.log(` ✓ securityNote: ${response.securityNote ? 'присутствует' : 'отсутствует'}`); - console.log(` ✓ alternativeAccess: ${response.alternativeAccess ? 'присутствует' : 'отсутствует'}`); - console.log(` ✓ understandingCredentials: ${response.understandingCredentials ? 'присутствует' : 'отсутствует'}`); - - // Display helpful information - console.log('\n📚 Информация для пользователя:'); - console.log(`\n${response.message}`); - console.log(`\n🔒 Безопасность: ${response.securityNote}`); - console.log(`\n💡 Рекомендация: ${response.recommendation}`); - - if (response.alternativeAccess && response.alternativeAccess.webInterface) { - console.log(`\n🌐 Доступ через веб-интерфейс:`); - response.alternativeAccess.webInterface.steps.forEach(step => { - console.log(` ${step}`); - }); - } - - console.log('\n✅ Тест пройден! list_credentials возвращает информативное сообщение.\n'); - - } catch (error) { - console.error(`\n❌ Ошибка: ${error.message}\n`); - process.exit(1); - } -} - -main(); diff --git a/test-debug-response.js b/test-debug-response.js deleted file mode 100644 index 6f18b43..0000000 --- a/test-debug-response.js +++ /dev/null @@ -1,55 +0,0 @@ -const axios = require('axios'); - -async function mcpRequest(method, params = {}) { - const response = await axios.post('http://localhost:3456/mcp', { - jsonrpc: '2.0', - id: Date.now(), - method, - params - }); - return response.data.result; -} - -async function callTool(toolName, args) { - const result = await mcpRequest('tools/call', { - name: toolName, - arguments: args - }); - - if (result.content && result.content[0]) { - try { - return JSON.parse(result.content[0].text); - } catch (e) { - return result.content[0].text; - } - } - return result; -} - -async function test() { - console.log('\n=== Testing list_executions ==='); - const executions = await callTool('list_executions', {}); - console.log('Type:', typeof executions); - console.log('Is Array:', Array.isArray(executions)); - if (typeof executions === 'object') { - console.log('Keys:', Object.keys(executions)); - if (executions.data) { - console.log('Has data property, is array:', Array.isArray(executions.data)); - } - } - console.log('First 500 chars:', JSON.stringify(executions, null, 2).substring(0, 500)); - - console.log('\n=== Testing get_tags ==='); - const tags = await callTool('get_tags', {}); - console.log('Type:', typeof tags); - console.log('Is Array:', Array.isArray(tags)); - if (typeof tags === 'object') { - console.log('Keys:', Object.keys(tags)); - if (tags.data) { - console.log('Has data property, is array:', Array.isArray(tags.data)); - } - } - console.log('First 500 chars:', JSON.stringify(tags, null, 2).substring(0, 500)); -} - -test().catch(console.error); diff --git a/test-delete-scenario.js b/test-delete-scenario.js deleted file mode 100644 index e633141..0000000 --- a/test-delete-scenario.js +++ /dev/null @@ -1,75 +0,0 @@ -#!/usr/bin/env node - -const axios = require('axios'); - -const mcp = axios.create({ baseURL: 'http://localhost:3456/mcp' }); - -async function mcpCall(method, args) { - const resp = await mcp.post('', { - jsonrpc: '2.0', - id: Date.now(), - method: 'tools/call', - params: { name: method, arguments: args } - }); - return resp.data.result; -} - -async function test() { - console.log('=== Full Delete Test Scenario ===\n'); - - // 1. Create workflow - console.log('1. Creating workflow...'); - const createResult = await mcpCall('create_workflow', { - name: `Full Test ${Date.now()}`, - nodes: [], - connections: {} - }); - - if (!createResult.content || !createResult.content[0]) { - console.log(' ERROR: No content in create result'); - console.log(' Result:', JSON.stringify(createResult, null, 2)); - return; - } - - const workflow = JSON.parse(createResult.content[0].text); - const id = workflow.id; - console.log(` ✅ Created: ${id}\n`); - - // 2. Delete workflow - console.log('2. Deleting workflow...'); - const deleteResult = await mcpCall('delete_workflow', { id }); - console.log(' isError:', deleteResult.isError); - console.log(' Content (first 150 chars):', deleteResult.content[0].text.substring(0, 150)); - - // Check if delete returned the workflow object - if (!deleteResult.isError) { - try { - const deletedWf = JSON.parse(deleteResult.content[0].text); - console.log(` ✅ Delete returned workflow object: ${deletedWf.name}`); - console.log(` Workflow ID in response: ${deletedWf.id}`); - } catch (e) { - console.log(' Delete returned non-JSON response'); - } - } - console.log(''); - - // 3. Try to get deleted workflow - console.log('3. Getting deleted workflow...'); - const getResult = await mcpCall('get_workflow', { id }); - console.log(' isError:', getResult.isError); - - if (getResult.isError) { - console.log(' ✅ CORRECT: get_workflow returns error for deleted workflow'); - console.log(' Error message:', getResult.content[0].text); - } else { - console.log(' ❌ PROBLEM: get_workflow still returns workflow!'); - const wf = JSON.parse(getResult.content[0].text); - console.log(` Workflow still accessible: ${wf.name} (id: ${wf.id})`); - } -} - -test().catch(err => { - console.error('Script error:', err.message); - console.error(err.stack); - process.exit(1); -}); diff --git a/test-different-tag-names.js b/test-different-tag-names.js deleted file mode 100644 index 02e94b9..0000000 --- a/test-different-tag-names.js +++ /dev/null @@ -1,52 +0,0 @@ -#!/usr/bin/env node - -const axios = require('axios'); -const fs = require('fs'); - -const config = JSON.parse(fs.readFileSync('.config.json', 'utf8')); -const env = config.environments[config.defaultEnv]; - -const api = axios.create({ - baseURL: `${env.n8n_host}/api/v1`, - headers: { - 'X-N8N-API-KEY': env.n8n_api_key, - 'Content-Type': 'application/json' - } -}); - -const testNames = [ - `DirectTest_${Date.now()}`, - `Test${Date.now()}`, - `ValidTag${Date.now()}`, - `MyTag_${Math.random().toString(36).substring(7)}`, - `UUID_${crypto.randomUUID()}`.substring(0, 20) -]; - -async function test() { - console.error('[INFO] Testing different tag name patterns...\n'); - - for (const tagName of testNames) { - try { - console.error(`[INFO] Trying: ${tagName}`); - const response = await api.post('/tags', { name: tagName }); - console.error(`[SUCCESS] Created! ID: ${response.data.id}`); - - // Clean up - await api.delete(`/tags/${response.data.id}`); - console.error(`[SUCCESS] Deleted\n`); - - // If we get here, we found a working pattern - console.error(`\n[SUCCESS] Working pattern found: ${tagName}`); - break; - - } catch (error) { - if (error.response) { - console.error(`[ERROR] Status ${error.response.status}: ${error.response.data.message || error.response.data}\n`); - } else { - console.error(`[ERROR] ${error.message}\n`); - } - } - } -} - -test(); diff --git a/test-direct-api.js b/test-direct-api.js deleted file mode 100644 index d44e763..0000000 --- a/test-direct-api.js +++ /dev/null @@ -1,51 +0,0 @@ -#!/usr/bin/env node - -/** - * Test n8n API directly (bypassing MCP) to see if 409 error is from n8n or our code - */ - -const axios = require('axios'); -const fs = require('fs'); - -// Load config -const config = JSON.parse(fs.readFileSync('.config.json', 'utf8')); -const env = config.environments[config.defaultEnv]; - -const api = axios.create({ - baseURL: `${env.n8n_host}/api/v1`, - headers: { - 'X-N8N-API-KEY': env.n8n_api_key, - 'Content-Type': 'application/json' - } -}); - -async function test() { - try { - console.error('[INFO] Testing direct n8n API...'); - console.error(`[INFO] Base URL: ${env.n8n_host}/api/v1`); - - // Try to create a tag - const tagName = `DirectTest_${Date.now()}_${Math.random().toString(36).substring(7)}`; - console.error(`\n[INFO] Creating tag: ${tagName}`); - - const response = await api.post('/tags', { name: tagName }); - console.error('[SUCCESS] Tag created!'); - console.error(JSON.stringify(response.data, null, 2)); - - // Clean up - console.error(`\n[INFO] Cleaning up tag: ${response.data.id}`); - await api.delete(`/tags/${response.data.id}`); - console.error('[SUCCESS] Tag deleted'); - - } catch (error) { - console.error('[ERROR] Request failed:'); - if (error.response) { - console.error(`Status: ${error.response.status}`); - console.error(`Data:`, error.response.data); - } else { - console.error(error.message); - } - } -} - -test(); diff --git a/test-e2e-all-tools.js b/test-e2e-all-tools.js deleted file mode 100644 index 8317b6a..0000000 --- a/test-e2e-all-tools.js +++ /dev/null @@ -1,670 +0,0 @@ -#!/usr/bin/env node - -/** - * Comprehensive E2E Test Suite for ALL 17 MCP Tools - * - * Tests all MCP server capabilities: - * - Workflow Management (8 tools) - * - Execution Management (4 tools) - * - Tag Management (5 tools) - * - Credential Management (6 tools) - * - * Creates test data, does NOT modify existing resources - */ - -const axios = require('axios'); - -// Configuration -const config = { - mcpServerUrl: 'http://localhost:3456/mcp', - healthCheckUrl: 'http://localhost:3456/health', - testWorkflowName: 'E2E Test Workflow', - testTagName: 'E2E Test Tag', - testCredentialName: 'E2E Test Credential', - cleanup: true // Set to false to keep test data -}; - -// Test data storage -const testData = { - workflowId: null, - tagId: null, - executionId: null, - credentialId: null, - results: { - total: 0, - passed: 0, - failed: 0, - skipped: 0, - tests: [] - } -}; - -// Utility: MCP JSON-RPC request -async function mcpRequest(method, params = {}) { - try { - const response = await axios.post(config.mcpServerUrl, { - jsonrpc: '2.0', - id: Date.now(), - method, - params - }); - - if (response.data.error) { - throw new Error(`MCP Error: ${response.data.error.message}`); - } - - return response.data.result; - } catch (error) { - if (error.response) { - throw new Error(`HTTP ${error.response.status}: ${JSON.stringify(error.response.data)}`); - } - throw error; - } -} - -// Utility: Call MCP tool -async function callTool(toolName, args) { - const result = await mcpRequest('tools/call', { - name: toolName, - arguments: args - }); - - if (result.content && result.content[0]) { - try { - return JSON.parse(result.content[0].text); - } catch (e) { - return result.content[0].text; - } - } - - return result; -} - -// Utility: Record test result -function recordTest(category, name, status, details = '') { - testData.results.total++; - - const result = { - category, - name, - status, - details - }; - - if (status === 'PASS') { - testData.results.passed++; - console.log(` ✅ ${name}`); - } else if (status === 'FAIL') { - testData.results.failed++; - console.log(` ❌ ${name}: ${details}`); - } else if (status === 'SKIP') { - testData.results.skipped++; - console.log(` ⏭️ ${name}: ${details}`); - } - - testData.results.tests.push(result); - return status === 'PASS'; -} - -// Health check -async function healthCheck() { - console.log('\n╔════════════════════════════════════════════════════════════╗'); - console.log('║ E2E Test Suite - ALL 17 MCP Tools Validation ║'); - console.log('╚════════════════════════════════════════════════════════════╝\n'); - - try { - const response = await axios.get(config.healthCheckUrl); - console.log('🏥 Health Check'); - console.log(` ✅ Server is healthy`); - console.log(` ✓ Version: ${response.data.version}`); - console.log(` ✓ Status: ${response.data.status}\n`); - return true; - } catch (error) { - console.error(' ❌ Health check failed:', error.message); - return false; - } -} - -// ============================================================================ -// WORKFLOW MANAGEMENT TESTS (8 tools) -// ============================================================================ - -async function testWorkflowManagement() { - console.log('📂 Workflow Management (8 tools)\n'); - const category = 'Workflow Management'; - - try { - // Test 1: list_workflows - console.log(' Testing list_workflows...'); - const workflows = await callTool('list_workflows', {}); - recordTest(category, 'list_workflows', - Array.isArray(workflows) ? 'PASS' : 'FAIL', - Array.isArray(workflows) ? `Found ${workflows.length} workflows` : 'Not an array' - ); - - // Test 2: create_workflow - console.log(' Testing create_workflow...'); - const timestamp = Date.now(); - const newWorkflow = await callTool('create_workflow', { - name: `${config.testWorkflowName} ${timestamp}`, - nodes: [ - { - name: 'Start', - type: 'n8n-nodes-base.start', - position: [250, 300], - parameters: {} - }, - { - name: 'Set', - type: 'n8n-nodes-base.set', - position: [450, 300], - parameters: { - values: { - string: [ - { - name: 'message', - value: 'E2E Test' - } - ] - } - } - } - ], - connections: [ - { - source: 'Start', - target: 'Set', - sourceOutput: 0, - targetInput: 0 - } - ] - }); - - if (newWorkflow && newWorkflow.id) { - testData.workflowId = newWorkflow.id; - recordTest(category, 'create_workflow', 'PASS', `Created workflow ID: ${newWorkflow.id}`); - } else { - recordTest(category, 'create_workflow', 'FAIL', 'No workflow ID returned'); - } - - // Test 3: get_workflow - console.log(' Testing get_workflow...'); - if (testData.workflowId) { - const workflow = await callTool('get_workflow', { id: testData.workflowId }); - recordTest(category, 'get_workflow', - workflow && workflow.id === testData.workflowId ? 'PASS' : 'FAIL', - workflow ? `Retrieved workflow: ${workflow.name}` : 'Failed to retrieve' - ); - } else { - recordTest(category, 'get_workflow', 'SKIP', 'No workflow ID available'); - } - - // Test 4: update_workflow - console.log(' Testing update_workflow...'); - if (testData.workflowId) { - const updatedWorkflow = await callTool('update_workflow', { - id: testData.workflowId, - name: `${config.testWorkflowName} ${timestamp} (Updated)`, - nodes: [ - { - name: 'Start', - type: 'n8n-nodes-base.start', - position: [250, 300], - parameters: {} - }, - { - name: 'Set', - type: 'n8n-nodes-base.set', - position: [450, 300], - parameters: { - values: { - string: [ - { - name: 'message', - value: 'E2E Test Updated' - } - ] - } - } - } - ], - connections: [ - { - source: 'Start', - target: 'Set', - sourceOutput: 0, - targetInput: 0 - } - ] - }); - recordTest(category, 'update_workflow', - updatedWorkflow && updatedWorkflow.name.includes('Updated') ? 'PASS' : 'FAIL', - updatedWorkflow ? 'Workflow updated successfully' : 'Update failed' - ); - } else { - recordTest(category, 'update_workflow', 'SKIP', 'No workflow ID available'); - } - - // Test 5: activate_workflow - console.log(' Testing activate_workflow...'); - if (testData.workflowId) { - try { - const activated = await callTool('activate_workflow', { id: testData.workflowId }); - recordTest(category, 'activate_workflow', - activated && activated.active === true ? 'PASS' : 'FAIL', - activated ? 'Workflow activated' : 'Activation failed' - ); - } catch (error) { - // n8n v2.0.3+ may not support activation via API - recordTest(category, 'activate_workflow', 'SKIP', - 'Activation not supported in n8n v2.0.3+ (read-only active field)'); - } - } else { - recordTest(category, 'activate_workflow', 'SKIP', 'No workflow ID available'); - } - - // Test 6: deactivate_workflow - console.log(' Testing deactivate_workflow...'); - if (testData.workflowId) { - try { - const deactivated = await callTool('deactivate_workflow', { id: testData.workflowId }); - recordTest(category, 'deactivate_workflow', - deactivated && deactivated.active === false ? 'PASS' : 'FAIL', - deactivated ? 'Workflow deactivated' : 'Deactivation failed' - ); - } catch (error) { - // n8n v2.0.3+ may not support deactivation via API - recordTest(category, 'deactivate_workflow', 'SKIP', - 'Deactivation not supported in n8n v2.0.3+ (read-only active field)'); - } - } else { - recordTest(category, 'deactivate_workflow', 'SKIP', 'No workflow ID available'); - } - - // Test 7: execute_workflow - console.log(' Testing execute_workflow...'); - if (testData.workflowId) { - // This will return guidance message since manual trigger workflows can't be executed via API - const result = await callTool('execute_workflow', { id: testData.workflowId }); - recordTest(category, 'execute_workflow', 'PASS', - 'Returns guidance (expected for manual trigger workflows)'); - } else { - recordTest(category, 'execute_workflow', 'SKIP', 'No workflow ID available'); - } - - // Test 8: delete_workflow (will be done in cleanup) - recordTest(category, 'delete_workflow', 'PASS', 'Will be tested in cleanup phase'); - - } catch (error) { - console.error(` ❌ Workflow Management tests error: ${error.message}`); - } -} - -// ============================================================================ -// EXECUTION MANAGEMENT TESTS (4 tools) -// ============================================================================ - -async function testExecutionManagement() { - console.log('\n⚡ Execution Management (4 tools)\n'); - const category = 'Execution Management'; - - try { - // Test 1: list_executions - console.log(' Testing list_executions...'); - const executionsResponse = await callTool('list_executions', {}); - const executions = executionsResponse.data || executionsResponse; - recordTest(category, 'list_executions', - Array.isArray(executions) ? 'PASS' : 'FAIL', - Array.isArray(executions) ? `Found ${executions.length} executions` : 'Not an array' - ); - - // Get an execution ID if available - if (Array.isArray(executions) && executions.length > 0) { - testData.executionId = executions[0].id; - } - - // Test 2: get_execution - console.log(' Testing get_execution...'); - if (testData.executionId) { - const execution = await callTool('get_execution', { id: testData.executionId }); - recordTest(category, 'get_execution', - execution && execution.id ? 'PASS' : 'FAIL', - execution ? `Retrieved execution: ${execution.id}` : 'Failed to retrieve' - ); - } else { - recordTest(category, 'get_execution', 'SKIP', 'No execution ID available'); - } - - // Test 3: retry_execution - console.log(' Testing retry_execution...'); - // Find a failed execution if available - const failedExecutions = Array.isArray(executions) - ? executions.filter(e => e.status === 'error') - : []; - - if (failedExecutions.length > 0) { - try { - const retried = await callTool('retry_execution', { id: failedExecutions[0].id }); - recordTest(category, 'retry_execution', - retried && retried.id ? 'PASS' : 'FAIL', - retried ? `Retried execution: ${retried.id}` : 'Retry failed' - ); - } catch (error) { - recordTest(category, 'retry_execution', 'PASS', - 'API returned expected error (retry may not be available for this execution)'); - } - } else { - recordTest(category, 'retry_execution', 'SKIP', 'No failed executions available'); - } - - // Test 4: delete_execution - console.log(' Testing delete_execution...'); - // We'll skip actual deletion to preserve execution history - recordTest(category, 'delete_execution', 'SKIP', - 'Skipped to preserve execution history (tested in other suites)'); - - } catch (error) { - console.error(` ❌ Execution Management tests error: ${error.message}`); - } -} - -// ============================================================================ -// TAG MANAGEMENT TESTS (5 tools) -// ============================================================================ - -async function testTagManagement() { - console.log('\n🏷️ Tag Management (5 tools)\n'); - const category = 'Tag Management'; - - try { - // Test 1: get_tags (list all tags) - console.log(' Testing get_tags...'); - const tagsResponse = await callTool('get_tags', {}); - const tags = tagsResponse.data || tagsResponse; - recordTest(category, 'get_tags', - Array.isArray(tags) ? 'PASS' : 'FAIL', - Array.isArray(tags) ? `Found ${tags.length} tags` : 'Not an array' - ); - - // Test 2: create_tag - console.log(' Testing create_tag...'); - const uuid = Math.random().toString(36).substring(2, 10); - const newTag = await callTool('create_tag', { - name: `E2E-Tag-${uuid}` - }); - - if (newTag && newTag.id) { - testData.tagId = newTag.id; - recordTest(category, 'create_tag', 'PASS', `Created tag ID: ${newTag.id}`); - } else { - recordTest(category, 'create_tag', 'FAIL', 'No tag ID returned'); - } - - // Test 3: get_tag (get single tag) - console.log(' Testing get_tag...'); - if (testData.tagId) { - const tag = await callTool('get_tag', { id: testData.tagId }); - recordTest(category, 'get_tag', - tag && tag.id === testData.tagId ? 'PASS' : 'FAIL', - tag ? `Retrieved tag: ${tag.name}` : 'Failed to retrieve' - ); - } else { - recordTest(category, 'get_tag', 'SKIP', 'No tag ID available'); - } - - // Test 4: update_tag - console.log(' Testing update_tag...'); - if (testData.tagId) { - const updatedTag = await callTool('update_tag', { - id: testData.tagId, - name: `E2E-Tag-${uuid}-Updated` - }); - recordTest(category, 'update_tag', - updatedTag && updatedTag.name.includes('Updated') ? 'PASS' : 'FAIL', - updatedTag ? 'Tag updated successfully' : 'Update failed' - ); - } else { - recordTest(category, 'update_tag', 'SKIP', 'No tag ID available'); - } - - // Test 5: delete_tag (will be done in cleanup) - recordTest(category, 'delete_tag', 'PASS', 'Will be tested in cleanup phase'); - - } catch (error) { - console.error(` ❌ Tag Management tests error: ${error.message}`); - } -} - -// ============================================================================ -// CREDENTIAL MANAGEMENT TESTS (6 tools) -// ============================================================================ - -async function testCredentialManagement() { - console.log('\n🔐 Credential Management (6 tools)\n'); - const category = 'Credential Management'; - - try { - // Test 1: list_credentials (informative message) - console.log(' Testing list_credentials...'); - const listResult = await callTool('list_credentials', {}); - recordTest(category, 'list_credentials', - listResult && listResult.success === false ? 'PASS' : 'FAIL', - 'Returns informative message (expected - blocked for security)' - ); - - // Test 2: get_credential_schema - console.log(' Testing get_credential_schema...'); - const schema = await callTool('get_credential_schema', { - typeName: 'httpBasicAuth' - }); - recordTest(category, 'get_credential_schema', - schema && schema.type === 'object' ? 'PASS' : 'FAIL', - schema ? `Retrieved schema with ${Object.keys(schema.properties || {}).length} properties` : 'Failed' - ); - - // Test 3: create_credential - console.log(' Testing create_credential...'); - const timestamp = Date.now(); - const newCredential = await callTool('create_credential', { - name: `${config.testCredentialName} ${timestamp}`, - type: 'httpBasicAuth', - data: { - user: 'e2e-test-user', - password: 'e2e-test-password' - } - }); - - if (newCredential && newCredential.id) { - testData.credentialId = newCredential.id; - recordTest(category, 'create_credential', 'PASS', - `Created credential ID: ${newCredential.id}`); - } else { - recordTest(category, 'create_credential', 'FAIL', 'No credential ID returned'); - } - - // Test 4: get_credential (informative message) - console.log(' Testing get_credential...'); - if (testData.credentialId) { - const getResult = await callTool('get_credential', { id: testData.credentialId }); - recordTest(category, 'get_credential', - getResult && getResult.success === false ? 'PASS' : 'FAIL', - 'Returns informative message (expected - blocked for security)' - ); - } else { - recordTest(category, 'get_credential', 'SKIP', 'No credential ID available'); - } - - // Test 5: update_credential (informative message) - console.log(' Testing update_credential...'); - if (testData.credentialId) { - const updateResult = await callTool('update_credential', { - id: testData.credentialId, - name: 'Updated Name', - type: 'httpBasicAuth', - data: { user: 'new-user', password: 'new-pass' } - }); - recordTest(category, 'update_credential', - updateResult && updateResult.success === false ? 'PASS' : 'FAIL', - 'Returns informative message (expected - blocked for immutability)' - ); - } else { - recordTest(category, 'update_credential', 'SKIP', 'No credential ID available'); - } - - // Test 6: delete_credential (will be done in cleanup) - recordTest(category, 'delete_credential', 'PASS', 'Will be tested in cleanup phase'); - - } catch (error) { - console.error(` ❌ Credential Management tests error: ${error.message}`); - } -} - -// ============================================================================ -// CLEANUP -// ============================================================================ - -async function cleanup() { - console.log('\n🧹 Cleanup Phase\n'); - const category = 'Cleanup'; - - if (!config.cleanup) { - console.log(' ⏭️ Cleanup disabled, keeping test data\n'); - return; - } - - let cleanupSuccess = 0; - let cleanupFailed = 0; - - // Delete test credential - if (testData.credentialId) { - try { - console.log(` Deleting test credential: ${testData.credentialId}...`); - await callTool('delete_credential', { id: testData.credentialId }); - console.log(` ✅ Credential deleted`); - recordTest(category, 'delete_credential (cleanup)', 'PASS', 'Credential deleted successfully'); - cleanupSuccess++; - } catch (error) { - console.log(` ❌ Failed to delete credential: ${error.message}`); - recordTest(category, 'delete_credential (cleanup)', 'FAIL', error.message); - cleanupFailed++; - } - } - - // Delete test tag - if (testData.tagId) { - try { - console.log(` Deleting test tag: ${testData.tagId}...`); - await callTool('delete_tag', { id: testData.tagId }); - console.log(` ✅ Tag deleted`); - recordTest(category, 'delete_tag (cleanup)', 'PASS', 'Tag deleted successfully'); - cleanupSuccess++; - } catch (error) { - console.log(` ❌ Failed to delete tag: ${error.message}`); - recordTest(category, 'delete_tag (cleanup)', 'FAIL', error.message); - cleanupFailed++; - } - } - - // Delete test workflow - if (testData.workflowId) { - try { - console.log(` Deleting test workflow: ${testData.workflowId}...`); - await callTool('delete_workflow', { id: testData.workflowId }); - console.log(` ✅ Workflow deleted`); - recordTest(category, 'delete_workflow (cleanup)', 'PASS', 'Workflow deleted successfully'); - cleanupSuccess++; - } catch (error) { - console.log(` ❌ Failed to delete workflow: ${error.message}`); - recordTest(category, 'delete_workflow (cleanup)', 'FAIL', error.message); - cleanupFailed++; - } - } - - console.log(`\n 📊 Cleanup Summary:`); - console.log(` ✅ Successful: ${cleanupSuccess}`); - if (cleanupFailed > 0) { - console.log(` ❌ Failed: ${cleanupFailed}`); - } -} - -// ============================================================================ -// FINAL REPORT -// ============================================================================ - -function printFinalReport() { - console.log('\n╔════════════════════════════════════════════════════════════╗'); - console.log('║ FINAL TEST REPORT ║'); - console.log('╚════════════════════════════════════════════════════════════╝\n'); - - console.log(`📊 Overall Statistics:`); - console.log(` Total Tests: ${testData.results.total}`); - console.log(` ✅ Passed: ${testData.results.passed}`); - console.log(` ❌ Failed: ${testData.results.failed}`); - console.log(` ⏭️ Skipped: ${testData.results.skipped}`); - - const successRate = testData.results.total > 0 - ? ((testData.results.passed / testData.results.total) * 100).toFixed(1) - : 0; - console.log(` 📈 Success Rate: ${successRate}%\n`); - - // Group by category - const categories = {}; - testData.results.tests.forEach(test => { - if (!categories[test.category]) { - categories[test.category] = { passed: 0, failed: 0, skipped: 0, total: 0 }; - } - categories[test.category].total++; - if (test.status === 'PASS') categories[test.category].passed++; - if (test.status === 'FAIL') categories[test.category].failed++; - if (test.status === 'SKIP') categories[test.category].skipped++; - }); - - console.log(`📋 Results by Category:\n`); - Object.keys(categories).forEach(category => { - const stats = categories[category]; - console.log(` ${category}:`); - console.log(` Total: ${stats.total} | ✅ ${stats.passed} | ❌ ${stats.failed} | ⏭️ ${stats.skipped}`); - }); - - // Failed tests detail - const failedTests = testData.results.tests.filter(t => t.status === 'FAIL'); - if (failedTests.length > 0) { - console.log(`\n❌ Failed Tests Detail:\n`); - failedTests.forEach(test => { - console.log(` ${test.category} > ${test.name}`); - console.log(` ${test.details}\n`); - }); - } - - console.log('\n╔════════════════════════════════════════════════════════════╗'); - console.log(`║ E2E Test Suite Complete: ${successRate}% Success Rate${' '.repeat(Math.max(0, 18 - successRate.toString().length))}║`); - console.log('╚════════════════════════════════════════════════════════════╝\n'); - - // Exit with appropriate code - process.exit(testData.results.failed > 0 ? 1 : 0); -} - -// ============================================================================ -// MAIN EXECUTION -// ============================================================================ - -async function runAllTests() { - const healthy = await healthCheck(); - if (!healthy) { - console.error('❌ Server not healthy, aborting tests\n'); - process.exit(1); - } - - await testWorkflowManagement(); - await testExecutionManagement(); - await testTagManagement(); - await testCredentialManagement(); - await cleanup(); - printFinalReport(); -} - -// Run tests -runAllTests().catch(error => { - console.error('\n💥 Unhandled error:', error); - process.exit(1); -}); diff --git a/test-executions-validation.js b/test-executions-validation.js deleted file mode 100644 index b11ac3e..0000000 --- a/test-executions-validation.js +++ /dev/null @@ -1,730 +0,0 @@ -#!/usr/bin/env node - -/** - * Executions API Validation Test Suite - Story 2.2 - * - * Validates all 3 Executions API methods against live n8n instance: - * - list_executions (GET /executions) - * - get_execution (GET /executions/{id}) - * - delete_execution (DELETE /executions/{id}) - * - * Test Categories: - * - Execution listing with filtering and pagination - * - Execution retrieval with full data - * - Execution deletion and cleanup - * - Error handling and edge cases - * - Multi-instance routing - */ - -const axios = require('axios'); - -// ======================================== -// Configuration -// ======================================== - -const config = { - mcpServerUrl: 'http://localhost:3456/mcp', - healthCheckUrl: 'http://localhost:3456/health', - testWorkflowPrefix: 'ExecTest_', - testFlags: { - generateExecutions: true, // Create test workflows and executions - runListTests: true, - runGetTests: true, - runDeleteTests: true, - runCleanup: true - }, - executionGeneration: { - minExecutions: 15, // Minimum executions needed for pagination testing - maxRetries: 30, // Max attempts to wait for executions - retryDelay: 2000 // Delay between retry attempts (ms) - } -}; - -// ======================================== -// Logger Utility -// ======================================== - -const logger = { - info: (msg) => console.error(`[INFO] ${msg}`), - test: (name, passed, details = '') => { - const status = passed ? '✓ PASS' : '✗ FAIL'; - const message = details ? ` - ${details}` : ''; - console.error(`[TEST] ${name}: ${status}${message}`); - }, - warn: (msg) => console.error(`[WARN] ⚠️ ${msg}`), - error: (msg) => console.error(`[ERROR] ❌ ${msg}`), - success: (msg) => console.error(`[SUCCESS] ✓ ${msg}`), - debug: (msg, data) => { - if (process.env.DEBUG) { - console.error(`[DEBUG] ${msg}`, data ? JSON.stringify(data, null, 2) : ''); - } - }, - validationFinding: (method, finding) => { - console.error(`[WARN] ⚠️ Validation Finding [${method}]: ${finding}`); - } -}; - -// ======================================== -// MCP Communication -// ======================================== - -let requestId = 1; - -async function sendMcpRequest(method, params = {}) { - try { - const response = await axios.post(config.mcpServerUrl, { - jsonrpc: '2.0', - id: requestId++, - method, - params - }); - - logger.debug(`MCP Response for ${method}:`, response.data); - return response.data.result; - } catch (error) { - logger.error(`MCP request failed: ${method}`); - if (error.response) { - logger.error(`Status: ${error.response.status}`); - logger.error(`Data: ${JSON.stringify(error.response.data)}`); - } - throw error; - } -} - -async function callTool(name, args = {}, maxRetries = 3) { - let lastError; - - for (let attempt = 1; attempt <= maxRetries; attempt++) { - try { - const result = await sendMcpRequest('tools/call', { name, arguments: args }); - - // Check if MCP tool returned an error - if (result.isError) { - const errorMessage = result.content && result.content[0] && result.content[0].text - ? result.content[0].text - : 'Unknown MCP tool error'; - throw new Error(errorMessage); - } - - return result; - } catch (error) { - lastError = error; - - if (attempt < maxRetries) { - logger.warn(`Retrying tools/call (${maxRetries - attempt} attempts remaining)`); - await sleep(1000 * attempt); // Exponential backoff - } - } - } - - throw lastError; -} - -function sleep(ms) { - return new Promise(resolve => setTimeout(resolve, ms)); -} - -// ======================================== -// Test Workflow Generators -// ======================================== - -/** - * Create a simple workflow that will execute successfully - */ -function createSuccessWorkflow(name) { - return { - name: name, - nodes: [ - { - name: 'Manual Trigger', - type: 'n8n-nodes-base.manualTrigger', - position: [250, 300], - parameters: {} - }, - { - name: 'Set Success', - type: 'n8n-nodes-base.set', - position: [450, 300], - parameters: { - values: { - string: [ - { - name: 'status', - value: 'success' - }, - { - name: 'timestamp', - value: '={{ $now }}' - } - ] - } - } - } - ], - connections: [ - { - source: 'Manual Trigger', - target: 'Set Success', - sourceOutput: 0, - targetInput: 0 - } - ] - }; -} - -/** - * Create a workflow with schedule trigger (can execute automatically) - */ -function createScheduledWorkflow(name) { - return { - name: name, - nodes: [ - { - name: 'Schedule Trigger', - type: 'n8n-nodes-base.scheduleTrigger', - position: [250, 300], - parameters: { - rule: { - interval: [ - { - field: 'hours', - hoursInterval: 1 - } - ] - } - } - }, - { - name: 'Set Data', - type: 'n8n-nodes-base.set', - position: [450, 300], - parameters: { - values: { - string: [ - { - name: 'executionTime', - value: '={{ $now }}' - } - ] - } - } - } - ], - connections: [ - { - source: 'Schedule Trigger', - target: 'Set Data', - sourceOutput: 0, - targetInput: 0 - } - ] - }; -} - -// ======================================== -// Test State Management -// ======================================== - -const testState = { - createdWorkflows: [], - createdExecutions: [], - testExecutionIds: [] -}; - -// ======================================== -// Test Execution Generation -// ======================================== - -async function generateTestExecutions() { - logger.info('Generating test executions...'); - - try { - // Create test workflow - const workflowName = `${config.testWorkflowPrefix}${Date.now()}`; - const workflowData = createSuccessWorkflow(workflowName); - - logger.info(`Creating test workflow: ${workflowName}`); - const createResult = await callTool('create_workflow', workflowData); - const workflow = JSON.parse(createResult.content[0].text); - testState.createdWorkflows.push(workflow.id); - logger.info(`Created workflow: ${workflow.id}`); - - // Note: We'll rely on existing executions in the n8n instance - // as manual trigger workflows cannot be executed via REST API - logger.info('Waiting for existing executions to be available...'); - - await sleep(2000); - - // List existing executions - const listResult = await callTool('list_executions', { limit: 50 }); - const execData = JSON.parse(listResult.content[0].text); - const executions = execData.data || []; - - logger.info(`Found ${executions.length} existing executions`); - - if (executions.length === 0) { - logger.warn('No executions found. Some tests may be limited.'); - logger.warn('Please execute some workflows manually through n8n interface for complete testing.'); - } else { - // Store some execution IDs for testing - testState.testExecutionIds = executions.slice(0, 10).map(e => e.id); - logger.info(`Using ${testState.testExecutionIds.length} executions for testing`); - } - - return executions.length > 0; - } catch (error) { - logger.error(`Failed to generate test executions: ${error.message}`); - throw error; - } -} - -// ======================================== -// Test Suite: list_executions -// ======================================== - -async function testListExecutions() { - logger.info('\n--- Task 2: Validate list_executions ---\n'); - - let testsPassed = 0; - let testsTotal = 0; - - // Test 2.1: List all executions - testsTotal++; - try { - const result = await callTool('list_executions', {}); - const data = JSON.parse(result.content[0].text); - - const isValid = data && Array.isArray(data.data); - logger.test( - 'list_executions - List all executions', - isValid, - isValid ? `Found ${data.data.length} executions` : 'Invalid response structure' - ); - if (isValid) testsPassed++; - - // Store for later tests - if (data.data.length > 0) { - testState.testExecutionIds = data.data.slice(0, 5).map(e => e.id); - } - } catch (error) { - logger.test('list_executions - List all executions', false, error.message); - } - - // Test 2.2: Response structure validation - testsTotal++; - try { - const result = await callTool('list_executions', { limit: 5 }); - const data = JSON.parse(result.content[0].text); - - if (data.data && data.data.length > 0) { - const exec = data.data[0]; - const hasRequiredFields = - exec.hasOwnProperty('id') && - exec.hasOwnProperty('finished') && - exec.hasOwnProperty('mode') && - exec.hasOwnProperty('startedAt') && - exec.hasOwnProperty('workflowId'); - - logger.test( - 'list_executions - Response structure validation', - hasRequiredFields, - hasRequiredFields ? 'All required fields present' : 'Missing required fields' - ); - if (hasRequiredFields) testsPassed++; - } else { - logger.test('list_executions - Response structure validation', false, 'No executions to validate'); - } - } catch (error) { - logger.test('list_executions - Response structure validation', false, error.message); - } - - // Test 2.3: Pagination with limit - testsTotal++; - try { - const result = await callTool('list_executions', { limit: 3 }); - const data = JSON.parse(result.content[0].text); - - const isValid = data.data && data.data.length <= 3; - logger.test( - 'list_executions - Pagination limit', - isValid, - isValid ? `Returned ${data.data.length} executions (limit: 3)` : 'Limit not respected' - ); - if (isValid) testsPassed++; - } catch (error) { - logger.test('list_executions - Pagination limit', false, error.message); - } - - // Test 2.4: Cursor-based pagination - testsTotal++; - try { - const firstPage = await callTool('list_executions', { limit: 5 }); - const firstData = JSON.parse(firstPage.content[0].text); - - if (firstData.nextCursor) { - const secondPage = await callTool('list_executions', { - limit: 5, - cursor: firstData.nextCursor - }); - const secondData = JSON.parse(secondPage.content[0].text); - - const isValid = secondData.data && secondData.data.length > 0; - logger.test( - 'list_executions - Cursor pagination', - isValid, - isValid ? `Next page retrieved with ${secondData.data.length} executions` : 'Cursor pagination failed' - ); - if (isValid) testsPassed++; - } else { - logger.test('list_executions - Cursor pagination', true, 'No next cursor (all results fit in first page)'); - testsPassed++; - } - } catch (error) { - logger.test('list_executions - Cursor pagination', false, error.message); - } - - // Test 2.5: Filter by workflowId - testsTotal++; - try { - const allExecs = await callTool('list_executions', { limit: 10 }); - const allData = JSON.parse(allExecs.content[0].text); - - if (allData.data && allData.data.length > 0) { - const testWorkflowId = allData.data[0].workflowId; - - const filtered = await callTool('list_executions', { - workflowId: testWorkflowId, - limit: 10 - }); - const filteredData = JSON.parse(filtered.content[0].text); - - const allMatch = filteredData.data.every(e => e.workflowId === testWorkflowId); - logger.test( - 'list_executions - Filter by workflowId', - allMatch, - allMatch ? `All ${filteredData.data.length} executions match workflow ${testWorkflowId}` : 'Filter not working correctly' - ); - if (allMatch) testsPassed++; - } else { - logger.test('list_executions - Filter by workflowId', false, 'No executions to test filtering'); - } - } catch (error) { - logger.test('list_executions - Filter by workflowId', false, error.message); - } - - // Test 2.6: includeData parameter - testsTotal++; - try { - const withoutData = await callTool('list_executions', { limit: 1, includeData: false }); - const withData = await callTool('list_executions', { limit: 1, includeData: true }); - - const withoutDataObj = JSON.parse(withoutData.content[0].text); - const withDataObj = JSON.parse(withData.content[0].text); - - if (withoutDataObj.data && withoutDataObj.data.length > 0 && withDataObj.data && withDataObj.data.length > 0) { - const withoutDataSize = JSON.stringify(withoutDataObj.data[0]).length; - const withDataSize = JSON.stringify(withDataObj.data[0]).length; - - const isValid = withDataSize >= withoutDataSize; - logger.test( - 'list_executions - includeData parameter', - isValid, - isValid ? `Data size: without=${withoutDataSize}, with=${withDataSize}` : 'includeData not working' - ); - if (isValid) testsPassed++; - } else { - logger.test('list_executions - includeData parameter', false, 'No executions to test'); - } - } catch (error) { - logger.test('list_executions - includeData parameter', false, error.message); - } - - return { passed: testsPassed, total: testsTotal }; -} - -// ======================================== -// Test Suite: get_execution -// ======================================== - -async function testGetExecution() { - logger.info('\n--- Task 3: Validate get_execution ---\n'); - - let testsPassed = 0; - let testsTotal = 0; - - // Ensure we have execution IDs to test - if (testState.testExecutionIds.length === 0) { - logger.warn('No execution IDs available for testing. Skipping get_execution tests.'); - return { passed: 0, total: 0 }; - } - - const testExecutionId = testState.testExecutionIds[0]; - logger.info(`Using execution ID: ${testExecutionId}`); - - // Test 3.1: Retrieve execution by ID - testsTotal++; - try { - const result = await callTool('get_execution', { id: testExecutionId }); - const execution = JSON.parse(result.content[0].text); - - const isValid = execution && execution.id === testExecutionId; - logger.test( - 'get_execution - Retrieve by ID', - isValid, - isValid ? `Execution retrieved successfully` : 'Failed to retrieve execution' - ); - if (isValid) testsPassed++; - } catch (error) { - logger.test('get_execution - Retrieve by ID', false, error.message); - } - - // Test 3.2: Structure validation - testsTotal++; - try { - const result = await callTool('get_execution', { id: testExecutionId }); - const execution = JSON.parse(result.content[0].text); - - const hasRequiredFields = - execution.hasOwnProperty('id') && - execution.hasOwnProperty('finished') && - execution.hasOwnProperty('mode') && - execution.hasOwnProperty('startedAt') && - execution.hasOwnProperty('workflowId'); - - logger.test( - 'get_execution - Structure validation', - hasRequiredFields, - hasRequiredFields ? 'All required fields present' : 'Missing required fields' - ); - if (hasRequiredFields) testsPassed++; - } catch (error) { - logger.test('get_execution - Structure validation', false, error.message); - } - - // Test 3.3: Execution data completeness - testsTotal++; - try { - const result = await callTool('get_execution', { id: testExecutionId, includeData: true }); - const execution = JSON.parse(result.content[0].text); - - const hasData = execution.data !== undefined; - logger.test( - 'get_execution - Data completeness', - hasData, - hasData ? 'Execution data present' : 'No execution data' - ); - if (hasData) testsPassed++; - } catch (error) { - logger.test('get_execution - Data completeness', false, error.message); - } - - // Test 3.4: 404 for non-existent ID - testsTotal++; - try { - await callTool('get_execution', { id: '99999999' }); - logger.test('get_execution - 404 for non-existent ID', false, 'Should have returned error'); - } catch (error) { - const is404 = error.message.includes('404') || error.message.includes('not found') || error.message.includes('Not Found'); - logger.test( - 'get_execution - 404 for non-existent ID', - is404, - is404 ? 'Correctly returned 404' : 'Wrong error type' - ); - if (is404) testsPassed++; - } - - return { passed: testsPassed, total: testsTotal }; -} - -// ======================================== -// Test Suite: delete_execution -// ======================================== - -async function testDeleteExecution() { - logger.info('\n--- Task 4: Validate delete_execution ---\n'); - - let testsPassed = 0; - let testsTotal = 0; - - // Test 4.1: Delete execution and verify - testsTotal++; - try { - // Use the last execution ID if available - if (testState.testExecutionIds.length > 0) { - const execIdToDelete = testState.testExecutionIds[testState.testExecutionIds.length - 1]; - - // Delete execution - await callTool('delete_execution', { id: execIdToDelete }); - - // Verify it's gone - try { - await callTool('get_execution', { id: execIdToDelete }); - logger.test('delete_execution - Delete and verify', false, 'Execution still exists after deletion'); - } catch (error) { - const is404 = error.message.includes('404') || error.message.includes('not found'); - logger.test( - 'delete_execution - Delete and verify', - is404, - is404 ? 'Execution successfully deleted' : 'Unexpected error' - ); - if (is404) testsPassed++; - } - } else { - logger.test('delete_execution - Delete and verify', false, 'No executions available to delete'); - } - } catch (error) { - logger.test('delete_execution - Delete and verify', false, error.message); - } - - // Test 4.2: 404 for non-existent ID - testsTotal++; - try { - await callTool('delete_execution', { id: 99999999 }); - logger.test('delete_execution - 404 for non-existent ID', false, 'Should have returned error'); - } catch (error) { - const is404 = error.message.includes('404') || error.message.includes('not found'); - logger.test( - 'delete_execution - 404 for non-existent ID', - is404, - is404 ? 'Correctly returned 404' : 'Wrong error type' - ); - if (is404) testsPassed++; - } - - return { passed: testsPassed, total: testsTotal }; -} - -// ======================================== -// Test Suite: Error Handling -// ======================================== - -async function testErrorHandling() { - logger.info('\n--- Task 5: Error Handling Validation ---\n'); - - logger.info('Error handling tests are integrated into each API method test suite:'); - logger.info(' - 404 errors for non-existent resources'); - logger.info(' - Error response format validation'); - logger.info(' - Invalid parameter handling'); - - return { passed: 0, total: 0 }; // Counted in individual test suites -} - -// ======================================== -// Cleanup -// ======================================== - -async function cleanup() { - if (!config.testFlags.runCleanup) { - logger.info('Cleanup disabled by configuration'); - return; - } - - logger.info('\n======================================================================'); - logger.info(' Cleanup'); - logger.info('======================================================================\n'); - - let cleanedWorkflows = 0; - - // Clean up test workflows - if (testState.createdWorkflows.length > 0) { - logger.info(`Cleaning up ${testState.createdWorkflows.length} test workflows...`); - - for (const workflowId of testState.createdWorkflows) { - try { - await callTool('delete_workflow', { id: workflowId }); - cleanedWorkflows++; - } catch (error) { - logger.debug(`Failed to delete workflow ${workflowId}: ${error.message}`); - } - } - - logger.success(`✓ Cleaned up ${cleanedWorkflows}/${testState.createdWorkflows.length} test workflows`); - } -} - -// ======================================== -// Main Test Runner -// ======================================== - -async function runTests() { - console.error('======================================================================'); - console.error(' Executions API Validation Test Suite - Story 2.2'); - console.error('======================================================================\n'); - - logger.info('Testing 3 Executions API methods against live n8n instance'); - logger.info(`MCP Server: ${config.mcpServerUrl}\n`); - - const results = { - list: { passed: 0, total: 0 }, - get: { passed: 0, total: 0 }, - delete: { passed: 0, total: 0 }, - error: { passed: 0, total: 0 } - }; - - try { - // Pre-flight checks - console.error('--- Pre-flight Checks ---\n'); - - const health = await axios.get(config.healthCheckUrl); - logger.info(`Server health: ${health.data.status}\n`); - - // Generate/find test executions - if (config.testFlags.generateExecutions) { - await generateTestExecutions(); - } - - // Run test suites - if (config.testFlags.runListTests) { - results.list = await testListExecutions(); - } - - if (config.testFlags.runGetTests) { - results.get = await testGetExecution(); - } - - if (config.testFlags.runDeleteTests) { - results.delete = await testDeleteExecution(); - } - - results.error = await testErrorHandling(); - - // Cleanup - await cleanup(); - - // Summary - console.error('\n======================================================================'); - console.error(' Test Summary Report'); - console.error('======================================================================\n'); - - const totalPassed = results.list.passed + results.get.passed + results.delete.passed + results.error.passed; - const totalTests = results.list.total + results.get.total + results.delete.total + results.error.total; - - console.error(`Total tests executed: ${totalTests}`); - console.error(`Passed: ${totalPassed} (${totalTests > 0 ? Math.round(totalPassed/totalTests*100) : 0}%)`); - console.error(`Failed: ${totalTests - totalPassed}`); - console.error(`Skipped: 0\n`); - - console.error('Test categories:'); - console.error(` list: ${results.list.passed}/${results.list.total} (${results.list.total > 0 ? Math.round(results.list.passed/results.list.total*100) : 0}%)`); - console.error(` get: ${results.get.passed}/${results.get.total} (${results.get.total > 0 ? Math.round(results.get.passed/results.get.total*100) : 0}%)`); - console.error(` delete: ${results.delete.passed}/${results.delete.total} (${results.delete.total > 0 ? Math.round(results.delete.passed/results.delete.total*100) : 0}%)`); - - console.error('\n======================================================================'); - if (totalPassed === totalTests && totalTests > 0) { - console.error('✓ ALL TESTS PASSED!'); - } else { - console.error(`⚠ ${totalTests - totalPassed} TESTS FAILED`); - } - console.error('======================================================================'); - - process.exit(totalPassed === totalTests ? 0 : 1); - - } catch (error) { - logger.error(`Test suite failed: ${error.message}`); - console.error(error.stack); - process.exit(1); - } -} - -// Run tests -runTests(); diff --git a/test-list-credentials.js b/test-list-credentials.js deleted file mode 100644 index 509ac9c..0000000 --- a/test-list-credentials.js +++ /dev/null @@ -1,109 +0,0 @@ -#!/usr/bin/env node - -/** - * Test: list_credentials MCP tool - * Tests the new Credentials API list endpoint - */ - -const axios = require('axios'); - -const config = { - mcpServerUrl: 'http://localhost:3456/mcp' -}; - -let requestId = 1; - -async function sendMcpRequest(method, params = {}) { - const response = await axios.post(config.mcpServerUrl, { - jsonrpc: '2.0', - id: requestId++, - method, - params - }); - return response.data.result; -} - -async function callTool(name, args = {}) { - const result = await sendMcpRequest('tools/call', { name, arguments: args }); - if (result.isError) { - const errorMessage = result.content && result.content[0] && result.content[0].text - ? result.content[0].text - : 'Unknown error'; - throw new Error(errorMessage); - } - return result; -} - -async function main() { - console.log('\n=== Test: list_credentials MCP tool ===\n'); - - try { - // Test 1: List all credentials - console.log('Test 1: List all credentials'); - const result = await callTool('list_credentials'); - const credentials = JSON.parse(result.content[0].text); - - console.log(`✅ Successfully retrieved credentials list`); - console.log(` Found ${credentials.data.length} credentials`); - - if (credentials.data.length > 0) { - console.log('\n📋 Credentials metadata:'); - credentials.data.forEach((cred, index) => { - console.log(`\n${index + 1}. ${cred.name}`); - console.log(` ID: ${cred.id}`); - console.log(` Type: ${cred.type}`); - console.log(` Created: ${cred.createdAt}`); - console.log(` Updated: ${cred.updatedAt}`); - - if (cred.nodesAccess && cred.nodesAccess.length > 0) { - console.log(` Nodes Access:`); - cred.nodesAccess.forEach(access => { - console.log(` - ${access.nodeType} (${access.date})`); - }); - } else { - console.log(` Nodes Access: None`); - } - - // Security check: ensure no sensitive data - if (cred.data) { - console.log(' ⚠️ WARNING: Sensitive data field found!'); - } else { - console.log(` ✅ Security: No sensitive data returned`); - } - }); - - console.log('\n📊 Credential types summary:'); - const typeCount = credentials.data.reduce((acc, cred) => { - acc[cred.type] = (acc[cred.type] || 0) + 1; - return acc; - }, {}); - Object.entries(typeCount).forEach(([type, count]) => { - console.log(` - ${type}: ${count}`); - }); - } else { - console.log('ℹ️ No credentials found in this n8n instance'); - } - - // Test 2: Pagination (if applicable) - if (credentials.data.length >= 5) { - console.log('\n\nTest 2: Pagination with limit'); - const limitedResult = await callTool('list_credentials', { limit: 2 }); - const limitedCredentials = JSON.parse(limitedResult.content[0].text); - console.log(`✅ Retrieved ${limitedCredentials.data.length} credentials with limit=2`); - if (limitedCredentials.nextCursor) { - console.log(` Next cursor: ${limitedCredentials.nextCursor}`); - } - } - - console.log('\n✅ All tests passed!\n'); - - } catch (error) { - console.error(`\n❌ Error: ${error.message}\n`); - if (error.response) { - console.error('Response data:', error.response.data); - } - process.exit(1); - } -} - -main(); diff --git a/test-mcp-tools.js b/test-mcp-tools.js deleted file mode 100644 index 7d7f61a..0000000 --- a/test-mcp-tools.js +++ /dev/null @@ -1,775 +0,0 @@ -#!/usr/bin/env node - -/** - * Тестовый скрипт для проверки инструментов MCP сервера n8n-workflow-builder - * Проверяет все доступные методы через прямые вызовы API сервера MCP - */ - -const fetch = require('node-fetch'); - -// Конфигурация -const config = { - mcpServerUrl: 'http://localhost:3456/mcp', - healthCheckUrl: 'http://localhost:3456/health', - testWorkflowName: 'Тестовый рабочий процесс MCP', - newWorkflowName: 'Обновленный тестовый процесс MCP', - testTagName: 'Тестовый тег MCP', - maxRetries: 3, - retryDelay: 1000, - n8nApiUrl: 'http://localhost:5678/api' -}; - -// Управление тестами -const testFlags = { - runWorkflowTests: true, - runTagTests: true, - runExecutionTests: true, - runCleanup: true // Флаг для отключения очистки (полезно для отладки) -}; - -// Хранилище для идентификаторов созданных объектов и результатов тестов -const testData = { - workflowId: null, - tagId: null, - executionId: null, - workflowActivated: false, - testResults: { - passed: 0, - failed: 0, - tests: {} - } -}; - -// Структурированный вывод логов -class Logger { - info(message) { - console.log(`[INFO] ${message}`); - } - - success(message) { - console.log(`[SUCCESS] ${message}`); - } - - error(message, error) { - if (error && error.message) { - console.error(`[ERROR] ${message}`, error.message); - } else { - console.error(`[ERROR] ${message}`, error || ''); - } - } - - warn(message) { - console.log(`[WARN] ${message}`); - } - - test(name, status) { - console.log(`[TEST] ${name}: ${status ? 'PASS' : 'FAIL'}`); - - // Сохраняем результаты тестов - testData.testResults.tests[name] = status; - if (status) { - testData.testResults.passed++; - } else { - testData.testResults.failed++; - } - } - - section(name) { - console.log(`\n===== ${name} =====\n`); - } - - debug(message, data) { - if (process.env.DEBUG) { - console.log(`[DEBUG] ${message}`, data ? JSON.stringify(data).substring(0, 200) + '...' : ''); - } - } - - summaryReport() { - const { passed, failed, tests } = testData.testResults; - const total = passed + failed; - const passRate = total > 0 ? Math.round((passed / total) * 100) : 0; - - this.section('Test Summary Report'); - console.log(`Total tests: ${total}`); - console.log(`Passed: ${passed} (${passRate}%)`); - console.log(`Failed: ${failed}`); - - if (failed > 0) { - console.log('\nFailed tests:'); - Object.entries(tests) - .filter(([_, status]) => !status) - .forEach(([name]) => console.log(`- ${name}`)); - } - - console.log('\nTest categories:'); - ['workflow', 'tag', 'execution'].forEach(category => { - const categoryTests = Object.entries(tests) - .filter(([name]) => name.toLowerCase().includes(category)) - .map(([_, status]) => status); - - const categoryTotal = categoryTests.length; - const categoryPassed = categoryTests.filter(Boolean).length; - const categoryRate = categoryTotal > 0 ? Math.round((categoryPassed / categoryTotal) * 100) : 0; - - console.log(`- ${category}: ${categoryPassed}/${categoryTotal} (${categoryRate}%)`); - }); - } -} - -const logger = new Logger(); - -/** - * Отправляет JSON-RPC запрос к MCP серверу с повторными попытками - * @param {string} method - Метод JSON-RPC - * @param {object} params - Параметры запроса - * @param {number} retries - Количество повторных попыток - * @returns {Promise} - Ответ сервера - */ -async function sendMcpRequest(method, params = {}, retries = config.maxRetries) { - try { - logger.debug(`Отправка запроса ${method}`, params); - - const response = await fetch(config.mcpServerUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - jsonrpc: '2.0', - method, - params, - id: Date.now() - }) - }); - - if (!response.ok) { - const errorText = await response.text(); - throw new Error(`HTTP error! Status: ${response.status}, Response: ${errorText}`); - } - - const data = await response.json(); - if (data.error) { - throw new Error(`JSON-RPC error: ${JSON.stringify(data.error)}`); - } - - logger.debug(`Получен ответ для ${method}`, data.result); - return data.result; - } catch (error) { - if (retries > 0) { - logger.warn(`Повторная попытка запроса ${method} (осталось попыток: ${retries})`); - await new Promise(resolve => setTimeout(resolve, config.retryDelay)); - return sendMcpRequest(method, params, retries - 1); - } - - logger.error(`Failed to send MCP request: ${method}`, error); - throw error; - } -} - -/** - * Проверяет работоспособность MCP сервера - */ -async function checkServerHealth() { - try { - const response = await fetch(config.healthCheckUrl); - if (!response.ok) { - throw new Error(`HTTP error! Status: ${response.status}`); - } - - const data = await response.json(); - logger.info(`Server health check: ${data.status}`); - return data.status === 'ok'; - } catch (error) { - logger.error('Server health check failed', error); - return false; - } -} - -/** - * Получает список инструментов MCP - * @returns {Promise} - Список инструментов - */ -async function getToolsList() { - try { - const result = await sendMcpRequest('tools/list'); - logger.info(`Found ${result.tools.length} tools available`); - return result.tools; - } catch (error) { - logger.error('Failed to get tools list', error); - throw error; - } -} - -/** - * Вызывает указанный инструмент MCP - * @param {string} name - Имя инструмента - * @param {object} arguments - Аргументы инструмента - * @returns {Promise} - Результат вызова инструмента - */ -async function callTool(name, arguments = {}) { - try { - const result = await sendMcpRequest('tools/call', { name, arguments }); - return result; - } catch (error) { - logger.error(`Failed to call tool: ${name}`, error); - throw error; - } -} - -/** - * Проверяет существование рабочего процесса - * @param {string} id - ID рабочего процесса - * @returns {Promise} - Существует ли рабочий процесс - */ -async function checkWorkflowExists(id) { - try { - const getResult = await callTool('get_workflow', { id }); - return !!getResult; - } catch (error) { - return false; - } -} - -/** - * Проверяет существование тега - * @param {string} id - ID тега - * @returns {Promise} - Существует ли тег - */ -async function checkTagExists(id) { - try { - const getResult = await callTool('get_tag', { id }); - return !!getResult; - } catch (error) { - return false; - } -} - -/** - * Создает тестовый рабочий процесс - */ -async function createTestWorkflow() { - try { - logger.info(`Creating test workflow: ${config.testWorkflowName}`); - - // Определяем структуру manual trigger, добавляя необходимые атрибуты по аналогии с GoogleCalendarTrigger - const manualTrigger = { - id: "ManualTrigger", - name: "Manual Trigger", - type: "n8n-nodes-base.manualTrigger", - typeVersion: 1, - position: [0, 0], - // Важные атрибуты для правильного распознавания как триггер - group: ['trigger'], - // Дополнительные атрибуты из GoogleCalendarTrigger - inputs: [], - outputs: [ - { - type: "main", // Соответствует NodeConnectionType.Main в GoogleCalendarTrigger - index: 0 - } - ], - // Уникальный идентификатор для этого триггера - triggerId: "manual-trigger-" + Date.now() - }; - - // Узел Set для установки данных - const setNode = { - id: "Set", - name: "Set", - type: "n8n-nodes-base.set", - parameters: { - propertyValues: { - number: [ - { - name: "test", - value: 1, - type: "number" - } - ] - }, - options: { - dotNotation: true - }, - mode: "manual" - }, - typeVersion: 1, - position: [220, 0] - }; - - // Подготавливаем подключения между узлами - const connections = [ - { - source: manualTrigger.id, - sourceOutput: 0, - target: setNode.id, - targetInput: 0 - } - ]; - - // Создаем рабочий процесс - const createResult = await callTool('create_workflow', { - name: config.testWorkflowName, - nodes: [manualTrigger, setNode], - connections - }); - - // Обрабатываем результат - if (createResult) { - const createdWorkflow = JSON.parse(createResult.content[0].text); - testData.workflowId = createdWorkflow.id; - - logger.test('create_workflow', !!testData.workflowId); - logger.info(`Created workflow with ID: ${testData.workflowId}`); - return true; - } else { - throw new Error('No result from create_workflow call'); - } - } catch (error) { - logger.error('Failed to create test workflow', error); - logger.test('create_workflow', false); - return false; - } -} - -/** - * Обновляет рабочий процесс - */ -async function updateWorkflow() { - if (!testData.workflowId) { - logger.warn('No workflow ID available for update, skipping'); - return false; - } - - try { - logger.info(`Updating workflow name to: ${config.newWorkflowName}`); - - // Получаем текущий workflow для сохранения структуры узлов - const getResult = await callTool('get_workflow', { id: testData.workflowId }); - const currentWorkflow = JSON.parse(getResult.content[0].text); - - // Сохраняем исходную структуру узлов - const nodes = currentWorkflow.nodes; - - // Преобразуем структуру connections для API - const connectionsStructure = currentWorkflow.connections; - logger.info(`Workflow connections structure: ${JSON.stringify(connectionsStructure).substring(0, 100)}...`); - - // Преобразуем в формат, который ожидает API - const transformedConnections = []; - for (const sourceNode in connectionsStructure) { - const sourceConnections = connectionsStructure[sourceNode]; - if (sourceConnections && sourceConnections.main) { - sourceConnections.main.forEach((targetConnections, sourceIndex) => { - targetConnections.forEach(targetConnection => { - transformedConnections.push({ - source: sourceNode, - sourceOutput: sourceIndex, - target: targetConnection.node, - targetInput: targetConnection.index || 0 - }); - }); - }); - } - } - logger.info(`Transformed connections: ${JSON.stringify(transformedConnections).substring(0, 100)}...`); - - // Обновляем только имя, сохраняя все ноды, включая триггер - const updateResult = await callTool('update_workflow', { - id: testData.workflowId, - name: config.newWorkflowName, - nodes: nodes, - connections: transformedConnections - }); - - logger.test('update_workflow', !!updateResult); - return !!updateResult; - } catch (error) { - logger.error('Failed to update workflow', error); - logger.test('update_workflow', false); - return false; - } -} - -/** - * Генерирует UUID v4 - * @returns {string} UUID версии 4 - */ -function generateUUID() { - return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { - const r = Math.random() * 16 | 0, - v = c === 'x' ? r : (r & 0x3 | 0x8); - return v.toString(16); - }); -} - -/** - * Тесты для инструментов управления тегами - */ -async function runTagTests() { - logger.section('Tag Tools Tests'); - - try { - // 0. Предварительная очистка - получить и удалить ВСЕ существующие теги - logger.info('Getting all existing tags for cleanup'); - const allTagsResult = await callTool('get_tags', {}); - const allTags = JSON.parse(allTagsResult.content[0].text); - - if (allTags && allTags.data && allTags.data.length > 0) { - logger.info(`Found ${allTags.data.length} existing tags, cleaning up all tags`); - - // Удаляем все теги для обеспечения чистого окружения - for (const tag of allTags.data) { - logger.info(`Deleting existing tag: ${tag.id} (${tag.name})`); - try { - await callTool('delete_tag', { id: tag.id }); - // Добавляем небольшую задержку между удалениями тегов - await new Promise(resolve => setTimeout(resolve, 500)); - } catch (error) { - logger.warn(`Failed to delete tag ${tag.id}, continuing with tests`); - } - } - - // Дополнительная пауза после удаления всех тегов - await new Promise(resolve => setTimeout(resolve, 1000)); - } else { - logger.info('No existing tags found, proceeding with tests'); - } - - // 1. Создание тега - logger.info(`Creating test tag: ${config.testTagName}`); - const createResult = await callTool('create_tag', { name: config.testTagName }); - - const createdTag = JSON.parse(createResult.content[0].text); - testData.tagId = createdTag.id; - - logger.test('create_tag', !!testData.tagId); - logger.info(`Created tag with ID: ${testData.tagId}`); - - // Небольшая пауза после создания тега - await new Promise(resolve => setTimeout(resolve, 500)); - - // Проверяем, что тег действительно создан - const tagExists = await checkTagExists(testData.tagId); - if (!tagExists) { - throw new Error(`Tag ${testData.tagId} was not created properly`); - } - - // 2. Получение тегов - logger.info('Getting all tags'); - const getResult = await callTool('get_tags', {}); - logger.test('get_tags', !!getResult); - - // 3. Получение тега по ID - if (testData.tagId) { - logger.info(`Getting tag by ID: ${testData.tagId}`); - const getTagResult = await callTool('get_tag', { id: testData.tagId }); - logger.test('get_tag', !!getTagResult); - } - - // 4. Обновление тега - if (testData.tagId) { - // Формируем полностью уникальное имя для тега, используя UUID вместо временной метки - const uuid = generateUUID(); - let uniqueTagName = `${config.testTagName}-${uuid}`; - logger.info(`Updating tag name to: ${uniqueTagName}`); - - try { - // Добавляем задержку перед обновлением - await new Promise(resolve => setTimeout(resolve, 500)); - - // Получаем список всех тегов, чтобы проверить уникальность имени - const checkTagsResult = await callTool('get_tags', {}); - const allTagsBeforeUpdate = JSON.parse(checkTagsResult.content[0].text); - - // Проверяем, что имя уникально - const tagWithSameName = allTagsBeforeUpdate.data.find(tag => tag.name === uniqueTagName); - if (tagWithSameName) { - logger.warn(`Tag with name ${uniqueTagName} already exists, generating a new name`); - // Пробуем с другим UUID - const newUuid = generateUUID(); - uniqueTagName = `${config.testTagName}-${newUuid}`; - logger.info(`New tag name: ${uniqueTagName}`); - } - - const updateResult = await callTool('update_tag', { - id: testData.tagId, - name: uniqueTagName - }); - logger.test('update_tag', !!updateResult); - } catch (error) { - logger.error('Failed to update tag', error); - logger.test('update_tag', false); - // Не прерываем тест из-за ошибки обновления тега, так как это не критично - } - } - - } catch (error) { - logger.error('Tag tests failed', error); - throw error; - } -} - -/** - * Тесты для инструментов управления рабочими процессами - */ -async function runWorkflowTests() { - logger.section('Workflow Tools Tests'); - - try { - // 1. Список рабочих процессов - logger.info('Testing list_workflows tool...'); - const workflowsList = await callTool('list_workflows'); - logger.test('list_workflows', !!workflowsList); - - // Проверяем наличие уже существующих рабочих процессов - const existingWorkflows = JSON.parse(workflowsList.content[0].text); - if (existingWorkflows.data && existingWorkflows.data.length > 0) { - logger.warn(`Found ${existingWorkflows.data.length} existing workflows`); - } - - // 2. Создание рабочего процесса - await createTestWorkflow(); - - // Проверяем, что рабочий процесс действительно создан - const workflowExists = await checkWorkflowExists(testData.workflowId); - if (!workflowExists) { - throw new Error(`Workflow ${testData.workflowId} was not created properly`); - } - - // 3. Получение рабочего процесса - logger.info(`Getting workflow by ID: ${testData.workflowId}`); - const getResult = await callTool('get_workflow', { id: testData.workflowId }); - logger.test('get_workflow', !!getResult); - - // 4. Обновление рабочего процесса - await updateWorkflow(); - - // 5. Активация рабочего процесса - try { - logger.info(`Activating workflow: ${testData.workflowId}`); - const activateResult = await callTool('activate_workflow', { id: testData.workflowId }); - testData.workflowActivated = true; - logger.test('activate_workflow', !!activateResult); - } catch (error) { - logger.error('Activation failed', error); - logger.test('activate_workflow', false); - // Прерываем тест, если не удалось активировать рабочий процесс - throw error; - } - - } catch (error) { - logger.error('Workflow tests failed', error); - throw error; // Пробрасываем ошибку дальше - } -} - -/** - * Тесты для инструментов управления выполнениями workflow - */ -async function runExecutionTests() { - logger.section('Execution Tools Tests'); - - if (!testData.workflowId) { - logger.warn('No workflow ID available for execution tests, skipping'); - return; - } - - try { - // 1. Выполнение рабочего процесса - // Для workflow с manual trigger должно правильно работать через API - logger.info(`Executing workflow: ${testData.workflowId}`); - - try { - // Ждем некоторое время после активации workflow - logger.info('Waiting for workflow activation to complete...'); - await new Promise(resolve => setTimeout(resolve, 2000)); - - // Запускаем workflow - const executeResult = await callTool('execute_workflow', { - id: testData.workflowId, - // Передаем пустые данные для запуска - runData: {} - }); - logger.test('execute_workflow', !!executeResult); - } catch (error) { - // Если manual trigger не распознается системой для выполнения, - // обрабатываем ошибку как ожидаемую - if (error.message && (error.message.includes('Status: 404') || error.message.includes('Status: 400'))) { - logger.warn(`Manual trigger execution returned error - this is an expected limitation in n8n 1.82`); - logger.warn(`See documentation about workflow execution limitations with trigger nodes`); - // Помечаем тест как пройденный, так как это ожидаемое поведение - logger.test('execute_workflow', true); - } else { - // Другие ошибки считаем проблемой - logger.error('Failed to execute workflow', error); - logger.test('execute_workflow', false); - } - } - - // Ждем некоторое время чтобы дать системе обработать выполнение - await new Promise(resolve => setTimeout(resolve, 2000)); - - // 2. Получение списка выполнений - logger.info('Getting executions list'); - const listExecutionsResult = await callTool('list_executions', {}); - logger.test('list_executions', !!listExecutionsResult); - - // Анализируем результат для получения ID выполнения - try { - const executions = JSON.parse(listExecutionsResult.content[0].text); - if (executions && executions.data && executions.data.length > 0) { - const execution = executions.data.find(exec => exec.workflowId === testData.workflowId); - if (execution) { - testData.executionId = execution.id; - logger.info(`Found execution with ID: ${testData.executionId}`); - - // 3. Получение данных о выполнении по ID - logger.info(`Getting execution details for ID: ${testData.executionId}`); - const getExecutionResult = await callTool('get_execution', { id: testData.executionId }); - logger.test('get_execution', !!getExecutionResult); - - // 4. Удаление выполнения - logger.info(`Deleting execution: ${testData.executionId}`); - const deleteExecutionResult = await callTool('delete_execution', { id: testData.executionId }); - logger.test('delete_execution', !!deleteExecutionResult); - } else { - logger.info(`No executions found for workflow ${testData.workflowId}`); - } - } else { - logger.info('No executions found for testing get_execution and delete_execution'); - } - } catch (error) { - logger.error('Failed to parse executions list', error); - } - - } catch (error) { - logger.error('Execution tests failed', error); - throw error; - } -} - -/** - * Деактивация рабочего процесса перед удалением - */ -async function deactivateWorkflow() { - try { - if (testData.workflowId && testData.workflowActivated) { - logger.info(`Deactivating workflow: ${testData.workflowId}`); - const deactivateResult = await callTool('deactivate_workflow', { id: testData.workflowId }); - logger.test('deactivate_workflow', !!deactivateResult); - testData.workflowActivated = false; - } - } catch (error) { - logger.error('Failed to deactivate workflow', error); - } -} - -/** - * Очистка тестовых данных - */ -async function cleanup() { - if (!testFlags.runCleanup) { - logger.warn('Cleanup is disabled, skipping'); - return; - } - - logger.section('Cleanup'); - - // Сначала деактивируем рабочий процесс - await deactivateWorkflow(); - - // Удаление тестового рабочего процесса - if (testData.workflowId) { - try { - // Проверяем, существует ли рабочий процесс перед удалением - const workflowExists = await checkWorkflowExists(testData.workflowId); - if (workflowExists) { - logger.info(`Deleting test workflow: ${testData.workflowId}`); - const deleteWorkflowResult = await callTool('delete_workflow', { id: testData.workflowId }); - logger.test('delete_workflow', !!deleteWorkflowResult); - } else { - logger.warn(`Workflow ${testData.workflowId} no longer exists, skipping deletion`); - } - } catch (error) { - logger.error(`Failed to delete workflow: ${testData.workflowId}`, error); - } - } - - // Удаление тестового тега - if (testData.tagId) { - try { - // Проверяем, существует ли тег перед удалением - const tagExists = await checkTagExists(testData.tagId); - if (tagExists) { - logger.info(`Deleting test tag: ${testData.tagId}`); - const deleteTagResult = await callTool('delete_tag', { id: testData.tagId }); - logger.test('delete_tag', !!deleteTagResult); - } else { - logger.warn(`Tag ${testData.tagId} no longer exists, skipping deletion`); - } - } catch (error) { - logger.error(`Failed to delete tag: ${testData.tagId}`, error); - } - } -} - -/** - * Основная функция запуска тестов - */ -async function runTests() { - logger.section('MCP Server Tests'); - - try { - // Проверка работоспособности сервера - const isHealthy = await checkServerHealth(); - if (!isHealthy) { - logger.error('MCP server is not healthy, aborting tests'); - return; - } - - // Получение списка инструментов - const tools = await getToolsList(); - logger.info(`Available tools: ${tools.map(t => t.name).join(', ')}`); - - // Оптимальная последовательность запуска тестов: - // 1. Сначала создаем рабочие процессы и теги - if (testFlags.runWorkflowTests) { - await runWorkflowTests(); - } - - if (testFlags.runTagTests) { - await runTagTests(); - } - - // 2. Затем выполняем рабочие процессы и тестируем выполнения - if (testFlags.runExecutionTests) { - await runExecutionTests(); - } - - // 3. В конце очищаем все созданные данные - await cleanup(); - - logger.section('Tests Completed'); - - // 4. Выводим сводный отчет о результатах тестирования - logger.summaryReport(); - - } catch (error) { - logger.error('Tests failed', error); - // Пытаемся выполнить очистку даже при ошибке - try { - await cleanup(); - } catch (cleanupError) { - logger.error('Cleanup failed after test error', cleanupError); - } - - // Все равно выводим отчет о результатах, даже если были ошибки - logger.summaryReport(); - } -} - -// Запускаем тесты -runTests().catch(error => { - logger.error('Unhandled error', error); - process.exit(1); -}); \ No newline at end of file diff --git a/test-notification.js b/test-notification.js deleted file mode 100644 index 96bc126..0000000 --- a/test-notification.js +++ /dev/null @@ -1,150 +0,0 @@ -#!/usr/bin/env node - -/** - * Test script to verify MCP notification handling - * Tests both notifications (no id) and regular requests (with id) - */ - -const http = require('http'); - -const PORT = process.env.MCP_PORT || 3456; -const HOST = 'localhost'; - -// Helper function to send JSON-RPC request -function sendRequest(data) { - return new Promise((resolve, reject) => { - const postData = JSON.stringify(data); - - const options = { - hostname: HOST, - port: PORT, - path: '/mcp', - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Content-Length': Buffer.byteLength(postData) - } - }; - - const req = http.request(options, (res) => { - let body = ''; - - res.on('data', (chunk) => { - body += chunk; - }); - - res.on('end', () => { - resolve({ - statusCode: res.statusCode, - statusMessage: res.statusMessage, - headers: res.headers, - body: body ? JSON.parse(body) : null - }); - }); - }); - - req.on('error', reject); - req.write(postData); - req.end(); - }); -} - -// Test cases -async function runTests() { - console.log('Testing MCP Server Notification Handling\n'); - console.log('==========================================\n'); - - try { - // Test 1: Send notification (no id field) - console.log('Test 1: Sending notification/initialized (should return 204 No Content)'); - const notificationRequest = { - jsonrpc: '2.0', - method: 'notifications/initialized', - params: {} - // Note: no 'id' field - this makes it a notification - }; - - const notificationResult = await sendRequest(notificationRequest); - console.log(` Status Code: ${notificationResult.statusCode}`); - console.log(` Status Message: ${notificationResult.statusMessage}`); - console.log(` Response Body: ${notificationResult.body ? JSON.stringify(notificationResult.body) : 'null (empty)'}`); - - if (notificationResult.statusCode === 204) { - console.log(' ✅ PASS: Notification handled correctly with 204 No Content\n'); - } else { - console.log(' ❌ FAIL: Expected 204, got', notificationResult.statusCode, '\n'); - } - - // Test 2: Send regular request (with id field) - console.log('Test 2: Sending tools/list request (should return 200 with JSON response)'); - const requestWithId = { - jsonrpc: '2.0', - method: 'tools/list', - params: {}, - id: 1 - }; - - const requestResult = await sendRequest(requestWithId); - console.log(` Status Code: ${requestResult.statusCode}`); - console.log(` Response Body: ${requestResult.body ? 'JSON response received' : 'null'}`); - - if (requestResult.statusCode === 200 && requestResult.body) { - console.log(' ✅ PASS: Regular request handled correctly with JSON response\n'); - } else { - console.log(' ❌ FAIL: Expected 200 with JSON body\n'); - } - - // Test 3: Send notification/cancelled - console.log('Test 3: Sending notification/cancelled (should return 204 No Content)'); - const cancelNotification = { - jsonrpc: '2.0', - method: 'notifications/cancelled', - params: { requestId: 123 } - // Note: no 'id' field - }; - - const cancelResult = await sendRequest(cancelNotification); - console.log(` Status Code: ${cancelResult.statusCode}`); - console.log(` Status Message: ${cancelResult.statusMessage}`); - - if (cancelResult.statusCode === 204) { - console.log(' ✅ PASS: Cancel notification handled correctly\n'); - } else { - console.log(' ❌ FAIL: Expected 204, got', cancelResult.statusCode, '\n'); - } - - // Test 4: Health check - console.log('Test 4: Health check (should return 200)'); - const healthResult = await new Promise((resolve, reject) => { - http.get(`http://${HOST}:${PORT}/health`, (res) => { - let body = ''; - res.on('data', chunk => body += chunk); - res.on('end', () => resolve({ - statusCode: res.statusCode, - body: JSON.parse(body) - })); - }).on('error', reject); - }); - - console.log(` Status Code: ${healthResult.statusCode}`); - console.log(` Response: ${JSON.stringify(healthResult.body)}`); - - if (healthResult.statusCode === 200) { - console.log(' ✅ PASS: Health check successful\n'); - } else { - console.log(' ❌ FAIL: Expected 200\n'); - } - - console.log('=========================================='); - console.log('All tests completed!'); - - } catch (error) { - console.error('Test failed with error:', error.message); - console.error('\nMake sure the MCP server is running with:'); - console.error(' MCP_STANDALONE=true npm start'); - process.exit(1); - } -} - -// Run tests -runTests(); diff --git a/test-patch-check.js b/test-patch-check.js deleted file mode 100644 index 5d069ea..0000000 --- a/test-patch-check.js +++ /dev/null @@ -1,124 +0,0 @@ -#!/usr/bin/env node - -/** - * Quick Test: Check if PATCH /workflows/{id} works on n8n v2.1.4 - */ - -const axios = require('axios'); - -const config = { - mcpServerUrl: 'http://localhost:3456/mcp', - healthCheckUrl: 'http://localhost:3456/health' -}; - -let requestId = 1; - -async function sendMcpRequest(method, params = {}) { - const response = await axios.post(config.mcpServerUrl, { - jsonrpc: '2.0', - id: requestId++, - method, - params - }); - return response.data.result; -} - -async function callTool(name, args = {}) { - const result = await sendMcpRequest('tools/call', { name, arguments: args }); - if (result.isError) { - const errorMessage = result.content && result.content[0] && result.content[0].text - ? result.content[0].text - : 'Unknown error'; - throw new Error(errorMessage); - } - return result; -} - -async function main() { - console.error('=== Проверка: PATCH /workflows/{id} на n8n v2.1.4 ===\n'); - - let workflowId = null; - - try { - // Check server health - const health = await axios.get(config.healthCheckUrl); - console.error(`✓ Сервер работает: ${health.data.status}\n`); - - // Step 1: Create test workflow - console.error('Шаг 1: Создаю тестовый workflow...'); - const createResult = await callTool('create_workflow', { - name: 'PATCH Test Workflow', - nodes: [ - { - name: 'Start', - type: 'n8n-nodes-base.scheduleTrigger', - position: [250, 300], - parameters: { - rule: { interval: [{ field: 'hours', hoursInterval: 1 }] } - } - } - ], - connections: [], - tags: ['test-tag'] - }); - const workflow = JSON.parse(createResult.content[0].text); - workflowId = workflow.id; - console.error(`✓ Workflow создан: ${workflowId}`); - console.error(` Имя: ${workflow.name}`); - console.error(` Теги: ${workflow.tags}\n`); - - // Step 2: Try PATCH to update name only - console.error('Шаг 2: Пробую PATCH для изменения имени...'); - try { - const patchResult = await callTool('patch_workflow', { - id: workflowId, - name: 'UPDATED via PATCH on v2.1.4' - }); - const patched = JSON.parse(patchResult.content[0].text); - - console.error('✅ УСПЕХ! PATCH работает!'); - console.error(` Новое имя: ${patched.name}`); - console.error(` Nodes сохранены: ${patched.nodes.length} nodes`); - console.error(` Теги сохранены: ${patched.tags}\n`); - - console.error('🎉 PATCH МЕТОД РАБОТАЕТ НА n8n v2.1.4! 🎉'); - console.error('Story 2.4 можно разблокировать!\n'); - - } catch (patchError) { - if (patchError.message.includes('405')) { - console.error('❌ PATCH всё ещё не поддерживается'); - console.error(' Ошибка: 405 Method Not Allowed'); - console.error(' Story 2.4 остаётся BLOCKED\n'); - } else if (patchError.message.includes('DISABLED')) { - console.error('⚠️ Инструмент patch_workflow отключен'); - console.error(' Нужно включить в src/index.ts\n'); - } else { - throw patchError; - } - } - - // Cleanup - if (workflowId) { - console.error('Очистка: Удаляю тестовый workflow...'); - await callTool('delete_workflow', { id: workflowId }); - console.error('✓ Тестовый workflow удалён\n'); - } - - } catch (error) { - console.error(`\n✗ Ошибка: ${error.message}\n`); - - // Cleanup on error - if (workflowId) { - try { - await callTool('delete_workflow', { id: workflowId }); - console.error('✓ Тестовый workflow удалён\n'); - } catch (cleanupError) { - // Ignore cleanup errors - } - } - - process.exit(1); - } -} - -main(); diff --git a/test-patch-message.js b/test-patch-message.js deleted file mode 100644 index 57425e5..0000000 --- a/test-patch-message.js +++ /dev/null @@ -1,68 +0,0 @@ -#!/usr/bin/env node - -/** - * Test: patch_workflow informative message - */ - -const axios = require('axios'); - -const config = { - mcpServerUrl: 'http://localhost:3456/mcp' -}; - -let requestId = 1; - -async function sendMcpRequest(method, params = {}) { - const response = await axios.post(config.mcpServerUrl, { - jsonrpc: '2.0', - id: requestId++, - method, - params - }); - return response.data.result; -} - -async function callTool(name, args = {}) { - const result = await sendMcpRequest('tools/call', { name, arguments: args }); - if (result.isError) { - const errorMessage = result.content && result.content[0] && result.content[0].text - ? result.content[0].text - : 'Unknown error'; - throw new Error(errorMessage); - } - return result; -} - -async function main() { - console.log('\n=== Тест: patch_workflow информационное сообщение ===\n'); - - try { - // Call patch_workflow - const result = await callTool('patch_workflow', { - id: 'test-workflow-id', - name: 'New Name' - }); - - const response = JSON.parse(result.content[0].text); - - console.log('✅ УСПЕХ! Получен информационный ответ:\n'); - console.log(JSON.stringify(response, null, 2)); - - // Verify response structure - console.log('\n📋 Проверка структуры ответа:'); - console.log(` ✓ success: ${response.success === false ? 'false (ожидаемо)' : response.success}`); - console.log(` ✓ method: ${response.method}`); - console.log(` ✓ message: ${response.message}`); - console.log(` ✓ recommendation: ${response.recommendation ? 'присутствует' : 'отсутствует'}`); - console.log(` ✓ workaround: ${response.workaround ? 'присутствует' : 'отсутствует'}`); - console.log(` ✓ alternativeTools: ${response.alternativeTools ? 'присутствует' : 'отсутствует'}`); - - console.log('\n✅ Тест пройден! patch_workflow возвращает информативное сообщение.\n'); - - } catch (error) { - console.error(`\n❌ Ошибка: ${error.message}\n`); - process.exit(1); - } -} - -main(); diff --git a/test-patch-workflow.js b/test-patch-workflow.js deleted file mode 100644 index 38a666a..0000000 --- a/test-patch-workflow.js +++ /dev/null @@ -1,512 +0,0 @@ -#!/usr/bin/env node - -/** - * Test Suite: PATCH /workflows/{id} - Story 2.4 - * - * Comprehensive validation of patch_workflow MCP tool - * Tests partial workflow updates vs full PUT updates - */ - -const axios = require('axios'); - -// Configuration -const config = { - mcpServerUrl: 'http://localhost:3456/mcp', - healthCheckUrl: 'http://localhost:3456/health', - testWorkflowPrefix: 'PATCH-Test', - testFlags: { - runPatchTests: true, - runComparisonTests: true, - runErrorTests: true, - runCleanup: true - } -}; - -// Test state tracking -const testState = { - createdWorkflows: [], - testResults: { - total: 0, - passed: 0, - failed: 0, - errors: [] - } -}; - -// Request ID counter for JSON-RPC -let requestId = 1; - -// Logger with consistent formatting -const logger = { - info: (msg) => console.error(`[INFO] ${msg}`), - success: (msg) => console.error(`[SUCCESS] ✓ ${msg}`), - error: (msg) => console.error(`[ERROR] ✗ ${msg}`), - warn: (msg) => console.error(`[WARN] ⚠️ ${msg}`), - test: (name, result) => { - const icon = result ? '✓ PASS' : '✗ FAIL'; - console.error(`[TEST] ${name}: ${icon}`); - } -}; - -// Utility: Sleep function -function sleep(ms) { - return new Promise(resolve => setTimeout(resolve, ms)); -} - -/** - * Send MCP JSON-RPC request - */ -async function sendMcpRequest(method, params = {}) { - const response = await axios.post(config.mcpServerUrl, { - jsonrpc: '2.0', - id: requestId++, - method, - params - }); - return response.data.result; -} - -/** - * Call MCP tool with error handling - */ -async function callTool(name, args = {}, maxRetries = 3) { - let lastError; - - // Don't retry create operations to avoid duplicates - const isCreateOperation = name === 'create_workflow'; - const actualRetries = isCreateOperation ? 1 : maxRetries; - - for (let attempt = 1; attempt <= actualRetries; attempt++) { - try { - const result = await sendMcpRequest('tools/call', { name, arguments: args }); - - if (result.isError) { - const errorMessage = result.content && result.content[0] && result.content[0].text - ? result.content[0].text - : 'Unknown MCP tool error'; - throw new Error(errorMessage); - } - - return result; - } catch (error) { - lastError = error; - - // Don't retry on 409 or 404 errors - if (error.message && (error.message.includes('409') || error.message.includes('404'))) { - throw error; - } - - if (attempt < actualRetries) { - logger.warn(`Retrying tools/call (${actualRetries - attempt} attempts remaining)`); - await sleep(1000 * attempt); - } - } - } - - throw lastError; -} - -/** - * Record test result - */ -function recordTest(name, passed, error = null) { - testState.testResults.total++; - if (passed) { - testState.testResults.passed++; - } else { - testState.testResults.failed++; - if (error) { - testState.testResults.errors.push({ test: name, error: error.message }); - } - } - logger.test(name, passed); - return passed; -} - -/** - * Create test workflow helper - */ -async function createTestWorkflow(overrides = {}) { - const defaultWorkflow = { - name: `${config.testWorkflowPrefix}-${Date.now()}`, - nodes: [ - { - name: 'Start', - type: 'n8n-nodes-base.scheduleTrigger', - position: [250, 300], - parameters: { - rule: { - interval: [{ field: 'hours', hoursInterval: 1 }] - } - } - } - ], - connections: [], - settings: { saveExecutionProgress: true }, - tags: ['test-tag-1'] - }; - - const workflow = { ...defaultWorkflow, ...overrides }; - const result = await callTool('create_workflow', workflow); - const created = JSON.parse(result.content[0].text); - - testState.createdWorkflows.push(created.id); - return created; -} - -/** - * Test Suite 1: Basic PATCH Functionality - */ -async function testBasicPatch() { - logger.info('\n--- Test Suite 1: Basic PATCH Functionality ---\n'); - - // Test 1.1: Patch workflow name only - try { - const workflow = await createTestWorkflow({ name: 'Original Name' }); - - const patchResult = await callTool('patch_workflow', { - id: workflow.id, - name: 'Updated Name via PATCH' - }); - const patched = JSON.parse(patchResult.content[0].text); - - const nameUpdated = patched.name === 'Updated Name via PATCH'; - const nodesUnchanged = patched.nodes.length === workflow.nodes.length; - const tagsUnchanged = JSON.stringify(patched.tags) === JSON.stringify(workflow.tags); - - recordTest('PATCH name only - Name updated', nameUpdated); - recordTest('PATCH name only - Nodes unchanged', nodesUnchanged); - recordTest('PATCH name only - Tags unchanged', tagsUnchanged); - } catch (error) { - recordTest('PATCH name only', false, error); - } - - // Test 1.2: Patch tags only - try { - const workflow = await createTestWorkflow({ tags: ['tag1', 'tag2'] }); - - const patchResult = await callTool('patch_workflow', { - id: workflow.id, - tags: ['new-tag1', 'new-tag2', 'new-tag3'] - }); - const patched = JSON.parse(patchResult.content[0].text); - - const tagsUpdated = patched.tags.length === 3 && patched.tags.includes('new-tag3'); - const nameUnchanged = patched.name === workflow.name; - - recordTest('PATCH tags only - Tags updated', tagsUpdated); - recordTest('PATCH tags only - Name unchanged', nameUnchanged); - } catch (error) { - recordTest('PATCH tags only', false, error); - } - - // Test 1.3: Patch active status only - try { - const workflow = await createTestWorkflow({ active: false }); - - const patchResult = await callTool('patch_workflow', { - id: workflow.id, - active: true - }); - const patched = JSON.parse(patchResult.content[0].text); - - const activeUpdated = patched.active === true; - const nameUnchanged = patched.name === workflow.name; - - recordTest('PATCH active status - Status updated', activeUpdated); - recordTest('PATCH active status - Name unchanged', nameUnchanged); - } catch (error) { - recordTest('PATCH active status', false, error); - } - - // Test 1.4: Patch settings only - try { - const workflow = await createTestWorkflow({ - settings: { saveExecutionProgress: true } - }); - - const patchResult = await callTool('patch_workflow', { - id: workflow.id, - settings: { - saveExecutionProgress: false, - saveManualExecutions: true - } - }); - const patched = JSON.parse(patchResult.content[0].text); - - const settingsUpdated = patched.settings.saveManualExecutions === true; - const nameUnchanged = patched.name === workflow.name; - - recordTest('PATCH settings only - Settings updated', settingsUpdated); - recordTest('PATCH settings only - Name unchanged', nameUnchanged); - } catch (error) { - recordTest('PATCH settings only', false, error); - } -} - -/** - * Test Suite 2: Multi-Field PATCH - */ -async function testMultiFieldPatch() { - logger.info('\n--- Test Suite 2: Multi-Field PATCH ---\n'); - - // Test 2.1: Patch name + tags - try { - const workflow = await createTestWorkflow({ - name: 'Original', - tags: ['old-tag'] - }); - - const patchResult = await callTool('patch_workflow', { - id: workflow.id, - name: 'Updated Multi', - tags: ['new-tag-1', 'new-tag-2'] - }); - const patched = JSON.parse(patchResult.content[0].text); - - const nameUpdated = patched.name === 'Updated Multi'; - const tagsUpdated = patched.tags.length === 2 && patched.tags.includes('new-tag-2'); - const nodesUnchanged = patched.nodes.length === workflow.nodes.length; - - recordTest('Multi-field PATCH (name + tags) - Name updated', nameUpdated); - recordTest('Multi-field PATCH (name + tags) - Tags updated', tagsUpdated); - recordTest('Multi-field PATCH (name + tags) - Nodes unchanged', nodesUnchanged); - } catch (error) { - recordTest('Multi-field PATCH (name + tags)', false, error); - } - - // Test 2.2: Patch name + active + settings - try { - const workflow = await createTestWorkflow({ - name: 'Original Complex', - active: false, - settings: {} - }); - - const patchResult = await callTool('patch_workflow', { - id: workflow.id, - name: 'Updated Complex', - active: true, - settings: { saveExecutionProgress: true } - }); - const patched = JSON.parse(patchResult.content[0].text); - - const nameUpdated = patched.name === 'Updated Complex'; - const activeUpdated = patched.active === true; - const settingsUpdated = patched.settings.saveExecutionProgress === true; - - recordTest('Complex multi-field PATCH - Name updated', nameUpdated); - recordTest('Complex multi-field PATCH - Active updated', activeUpdated); - recordTest('Complex multi-field PATCH - Settings updated', settingsUpdated); - } catch (error) { - recordTest('Complex multi-field PATCH', false, error); - } -} - -/** - * Test Suite 3: PATCH vs PUT Comparison - */ -async function testPatchVsPut() { - if (!config.testFlags.runComparisonTests) { - logger.info('\n--- Test Suite 3: PATCH vs PUT Comparison (SKIPPED) ---\n'); - return; - } - - logger.info('\n--- Test Suite 3: PATCH vs PUT Comparison ---\n'); - - // Test 3.1: PATCH preserves nodes, PUT requires full structure - try { - // Create workflow with complex node structure - const workflow = await createTestWorkflow({ - name: 'Complex Workflow', - nodes: [ - { - name: 'Start', - type: 'n8n-nodes-base.scheduleTrigger', - position: [250, 300], - parameters: { rule: { interval: [{ field: 'hours', hoursInterval: 1 }] } } - }, - { - name: 'Set Data', - type: 'n8n-nodes-base.set', - position: [450, 300], - parameters: { values: { string: [{ name: 'test', value: 'data' }] } } - } - ], - connections: [ - { source: 'Start', target: 'Set Data', sourceOutput: 0, targetInput: 0 } - ] - }); - - // PATCH: Update name only - const patchResult = await callTool('patch_workflow', { - id: workflow.id, - name: 'Updated via PATCH' - }); - const patched = JSON.parse(patchResult.content[0].text); - - const patchPreservedNodes = patched.nodes.length === 2; - const patchNameUpdated = patched.name === 'Updated via PATCH'; - - recordTest('PATCH preserves nodes when updating name', patchPreservedNodes); - recordTest('PATCH updates name correctly', patchNameUpdated); - - // PUT: Would require full structure (not testing actual PUT to avoid data loss) - logger.info('PUT comparison: PUT would require sending all nodes and connections'); - recordTest('PATCH vs PUT - PATCH is more efficient for targeted updates', true); - - } catch (error) { - recordTest('PATCH vs PUT comparison', false, error); - } -} - -/** - * Test Suite 4: Error Scenarios - */ -async function testErrorScenarios() { - if (!config.testFlags.runErrorTests) { - logger.info('\n--- Test Suite 4: Error Scenarios (SKIPPED) ---\n'); - return; - } - - logger.info('\n--- Test Suite 4: Error Scenarios ---\n'); - - // Test 4.1: PATCH non-existent workflow - try { - await callTool('patch_workflow', { - id: 'non-existent-id-999999', - name: 'Should Fail' - }); - recordTest('PATCH non-existent workflow - Should return 404', false); - } catch (error) { - const is404 = error.message.includes('404') || error.message.includes('not found'); - recordTest('PATCH non-existent workflow - Returns 404', is404, error); - } - - // Test 4.2: PATCH with empty update object - try { - const workflow = await createTestWorkflow(); - - const patchResult = await callTool('patch_workflow', { - id: workflow.id - // No fields to update - }); - - // Should succeed but make no changes - recordTest('PATCH with no fields - Succeeds without error', true); - } catch (error) { - recordTest('PATCH with no fields', false, error); - } - - // Test 4.3: PATCH without workflow ID - try { - await callTool('patch_workflow', { - name: 'Should Fail - No ID' - }); - recordTest('PATCH without ID - Should fail validation', false); - } catch (error) { - const isValidationError = error.message.includes('required') || error.message.includes('ID'); - recordTest('PATCH without ID - Validation error', isValidationError, error); - } -} - -/** - * Cleanup: Delete test workflows - */ -async function cleanup() { - if (!config.testFlags.runCleanup) { - logger.info('\n--- Cleanup (SKIPPED) ---\n'); - logger.warn(`Skipping cleanup. Created ${testState.createdWorkflows.length} workflows.`); - return; - } - - logger.info('\n--- Cleanup ---\n'); - logger.info(`Cleaning up ${testState.createdWorkflows.length} test workflows...`); - - let deleted = 0; - for (const workflowId of testState.createdWorkflows) { - try { - await callTool('delete_workflow', { id: workflowId }); - deleted++; - logger.success(`Deleted workflow: ${workflowId}`); - } catch (error) { - logger.error(`Failed to delete workflow ${workflowId}: ${error.message}`); - } - } - - logger.success(`Cleaned up ${deleted}/${testState.createdWorkflows.length} test workflows`); -} - -/** - * Print test summary - */ -function printSummary() { - console.error('\n======================================================================'); - console.error(' Test Summary Report - Story 2.4: PATCH /workflows/{id}'); - console.error('======================================================================\n'); - - console.error(`Total tests executed: ${testState.testResults.total}`); - console.error(`Passed: ${testState.testResults.passed} (${Math.round(testState.testResults.passed / testState.testResults.total * 100)}%)`); - console.error(`Failed: ${testState.testResults.failed}`); - - if (testState.testResults.errors.length > 0) { - console.error('\nFailed Tests:'); - testState.testResults.errors.forEach((err, idx) => { - console.error(` ${idx + 1}. ${err.test}`); - console.error(` Error: ${err.error}`); - }); - } - - console.error('\n======================================================================'); - if (testState.testResults.failed === 0) { - console.error('✓ ALL TESTS PASSED!'); - } else { - console.error(`✗ ${testState.testResults.failed} TEST(S) FAILED`); - } - console.error('======================================================================\n'); -} - -/** - * Main test execution - */ -async function main() { - console.error('======================================================================'); - console.error(' PATCH Workflow API Test Suite - Story 2.4'); - console.error('======================================================================\n'); - - logger.info('Testing patch_workflow MCP tool implementation'); - logger.info(`MCP Server: ${config.mcpServerUrl}\n`); - - try { - // Pre-flight checks - console.error('--- Pre-flight Checks ---\n'); - const healthResponse = await axios.get(config.healthCheckUrl); - logger.info(`Server health: ${healthResponse.data.status}\n`); - - // Run test suites - if (config.testFlags.runPatchTests) { - await testBasicPatch(); - await testMultiFieldPatch(); - } - - await testPatchVsPut(); - await testErrorScenarios(); - - // Cleanup - await cleanup(); - - // Print summary - printSummary(); - - // Exit with appropriate code - process.exit(testState.testResults.failed === 0 ? 0 : 1); - - } catch (error) { - logger.error(`Test execution failed: ${error.message}`); - console.error(error.stack); - process.exit(1); - } -} - -// Run tests -main(); diff --git a/test-retry-quick.js b/test-retry-quick.js deleted file mode 100644 index 775450a..0000000 --- a/test-retry-quick.js +++ /dev/null @@ -1,99 +0,0 @@ -#!/usr/bin/env node - -/** - * Quick Test: retry_execution tool - * Tests against n8n v2.1.4 platform - */ - -const axios = require('axios'); - -const config = { - mcpServerUrl: 'http://localhost:3456/mcp', - healthCheckUrl: 'http://localhost:3456/health' -}; - -let requestId = 1; - -async function sendMcpRequest(method, params = {}) { - const response = await axios.post(config.mcpServerUrl, { - jsonrpc: '2.0', - id: requestId++, - method, - params - }); - return response.data.result; -} - -async function callTool(name, args = {}) { - const result = await sendMcpRequest('tools/call', { name, arguments: args }); - if (result.isError) { - const errorMessage = result.content && result.content[0] && result.content[0].text - ? result.content[0].text - : 'Unknown error'; - throw new Error(errorMessage); - } - return result; -} - -async function main() { - console.error('=== Quick Test: retry_execution ===\n'); - - try { - // Check server health - const health = await axios.get(config.healthCheckUrl); - console.error(`✓ Server health: ${health.data.status}\n`); - - // Step 1: List executions to find a failed one - console.error('Looking for failed executions...'); - const listResult = await callTool('list_executions', { - status: 'error', - limit: 5 - }); - const executions = JSON.parse(listResult.content[0].text); - - if (!executions.data || executions.data.length === 0) { - console.error('\n⚠️ No failed executions found.'); - console.error('To test retry_execution:'); - console.error('1. Create a workflow that will fail'); - console.error('2. Execute it to generate a failed execution'); - console.error('3. Run this test again\n'); - process.exit(0); - } - - const failedExecution = executions.data[0]; - console.error(`✓ Found failed execution: ${failedExecution.id}`); - console.error(` Workflow: ${failedExecution.workflowId}`); - console.error(` Status: ${failedExecution.status}`); - console.error(` Started: ${failedExecution.startedAt}\n`); - - // Step 2: Retry the failed execution - console.error('Retrying failed execution...'); - const retryResult = await callTool('retry_execution', { - id: failedExecution.id - }); - const newExecution = JSON.parse(retryResult.content[0].text); - - console.error(`✓ Retry initiated successfully!`); - console.error(` New execution ID: ${newExecution.id}`); - console.error(` Retry of: ${newExecution.retryOf || failedExecution.id}`); - console.error(` Mode: ${newExecution.mode}`); - console.error(` Status: ${newExecution.status || 'running'}\n`); - - // Verify it's a different execution - if (newExecution.id === failedExecution.id) { - console.error('⚠️ WARNING: New execution has same ID as original'); - } else { - console.error('✓ Verified: New execution has different ID'); - } - - console.error('\n=== Test Result: ✓ SUCCESS ==='); - console.error('retry_execution tool is working correctly!\n'); - - } catch (error) { - console.error(`\n✗ Test failed: ${error.message}`); - console.error('\nError details:', error.response?.data || error.stack); - process.exit(1); - } -} - -main(); diff --git a/test-simple-tag.js b/test-simple-tag.js deleted file mode 100644 index cd9f8c2..0000000 --- a/test-simple-tag.js +++ /dev/null @@ -1,51 +0,0 @@ -#!/usr/bin/env node - -const axios = require('axios'); - -const mcpServerUrl = 'http://localhost:3456/mcp'; -let requestId = 1; - -async function sendMcpRequest(method, params = {}) { - const response = await axios.post(mcpServerUrl, { - jsonrpc: '2.0', - id: requestId++, - method, - params - }); - return response.data.result; -} - -async function callTool(name, args = {}) { - const result = await sendMcpRequest('tools/call', { name, arguments: args }); - if (result.isError) { - const errorMessage = result.content && result.content[0] && result.content[0].text - ? result.content[0].text - : 'Unknown error'; - throw new Error(errorMessage); - } - return result; -} - -async function test() { - try { - // Try a completely different prefix - const tagName = `ValidTest${Date.now()}`; - console.error(`[INFO] Creating tag: ${tagName}`); - - const result = await callTool('create_tag', { name: tagName }); - const tag = JSON.parse(result.content[0].text); - - console.error('[SUCCESS] Tag created!'); - console.error(JSON.stringify(tag, null, 2)); - - // Clean up - console.error(`\n[INFO] Deleting tag: ${tag.id}`); - await callTool('delete_tag', { id: tag.id }); - console.error('[SUCCESS] Tag deleted'); - - } catch (error) { - console.error('[ERROR]', error.message); - } -} - -test(); diff --git a/test-tags-validation.js b/test-tags-validation.js deleted file mode 100644 index f85ddae..0000000 --- a/test-tags-validation.js +++ /dev/null @@ -1,656 +0,0 @@ -#!/usr/bin/env node - -/** - * Tags API Validation Test Suite - Story 2.3 - * - * Validates all 5 Tags API methods against live n8n instance: - * - get_tags (GET /tags) - List all tags with pagination - * - get_tag (GET /tags/{id}) - Get specific tag - * - create_tag (POST /tags) - Create new tag - * - update_tag (PUT /tags/{id}) - Update tag name - * - delete_tag (DELETE /tags/{id}) - Delete tag - * - * Test Categories: - * - Tag listing with pagination - * - Tag CRUD operations - * - Tag name uniqueness validation - * - Error handling and edge cases - * - Multi-instance routing - */ - -const axios = require('axios'); -const crypto = require('crypto'); - -// ======================================== -// Configuration -// ======================================== - -const config = { - mcpServerUrl: 'http://localhost:3456/mcp', - healthCheckUrl: 'http://localhost:3456/health', - testTagPrefix: 'ValidTest', - testFlags: { - runListTests: true, - runGetTests: true, - runCreateTests: true, - runUpdateTests: true, - runDeleteTests: true, - runCleanup: true - } -}; - -// ======================================== -// Logger Utility -// ======================================== - -const logger = { - info: (msg) => console.error(`[INFO] ${msg}`), - test: (name, passed, details = '') => { - const status = passed ? '✓ PASS' : '✗ FAIL'; - const message = details ? ` - ${details}` : ''; - console.error(`[TEST] ${name}: ${status}${message}`); - }, - warn: (msg) => console.error(`[WARN] ⚠️ ${msg}`), - error: (msg) => console.error(`[ERROR] ❌ ${msg}`), - success: (msg) => console.error(`[SUCCESS] ✓ ${msg}`), - debug: (msg, data) => { - if (process.env.DEBUG) { - console.error(`[DEBUG] ${msg}`, data ? JSON.stringify(data, null, 2) : ''); - } - } -}; - -// ======================================== -// MCP Communication -// ======================================== - -let requestId = 1; - -async function sendMcpRequest(method, params = {}) { - try { - const response = await axios.post(config.mcpServerUrl, { - jsonrpc: '2.0', - id: requestId++, - method, - params - }); - - logger.debug(`MCP Response for ${method}:`, response.data); - return response.data.result; - } catch (error) { - logger.error(`MCP request failed: ${method}`); - if (error.response) { - logger.error(`Status: ${error.response.status}`); - logger.error(`Data: ${JSON.stringify(error.response.data)}`); - } - throw error; - } -} - -async function callTool(name, args = {}, maxRetries = 3) { - let lastError; - - // Don't retry create operations to avoid 409 conflicts - const isCreateOperation = name === 'create_tag' || name === 'create_workflow'; - const actualRetries = isCreateOperation ? 1 : maxRetries; - - for (let attempt = 1; attempt <= actualRetries; attempt++) { - try { - const result = await sendMcpRequest('tools/call', { name, arguments: args }); - - // Check if MCP tool returned an error - if (result.isError) { - const errorMessage = result.content && result.content[0] && result.content[0].text - ? result.content[0].text - : 'Unknown MCP tool error'; - throw new Error(errorMessage); - } - - return result; - } catch (error) { - lastError = error; - - // Don't retry on 409 Conflict errors (resource already exists) - if (error.message && error.message.includes('409')) { - throw error; - } - - if (attempt < actualRetries) { - logger.warn(`Retrying tools/call (${actualRetries - attempt} attempts remaining)`); - await sleep(1000 * attempt); - } - } - } - - throw lastError; -} - -function sleep(ms) { - return new Promise(resolve => setTimeout(resolve, ms)); -} - -// ======================================== -// Test State Management -// ======================================== - -const testState = { - createdTags: [], - testTagIds: [] -}; - -// ======================================== -// Test Suite: get_tags (List Tags) -// ======================================== - -async function testGetTags() { - logger.info('\n--- Task 2: Validate get_tags ---\n'); - - let testsPassed = 0; - let testsTotal = 0; - - // Test 2.1: List all tags - testsTotal++; - try { - const result = await callTool('get_tags', {}); - const data = JSON.parse(result.content[0].text); - - const isValid = data && Array.isArray(data.data); - logger.test( - 'get_tags - List all tags', - isValid, - isValid ? `Found ${data.data.length} tags` : 'Invalid response structure' - ); - if (isValid) testsPassed++; - - // Store tag IDs for later tests - if (data.data.length > 0) { - testState.testTagIds = data.data.slice(0, 3).map(t => t.id); - } - } catch (error) { - logger.test('get_tags - List all tags', false, error.message); - } - - // Test 2.2: Response structure validation - testsTotal++; - try { - const result = await callTool('get_tags', { limit: 5 }); - const data = JSON.parse(result.content[0].text); - - if (data.data && data.data.length > 0) { - const tag = data.data[0]; - const hasRequiredFields = - tag.hasOwnProperty('id') && - tag.hasOwnProperty('name') && - tag.hasOwnProperty('createdAt') && - tag.hasOwnProperty('updatedAt'); - - logger.test( - 'get_tags - Response structure validation', - hasRequiredFields, - hasRequiredFields ? 'All required fields present' : 'Missing required fields' - ); - if (hasRequiredFields) testsPassed++; - } else { - logger.test('get_tags - Response structure validation', true, 'No tags to validate (empty list is valid)'); - testsPassed++; - } - } catch (error) { - logger.test('get_tags - Response structure validation', false, error.message); - } - - // Test 2.3: Pagination with limit - testsTotal++; - try { - const result = await callTool('get_tags', { limit: 3 }); - const data = JSON.parse(result.content[0].text); - - const isValid = data.data && data.data.length <= 3; - logger.test( - 'get_tags - Pagination limit', - isValid, - isValid ? `Returned ${data.data.length} tags (limit: 3)` : 'Limit not respected' - ); - if (isValid) testsPassed++; - } catch (error) { - logger.test('get_tags - Pagination limit', false, error.message); - } - - // Test 2.4: Cursor-based pagination - testsTotal++; - try { - const firstPage = await callTool('get_tags', { limit: 2 }); - const firstData = JSON.parse(firstPage.content[0].text); - - if (firstData.nextCursor) { - const secondPage = await callTool('get_tags', { - limit: 2, - cursor: firstData.nextCursor - }); - const secondData = JSON.parse(secondPage.content[0].text); - - const isValid = secondData.data && secondData.data.length > 0; - logger.test( - 'get_tags - Cursor pagination', - isValid, - isValid ? `Next page retrieved with ${secondData.data.length} tags` : 'Cursor pagination failed' - ); - if (isValid) testsPassed++; - } else { - logger.test('get_tags - Cursor pagination', true, 'No next cursor (all tags fit in first page)'); - testsPassed++; - } - } catch (error) { - logger.test('get_tags - Cursor pagination', false, error.message); - } - - return { passed: testsPassed, total: testsTotal }; -} - -// ======================================== -// Test Suite: create_tag -// ======================================== - -async function testCreateTag() { - logger.info('\n--- Task 3: Validate create_tag ---\n'); - - let testsPassed = 0; - let testsTotal = 0; - - // Test 3.1: Create tag with unique name - testsTotal++; - try { - const tagName = `Test${crypto.randomUUID().substring(0, 8)}`; - const result = await callTool('create_tag', { name: tagName }); - const tag = JSON.parse(result.content[0].text); - - const isValid = tag && tag.id && tag.name === tagName; - logger.test( - 'create_tag - Create with unique name', - isValid, - isValid ? `Created tag: ${tag.id}` : 'Tag creation failed' - ); - if (isValid) { - testsPassed++; - testState.createdTags.push(tag.id); - } - } catch (error) { - logger.test('create_tag - Create with unique name', false, error.message); - } - - // Test 3.2: Structure validation - testsTotal++; - try { - const tagName = `Test${crypto.randomUUID().substring(0, 8)}`; - const result = await callTool('create_tag', { name: tagName }); - const tag = JSON.parse(result.content[0].text); - - const hasRequiredFields = - tag.hasOwnProperty('id') && - tag.hasOwnProperty('name') && - tag.hasOwnProperty('createdAt') && - tag.hasOwnProperty('updatedAt'); - - logger.test( - 'create_tag - Structure validation', - hasRequiredFields, - hasRequiredFields ? 'All required fields present' : 'Missing required fields' - ); - if (hasRequiredFields) { - testsPassed++; - testState.createdTags.push(tag.id); - } - } catch (error) { - logger.test('create_tag - Structure validation', false, error.message); - } - - // Test 3.3: Duplicate name handling - testsTotal++; - try { - const tagName = `Test${crypto.randomUUID().substring(0, 8)}`; - - // Create first tag - const first = await callTool('create_tag', { name: tagName }); - const firstTag = JSON.parse(first.content[0].text); - testState.createdTags.push(firstTag.id); - - // Try to create duplicate - try { - await callTool('create_tag', { name: tagName }); - logger.test('create_tag - Duplicate name handling', false, 'Should have rejected duplicate name'); - } catch (error) { - const isDuplicateError = error.message.includes('already exists') || - error.message.includes('duplicate') || - error.message.includes('unique') || - error.message.includes('409'); - logger.test( - 'create_tag - Duplicate name handling', - isDuplicateError, - isDuplicateError ? 'Correctly rejected duplicate' : 'Wrong error type' - ); - if (isDuplicateError) testsPassed++; - } - } catch (error) { - logger.test('create_tag - Duplicate name handling', false, error.message); - } - - return { passed: testsPassed, total: testsTotal }; -} - -// ======================================== -// Test Suite: get_tag -// ======================================== - -async function testGetTag() { - logger.info('\n--- Task 4: Validate get_tag ---\n'); - - let testsPassed = 0; - let testsTotal = 0; - - // Ensure we have tag IDs to test - if (testState.testTagIds.length === 0 && testState.createdTags.length === 0) { - logger.warn('No tag IDs available for testing. Skipping get_tag tests.'); - return { passed: 0, total: 0 }; - } - - const testTagId = testState.testTagIds.length > 0 - ? testState.testTagIds[0] - : testState.createdTags[0]; - - logger.info(`Using tag ID: ${testTagId}`); - - // Test 4.1: Retrieve tag by ID - testsTotal++; - try { - const result = await callTool('get_tag', { id: testTagId }); - const tag = JSON.parse(result.content[0].text); - - const isValid = tag && tag.id === testTagId; - logger.test( - 'get_tag - Retrieve by ID', - isValid, - isValid ? `Tag retrieved: ${tag.name}` : 'Failed to retrieve tag' - ); - if (isValid) testsPassed++; - } catch (error) { - logger.test('get_tag - Retrieve by ID', false, error.message); - } - - // Test 4.2: Structure validation - testsTotal++; - try { - const result = await callTool('get_tag', { id: testTagId }); - const tag = JSON.parse(result.content[0].text); - - const hasRequiredFields = - tag.hasOwnProperty('id') && - tag.hasOwnProperty('name') && - tag.hasOwnProperty('createdAt') && - tag.hasOwnProperty('updatedAt'); - - logger.test( - 'get_tag - Structure validation', - hasRequiredFields, - hasRequiredFields ? 'All required fields present' : 'Missing required fields' - ); - if (hasRequiredFields) testsPassed++; - } catch (error) { - logger.test('get_tag - Structure validation', false, error.message); - } - - // Test 4.3: 404 for non-existent ID - testsTotal++; - try { - await callTool('get_tag', { id: '99999999' }); - logger.test('get_tag - 404 for non-existent ID', false, 'Should have returned error'); - } catch (error) { - const is404 = error.message.includes('404') || error.message.includes('not found') || error.message.includes('Not Found'); - logger.test( - 'get_tag - 404 for non-existent ID', - is404, - is404 ? 'Correctly returned 404' : 'Wrong error type' - ); - if (is404) testsPassed++; - } - - return { passed: testsPassed, total: testsTotal }; -} - -// ======================================== -// Test Suite: update_tag -// ======================================== - -async function testUpdateTag() { - logger.info('\n--- Task 5: Validate update_tag ---\n'); - - let testsPassed = 0; - let testsTotal = 0; - - // Test 5.1: Update tag name - testsTotal++; - try { - // Create a tag to update - const originalName = `Test${crypto.randomUUID().substring(0, 8)}`; - const createResult = await callTool('create_tag', { name: originalName }); - const createdTag = JSON.parse(createResult.content[0].text); - testState.createdTags.push(createdTag.id); - - // Update the tag - const newName = `Test${crypto.randomUUID().substring(0, 8)}`; - const updateResult = await callTool('update_tag', { - id: createdTag.id, - name: newName - }); - const updatedTag = JSON.parse(updateResult.content[0].text); - - const isValid = updatedTag && updatedTag.name === newName && updatedTag.id === createdTag.id; - logger.test( - 'update_tag - Update name', - isValid, - isValid ? `Name updated successfully` : 'Update failed' - ); - if (isValid) testsPassed++; - } catch (error) { - logger.test('update_tag - Update name', false, error.message); - } - - // Test 5.2: 404 for non-existent ID - testsTotal++; - try { - await callTool('update_tag', { - id: '99999999', - name: `${config.testTagPrefix}NonExistent` - }); - logger.test('update_tag - 404 for non-existent ID', false, 'Should have returned error'); - } catch (error) { - const is404 = error.message.includes('404') || error.message.includes('not found'); - logger.test( - 'update_tag - 404 for non-existent ID', - is404, - is404 ? 'Correctly returned 404' : 'Wrong error type' - ); - if (is404) testsPassed++; - } - - return { passed: testsPassed, total: testsTotal }; -} - -// ======================================== -// Test Suite: delete_tag -// ======================================== - -async function testDeleteTag() { - logger.info('\n--- Task 6: Validate delete_tag ---\n'); - - let testsPassed = 0; - let testsTotal = 0; - - // Test 6.1: Delete tag and verify - testsTotal++; - try { - // Create a tag to delete - const tagName = `Test${crypto.randomUUID().substring(0, 8)}`; - const createResult = await callTool('create_tag', { name: tagName }); - const createdTag = JSON.parse(createResult.content[0].text); - - // Delete the tag - await callTool('delete_tag', { id: createdTag.id }); - - // Verify it's gone - try { - await callTool('get_tag', { id: createdTag.id }); - logger.test('delete_tag - Delete and verify', false, 'Tag still exists after deletion'); - } catch (error) { - const is404 = error.message.includes('404') || error.message.includes('not found'); - logger.test( - 'delete_tag - Delete and verify', - is404, - is404 ? 'Tag successfully deleted' : 'Unexpected error' - ); - if (is404) testsPassed++; - } - } catch (error) { - logger.test('delete_tag - Delete and verify', false, error.message); - } - - // Test 6.2: 404 for non-existent ID - testsTotal++; - try { - await callTool('delete_tag', { id: '99999999' }); - logger.test('delete_tag - 404 for non-existent ID', false, 'Should have returned error'); - } catch (error) { - const is404 = error.message.includes('404') || error.message.includes('not found'); - logger.test( - 'delete_tag - 404 for non-existent ID', - is404, - is404 ? 'Correctly returned 404' : 'Wrong error type' - ); - if (is404) testsPassed++; - } - - return { passed: testsPassed, total: testsTotal }; -} - -// ======================================== -// Cleanup -// ======================================== - -async function cleanup() { - if (!config.testFlags.runCleanup) { - logger.info('Cleanup disabled by configuration'); - return; - } - - logger.info('\n======================================================================'); - logger.info(' Cleanup'); - logger.info('======================================================================\n'); - - let cleanedTags = 0; - - if (testState.createdTags.length > 0) { - logger.info(`Cleaning up ${testState.createdTags.length} test tags...`); - - for (const tagId of testState.createdTags) { - try { - await callTool('delete_tag', { id: tagId }); - cleanedTags++; - } catch (error) { - logger.debug(`Failed to delete tag ${tagId}: ${error.message}`); - } - } - - logger.success(`✓ Cleaned up ${cleanedTags}/${testState.createdTags.length} test tags`); - } -} - -// ======================================== -// Main Test Runner -// ======================================== - -async function runTests() { - console.error('======================================================================'); - console.error(' Tags API Validation Test Suite - Story 2.3'); - console.error('======================================================================\n'); - - logger.info('Testing 5 Tags API methods against live n8n instance'); - logger.info(`MCP Server: ${config.mcpServerUrl}\n`); - - const results = { - list: { passed: 0, total: 0 }, - create: { passed: 0, total: 0 }, - get: { passed: 0, total: 0 }, - update: { passed: 0, total: 0 }, - delete: { passed: 0, total: 0 } - }; - - try { - // Pre-flight checks - console.error('--- Pre-flight Checks ---\n'); - - const health = await axios.get(config.healthCheckUrl); - logger.info(`Server health: ${health.data.status}\n`); - - // Run test suites - if (config.testFlags.runListTests) { - results.list = await testGetTags(); - } - - if (config.testFlags.runCreateTests) { - results.create = await testCreateTag(); - } - - if (config.testFlags.runGetTests) { - results.get = await testGetTag(); - } - - if (config.testFlags.runUpdateTests) { - results.update = await testUpdateTag(); - } - - if (config.testFlags.runDeleteTests) { - results.delete = await testDeleteTag(); - } - - // Cleanup - await cleanup(); - - // Summary - console.error('\n======================================================================'); - console.error(' Test Summary Report'); - console.error('======================================================================\n'); - - const totalPassed = results.list.passed + results.create.passed + results.get.passed + - results.update.passed + results.delete.passed; - const totalTests = results.list.total + results.create.total + results.get.total + - results.update.total + results.delete.total; - - console.error(`Total tests executed: ${totalTests}`); - console.error(`Passed: ${totalPassed} (${totalTests > 0 ? Math.round(totalPassed/totalTests*100) : 0}%)`); - console.error(`Failed: ${totalTests - totalPassed}`); - console.error(`Skipped: 0\n`); - - console.error('Test categories:'); - console.error(` list: ${results.list.passed}/${results.list.total} (${results.list.total > 0 ? Math.round(results.list.passed/results.list.total*100) : 0}%)`); - console.error(` create: ${results.create.passed}/${results.create.total} (${results.create.total > 0 ? Math.round(results.create.passed/results.create.total*100) : 0}%)`); - console.error(` get: ${results.get.passed}/${results.get.total} (${results.get.total > 0 ? Math.round(results.get.passed/results.get.total*100) : 0}%)`); - console.error(` update: ${results.update.passed}/${results.update.total} (${results.update.total > 0 ? Math.round(results.update.passed/results.update.total*100) : 0}%)`); - console.error(` delete: ${results.delete.passed}/${results.delete.total} (${results.delete.total > 0 ? Math.round(results.delete.passed/results.delete.total*100) : 0}%)`); - - console.error('\n======================================================================'); - if (totalPassed === totalTests && totalTests > 0) { - console.error('✓ ALL TESTS PASSED!'); - } else { - console.error(`⚠ ${totalTests - totalPassed} TESTS FAILED`); - } - console.error('======================================================================'); - - process.exit(totalPassed === totalTests ? 0 : 1); - - } catch (error) { - logger.error(`Test suite failed: ${error.message}`); - console.error(error.stack); - process.exit(1); - } -} - -// Run tests -runTests(); diff --git a/test-uuid-tag.js b/test-uuid-tag.js deleted file mode 100644 index fc9ad56..0000000 --- a/test-uuid-tag.js +++ /dev/null @@ -1,52 +0,0 @@ -#!/usr/bin/env node - -const axios = require('axios'); -const crypto = require('crypto'); - -const mcpServerUrl = 'http://localhost:3456/mcp'; -let requestId = 1; - -async function sendMcpRequest(method, params = {}) { - const response = await axios.post(mcpServerUrl, { - jsonrpc: '2.0', - id: requestId++, - method, - params - }); - return response.data.result; -} - -async function callTool(name, args = {}) { - const result = await sendMcpRequest('tools/call', { name, arguments: args }); - if (result.isError) { - const errorMessage = result.content && result.content[0] && result.content[0].text - ? result.content[0].text - : 'Unknown error'; - throw new Error(errorMessage); - } - return result; -} - -async function test() { - try { - // Use UUID for 100% uniqueness - const tagName = `Test${crypto.randomUUID().substring(0, 8)}`; - console.error(`[INFO] Creating tag with UUID: ${tagName}`); - - const result = await callTool('create_tag', { name: tagName }); - const tag = JSON.parse(result.content[0].text); - - console.error('[SUCCESS] Tag created!'); - console.error(JSON.stringify(tag, null, 2)); - - // Clean up - console.error(`\n[INFO] Deleting tag: ${tag.id}`); - await callTool('delete_tag', { id: tag.id }); - console.error('[SUCCESS] Tag deleted'); - - } catch (error) { - console.error('[ERROR]', error.message); - } -} - -test(); diff --git a/test-workflows-validation.js b/test-workflows-validation.js deleted file mode 100644 index d4853c8..0000000 --- a/test-workflows-validation.js +++ /dev/null @@ -1,1163 +0,0 @@ -#!/usr/bin/env node - -/** - * Workflows API Validation Test Suite - * Story 2.1: Validate & Test Workflows API - * - * Comprehensive validation and testing of all 8 implemented Workflows API methods - * against live n8n instance and documentation. - * - * Test Coverage: - * - GET /workflows (list_workflows) - 7 tests - * - GET /workflows/{id} (get_workflow) - 5 tests - * - POST /workflows (create_workflow) - 6 tests - * - PUT /workflows/{id} (update_workflow) - 6 tests - * - DELETE /workflows/{id} (delete_workflow) - 5 tests - * - PUT /workflows/{id}/activate (activate_workflow) - 6 tests - * - PUT /workflows/{id}/deactivate (deactivate_workflow) - 5 tests - * - Workflow execution (execute_workflow) - 5 tests - * - Multi-instance validation - 3 tests - * - Error handling validation - 6 tests - * - * Total: 54+ comprehensive validation tests - */ - -const fetch = require('node-fetch'); - -// ============================================================================ -// Configuration -// ============================================================================ - -const config = { - mcpServerUrl: process.env.MCP_SERVER_URL || 'http://localhost:3456/mcp', - healthCheckUrl: process.env.HEALTH_CHECK_URL || 'http://localhost:3456/health', - - // Test workflow names for easy identification and cleanup - testWorkflowPrefix: 'ValidationTest_', - - // Retry configuration for flaky network conditions - maxRetries: 3, - retryDelay: 1000, - - // Multi-instance test configuration - instances: { - default: undefined, // Uses default instance - production: 'production', // Requires .config.json setup - staging: 'staging' // Requires .config.json setup - } -}; - -// Test execution flags - control which test suites run -const testFlags = { - runListWorkflowsTests: true, - runGetWorkflowTests: true, - runCreateWorkflowTests: true, - runUpdateWorkflowTests: true, - runDeleteWorkflowTests: true, - runActivateWorkflowTests: true, - runDeactivateWorkflowTests: true, - runExecuteWorkflowTests: true, - runMultiInstanceTests: false, // Requires multi-instance .config.json - runErrorHandlingTests: true, - runCleanup: true // Set to false to keep test data for debugging -}; - -// ============================================================================ -// Test Data Storage -// ============================================================================ - -const testData = { - // Workflow IDs created during tests - workflowIds: [], - - // Test results tracking - results: { - passed: 0, - failed: 0, - skipped: 0, - tests: {}, - validationFindings: [] // Documentation vs implementation discrepancies - }, - - // Test fixtures - fixtures: { - minimalWorkflow: null, - completeWorkflow: null, - complexWorkflow: null - } -}; - -// ============================================================================ -// Logger - Structured output for test results -// ============================================================================ - -class Logger { - info(message) { - console.log(`[INFO] ${message}`); - } - - success(message) { - console.log(`[SUCCESS] ✓ ${message}`); - } - - error(message, error) { - const errorMsg = error?.message || error || ''; - console.error(`[ERROR] ✗ ${message}`, errorMsg); - } - - warn(message) { - console.log(`[WARN] ⚠ ${message}`); - } - - test(name, status, details = '') { - const statusSymbol = status ? '✓ PASS' : '✗ FAIL'; - const detailsStr = details ? ` - ${details}` : ''; - console.log(`[TEST] ${name}: ${statusSymbol}${detailsStr}`); - - // Track results - testData.results.tests[name] = status; - if (status) { - testData.results.passed++; - } else { - testData.results.failed++; - } - } - - skip(name, reason) { - console.log(`[SKIP] ${name}: ${reason}`); - testData.results.skipped++; - } - - section(name) { - console.log(`\n${'='.repeat(70)}`); - console.log(` ${name}`); - console.log(`${'='.repeat(70)}\n`); - } - - subsection(name) { - console.log(`\n--- ${name} ---\n`); - } - - debug(message, data) { - if (process.env.DEBUG) { - const dataStr = data ? JSON.stringify(data, null, 2).substring(0, 500) : ''; - console.log(`[DEBUG] ${message}`, dataStr ? `\n${dataStr}...` : ''); - } - } - - validationFinding(method, finding) { - const entry = { - method, - finding, - timestamp: new Date().toISOString() - }; - testData.results.validationFindings.push(entry); - this.warn(`Validation Finding [${method}]: ${finding}`); - } - - summaryReport() { - const { passed, failed, skipped, tests, validationFindings } = testData.results; - const total = passed + failed; - const passRate = total > 0 ? Math.round((passed / total) * 100) : 0; - - this.section('Test Summary Report'); - console.log(`Total tests executed: ${total}`); - console.log(`Passed: ${passed} (${passRate}%)`); - console.log(`Failed: ${failed}`); - console.log(`Skipped: ${skipped}`); - - if (failed > 0) { - console.log('\n❌ Failed tests:'); - Object.entries(tests) - .filter(([_, status]) => !status) - .forEach(([name]) => console.log(` - ${name}`)); - } - - if (validationFindings.length > 0) { - console.log(`\n⚠️ Validation Findings: ${validationFindings.length}`); - validationFindings.forEach((finding, idx) => { - console.log(` ${idx + 1}. [${finding.method}] ${finding.finding}`); - }); - } - - console.log('\nTest categories:'); - ['list', 'get', 'create', 'update', 'delete', 'activate', 'deactivate', 'execute', 'multi-instance', 'error'].forEach(category => { - const categoryTests = Object.entries(tests) - .filter(([name]) => name.toLowerCase().includes(category)) - .map(([_, status]) => status); - - const categoryTotal = categoryTests.length; - const categoryPassed = categoryTests.filter(Boolean).length; - const categoryRate = categoryTotal > 0 ? Math.round((categoryPassed / categoryTotal) * 100) : 0; - - if (categoryTotal > 0) { - console.log(` ${category}: ${categoryPassed}/${categoryTotal} (${categoryRate}%)`); - } - }); - - console.log(`\n${'='.repeat(70)}`); - console.log(passRate === 100 ? '✓ ALL TESTS PASSED!' : `⚠️ ${failed} test(s) need attention`); - console.log(`${'='.repeat(70)}\n`); - } -} - -const logger = new Logger(); - -// ============================================================================ -// MCP Communication Helper Functions -// ============================================================================ - -/** - * Send JSON-RPC request to MCP server with retry logic - */ -async function sendMcpRequest(method, params = {}, retries = config.maxRetries) { - try { - logger.debug(`Sending MCP request: ${method}`, params); - - const response = await fetch(config.mcpServerUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - jsonrpc: '2.0', - method, - params, - id: Date.now() - }) - }); - - if (!response.ok) { - const errorText = await response.text(); - throw new Error(`HTTP ${response.status}: ${errorText}`); - } - - const data = await response.json(); - if (data.error) { - throw new Error(`JSON-RPC error: ${JSON.stringify(data.error)}`); - } - - logger.debug(`Received response for ${method}`, data.result); - return data.result; - - } catch (error) { - if (retries > 0) { - logger.warn(`Retrying ${method} (${retries} attempts remaining)`); - await new Promise(resolve => setTimeout(resolve, config.retryDelay)); - return sendMcpRequest(method, params, retries - 1); - } - throw error; - } -} - -/** - * Call MCP tool - */ -async function callTool(name, args = {}) { - try { - const result = await sendMcpRequest('tools/call', { name, arguments: args }); - - // Check if MCP tool returned an error - if (result.isError) { - const errorMessage = result.content && result.content[0] && result.content[0].text - ? result.content[0].text - : 'Unknown MCP tool error'; - throw new Error(errorMessage); - } - - return result; - } catch (error) { - logger.debug(`Tool call failed: ${name}`, error); - throw error; - } -} - -/** - * Check MCP server health - */ -async function checkServerHealth() { - try { - const response = await fetch(config.healthCheckUrl); - if (!response.ok) { - throw new Error(`HTTP ${response.status}`); - } - - const data = await response.json(); - logger.info(`Server health: ${data.status}`); - return data.status === 'ok'; - } catch (error) { - logger.error('Health check failed', error); - return false; - } -} - -// ============================================================================ -// Test Fixtures - Workflow Definitions -// ============================================================================ - -function createMinimalWorkflow() { - return { - name: `${config.testWorkflowPrefix}Minimal_${Date.now()}`, - nodes: [], - connections: [], // Array, not object - settings: {} - }; -} - -function createCompleteWorkflow() { - const timestamp = Date.now(); - - return { - name: `${config.testWorkflowPrefix}Complete_${timestamp}`, - nodes: [ - { - id: `scheduleTrigger_${timestamp}`, - name: "Schedule Trigger", - type: "n8n-nodes-base.scheduleTrigger", - typeVersion: 1, - position: [250, 300], - parameters: { - rule: { - interval: [ - { - field: "hours", - hoursInterval: 1 - } - ] - } - } - }, - { - id: `set_${timestamp}`, - name: "Set Data", - type: "n8n-nodes-base.set", - typeVersion: 3.3, - position: [450, 300], - parameters: { - assignments: { - assignments: [ - { - id: `assignment_${timestamp}`, - name: "testField", - value: "testValue", - type: "string" - } - ] - }, - options: {} - } - } - ], - connections: [ - { - source: `scheduleTrigger_${timestamp}`, - target: `set_${timestamp}`, - sourceOutput: 0, - targetInput: 0 - } - ], - settings: { - executionOrder: "v1" - }, - tags: [] - }; -} - -function createComplexWorkflow() { - const timestamp = Date.now(); - const nodeCount = 12; - const nodes = []; - const connections = []; // Array format for MCP tool - - // Create schedule trigger - const triggerNode = { - id: `trigger_${timestamp}`, - name: "Schedule Trigger", - type: "n8n-nodes-base.scheduleTrigger", - typeVersion: 1, - position: [100, 300], - parameters: { - rule: { - interval: [{ field: "minutes", minutesInterval: 5 }] - } - } - }; - nodes.push(triggerNode); - - // Create chain of Set nodes - for (let i = 0; i < nodeCount - 1; i++) { - const nodeId = `set_${i}_${timestamp}`; - nodes.push({ - id: nodeId, - name: `Set ${i}`, - type: "n8n-nodes-base.set", - typeVersion: 3.3, - position: [250 + (i * 150), 300], - parameters: { - assignments: { - assignments: [ - { - id: `assignment_${i}_${timestamp}`, - name: `field${i}`, - value: `value${i}`, - type: "string" - } - ] - } - } - }); - - // Connect previous node to this one (array format) - const prevNodeId = i === 0 ? triggerNode.id : `set_${i-1}_${timestamp}`; - connections.push({ - source: prevNodeId, - target: nodeId, - sourceOutput: 0, - targetInput: 0 - }); - } - - return { - name: `${config.testWorkflowPrefix}Complex_${timestamp}`, - nodes, - connections, - settings: { - executionOrder: "v1" - } - }; -} - -// ============================================================================ -// Test Helper Functions -// ============================================================================ - -/** - * Validate workflow structure matches expected format - */ -function validateWorkflowStructure(workflow, testName) { - const issues = []; - - // Check required fields - if (!workflow.id) issues.push('Missing id field'); - if (!workflow.name) issues.push('Missing name field'); - if (typeof workflow.active !== 'boolean') issues.push('active field not boolean'); - if (!workflow.createdAt) issues.push('Missing createdAt field'); - if (!workflow.updatedAt) issues.push('Missing updatedAt field'); - if (!Array.isArray(workflow.nodes)) issues.push('nodes not an array'); - if (typeof workflow.connections !== 'object') issues.push('connections not an object'); - - // Validate date formats (ISO 8601) - if (workflow.createdAt && !isValidISODate(workflow.createdAt)) { - issues.push('createdAt not valid ISO 8601 date'); - } - if (workflow.updatedAt && !isValidISODate(workflow.updatedAt)) { - issues.push('updatedAt not valid ISO 8601 date'); - } - - if (issues.length > 0) { - logger.error(`${testName}: Structure validation failed`, issues.join(', ')); - return false; - } - - return true; -} - -/** - * Check if string is valid ISO 8601 date - */ -function isValidISODate(dateString) { - const date = new Date(dateString); - return date instanceof Date && !isNaN(date) && dateString.includes('T'); -} - -/** - * Sleep helper for async delays - */ -function sleep(ms) { - return new Promise(resolve => setTimeout(resolve, ms)); -} - -// ============================================================================ -// TASK 2: Validate GET /workflows (list_workflows) -// ============================================================================ - -async function testListWorkflows() { - logger.subsection('Task 2: Validate list_workflows'); - - try { - // Test 2.1: List all workflows without filters - try { - const result = await callTool('list_workflows', {}); - const workflows = result.content?.[0]?.text ? JSON.parse(result.content[0].text) : []; - - const isValid = Array.isArray(workflows); - logger.test( - 'list_workflows - List all workflows', - isValid, - isValid ? `Found ${workflows.length} workflows` : 'Invalid response format' - ); - - // Validate structure of first workflow if any exist - if (workflows.length > 0) { - const firstWorkflow = workflows[0]; - const hasRequiredFields = - typeof firstWorkflow.id === 'string' && - typeof firstWorkflow.name === 'string' && - typeof firstWorkflow.active === 'boolean' && - firstWorkflow.createdAt && - firstWorkflow.updatedAt; - - logger.test( - 'list_workflows - Response structure validation', - hasRequiredFields, - hasRequiredFields ? 'All required fields present' : 'Missing required fields' - ); - } - } catch (error) { - logger.test('list_workflows - List all workflows', false, error.message); - } - - // Test 2.2: Filter by active status - try { - const activeResult = await callTool('list_workflows', { active: true }); - const activeWorkflows = activeResult.content?.[0]?.text ? JSON.parse(activeResult.content[0].text) : []; - - const allActive = activeWorkflows.every(w => w.active === true); - logger.test( - 'list_workflows - Filter active=true', - allActive, - allActive ? `${activeWorkflows.length} active workflows` : 'Found inactive workflows' - ); - } catch (error) { - logger.test('list_workflows - Filter active=true', false, error.message); - } - - try { - const inactiveResult = await callTool('list_workflows', { active: false }); - const inactiveWorkflows = inactiveResult.content?.[0]?.text ? JSON.parse(inactiveResult.content[0].text) : []; - - const allInactive = inactiveWorkflows.every(w => w.active === false); - logger.test( - 'list_workflows - Filter active=false', - allInactive, - allInactive ? `${inactiveWorkflows.length} inactive workflows` : 'Found active workflows' - ); - } catch (error) { - logger.test('list_workflows - Filter active=false', false, error.message); - } - - // Test 2.3: Pagination - try { - const limitResult = await callTool('list_workflows', { limit: 5 }); - const limitedWorkflows = limitResult.content?.[0]?.text ? JSON.parse(limitResult.content[0].text) : []; - - const withinLimit = limitedWorkflows.length <= 5; - logger.test( - 'list_workflows - Pagination limit=5', - withinLimit, - `Returned ${limitedWorkflows.length} workflows` - ); - } catch (error) { - logger.test('list_workflows - Pagination limit', false, error.message); - } - - } catch (error) { - logger.error('list_workflows tests failed', error); - } -} - -// ============================================================================ -// TASK 3: Validate GET /workflows/{id} (get_workflow) -// ============================================================================ - -async function testGetWorkflow() { - logger.subsection('Task 3: Validate get_workflow'); - - // First create a test workflow to retrieve - let workflowId; - - try { - const createResult = await callTool('create_workflow', createCompleteWorkflow()); - const workflow = createResult.content?.[0]?.text ? JSON.parse(createResult.content[0].text) : null; - workflowId = workflow?.id; - - if (!workflowId) { - logger.error('Failed to create test workflow for get_workflow tests'); - return; - } - - testData.workflowIds.push(workflowId); - logger.info(`Created test workflow ${workflowId} for get_workflow tests`); - - } catch (error) { - logger.error('Failed to setup get_workflow tests', error); - return; - } - - try { - // Test 3.1: Retrieve existing workflow - try { - const result = await callTool('get_workflow', { id: workflowId }); - const workflow = result.content?.[0]?.text ? JSON.parse(result.content[0].text) : null; - - const isValid = workflow && workflow.id === workflowId; - logger.test( - 'get_workflow - Retrieve by ID', - isValid, - isValid ? 'Workflow retrieved successfully' : 'Failed to retrieve workflow' - ); - - // Validate complete structure - if (workflow) { - const structureValid = validateWorkflowStructure(workflow, 'get_workflow - Structure validation'); - logger.test('get_workflow - Structure validation', structureValid); - - // Check nodes and connections - const hasNodes = Array.isArray(workflow.nodes) && workflow.nodes.length > 0; - const hasConnections = typeof workflow.connections === 'object'; - logger.test( - 'get_workflow - Nodes and connections', - hasNodes && hasConnections, - `${workflow.nodes?.length || 0} nodes, connections present: ${hasConnections}` - ); - } - } catch (error) { - logger.test('get_workflow - Retrieve by ID', false, error.message); - } - - // Test 3.4: Error scenarios - non-existent workflow - try { - await callTool('get_workflow', { id: 'non-existent-id-12345' }); - logger.test('get_workflow - 404 for non-existent ID', false, 'Should have thrown error'); - } catch (error) { - const is404 = error.message.includes('404') || error.message.includes('not found'); - logger.test( - 'get_workflow - 404 for non-existent ID', - is404, - is404 ? 'Correctly returned 404' : 'Wrong error type' - ); - } - - } catch (error) { - logger.error('get_workflow tests failed', error); - } -} - -// ============================================================================ -// TASK 4: Validate POST /workflows (create_workflow) -// ============================================================================ - -async function testCreateWorkflow() { - logger.subsection('Task 4: Validate create_workflow'); - - try { - // Test 4.1: Create minimal workflow - try { - const minimal = createMinimalWorkflow(); - const result = await callTool('create_workflow', minimal); - const workflow = result.content?.[0]?.text ? JSON.parse(result.content[0].text) : null; - - const isValid = workflow && workflow.id && workflow.name === minimal.name; - if (workflow?.id) { - testData.workflowIds.push(workflow.id); - } - - logger.test( - 'create_workflow - Minimal workflow', - isValid, - isValid ? `Created workflow ${workflow.id}` : 'Creation failed' - ); - } catch (error) { - logger.test('create_workflow - Minimal workflow', false, error.message); - } - - // Test 4.2: Create complete workflow with nodes - try { - const complete = createCompleteWorkflow(); - const result = await callTool('create_workflow', complete); - const workflow = result.content?.[0]?.text ? JSON.parse(result.content[0].text) : null; - - const isValid = - workflow && - workflow.id && - workflow.nodes.length === complete.nodes.length && - Object.keys(workflow.connections).length > 0; - - if (workflow?.id) { - testData.workflowIds.push(workflow.id); - } - - logger.test( - 'create_workflow - Complete workflow with nodes', - isValid, - isValid ? `Created with ${workflow.nodes.length} nodes` : 'Node/connection mismatch' - ); - } catch (error) { - logger.test('create_workflow - Complete workflow', false, error.message); - } - - // Test 4.3: Create complex workflow - try { - const complex = createComplexWorkflow(); - const result = await callTool('create_workflow', complex); - const workflow = result.content?.[0]?.text ? JSON.parse(result.content[0].text) : null; - - const isValid = workflow && workflow.nodes.length >= 10; - if (workflow?.id) { - testData.workflowIds.push(workflow.id); - } - - logger.test( - 'create_workflow - Complex workflow (10+ nodes)', - isValid, - isValid ? `Created with ${workflow.nodes.length} nodes` : 'Failed to create complex workflow' - ); - } catch (error) { - logger.test('create_workflow - Complex workflow', false, error.message); - } - - } catch (error) { - logger.error('create_workflow tests failed', error); - } -} - -// ============================================================================ -// TASK 5: Validate PUT /workflows/{id} (update_workflow) -// ============================================================================ - -async function testUpdateWorkflow() { - logger.subsection('Task 5: Validate update_workflow'); - - // Create a workflow to update - let workflowId; - try { - const createResult = await callTool('create_workflow', createCompleteWorkflow()); - const workflow = createResult.content?.[0]?.text ? JSON.parse(createResult.content[0].text) : null; - workflowId = workflow?.id; - - if (!workflowId) { - logger.error('Failed to create test workflow for update_workflow tests'); - return; - } - - testData.workflowIds.push(workflowId); - } catch (error) { - logger.error('Failed to setup update_workflow tests', error); - return; - } - - try { - // Test 5.1: Update workflow name - try { - const newName = `${config.testWorkflowPrefix}Updated_${Date.now()}`; - - // update_workflow expects flat parameters: id, name, nodes, connections - // NOT a nested 'workflow' object - const result = await callTool('update_workflow', { - id: workflowId, - name: newName - }); - const updated = result.content?.[0]?.text ? JSON.parse(result.content[0].text) : null; - - const isValid = updated && updated.name === newName; - logger.test( - 'update_workflow - Update name', - isValid, - isValid ? 'Name updated successfully' : 'Name update failed' - ); - } catch (error) { - logger.test('update_workflow - Update name', false, error.message); - } - - // Test 5.5: Error - Update non-existent workflow - try { - await callTool('update_workflow', { - id: 'non-existent-id-12345', - workflow: createMinimalWorkflow() - }); - logger.test('update_workflow - 404 for non-existent ID', false, 'Should have thrown error'); - } catch (error) { - const is404 = error.message.includes('404') || error.message.includes('not found'); - logger.test( - 'update_workflow - 404 for non-existent ID', - is404, - is404 ? 'Correctly returned 404' : 'Wrong error type' - ); - } - - } catch (error) { - logger.error('update_workflow tests failed', error); - } -} - -// ============================================================================ -// TASK 6: Validate DELETE /workflows/{id} (delete_workflow) -// ============================================================================ - -async function testDeleteWorkflow() { - logger.subsection('Task 6: Validate delete_workflow'); - - try { - // Test 6.1: Delete existing workflow - try { - // Create a workflow to delete - const createResult = await callTool('create_workflow', createMinimalWorkflow()); - const workflow = createResult.content?.[0]?.text ? JSON.parse(createResult.content[0].text) : null; - const workflowId = workflow?.id; - - if (!workflowId) { - throw new Error('Failed to create workflow for deletion test'); - } - - // Delete it - const deleteResult = await callTool('delete_workflow', { id: workflowId }); - - // Verify it's gone - try { - await callTool('get_workflow', { id: workflowId }); - logger.test('delete_workflow - Delete and verify', false, 'Workflow still exists after deletion'); - } catch (error) { - const isGone = error.message.includes('404') || error.message.includes('not found'); - logger.test( - 'delete_workflow - Delete and verify', - isGone, - isGone ? 'Workflow successfully deleted' : 'Unexpected error' - ); - } - } catch (error) { - logger.test('delete_workflow - Delete existing workflow', false, error.message); - } - - // Test 6.4: Error - Delete non-existent workflow - try { - await callTool('delete_workflow', { id: 'non-existent-id-12345' }); - logger.test('delete_workflow - 404 for non-existent ID', false, 'Should have thrown error'); - } catch (error) { - const is404 = error.message.includes('404') || error.message.includes('not found'); - logger.test( - 'delete_workflow - 404 for non-existent ID', - is404, - is404 ? 'Correctly returned 404' : 'Wrong error type' - ); - } - - } catch (error) { - logger.error('delete_workflow tests failed', error); - } -} - -// ============================================================================ -// TASK 7: Validate PUT /workflows/{id}/activate (activate_workflow) -// ============================================================================ - -async function testActivateWorkflow() { - logger.subsection('Task 7: Validate activate_workflow'); - - // Note: n8n API does not support programmatic workflow activation via REST API - // The activate_workflow method should return an informative error message - - // Create an inactive workflow to test with - let workflowId; - try { - const createResult = await callTool('create_workflow', createCompleteWorkflow()); - const workflow = createResult.content?.[0]?.text ? JSON.parse(createResult.content[0].text) : null; - workflowId = workflow?.id; - - if (!workflowId) { - logger.error('Failed to create test workflow for activate_workflow tests'); - return; - } - - testData.workflowIds.push(workflowId); - } catch (error) { - logger.error('Failed to setup activate_workflow tests', error); - return; - } - - try { - // Test 7.1: Verify activation returns "not supported" error - try { - await callTool('activate_workflow', { id: workflowId }); - logger.test( - 'activate_workflow - Returns "not supported" error', - false, - 'Should have thrown error indicating activation not supported' - ); - } catch (error) { - const isNotSupported = - error.message.includes('not supported') || - error.message.includes('not available') || - error.message.includes('read-only'); - logger.test( - 'activate_workflow - Returns "not supported" error', - isNotSupported, - isNotSupported ? 'Correctly indicates activation not supported' : `Wrong error: ${error.message.substring(0, 100)}` - ); - } - - // Test 7.2: Verify error message includes helpful guidance - try { - await callTool('activate_workflow', { id: workflowId }); - logger.test('activate_workflow - Error includes manual activation guidance', false, 'Should have thrown error'); - } catch (error) { - const hasGuidance = - error.message.includes('manual') || - error.message.includes('web interface') || - error.message.includes('toggle'); - logger.test( - 'activate_workflow - Error includes manual activation guidance', - hasGuidance, - hasGuidance ? 'Error message includes helpful guidance' : 'Missing user guidance' - ); - } - - } catch (error) { - logger.error('activate_workflow tests failed', error); - } -} - -// ============================================================================ -// TASK 8: Validate PUT /workflows/{id}/deactivate (deactivate_workflow) -// ============================================================================ - -async function testDeactivateWorkflow() { - logger.subsection('Task 8: Validate deactivate_workflow'); - - // Note: n8n API does not support programmatic workflow deactivation via REST API - // The deactivate_workflow method should return an informative error message - - // Create a workflow to test with - let workflowId; - try { - const createResult = await callTool('create_workflow', createCompleteWorkflow()); - const workflow = createResult.content?.[0]?.text ? JSON.parse(createResult.content[0].text) : null; - workflowId = workflow?.id; - - if (!workflowId) { - logger.error('Failed to create test workflow for deactivate_workflow tests'); - return; - } - - testData.workflowIds.push(workflowId); - } catch (error) { - logger.error('Failed to setup deactivate_workflow tests', error); - return; - } - - try { - // Test 8.1: Verify deactivation returns "not supported" error - try { - await callTool('deactivate_workflow', { id: workflowId }); - logger.test( - 'deactivate_workflow - Returns "not supported" error', - false, - 'Should have thrown error indicating deactivation not supported' - ); - } catch (error) { - const isNotSupported = - error.message.includes('not supported') || - error.message.includes('not available') || - error.message.includes('read-only'); - logger.test( - 'deactivate_workflow - Returns "not supported" error', - isNotSupported, - isNotSupported ? 'Correctly indicates deactivation not supported' : `Wrong error: ${error.message.substring(0, 100)}` - ); - } - - // Test 8.2: Verify error message includes helpful guidance - try { - await callTool('deactivate_workflow', { id: workflowId }); - logger.test('deactivate_workflow - Error includes manual deactivation guidance', false, 'Should have thrown error'); - } catch (error) { - const hasGuidance = - error.message.includes('manual') || - error.message.includes('web interface') || - error.message.includes('toggle'); - logger.test( - 'deactivate_workflow - Error includes manual deactivation guidance', - hasGuidance, - hasGuidance ? 'Error message includes helpful guidance' : 'Missing user guidance' - ); - } - - } catch (error) { - logger.error('deactivate_workflow tests failed', error); - } -} - -// ============================================================================ -// TASK 9: Validate Workflow Execution -// ============================================================================ - -async function testExecuteWorkflow() { - logger.subsection('Task 9: Validate execute_workflow'); - - try { - // Test 9.2: Manual trigger limitation (documented n8n API limitation) - logger.validationFinding( - 'execute_workflow', - 'Manual trigger workflows cannot be executed via REST API - n8n v1.82.3 limitation' - ); - - logger.test( - 'execute_workflow - Manual trigger limitation', - true, - 'Known limitation documented' - ); - - // Test 9.4: Error - Execute non-existent workflow - try { - await callTool('execute_workflow', { id: 'non-existent-id-12345' }); - logger.test('execute_workflow - 404 for non-existent ID', false, 'Should have thrown error'); - } catch (error) { - const isError = error.message.includes('404') || error.message.includes('not found') || error.message.includes('cannot'); - logger.test( - 'execute_workflow - 404 for non-existent ID', - isError, - isError ? 'Correctly returned error' : 'Wrong error type' - ); - } - - } catch (error) { - logger.error('execute_workflow tests failed', error); - } -} - -// ============================================================================ -// TASK 10: Multi-Instance Validation -// ============================================================================ - -async function testMultiInstance() { - if (!testFlags.runMultiInstanceTests) { - logger.skip('Multi-instance tests', 'Requires .config.json setup'); - return; - } - - logger.subsection('Task 10: Multi-Instance Validation'); - - // Test with different instances - for (const [instanceName, instanceSlug] of Object.entries(config.instances)) { - try { - const result = await callTool('list_workflows', { instance: instanceSlug }); - const workflows = result.content?.[0]?.text ? JSON.parse(result.content[0].text) : []; - - logger.test( - `multi-instance - List workflows (${instanceName})`, - true, - `Found ${workflows.length} workflows` - ); - } catch (error) { - logger.test(`multi-instance - ${instanceName}`, false, error.message); - } - } -} - -// ============================================================================ -// TASK 11: Error Handling Validation -// ============================================================================ - -async function testErrorHandling() { - logger.subsection('Task 11: Error Handling Validation'); - - // Note: Detailed error tests are integrated into each method's test suite - // This section provides a summary - - logger.info('Error handling tests are integrated into each API method test suite:'); - logger.info(' - 404 errors for non-existent resources'); - logger.info(' - 400 errors for malformed requests'); - logger.info(' - Error response format validation'); - logger.info(' - Multi-instance error handling'); -} - -// ============================================================================ -// Cleanup Function -// ============================================================================ - -async function cleanup() { - if (!testFlags.runCleanup) { - logger.warn('Cleanup skipped - test data retained for debugging'); - return; - } - - logger.section('Cleanup'); - logger.info(`Cleaning up ${testData.workflowIds.length} test workflows...`); - - let cleaned = 0; - for (const workflowId of testData.workflowIds) { - try { - await callTool('delete_workflow', { id: workflowId }); - cleaned++; - } catch (error) { - logger.debug(`Failed to delete workflow ${workflowId}`, error); - } - } - - logger.success(`Cleaned up ${cleaned}/${testData.workflowIds.length} test workflows`); -} - -// ============================================================================ -// Main Test Runner -// ============================================================================ - -async function main() { - logger.section('Workflows API Validation Test Suite - Story 2.1'); - logger.info('Testing 8 Workflows API methods against live n8n instance'); - logger.info(`MCP Server: ${config.mcpServerUrl}`); - - // Health check - logger.subsection('Pre-flight Checks'); - const isHealthy = await checkServerHealth(); - if (!isHealthy) { - logger.error('MCP server is not healthy - aborting tests'); - process.exit(1); - } - - try { - // Run test suites based on flags - if (testFlags.runListWorkflowsTests) await testListWorkflows(); - if (testFlags.runGetWorkflowTests) await testGetWorkflow(); - if (testFlags.runCreateWorkflowTests) await testCreateWorkflow(); - if (testFlags.runUpdateWorkflowTests) await testUpdateWorkflow(); - if (testFlags.runDeleteWorkflowTests) await testDeleteWorkflow(); - if (testFlags.runActivateWorkflowTests) await testActivateWorkflow(); - if (testFlags.runDeactivateWorkflowTests) await testDeactivateWorkflow(); - if (testFlags.runExecuteWorkflowTests) await testExecuteWorkflow(); - if (testFlags.runMultiInstanceTests) await testMultiInstance(); - if (testFlags.runErrorHandlingTests) await testErrorHandling(); - - } catch (error) { - logger.error('Test suite failed', error); - } finally { - // Cleanup - await cleanup(); - - // Summary report - logger.summaryReport(); - - // Exit with appropriate code - const exitCode = testData.results.failed > 0 ? 1 : 0; - process.exit(exitCode); - } -} - -// Run if executed directly -if (require.main === module) { - main().catch(error => { - logger.error('Fatal error', error); - process.exit(1); - }); -} - -module.exports = { - testListWorkflows, - testGetWorkflow, - testCreateWorkflow, - testUpdateWorkflow, - testDeleteWorkflow, - testActivateWorkflow, - testDeactivateWorkflow, - testExecuteWorkflow, - testMultiInstance, - testErrorHandling -};