This is page 50 of 59. Use http://codebase.md/czlonkowski/n8n-mcp?lines=true&page={x} to view the full context. # Directory Structure ``` ├── _config.yml ├── .claude │ └── agents │ ├── code-reviewer.md │ ├── context-manager.md │ ├── debugger.md │ ├── deployment-engineer.md │ ├── mcp-backend-engineer.md │ ├── n8n-mcp-tester.md │ ├── technical-researcher.md │ └── test-automator.md ├── .dockerignore ├── .env.docker ├── .env.example ├── .env.n8n.example ├── .env.test ├── .env.test.example ├── .github │ ├── ABOUT.md │ ├── BENCHMARK_THRESHOLDS.md │ ├── FUNDING.yml │ ├── gh-pages.yml │ ├── secret_scanning.yml │ └── workflows │ ├── benchmark-pr.yml │ ├── benchmark.yml │ ├── docker-build-fast.yml │ ├── docker-build-n8n.yml │ ├── docker-build.yml │ ├── release.yml │ ├── test.yml │ └── update-n8n-deps.yml ├── .gitignore ├── .npmignore ├── ATTRIBUTION.md ├── CHANGELOG.md ├── CLAUDE.md ├── codecov.yml ├── coverage.json ├── data │ ├── .gitkeep │ ├── nodes.db │ ├── nodes.db-shm │ ├── nodes.db-wal │ └── templates.db ├── deploy │ └── quick-deploy-n8n.sh ├── docker │ ├── docker-entrypoint.sh │ ├── n8n-mcp │ ├── parse-config.js │ └── README.md ├── docker-compose.buildkit.yml ├── docker-compose.extract.yml ├── docker-compose.n8n.yml ├── docker-compose.override.yml.example ├── docker-compose.test-n8n.yml ├── docker-compose.yml ├── Dockerfile ├── Dockerfile.railway ├── Dockerfile.test ├── docs │ ├── AUTOMATED_RELEASES.md │ ├── BENCHMARKS.md │ ├── CHANGELOG.md │ ├── CLAUDE_CODE_SETUP.md │ ├── CLAUDE_INTERVIEW.md │ ├── CODECOV_SETUP.md │ ├── CODEX_SETUP.md │ ├── CURSOR_SETUP.md │ ├── DEPENDENCY_UPDATES.md │ ├── DOCKER_README.md │ ├── DOCKER_TROUBLESHOOTING.md │ ├── FINAL_AI_VALIDATION_SPEC.md │ ├── FLEXIBLE_INSTANCE_CONFIGURATION.md │ ├── HTTP_DEPLOYMENT.md │ ├── img │ │ ├── cc_command.png │ │ ├── cc_connected.png │ │ ├── codex_connected.png │ │ ├── cursor_tut.png │ │ ├── Railway_api.png │ │ ├── Railway_server_address.png │ │ ├── vsc_ghcp_chat_agent_mode.png │ │ ├── vsc_ghcp_chat_instruction_files.png │ │ ├── vsc_ghcp_chat_thinking_tool.png │ │ └── windsurf_tut.png │ ├── INSTALLATION.md │ ├── LIBRARY_USAGE.md │ ├── local │ │ ├── DEEP_DIVE_ANALYSIS_2025-10-02.md │ │ ├── DEEP_DIVE_ANALYSIS_README.md │ │ ├── Deep_dive_p1_p2.md │ │ ├── integration-testing-plan.md │ │ ├── integration-tests-phase1-summary.md │ │ ├── N8N_AI_WORKFLOW_BUILDER_ANALYSIS.md │ │ ├── P0_IMPLEMENTATION_PLAN.md │ │ └── TEMPLATE_MINING_ANALYSIS.md │ ├── MCP_ESSENTIALS_README.md │ ├── MCP_QUICK_START_GUIDE.md │ ├── N8N_DEPLOYMENT.md │ ├── RAILWAY_DEPLOYMENT.md │ ├── README_CLAUDE_SETUP.md │ ├── README.md │ ├── tools-documentation-usage.md │ ├── VS_CODE_PROJECT_SETUP.md │ ├── WINDSURF_SETUP.md │ └── workflow-diff-examples.md ├── examples │ └── enhanced-documentation-demo.js ├── fetch_log.txt ├── LICENSE ├── MEMORY_N8N_UPDATE.md ├── MEMORY_TEMPLATE_UPDATE.md ├── monitor_fetch.sh ├── N8N_HTTP_STREAMABLE_SETUP.md ├── n8n-nodes.db ├── P0-R3-TEST-PLAN.md ├── package-lock.json ├── package.json ├── package.runtime.json ├── PRIVACY.md ├── railway.json ├── README.md ├── renovate.json ├── scripts │ ├── analyze-optimization.sh │ ├── audit-schema-coverage.ts │ ├── build-optimized.sh │ ├── compare-benchmarks.js │ ├── demo-optimization.sh │ ├── deploy-http.sh │ ├── deploy-to-vm.sh │ ├── export-webhook-workflows.ts │ ├── extract-changelog.js │ ├── extract-from-docker.js │ ├── extract-nodes-docker.sh │ ├── extract-nodes-simple.sh │ ├── format-benchmark-results.js │ ├── generate-benchmark-stub.js │ ├── generate-detailed-reports.js │ ├── generate-test-summary.js │ ├── http-bridge.js │ ├── mcp-http-client.js │ ├── migrate-nodes-fts.ts │ ├── migrate-tool-docs.ts │ ├── n8n-docs-mcp.service │ ├── nginx-n8n-mcp.conf │ ├── prebuild-fts5.ts │ ├── prepare-release.js │ ├── publish-npm-quick.sh │ ├── publish-npm.sh │ ├── quick-test.ts │ ├── run-benchmarks-ci.js │ ├── sync-runtime-version.js │ ├── test-ai-validation-debug.ts │ ├── test-code-node-enhancements.ts │ ├── test-code-node-fixes.ts │ ├── test-docker-config.sh │ ├── test-docker-fingerprint.ts │ ├── test-docker-optimization.sh │ ├── test-docker.sh │ ├── test-empty-connection-validation.ts │ ├── test-error-message-tracking.ts │ ├── test-error-output-validation.ts │ ├── test-error-validation.js │ ├── test-essentials.ts │ ├── test-expression-code-validation.ts │ ├── test-expression-format-validation.js │ ├── test-fts5-search.ts │ ├── test-fuzzy-fix.ts │ ├── test-fuzzy-simple.ts │ ├── test-helpers-validation.ts │ ├── test-http-search.ts │ ├── test-http.sh │ ├── test-jmespath-validation.ts │ ├── test-multi-tenant-simple.ts │ ├── test-multi-tenant.ts │ ├── test-n8n-integration.sh │ ├── test-node-info.js │ ├── test-node-type-validation.ts │ ├── test-nodes-base-prefix.ts │ ├── test-operation-validation.ts │ ├── test-optimized-docker.sh │ ├── test-release-automation.js │ ├── test-search-improvements.ts │ ├── test-security.ts │ ├── test-single-session.sh │ ├── test-sqljs-triggers.ts │ ├── test-telemetry-debug.ts │ ├── test-telemetry-direct.ts │ ├── test-telemetry-env.ts │ ├── test-telemetry-integration.ts │ ├── test-telemetry-no-select.ts │ ├── test-telemetry-security.ts │ ├── test-telemetry-simple.ts │ ├── test-typeversion-validation.ts │ ├── test-url-configuration.ts │ ├── test-user-id-persistence.ts │ ├── test-webhook-validation.ts │ ├── test-workflow-insert.ts │ ├── test-workflow-sanitizer.ts │ ├── test-workflow-tracking-debug.ts │ ├── update-and-publish-prep.sh │ ├── update-n8n-deps.js │ ├── update-readme-version.js │ ├── vitest-benchmark-json-reporter.js │ └── vitest-benchmark-reporter.ts ├── SECURITY.md ├── src │ ├── config │ │ └── n8n-api.ts │ ├── data │ │ └── canonical-ai-tool-examples.json │ ├── database │ │ ├── database-adapter.ts │ │ ├── migrations │ │ │ └── add-template-node-configs.sql │ │ ├── node-repository.ts │ │ ├── nodes.db │ │ ├── schema-optimized.sql │ │ └── schema.sql │ ├── errors │ │ └── validation-service-error.ts │ ├── http-server-single-session.ts │ ├── http-server.ts │ ├── index.ts │ ├── loaders │ │ └── node-loader.ts │ ├── mappers │ │ └── docs-mapper.ts │ ├── mcp │ │ ├── handlers-n8n-manager.ts │ │ ├── handlers-workflow-diff.ts │ │ ├── index.ts │ │ ├── server.ts │ │ ├── stdio-wrapper.ts │ │ ├── tool-docs │ │ │ ├── configuration │ │ │ │ ├── get-node-as-tool-info.ts │ │ │ │ ├── get-node-documentation.ts │ │ │ │ ├── get-node-essentials.ts │ │ │ │ ├── get-node-info.ts │ │ │ │ ├── get-property-dependencies.ts │ │ │ │ ├── index.ts │ │ │ │ └── search-node-properties.ts │ │ │ ├── discovery │ │ │ │ ├── get-database-statistics.ts │ │ │ │ ├── index.ts │ │ │ │ ├── list-ai-tools.ts │ │ │ │ ├── list-nodes.ts │ │ │ │ └── search-nodes.ts │ │ │ ├── guides │ │ │ │ ├── ai-agents-guide.ts │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ ├── system │ │ │ │ ├── index.ts │ │ │ │ ├── n8n-diagnostic.ts │ │ │ │ ├── n8n-health-check.ts │ │ │ │ ├── n8n-list-available-tools.ts │ │ │ │ └── tools-documentation.ts │ │ │ ├── templates │ │ │ │ ├── get-template.ts │ │ │ │ ├── get-templates-for-task.ts │ │ │ │ ├── index.ts │ │ │ │ ├── list-node-templates.ts │ │ │ │ ├── list-tasks.ts │ │ │ │ ├── search-templates-by-metadata.ts │ │ │ │ └── search-templates.ts │ │ │ ├── types.ts │ │ │ ├── validation │ │ │ │ ├── index.ts │ │ │ │ ├── validate-node-minimal.ts │ │ │ │ ├── validate-node-operation.ts │ │ │ │ ├── validate-workflow-connections.ts │ │ │ │ ├── validate-workflow-expressions.ts │ │ │ │ └── validate-workflow.ts │ │ │ └── workflow_management │ │ │ ├── index.ts │ │ │ ├── n8n-autofix-workflow.ts │ │ │ ├── n8n-create-workflow.ts │ │ │ ├── n8n-delete-execution.ts │ │ │ ├── n8n-delete-workflow.ts │ │ │ ├── n8n-get-execution.ts │ │ │ ├── n8n-get-workflow-details.ts │ │ │ ├── n8n-get-workflow-minimal.ts │ │ │ ├── n8n-get-workflow-structure.ts │ │ │ ├── n8n-get-workflow.ts │ │ │ ├── n8n-list-executions.ts │ │ │ ├── n8n-list-workflows.ts │ │ │ ├── n8n-trigger-webhook-workflow.ts │ │ │ ├── n8n-update-full-workflow.ts │ │ │ ├── n8n-update-partial-workflow.ts │ │ │ └── n8n-validate-workflow.ts │ │ ├── tools-documentation.ts │ │ ├── tools-n8n-friendly.ts │ │ ├── tools-n8n-manager.ts │ │ ├── tools.ts │ │ └── workflow-examples.ts │ ├── mcp-engine.ts │ ├── mcp-tools-engine.ts │ ├── n8n │ │ ├── MCPApi.credentials.ts │ │ └── MCPNode.node.ts │ ├── parsers │ │ ├── node-parser.ts │ │ ├── property-extractor.ts │ │ └── simple-parser.ts │ ├── scripts │ │ ├── debug-http-search.ts │ │ ├── extract-from-docker.ts │ │ ├── fetch-templates-robust.ts │ │ ├── fetch-templates.ts │ │ ├── rebuild-database.ts │ │ ├── rebuild-optimized.ts │ │ ├── rebuild.ts │ │ ├── sanitize-templates.ts │ │ ├── seed-canonical-ai-examples.ts │ │ ├── test-autofix-documentation.ts │ │ ├── test-autofix-workflow.ts │ │ ├── test-execution-filtering.ts │ │ ├── test-node-suggestions.ts │ │ ├── test-protocol-negotiation.ts │ │ ├── test-summary.ts │ │ ├── test-webhook-autofix.ts │ │ ├── validate.ts │ │ └── validation-summary.ts │ ├── services │ │ ├── ai-node-validator.ts │ │ ├── ai-tool-validators.ts │ │ ├── confidence-scorer.ts │ │ ├── config-validator.ts │ │ ├── enhanced-config-validator.ts │ │ ├── example-generator.ts │ │ ├── execution-processor.ts │ │ ├── expression-format-validator.ts │ │ ├── expression-validator.ts │ │ ├── n8n-api-client.ts │ │ ├── n8n-validation.ts │ │ ├── node-documentation-service.ts │ │ ├── node-similarity-service.ts │ │ ├── node-specific-validators.ts │ │ ├── operation-similarity-service.ts │ │ ├── property-dependencies.ts │ │ ├── property-filter.ts │ │ ├── resource-similarity-service.ts │ │ ├── sqlite-storage-service.ts │ │ ├── task-templates.ts │ │ ├── universal-expression-validator.ts │ │ ├── workflow-auto-fixer.ts │ │ ├── workflow-diff-engine.ts │ │ └── workflow-validator.ts │ ├── telemetry │ │ ├── batch-processor.ts │ │ ├── config-manager.ts │ │ ├── early-error-logger.ts │ │ ├── error-sanitization-utils.ts │ │ ├── error-sanitizer.ts │ │ ├── event-tracker.ts │ │ ├── event-validator.ts │ │ ├── index.ts │ │ ├── performance-monitor.ts │ │ ├── rate-limiter.ts │ │ ├── startup-checkpoints.ts │ │ ├── telemetry-error.ts │ │ ├── telemetry-manager.ts │ │ ├── telemetry-types.ts │ │ └── workflow-sanitizer.ts │ ├── templates │ │ ├── batch-processor.ts │ │ ├── metadata-generator.ts │ │ ├── README.md │ │ ├── template-fetcher.ts │ │ ├── template-repository.ts │ │ └── template-service.ts │ ├── types │ │ ├── index.ts │ │ ├── instance-context.ts │ │ ├── n8n-api.ts │ │ ├── node-types.ts │ │ └── workflow-diff.ts │ └── utils │ ├── auth.ts │ ├── bridge.ts │ ├── cache-utils.ts │ ├── console-manager.ts │ ├── documentation-fetcher.ts │ ├── enhanced-documentation-fetcher.ts │ ├── error-handler.ts │ ├── example-generator.ts │ ├── fixed-collection-validator.ts │ ├── logger.ts │ ├── mcp-client.ts │ ├── n8n-errors.ts │ ├── node-source-extractor.ts │ ├── node-type-normalizer.ts │ ├── node-type-utils.ts │ ├── node-utils.ts │ ├── npm-version-checker.ts │ ├── protocol-version.ts │ ├── simple-cache.ts │ ├── ssrf-protection.ts │ ├── template-node-resolver.ts │ ├── template-sanitizer.ts │ ├── url-detector.ts │ ├── validation-schemas.ts │ └── version.ts ├── test-output.txt ├── test-reinit-fix.sh ├── tests │ ├── __snapshots__ │ │ └── .gitkeep │ ├── auth.test.ts │ ├── benchmarks │ │ ├── database-queries.bench.ts │ │ ├── index.ts │ │ ├── mcp-tools.bench.ts │ │ ├── mcp-tools.bench.ts.disabled │ │ ├── mcp-tools.bench.ts.skip │ │ ├── node-loading.bench.ts.disabled │ │ ├── README.md │ │ ├── search-operations.bench.ts.disabled │ │ └── validation-performance.bench.ts.disabled │ ├── bridge.test.ts │ ├── comprehensive-extraction-test.js │ ├── data │ │ └── .gitkeep │ ├── debug-slack-doc.js │ ├── demo-enhanced-documentation.js │ ├── docker-tests-README.md │ ├── error-handler.test.ts │ ├── examples │ │ └── using-database-utils.test.ts │ ├── extracted-nodes-db │ │ ├── database-import.json │ │ ├── extraction-report.json │ │ ├── insert-nodes.sql │ │ ├── n8n-nodes-base__Airtable.json │ │ ├── n8n-nodes-base__Discord.json │ │ ├── n8n-nodes-base__Function.json │ │ ├── n8n-nodes-base__HttpRequest.json │ │ ├── n8n-nodes-base__If.json │ │ ├── n8n-nodes-base__Slack.json │ │ ├── n8n-nodes-base__SplitInBatches.json │ │ └── n8n-nodes-base__Webhook.json │ ├── factories │ │ ├── node-factory.ts │ │ └── property-definition-factory.ts │ ├── fixtures │ │ ├── .gitkeep │ │ ├── database │ │ │ └── test-nodes.json │ │ ├── factories │ │ │ ├── node.factory.ts │ │ │ └── parser-node.factory.ts │ │ └── template-configs.ts │ ├── helpers │ │ └── env-helpers.ts │ ├── http-server-auth.test.ts │ ├── integration │ │ ├── ai-validation │ │ │ ├── ai-agent-validation.test.ts │ │ │ ├── ai-tool-validation.test.ts │ │ │ ├── chat-trigger-validation.test.ts │ │ │ ├── e2e-validation.test.ts │ │ │ ├── helpers.ts │ │ │ ├── llm-chain-validation.test.ts │ │ │ ├── README.md │ │ │ └── TEST_REPORT.md │ │ ├── ci │ │ │ └── database-population.test.ts │ │ ├── database │ │ │ ├── connection-management.test.ts │ │ │ ├── empty-database.test.ts │ │ │ ├── fts5-search.test.ts │ │ │ ├── node-fts5-search.test.ts │ │ │ ├── node-repository.test.ts │ │ │ ├── performance.test.ts │ │ │ ├── template-node-configs.test.ts │ │ │ ├── template-repository.test.ts │ │ │ ├── test-utils.ts │ │ │ └── transactions.test.ts │ │ ├── database-integration.test.ts │ │ ├── docker │ │ │ ├── docker-config.test.ts │ │ │ ├── docker-entrypoint.test.ts │ │ │ └── test-helpers.ts │ │ ├── flexible-instance-config.test.ts │ │ ├── mcp │ │ │ └── template-examples-e2e.test.ts │ │ ├── mcp-protocol │ │ │ ├── basic-connection.test.ts │ │ │ ├── error-handling.test.ts │ │ │ ├── performance.test.ts │ │ │ ├── protocol-compliance.test.ts │ │ │ ├── README.md │ │ │ ├── session-management.test.ts │ │ │ ├── test-helpers.ts │ │ │ ├── tool-invocation.test.ts │ │ │ └── workflow-error-validation.test.ts │ │ ├── msw-setup.test.ts │ │ ├── n8n-api │ │ │ ├── executions │ │ │ │ ├── delete-execution.test.ts │ │ │ │ ├── get-execution.test.ts │ │ │ │ ├── list-executions.test.ts │ │ │ │ └── trigger-webhook.test.ts │ │ │ ├── scripts │ │ │ │ └── cleanup-orphans.ts │ │ │ ├── system │ │ │ │ ├── diagnostic.test.ts │ │ │ │ ├── health-check.test.ts │ │ │ │ └── list-tools.test.ts │ │ │ ├── test-connection.ts │ │ │ ├── types │ │ │ │ └── mcp-responses.ts │ │ │ ├── utils │ │ │ │ ├── cleanup-helpers.ts │ │ │ │ ├── credentials.ts │ │ │ │ ├── factories.ts │ │ │ │ ├── fixtures.ts │ │ │ │ ├── mcp-context.ts │ │ │ │ ├── n8n-client.ts │ │ │ │ ├── node-repository.ts │ │ │ │ ├── response-types.ts │ │ │ │ ├── test-context.ts │ │ │ │ └── webhook-workflows.ts │ │ │ └── workflows │ │ │ ├── autofix-workflow.test.ts │ │ │ ├── create-workflow.test.ts │ │ │ ├── delete-workflow.test.ts │ │ │ ├── get-workflow-details.test.ts │ │ │ ├── get-workflow-minimal.test.ts │ │ │ ├── get-workflow-structure.test.ts │ │ │ ├── get-workflow.test.ts │ │ │ ├── list-workflows.test.ts │ │ │ ├── smart-parameters.test.ts │ │ │ ├── update-partial-workflow.test.ts │ │ │ ├── update-workflow.test.ts │ │ │ └── validate-workflow.test.ts │ │ ├── security │ │ │ ├── command-injection-prevention.test.ts │ │ │ └── rate-limiting.test.ts │ │ ├── setup │ │ │ ├── integration-setup.ts │ │ │ └── msw-test-server.ts │ │ ├── telemetry │ │ │ ├── docker-user-id-stability.test.ts │ │ │ └── mcp-telemetry.test.ts │ │ ├── templates │ │ │ └── metadata-operations.test.ts │ │ └── workflow-creation-node-type-format.test.ts │ ├── logger.test.ts │ ├── MOCKING_STRATEGY.md │ ├── mocks │ │ ├── n8n-api │ │ │ ├── data │ │ │ │ ├── credentials.ts │ │ │ │ ├── executions.ts │ │ │ │ └── workflows.ts │ │ │ ├── handlers.ts │ │ │ └── index.ts │ │ └── README.md │ ├── node-storage-export.json │ ├── setup │ │ ├── global-setup.ts │ │ ├── msw-setup.ts │ │ ├── TEST_ENV_DOCUMENTATION.md │ │ └── test-env.ts │ ├── test-database-extraction.js │ ├── test-direct-extraction.js │ ├── test-enhanced-documentation.js │ ├── test-enhanced-integration.js │ ├── test-mcp-extraction.js │ ├── test-mcp-server-extraction.js │ ├── test-mcp-tools-integration.js │ ├── test-node-documentation-service.js │ ├── test-node-list.js │ ├── test-package-info.js │ ├── test-parsing-operations.js │ ├── test-slack-node-complete.js │ ├── test-small-rebuild.js │ ├── test-sqlite-search.js │ ├── test-storage-system.js │ ├── unit │ │ ├── __mocks__ │ │ │ ├── n8n-nodes-base.test.ts │ │ │ ├── n8n-nodes-base.ts │ │ │ └── README.md │ │ ├── database │ │ │ ├── __mocks__ │ │ │ │ └── better-sqlite3.ts │ │ │ ├── database-adapter-unit.test.ts │ │ │ ├── node-repository-core.test.ts │ │ │ ├── node-repository-operations.test.ts │ │ │ ├── node-repository-outputs.test.ts │ │ │ ├── README.md │ │ │ └── template-repository-core.test.ts │ │ ├── docker │ │ │ ├── config-security.test.ts │ │ │ ├── edge-cases.test.ts │ │ │ ├── parse-config.test.ts │ │ │ └── serve-command.test.ts │ │ ├── errors │ │ │ └── validation-service-error.test.ts │ │ ├── examples │ │ │ └── using-n8n-nodes-base-mock.test.ts │ │ ├── flexible-instance-security-advanced.test.ts │ │ ├── flexible-instance-security.test.ts │ │ ├── http-server │ │ │ └── multi-tenant-support.test.ts │ │ ├── http-server-n8n-mode.test.ts │ │ ├── http-server-n8n-reinit.test.ts │ │ ├── http-server-session-management.test.ts │ │ ├── loaders │ │ │ └── node-loader.test.ts │ │ ├── mappers │ │ │ └── docs-mapper.test.ts │ │ ├── mcp │ │ │ ├── get-node-essentials-examples.test.ts │ │ │ ├── handlers-n8n-manager-simple.test.ts │ │ │ ├── handlers-n8n-manager.test.ts │ │ │ ├── handlers-workflow-diff.test.ts │ │ │ ├── lru-cache-behavior.test.ts │ │ │ ├── multi-tenant-tool-listing.test.ts.disabled │ │ │ ├── parameter-validation.test.ts │ │ │ ├── search-nodes-examples.test.ts │ │ │ ├── tools-documentation.test.ts │ │ │ └── tools.test.ts │ │ ├── monitoring │ │ │ └── cache-metrics.test.ts │ │ ├── MULTI_TENANT_TEST_COVERAGE.md │ │ ├── multi-tenant-integration.test.ts │ │ ├── parsers │ │ │ ├── node-parser-outputs.test.ts │ │ │ ├── node-parser.test.ts │ │ │ ├── property-extractor.test.ts │ │ │ └── simple-parser.test.ts │ │ ├── scripts │ │ │ └── fetch-templates-extraction.test.ts │ │ ├── services │ │ │ ├── ai-node-validator.test.ts │ │ │ ├── ai-tool-validators.test.ts │ │ │ ├── confidence-scorer.test.ts │ │ │ ├── config-validator-basic.test.ts │ │ │ ├── config-validator-edge-cases.test.ts │ │ │ ├── config-validator-node-specific.test.ts │ │ │ ├── config-validator-security.test.ts │ │ │ ├── debug-validator.test.ts │ │ │ ├── enhanced-config-validator-integration.test.ts │ │ │ ├── enhanced-config-validator-operations.test.ts │ │ │ ├── enhanced-config-validator.test.ts │ │ │ ├── example-generator.test.ts │ │ │ ├── execution-processor.test.ts │ │ │ ├── expression-format-validator.test.ts │ │ │ ├── expression-validator-edge-cases.test.ts │ │ │ ├── expression-validator.test.ts │ │ │ ├── fixed-collection-validation.test.ts │ │ │ ├── loop-output-edge-cases.test.ts │ │ │ ├── n8n-api-client.test.ts │ │ │ ├── n8n-validation.test.ts │ │ │ ├── node-similarity-service.test.ts │ │ │ ├── node-specific-validators.test.ts │ │ │ ├── operation-similarity-service-comprehensive.test.ts │ │ │ ├── operation-similarity-service.test.ts │ │ │ ├── property-dependencies.test.ts │ │ │ ├── property-filter-edge-cases.test.ts │ │ │ ├── property-filter.test.ts │ │ │ ├── resource-similarity-service-comprehensive.test.ts │ │ │ ├── resource-similarity-service.test.ts │ │ │ ├── task-templates.test.ts │ │ │ ├── template-service.test.ts │ │ │ ├── universal-expression-validator.test.ts │ │ │ ├── validation-fixes.test.ts │ │ │ ├── workflow-auto-fixer.test.ts │ │ │ ├── workflow-diff-engine.test.ts │ │ │ ├── workflow-fixed-collection-validation.test.ts │ │ │ ├── workflow-validator-comprehensive.test.ts │ │ │ ├── workflow-validator-edge-cases.test.ts │ │ │ ├── workflow-validator-error-outputs.test.ts │ │ │ ├── workflow-validator-expression-format.test.ts │ │ │ ├── workflow-validator-loops-simple.test.ts │ │ │ ├── workflow-validator-loops.test.ts │ │ │ ├── workflow-validator-mocks.test.ts │ │ │ ├── workflow-validator-performance.test.ts │ │ │ ├── workflow-validator-with-mocks.test.ts │ │ │ └── workflow-validator.test.ts │ │ ├── telemetry │ │ │ ├── batch-processor.test.ts │ │ │ ├── config-manager.test.ts │ │ │ ├── event-tracker.test.ts │ │ │ ├── event-validator.test.ts │ │ │ ├── rate-limiter.test.ts │ │ │ ├── telemetry-error.test.ts │ │ │ ├── telemetry-manager.test.ts │ │ │ ├── v2.18.3-fixes-verification.test.ts │ │ │ └── workflow-sanitizer.test.ts │ │ ├── templates │ │ │ ├── batch-processor.test.ts │ │ │ ├── metadata-generator.test.ts │ │ │ ├── template-repository-metadata.test.ts │ │ │ └── template-repository-security.test.ts │ │ ├── test-env-example.test.ts │ │ ├── test-infrastructure.test.ts │ │ ├── types │ │ │ ├── instance-context-coverage.test.ts │ │ │ └── instance-context-multi-tenant.test.ts │ │ ├── utils │ │ │ ├── auth-timing-safe.test.ts │ │ │ ├── cache-utils.test.ts │ │ │ ├── console-manager.test.ts │ │ │ ├── database-utils.test.ts │ │ │ ├── fixed-collection-validator.test.ts │ │ │ ├── n8n-errors.test.ts │ │ │ ├── node-type-normalizer.test.ts │ │ │ ├── node-type-utils.test.ts │ │ │ ├── node-utils.test.ts │ │ │ ├── simple-cache-memory-leak-fix.test.ts │ │ │ ├── ssrf-protection.test.ts │ │ │ └── template-node-resolver.test.ts │ │ └── validation-fixes.test.ts │ └── utils │ ├── assertions.ts │ ├── builders │ │ └── workflow.builder.ts │ ├── data-generators.ts │ ├── database-utils.ts │ ├── README.md │ └── test-helpers.ts ├── thumbnail.png ├── tsconfig.build.json ├── tsconfig.json ├── types │ ├── mcp.d.ts │ └── test-env.d.ts ├── verify-telemetry-fix.js ├── versioned-nodes.md ├── vitest.config.benchmark.ts ├── vitest.config.integration.ts └── vitest.config.ts ``` # Files -------------------------------------------------------------------------------- /tests/unit/services/node-specific-validators.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { describe, it, expect, beforeEach } from 'vitest'; 2 | import { NodeSpecificValidators, NodeValidationContext } from '@/services/node-specific-validators'; 3 | import { ValidationError, ValidationWarning } from '@/services/config-validator'; 4 | 5 | describe('NodeSpecificValidators', () => { 6 | let context: NodeValidationContext; 7 | 8 | beforeEach(() => { 9 | context = { 10 | config: {}, 11 | errors: [], 12 | warnings: [], 13 | suggestions: [], 14 | autofix: {} 15 | }; 16 | }); 17 | 18 | describe('validateSlack', () => { 19 | describe('message send operation', () => { 20 | beforeEach(() => { 21 | context.config = { 22 | resource: 'message', 23 | operation: 'send' 24 | }; 25 | }); 26 | 27 | it('should require channel for sending messages', () => { 28 | NodeSpecificValidators.validateSlack(context); 29 | 30 | expect(context.errors).toHaveLength(2); // channel and text errors 31 | expect(context.errors[0]).toMatchObject({ 32 | type: 'missing_required', 33 | property: 'channel', 34 | message: 'Channel is required to send a message' 35 | }); 36 | }); 37 | 38 | it('should accept channelId as alternative to channel', () => { 39 | context.config.channelId = 'C1234567890'; 40 | context.config.text = 'Hello'; 41 | 42 | NodeSpecificValidators.validateSlack(context); 43 | 44 | const channelErrors = context.errors.filter(e => e.property === 'channel'); 45 | expect(channelErrors).toHaveLength(0); 46 | }); 47 | 48 | it('should require message content', () => { 49 | context.config.channel = '#general'; 50 | 51 | NodeSpecificValidators.validateSlack(context); 52 | 53 | expect(context.errors).toContainEqual({ 54 | type: 'missing_required', 55 | property: 'text', 56 | message: 'Message content is required - provide text, blocks, or attachments', 57 | fix: 'Add text field with your message content' 58 | }); 59 | }); 60 | 61 | it('should accept blocks as alternative to text', () => { 62 | context.config.channel = '#general'; 63 | context.config.blocks = [{ type: 'section', text: { type: 'mrkdwn', text: 'Hello' } }]; 64 | 65 | NodeSpecificValidators.validateSlack(context); 66 | 67 | const textErrors = context.errors.filter(e => e.property === 'text'); 68 | expect(textErrors).toHaveLength(0); 69 | }); 70 | 71 | it('should accept attachments as alternative to text', () => { 72 | context.config.channel = '#general'; 73 | context.config.attachments = [{ text: 'Attachment text' }]; 74 | 75 | NodeSpecificValidators.validateSlack(context); 76 | 77 | const textErrors = context.errors.filter(e => e.property === 'text'); 78 | expect(textErrors).toHaveLength(0); 79 | }); 80 | 81 | it('should warn about text exceeding character limit', () => { 82 | context.config.channel = '#general'; 83 | context.config.text = 'a'.repeat(40001); 84 | 85 | NodeSpecificValidators.validateSlack(context); 86 | 87 | expect(context.warnings).toContainEqual({ 88 | type: 'inefficient', 89 | property: 'text', 90 | message: 'Message text exceeds Slack\'s 40,000 character limit', 91 | suggestion: 'Split into multiple messages or use a file upload' 92 | }); 93 | }); 94 | 95 | it('should warn about missing threadTs when replying to thread', () => { 96 | context.config.channel = '#general'; 97 | context.config.text = 'Reply'; 98 | context.config.replyToThread = true; 99 | 100 | NodeSpecificValidators.validateSlack(context); 101 | 102 | expect(context.warnings).toContainEqual({ 103 | type: 'missing_common', 104 | property: 'threadTs', 105 | message: 'Thread timestamp required when replying to thread', 106 | suggestion: 'Set threadTs to the timestamp of the thread parent message' 107 | }); 108 | }); 109 | 110 | it('should suggest linkNames for mentions', () => { 111 | context.config.channel = '#general'; 112 | context.config.text = 'Hello @user'; 113 | 114 | NodeSpecificValidators.validateSlack(context); 115 | 116 | expect(context.suggestions).toContain('Set linkNames=true to convert @mentions to user links'); 117 | expect(context.autofix.linkNames).toBe(true); 118 | }); 119 | }); 120 | 121 | describe('message update operation', () => { 122 | beforeEach(() => { 123 | context.config = { 124 | resource: 'message', 125 | operation: 'update' 126 | }; 127 | }); 128 | 129 | it('should require timestamp for updating messages', () => { 130 | NodeSpecificValidators.validateSlack(context); 131 | 132 | expect(context.errors).toContainEqual({ 133 | type: 'missing_required', 134 | property: 'ts', 135 | message: 'Message timestamp (ts) is required to update a message', 136 | fix: 'Provide the timestamp of the message to update' 137 | }); 138 | }); 139 | 140 | it('should require channel for updating messages', () => { 141 | context.config.ts = '1234567890.123456'; 142 | 143 | NodeSpecificValidators.validateSlack(context); 144 | 145 | expect(context.errors).toContainEqual({ 146 | type: 'missing_required', 147 | property: 'channel', 148 | message: 'Channel is required to update a message', 149 | fix: 'Provide the channel where the message exists' 150 | }); 151 | }); 152 | }); 153 | 154 | describe('message delete operation', () => { 155 | beforeEach(() => { 156 | context.config = { 157 | resource: 'message', 158 | operation: 'delete' 159 | }; 160 | }); 161 | 162 | it('should require timestamp for deleting messages', () => { 163 | NodeSpecificValidators.validateSlack(context); 164 | 165 | expect(context.errors).toContainEqual({ 166 | type: 'missing_required', 167 | property: 'ts', 168 | message: 'Message timestamp (ts) is required to delete a message', 169 | fix: 'Provide the timestamp of the message to delete' 170 | }); 171 | }); 172 | 173 | it('should warn about permanent deletion', () => { 174 | context.config.ts = '1234567890.123456'; 175 | context.config.channel = '#general'; 176 | 177 | NodeSpecificValidators.validateSlack(context); 178 | 179 | expect(context.warnings).toContainEqual({ 180 | type: 'security', 181 | message: 'Message deletion is permanent and cannot be undone', 182 | suggestion: 'Consider archiving or updating the message instead if you need to preserve history' 183 | }); 184 | }); 185 | }); 186 | 187 | describe('channel create operation', () => { 188 | beforeEach(() => { 189 | context.config = { 190 | resource: 'channel', 191 | operation: 'create' 192 | }; 193 | }); 194 | 195 | it('should require channel name', () => { 196 | NodeSpecificValidators.validateSlack(context); 197 | 198 | expect(context.errors).toContainEqual({ 199 | type: 'missing_required', 200 | property: 'name', 201 | message: 'Channel name is required', 202 | fix: 'Provide a channel name (lowercase, no spaces, 1-80 characters)' 203 | }); 204 | }); 205 | 206 | it('should validate channel name format', () => { 207 | context.config.name = 'Test Channel'; 208 | 209 | NodeSpecificValidators.validateSlack(context); 210 | 211 | expect(context.errors).toContainEqual({ 212 | type: 'invalid_value', 213 | property: 'name', 214 | message: 'Channel names cannot contain spaces', 215 | fix: 'Use hyphens or underscores instead of spaces' 216 | }); 217 | }); 218 | 219 | it('should require lowercase channel names', () => { 220 | context.config.name = 'TestChannel'; 221 | 222 | NodeSpecificValidators.validateSlack(context); 223 | 224 | expect(context.errors).toContainEqual({ 225 | type: 'invalid_value', 226 | property: 'name', 227 | message: 'Channel names must be lowercase', 228 | fix: 'Convert the channel name to lowercase' 229 | }); 230 | }); 231 | 232 | it('should validate channel name length', () => { 233 | context.config.name = 'a'.repeat(81); 234 | 235 | NodeSpecificValidators.validateSlack(context); 236 | 237 | expect(context.errors).toContainEqual({ 238 | type: 'invalid_value', 239 | property: 'name', 240 | message: 'Channel name exceeds 80 character limit', 241 | fix: 'Shorten the channel name' 242 | }); 243 | }); 244 | }); 245 | 246 | describe('user operations', () => { 247 | it('should require user identifier for get operation', () => { 248 | context.config = { 249 | resource: 'user', 250 | operation: 'get' 251 | }; 252 | 253 | NodeSpecificValidators.validateSlack(context); 254 | 255 | expect(context.errors).toContainEqual({ 256 | type: 'missing_required', 257 | property: 'user', 258 | message: 'User identifier required - use email, user ID, or username', 259 | fix: 'Set user to an email like "[email protected]" or user ID like "U1234567890"' 260 | }); 261 | }); 262 | }); 263 | 264 | describe('error handling', () => { 265 | it('should suggest error handling for Slack operations', () => { 266 | context.config = { 267 | resource: 'message', 268 | operation: 'send', 269 | channel: '#general', 270 | text: 'Hello' 271 | }; 272 | 273 | NodeSpecificValidators.validateSlack(context); 274 | 275 | expect(context.warnings).toContainEqual({ 276 | type: 'best_practice', 277 | property: 'errorHandling', 278 | message: 'Slack API can have rate limits and transient failures', 279 | suggestion: 'Add onError: "continueRegularOutput" with retryOnFail for resilience' 280 | }); 281 | 282 | expect(context.autofix).toMatchObject({ 283 | onError: 'continueRegularOutput', 284 | retryOnFail: true, 285 | maxTries: 2, 286 | waitBetweenTries: 3000 287 | }); 288 | }); 289 | 290 | it('should warn about deprecated continueOnFail', () => { 291 | context.config = { 292 | resource: 'message', 293 | operation: 'send', 294 | channel: '#general', 295 | text: 'Hello', 296 | continueOnFail: true 297 | }; 298 | 299 | NodeSpecificValidators.validateSlack(context); 300 | 301 | expect(context.warnings).toContainEqual({ 302 | type: 'deprecated', 303 | property: 'continueOnFail', 304 | message: 'continueOnFail is deprecated. Use onError instead', 305 | suggestion: 'Replace with onError: "continueRegularOutput"' 306 | }); 307 | }); 308 | }); 309 | }); 310 | 311 | describe('validateGoogleSheets', () => { 312 | describe('common validations', () => { 313 | it('should require spreadsheet ID', () => { 314 | context.config = { 315 | operation: 'read' 316 | }; 317 | 318 | NodeSpecificValidators.validateGoogleSheets(context); 319 | 320 | expect(context.errors).toContainEqual({ 321 | type: 'missing_required', 322 | property: 'sheetId', 323 | message: 'Spreadsheet ID is required', 324 | fix: 'Provide the Google Sheets document ID from the URL' 325 | }); 326 | }); 327 | 328 | it('should accept documentId as alternative to sheetId', () => { 329 | context.config = { 330 | operation: 'read', 331 | documentId: '1234567890', 332 | range: 'Sheet1!A:B' 333 | }; 334 | 335 | NodeSpecificValidators.validateGoogleSheets(context); 336 | 337 | const sheetIdErrors = context.errors.filter(e => e.property === 'sheetId'); 338 | expect(sheetIdErrors).toHaveLength(0); 339 | }); 340 | }); 341 | 342 | describe('append operation', () => { 343 | beforeEach(() => { 344 | context.config = { 345 | operation: 'append', 346 | sheetId: '1234567890' 347 | }; 348 | }); 349 | 350 | it('should require range or columns for append', () => { 351 | NodeSpecificValidators.validateGoogleSheets(context); 352 | 353 | expect(context.errors).toContainEqual({ 354 | type: 'missing_required', 355 | property: 'range', 356 | message: 'Range or columns mapping is required for append operation', 357 | fix: 'Specify range like "Sheet1!A:B" OR use columns with mappingMode' 358 | }); 359 | }); 360 | 361 | it('should suggest valueInputMode', () => { 362 | context.config.range = 'Sheet1!A:B'; 363 | 364 | NodeSpecificValidators.validateGoogleSheets(context); 365 | 366 | expect(context.warnings).toContainEqual({ 367 | type: 'missing_common', 368 | property: 'options.valueInputMode', 369 | message: 'Consider setting valueInputMode for proper data formatting', 370 | suggestion: 'Use "USER_ENTERED" to parse formulas and dates, or "RAW" for literal values' 371 | }); 372 | 373 | expect(context.autofix.options).toMatchObject({ 374 | valueInputMode: 'USER_ENTERED' 375 | }); 376 | }); 377 | }); 378 | 379 | describe('read operation', () => { 380 | beforeEach(() => { 381 | context.config = { 382 | operation: 'read', 383 | sheetId: '1234567890' 384 | }; 385 | }); 386 | 387 | it('should require range for read', () => { 388 | NodeSpecificValidators.validateGoogleSheets(context); 389 | 390 | expect(context.errors).toContainEqual({ 391 | type: 'missing_required', 392 | property: 'range', 393 | message: 'Range is required for read operation', 394 | fix: 'Specify range like "Sheet1!A:B" or "Sheet1!A1:B10"' 395 | }); 396 | }); 397 | 398 | it('should suggest data structure option', () => { 399 | context.config.range = 'Sheet1!A:B'; 400 | 401 | NodeSpecificValidators.validateGoogleSheets(context); 402 | 403 | expect(context.suggestions).toContain('Consider setting options.dataStructure to "object" for easier data manipulation'); 404 | }); 405 | }); 406 | 407 | describe('update operation', () => { 408 | beforeEach(() => { 409 | context.config = { 410 | operation: 'update', 411 | sheetId: '1234567890' 412 | }; 413 | }); 414 | 415 | it('should require range for update', () => { 416 | NodeSpecificValidators.validateGoogleSheets(context); 417 | 418 | expect(context.errors).toContainEqual({ 419 | type: 'missing_required', 420 | property: 'range', 421 | message: 'Range is required for update operation', 422 | fix: 'Specify the exact range to update like "Sheet1!A1:B10"' 423 | }); 424 | }); 425 | 426 | it('should require values for update', () => { 427 | context.config.range = 'Sheet1!A1:B10'; 428 | 429 | NodeSpecificValidators.validateGoogleSheets(context); 430 | 431 | expect(context.errors).toContainEqual({ 432 | type: 'missing_required', 433 | property: 'values', 434 | message: 'Values are required for update operation', 435 | fix: 'Provide the data to write to the spreadsheet' 436 | }); 437 | }); 438 | 439 | it('should accept rawData as alternative to values', () => { 440 | context.config.range = 'Sheet1!A1:B10'; 441 | context.config.rawData = [[1, 2], [3, 4]]; 442 | 443 | NodeSpecificValidators.validateGoogleSheets(context); 444 | 445 | const valuesErrors = context.errors.filter(e => e.property === 'values'); 446 | expect(valuesErrors).toHaveLength(0); 447 | }); 448 | }); 449 | 450 | describe('delete operation', () => { 451 | beforeEach(() => { 452 | context.config = { 453 | operation: 'delete', 454 | sheetId: '1234567890' 455 | }; 456 | }); 457 | 458 | it('should require toDelete specification', () => { 459 | NodeSpecificValidators.validateGoogleSheets(context); 460 | 461 | expect(context.errors).toContainEqual({ 462 | type: 'missing_required', 463 | property: 'toDelete', 464 | message: 'Specify what to delete (rows or columns)', 465 | fix: 'Set toDelete to "rows" or "columns"' 466 | }); 467 | }); 468 | 469 | it('should require startIndex for row deletion', () => { 470 | context.config.toDelete = 'rows'; 471 | 472 | NodeSpecificValidators.validateGoogleSheets(context); 473 | 474 | expect(context.errors).toContainEqual({ 475 | type: 'missing_required', 476 | property: 'startIndex', 477 | message: 'Start index is required when deleting rows', 478 | fix: 'Specify the starting row index (0-based)' 479 | }); 480 | }); 481 | 482 | it('should accept startIndex of 0', () => { 483 | context.config.toDelete = 'rows'; 484 | context.config.startIndex = 0; 485 | 486 | NodeSpecificValidators.validateGoogleSheets(context); 487 | 488 | const startIndexErrors = context.errors.filter(e => e.property === 'startIndex'); 489 | expect(startIndexErrors).toHaveLength(0); 490 | }); 491 | 492 | it('should warn about permanent deletion', () => { 493 | context.config.toDelete = 'rows'; 494 | context.config.startIndex = 0; 495 | 496 | NodeSpecificValidators.validateGoogleSheets(context); 497 | 498 | expect(context.warnings).toContainEqual({ 499 | type: 'security', 500 | message: 'Deletion is permanent. Consider backing up data first', 501 | suggestion: 'Read the data before deletion to create a backup' 502 | }); 503 | }); 504 | }); 505 | 506 | describe('range validation', () => { 507 | beforeEach(() => { 508 | context.config = { 509 | operation: 'read', 510 | sheetId: '1234567890' 511 | }; 512 | }); 513 | 514 | it('should suggest including sheet name in range', () => { 515 | context.config.range = 'A1:B10'; 516 | 517 | NodeSpecificValidators.validateGoogleSheets(context); 518 | 519 | expect(context.warnings).toContainEqual({ 520 | type: 'inefficient', 521 | property: 'range', 522 | message: 'Range should include sheet name for clarity', 523 | suggestion: 'Format: "SheetName!A1:B10" or "SheetName!A:B"' 524 | }); 525 | }); 526 | 527 | it('should validate sheet names with spaces', () => { 528 | context.config.range = 'Sheet Name!A1:B10'; 529 | 530 | NodeSpecificValidators.validateGoogleSheets(context); 531 | 532 | expect(context.errors).toContainEqual({ 533 | type: 'invalid_value', 534 | property: 'range', 535 | message: 'Sheet names with spaces must be quoted', 536 | fix: 'Use single quotes around sheet name: \'Sheet Name\'!A1:B10' 537 | }); 538 | }); 539 | 540 | it('should accept quoted sheet names with spaces', () => { 541 | context.config.range = "'Sheet Name'!A1:B10"; 542 | 543 | NodeSpecificValidators.validateGoogleSheets(context); 544 | 545 | const rangeErrors = context.errors.filter(e => e.property === 'range' && e.message.includes('quoted')); 546 | expect(rangeErrors).toHaveLength(0); 547 | }); 548 | 549 | it('should validate A1 notation format', () => { 550 | // Use an invalid range that doesn't match the A1 pattern 551 | context.config.range = 'Sheet1!123ABC'; 552 | 553 | NodeSpecificValidators.validateGoogleSheets(context); 554 | 555 | expect(context.warnings).toContainEqual({ 556 | type: 'inefficient', 557 | property: 'range', 558 | message: 'Range may not be in valid A1 notation', 559 | suggestion: 'Examples: "Sheet1!A1:B10", "Sheet1!A:B", "Sheet1!1:10"' 560 | }); 561 | }); 562 | }); 563 | }); 564 | 565 | describe('validateOpenAI', () => { 566 | describe('chat create operation', () => { 567 | beforeEach(() => { 568 | context.config = { 569 | resource: 'chat', 570 | operation: 'create' 571 | }; 572 | }); 573 | 574 | it('should require model selection', () => { 575 | NodeSpecificValidators.validateOpenAI(context); 576 | 577 | expect(context.errors).toContainEqual({ 578 | type: 'missing_required', 579 | property: 'model', 580 | message: 'Model selection is required', 581 | fix: 'Choose a model like "gpt-4", "gpt-3.5-turbo", etc.' 582 | }); 583 | }); 584 | 585 | it('should warn about deprecated models', () => { 586 | context.config.model = 'text-davinci-003'; 587 | context.config.messages = [{ role: 'user', content: 'Hello' }]; 588 | 589 | NodeSpecificValidators.validateOpenAI(context); 590 | 591 | expect(context.warnings).toContainEqual({ 592 | type: 'deprecated', 593 | property: 'model', 594 | message: 'Model text-davinci-003 is deprecated', 595 | suggestion: 'Use "gpt-3.5-turbo" or "gpt-4" instead' 596 | }); 597 | }); 598 | 599 | it('should require messages or prompt', () => { 600 | context.config.model = 'gpt-4'; 601 | 602 | NodeSpecificValidators.validateOpenAI(context); 603 | 604 | expect(context.errors).toContainEqual({ 605 | type: 'missing_required', 606 | property: 'messages', 607 | message: 'Messages or prompt required for chat completion', 608 | fix: 'Add messages array or use the prompt field' 609 | }); 610 | }); 611 | 612 | it('should accept prompt as alternative to messages', () => { 613 | context.config.model = 'gpt-4'; 614 | context.config.prompt = 'Hello AI'; 615 | 616 | NodeSpecificValidators.validateOpenAI(context); 617 | 618 | const messageErrors = context.errors.filter(e => e.property === 'messages'); 619 | expect(messageErrors).toHaveLength(0); 620 | }); 621 | 622 | it('should warn about high token limits', () => { 623 | context.config.model = 'gpt-4'; 624 | context.config.messages = [{ role: 'user', content: 'Hello' }]; 625 | context.config.maxTokens = 5000; 626 | 627 | NodeSpecificValidators.validateOpenAI(context); 628 | 629 | expect(context.warnings).toContainEqual({ 630 | type: 'inefficient', 631 | property: 'maxTokens', 632 | message: 'High token limit may increase costs significantly', 633 | suggestion: 'Consider if you really need more than 4000 tokens' 634 | }); 635 | }); 636 | 637 | it('should validate temperature range', () => { 638 | context.config.model = 'gpt-4'; 639 | context.config.messages = [{ role: 'user', content: 'Hello' }]; 640 | context.config.temperature = 2.5; 641 | 642 | NodeSpecificValidators.validateOpenAI(context); 643 | 644 | expect(context.errors).toContainEqual({ 645 | type: 'invalid_value', 646 | property: 'temperature', 647 | message: 'Temperature must be between 0 and 2', 648 | fix: 'Set temperature between 0 (deterministic) and 2 (creative)' 649 | }); 650 | }); 651 | }); 652 | 653 | describe('error handling', () => { 654 | it('should suggest error handling for AI API calls', () => { 655 | context.config = { 656 | resource: 'chat', 657 | operation: 'create', 658 | model: 'gpt-4', 659 | messages: [{ role: 'user', content: 'Hello' }] 660 | }; 661 | 662 | NodeSpecificValidators.validateOpenAI(context); 663 | 664 | expect(context.warnings).toContainEqual({ 665 | type: 'best_practice', 666 | property: 'errorHandling', 667 | message: 'AI APIs have rate limits and can return errors', 668 | suggestion: 'Add onError: "continueRegularOutput" with retryOnFail and longer wait times' 669 | }); 670 | 671 | expect(context.autofix).toMatchObject({ 672 | onError: 'continueRegularOutput', 673 | retryOnFail: true, 674 | maxTries: 3, 675 | waitBetweenTries: 5000, 676 | alwaysOutputData: true 677 | }); 678 | }); 679 | 680 | it('should warn about deprecated continueOnFail', () => { 681 | context.config = { 682 | resource: 'chat', 683 | operation: 'create', 684 | model: 'gpt-4', 685 | messages: [{ role: 'user', content: 'Hello' }], 686 | continueOnFail: true 687 | }; 688 | 689 | NodeSpecificValidators.validateOpenAI(context); 690 | 691 | expect(context.warnings).toContainEqual({ 692 | type: 'deprecated', 693 | property: 'continueOnFail', 694 | message: 'continueOnFail is deprecated. Use onError instead', 695 | suggestion: 'Replace with onError: "continueRegularOutput"' 696 | }); 697 | }); 698 | }); 699 | }); 700 | 701 | describe('validateMongoDB', () => { 702 | describe('common validations', () => { 703 | it('should require collection name', () => { 704 | context.config = { 705 | operation: 'find' 706 | }; 707 | 708 | NodeSpecificValidators.validateMongoDB(context); 709 | 710 | expect(context.errors).toContainEqual({ 711 | type: 'missing_required', 712 | property: 'collection', 713 | message: 'Collection name is required', 714 | fix: 'Specify the MongoDB collection to work with' 715 | }); 716 | }); 717 | }); 718 | 719 | describe('find operation', () => { 720 | beforeEach(() => { 721 | context.config = { 722 | operation: 'find', 723 | collection: 'users' 724 | }; 725 | }); 726 | 727 | it('should validate query JSON', () => { 728 | context.config.query = '{ invalid json'; 729 | 730 | NodeSpecificValidators.validateMongoDB(context); 731 | 732 | expect(context.errors).toContainEqual({ 733 | type: 'invalid_value', 734 | property: 'query', 735 | message: 'Query must be valid JSON', 736 | fix: 'Ensure query is valid JSON like: {"name": "John"}' 737 | }); 738 | }); 739 | 740 | it('should accept valid JSON query', () => { 741 | context.config.query = '{"name": "John"}'; 742 | 743 | NodeSpecificValidators.validateMongoDB(context); 744 | 745 | const queryErrors = context.errors.filter(e => e.property === 'query'); 746 | expect(queryErrors).toHaveLength(0); 747 | }); 748 | }); 749 | 750 | describe('insert operation', () => { 751 | beforeEach(() => { 752 | context.config = { 753 | operation: 'insert', 754 | collection: 'users' 755 | }; 756 | }); 757 | 758 | it('should require document data', () => { 759 | NodeSpecificValidators.validateMongoDB(context); 760 | 761 | expect(context.errors).toContainEqual({ 762 | type: 'missing_required', 763 | property: 'fields', 764 | message: 'Document data is required for insert', 765 | fix: 'Provide the data to insert' 766 | }); 767 | }); 768 | 769 | it('should accept documents as alternative to fields', () => { 770 | context.config.documents = [{ name: 'John' }]; 771 | 772 | NodeSpecificValidators.validateMongoDB(context); 773 | 774 | const fieldsErrors = context.errors.filter(e => e.property === 'fields'); 775 | expect(fieldsErrors).toHaveLength(0); 776 | }); 777 | }); 778 | 779 | describe('update operation', () => { 780 | beforeEach(() => { 781 | context.config = { 782 | operation: 'update', 783 | collection: 'users' 784 | }; 785 | }); 786 | 787 | it('should warn about update without query', () => { 788 | NodeSpecificValidators.validateMongoDB(context); 789 | 790 | expect(context.warnings).toContainEqual({ 791 | type: 'security', 792 | message: 'Update without query will affect all documents', 793 | suggestion: 'Add a query to target specific documents' 794 | }); 795 | }); 796 | }); 797 | 798 | describe('delete operation', () => { 799 | beforeEach(() => { 800 | context.config = { 801 | operation: 'delete', 802 | collection: 'users' 803 | }; 804 | }); 805 | 806 | it('should error on delete without query', () => { 807 | NodeSpecificValidators.validateMongoDB(context); 808 | 809 | expect(context.errors).toContainEqual({ 810 | type: 'invalid_value', 811 | property: 'query', 812 | message: 'Delete without query would remove all documents - this is a critical security issue', 813 | fix: 'Add a query to specify which documents to delete' 814 | }); 815 | }); 816 | 817 | it('should error on delete with empty query', () => { 818 | context.config.query = '{}'; 819 | 820 | NodeSpecificValidators.validateMongoDB(context); 821 | 822 | expect(context.errors).toContainEqual({ 823 | type: 'invalid_value', 824 | property: 'query', 825 | message: 'Delete without query would remove all documents - this is a critical security issue', 826 | fix: 'Add a query to specify which documents to delete' 827 | }); 828 | }); 829 | }); 830 | 831 | describe('error handling', () => { 832 | it('should suggest error handling for find operations', () => { 833 | context.config = { 834 | operation: 'find', 835 | collection: 'users' 836 | }; 837 | 838 | NodeSpecificValidators.validateMongoDB(context); 839 | 840 | expect(context.warnings).toContainEqual({ 841 | type: 'best_practice', 842 | property: 'errorHandling', 843 | message: 'MongoDB queries can fail due to connection issues', 844 | suggestion: 'Add onError: "continueRegularOutput" with retryOnFail' 845 | }); 846 | 847 | expect(context.autofix).toMatchObject({ 848 | onError: 'continueRegularOutput', 849 | retryOnFail: true, 850 | maxTries: 3 851 | }); 852 | }); 853 | 854 | it('should suggest different error handling for write operations', () => { 855 | context.config = { 856 | operation: 'insert', 857 | collection: 'users', 858 | fields: { name: 'John' } 859 | }; 860 | 861 | NodeSpecificValidators.validateMongoDB(context); 862 | 863 | expect(context.warnings).toContainEqual({ 864 | type: 'best_practice', 865 | property: 'errorHandling', 866 | message: 'MongoDB write operations should handle errors carefully', 867 | suggestion: 'Add onError: "continueErrorOutput" to handle write failures separately' 868 | }); 869 | 870 | expect(context.autofix).toMatchObject({ 871 | onError: 'continueErrorOutput', 872 | retryOnFail: true, 873 | maxTries: 2, 874 | waitBetweenTries: 1000 875 | }); 876 | }); 877 | 878 | it('should warn about deprecated continueOnFail', () => { 879 | context.config = { 880 | operation: 'find', 881 | collection: 'users', 882 | continueOnFail: true 883 | }; 884 | 885 | NodeSpecificValidators.validateMongoDB(context); 886 | 887 | expect(context.warnings).toContainEqual({ 888 | type: 'deprecated', 889 | property: 'continueOnFail', 890 | message: 'continueOnFail is deprecated. Use onError instead', 891 | suggestion: 'Replace with onError: "continueRegularOutput" or "continueErrorOutput"' 892 | }); 893 | }); 894 | }); 895 | }); 896 | 897 | describe('validatePostgres', () => { 898 | describe('insert operation', () => { 899 | beforeEach(() => { 900 | context.config = { 901 | operation: 'insert' 902 | }; 903 | }); 904 | 905 | it('should require table name', () => { 906 | NodeSpecificValidators.validatePostgres(context); 907 | 908 | expect(context.errors).toContainEqual({ 909 | type: 'missing_required', 910 | property: 'table', 911 | message: 'Table name is required for insert operation', 912 | fix: 'Specify the table to insert data into' 913 | }); 914 | }); 915 | 916 | it('should warn about missing columns', () => { 917 | context.config.table = 'users'; 918 | 919 | NodeSpecificValidators.validatePostgres(context); 920 | 921 | expect(context.warnings).toContainEqual({ 922 | type: 'missing_common', 923 | property: 'columns', 924 | message: 'No columns specified for insert', 925 | suggestion: 'Define which columns to insert data into' 926 | }); 927 | }); 928 | 929 | it('should not warn if dataMode is set', () => { 930 | context.config.table = 'users'; 931 | context.config.dataMode = 'autoMapInputData'; 932 | 933 | NodeSpecificValidators.validatePostgres(context); 934 | 935 | const columnWarnings = context.warnings.filter(w => w.property === 'columns'); 936 | expect(columnWarnings).toHaveLength(0); 937 | }); 938 | }); 939 | 940 | describe('update operation', () => { 941 | beforeEach(() => { 942 | context.config = { 943 | operation: 'update' 944 | }; 945 | }); 946 | 947 | it('should require table name', () => { 948 | NodeSpecificValidators.validatePostgres(context); 949 | 950 | expect(context.errors).toContainEqual({ 951 | type: 'missing_required', 952 | property: 'table', 953 | message: 'Table name is required for update operation', 954 | fix: 'Specify the table to update' 955 | }); 956 | }); 957 | 958 | it('should warn about missing updateKey', () => { 959 | context.config.table = 'users'; 960 | 961 | NodeSpecificValidators.validatePostgres(context); 962 | 963 | expect(context.warnings).toContainEqual({ 964 | type: 'missing_common', 965 | property: 'updateKey', 966 | message: 'No update key specified', 967 | suggestion: 'Set updateKey to identify which rows to update (e.g., "id")' 968 | }); 969 | }); 970 | }); 971 | 972 | describe('delete operation', () => { 973 | beforeEach(() => { 974 | context.config = { 975 | operation: 'delete' 976 | }; 977 | }); 978 | 979 | it('should require table name', () => { 980 | NodeSpecificValidators.validatePostgres(context); 981 | 982 | expect(context.errors).toContainEqual({ 983 | type: 'missing_required', 984 | property: 'table', 985 | message: 'Table name is required for delete operation', 986 | fix: 'Specify the table to delete from' 987 | }); 988 | }); 989 | 990 | it('should require deleteKey', () => { 991 | context.config.table = 'users'; 992 | 993 | NodeSpecificValidators.validatePostgres(context); 994 | 995 | expect(context.errors).toContainEqual({ 996 | type: 'missing_required', 997 | property: 'deleteKey', 998 | message: 'Delete key is required to identify rows', 999 | fix: 'Set deleteKey (e.g., "id") to specify which rows to delete' 1000 | }); 1001 | }); 1002 | }); 1003 | 1004 | describe('execute operation', () => { 1005 | beforeEach(() => { 1006 | context.config = { 1007 | operation: 'execute' 1008 | }; 1009 | }); 1010 | 1011 | it('should require SQL query', () => { 1012 | NodeSpecificValidators.validatePostgres(context); 1013 | 1014 | expect(context.errors).toContainEqual({ 1015 | type: 'missing_required', 1016 | property: 'query', 1017 | message: 'SQL query is required', 1018 | fix: 'Provide the SQL query to execute' 1019 | }); 1020 | }); 1021 | }); 1022 | 1023 | describe('SQL query validation', () => { 1024 | beforeEach(() => { 1025 | context.config = { 1026 | operation: 'execute' 1027 | }; 1028 | }); 1029 | 1030 | it('should warn about SQL injection risks', () => { 1031 | context.config.query = 'SELECT * FROM users WHERE id = ${userId}'; 1032 | 1033 | NodeSpecificValidators.validatePostgres(context); 1034 | 1035 | expect(context.warnings).toContainEqual({ 1036 | type: 'security', 1037 | message: 'Query contains template expressions that might be vulnerable to SQL injection', 1038 | suggestion: 'Use parameterized queries with query parameters instead of string interpolation' 1039 | }); 1040 | }); 1041 | 1042 | it('should error on DELETE without WHERE', () => { 1043 | context.config.query = 'DELETE FROM users'; 1044 | 1045 | NodeSpecificValidators.validatePostgres(context); 1046 | 1047 | expect(context.errors).toContainEqual({ 1048 | type: 'invalid_value', 1049 | property: 'query', 1050 | message: 'DELETE query without WHERE clause will delete all records', 1051 | fix: 'Add a WHERE clause to specify which records to delete' 1052 | }); 1053 | }); 1054 | 1055 | it('should warn on UPDATE without WHERE', () => { 1056 | context.config.query = 'UPDATE users SET active = true'; 1057 | 1058 | NodeSpecificValidators.validatePostgres(context); 1059 | 1060 | expect(context.warnings).toContainEqual({ 1061 | type: 'security', 1062 | message: 'UPDATE query without WHERE clause will update all records', 1063 | suggestion: 'Add a WHERE clause to specify which records to update' 1064 | }); 1065 | }); 1066 | 1067 | it('should warn about TRUNCATE', () => { 1068 | context.config.query = 'TRUNCATE TABLE users'; 1069 | 1070 | NodeSpecificValidators.validatePostgres(context); 1071 | 1072 | expect(context.warnings).toContainEqual({ 1073 | type: 'security', 1074 | message: 'TRUNCATE will remove all data from the table', 1075 | suggestion: 'Consider using DELETE with WHERE clause if you need to keep some data' 1076 | }); 1077 | }); 1078 | 1079 | it('should error on DROP operations', () => { 1080 | context.config.query = 'DROP TABLE users'; 1081 | 1082 | NodeSpecificValidators.validatePostgres(context); 1083 | 1084 | expect(context.errors).toContainEqual({ 1085 | type: 'invalid_value', 1086 | property: 'query', 1087 | message: 'DROP operations are extremely dangerous and will permanently delete database objects', 1088 | fix: 'Use this only if you really intend to delete tables/databases permanently' 1089 | }); 1090 | }); 1091 | 1092 | it('should suggest specific columns instead of SELECT *', () => { 1093 | context.config.query = 'SELECT * FROM users'; 1094 | 1095 | NodeSpecificValidators.validatePostgres(context); 1096 | 1097 | expect(context.suggestions).toContain('Consider selecting specific columns instead of * for better performance'); 1098 | }); 1099 | 1100 | it('should suggest PostgreSQL-specific dollar quotes', () => { 1101 | context.config.query = 'CREATE FUNCTION test() RETURNS void AS $$ BEGIN END; $$ LANGUAGE plpgsql'; 1102 | 1103 | NodeSpecificValidators.validatePostgres(context); 1104 | 1105 | expect(context.suggestions).toContain('Dollar-quoted strings detected - ensure they are properly closed'); 1106 | }); 1107 | }); 1108 | 1109 | describe('connection and error handling', () => { 1110 | it('should suggest connection timeout', () => { 1111 | context.config = { 1112 | operation: 'execute', 1113 | query: 'SELECT * FROM users' 1114 | }; 1115 | 1116 | NodeSpecificValidators.validatePostgres(context); 1117 | 1118 | expect(context.suggestions).toContain('Consider setting connectionTimeout to handle slow connections'); 1119 | }); 1120 | 1121 | it('should suggest error handling for read operations', () => { 1122 | context.config = { 1123 | operation: 'execute', 1124 | query: 'SELECT * FROM users' 1125 | }; 1126 | 1127 | NodeSpecificValidators.validatePostgres(context); 1128 | 1129 | expect(context.warnings).toContainEqual({ 1130 | type: 'best_practice', 1131 | property: 'errorHandling', 1132 | message: 'Database reads can fail due to connection issues', 1133 | suggestion: 'Add onError: "continueRegularOutput" and retryOnFail: true' 1134 | }); 1135 | 1136 | expect(context.autofix).toMatchObject({ 1137 | onError: 'continueRegularOutput', 1138 | retryOnFail: true, 1139 | maxTries: 3 1140 | }); 1141 | }); 1142 | 1143 | it('should suggest different error handling for write operations', () => { 1144 | context.config = { 1145 | operation: 'insert', 1146 | table: 'users' 1147 | }; 1148 | 1149 | NodeSpecificValidators.validatePostgres(context); 1150 | 1151 | expect(context.warnings).toContainEqual({ 1152 | type: 'best_practice', 1153 | property: 'errorHandling', 1154 | message: 'Database writes should handle errors carefully', 1155 | suggestion: 'Add onError: "stopWorkflow" with retryOnFail for transient failures' 1156 | }); 1157 | 1158 | expect(context.autofix).toMatchObject({ 1159 | onError: 'stopWorkflow', 1160 | retryOnFail: true, 1161 | maxTries: 2, 1162 | waitBetweenTries: 2000 1163 | }); 1164 | }); 1165 | 1166 | it('should warn about deprecated continueOnFail', () => { 1167 | context.config = { 1168 | operation: 'execute', 1169 | query: 'SELECT * FROM users', 1170 | continueOnFail: true 1171 | }; 1172 | 1173 | NodeSpecificValidators.validatePostgres(context); 1174 | 1175 | expect(context.warnings).toContainEqual({ 1176 | type: 'deprecated', 1177 | property: 'continueOnFail', 1178 | message: 'continueOnFail is deprecated. Use onError instead', 1179 | suggestion: 'Replace with onError: "continueRegularOutput" or "stopWorkflow"' 1180 | }); 1181 | }); 1182 | }); 1183 | }); 1184 | 1185 | describe('validateMySQL', () => { 1186 | describe('operations', () => { 1187 | it('should validate insert operation', () => { 1188 | context.config = { 1189 | operation: 'insert' 1190 | }; 1191 | 1192 | NodeSpecificValidators.validateMySQL(context); 1193 | 1194 | expect(context.errors).toContainEqual({ 1195 | type: 'missing_required', 1196 | property: 'table', 1197 | message: 'Table name is required for insert operation', 1198 | fix: 'Specify the table to insert data into' 1199 | }); 1200 | }); 1201 | 1202 | it('should validate update operation', () => { 1203 | context.config = { 1204 | operation: 'update' 1205 | }; 1206 | 1207 | NodeSpecificValidators.validateMySQL(context); 1208 | 1209 | expect(context.errors).toContainEqual({ 1210 | type: 'missing_required', 1211 | property: 'table', 1212 | message: 'Table name is required for update operation', 1213 | fix: 'Specify the table to update' 1214 | }); 1215 | }); 1216 | 1217 | it('should validate delete operation', () => { 1218 | context.config = { 1219 | operation: 'delete' 1220 | }; 1221 | 1222 | NodeSpecificValidators.validateMySQL(context); 1223 | 1224 | expect(context.errors).toContainEqual({ 1225 | type: 'missing_required', 1226 | property: 'table', 1227 | message: 'Table name is required for delete operation', 1228 | fix: 'Specify the table to delete from' 1229 | }); 1230 | }); 1231 | 1232 | it('should validate execute operation', () => { 1233 | context.config = { 1234 | operation: 'execute' 1235 | }; 1236 | 1237 | NodeSpecificValidators.validateMySQL(context); 1238 | 1239 | expect(context.errors).toContainEqual({ 1240 | type: 'missing_required', 1241 | property: 'query', 1242 | message: 'SQL query is required', 1243 | fix: 'Provide the SQL query to execute' 1244 | }); 1245 | }); 1246 | }); 1247 | 1248 | describe('MySQL-specific features', () => { 1249 | it('should suggest timezone configuration', () => { 1250 | context.config = { 1251 | operation: 'execute', 1252 | query: 'SELECT NOW()' 1253 | }; 1254 | 1255 | NodeSpecificValidators.validateMySQL(context); 1256 | 1257 | expect(context.suggestions).toContain('Consider setting timezone to ensure consistent date/time handling'); 1258 | }); 1259 | 1260 | it('should check for MySQL backticks', () => { 1261 | context.config = { 1262 | operation: 'execute', 1263 | query: 'SELECT `name` FROM `users`' 1264 | }; 1265 | 1266 | NodeSpecificValidators.validateMySQL(context); 1267 | 1268 | expect(context.suggestions).toContain('Using backticks for identifiers - ensure they are properly paired'); 1269 | }); 1270 | }); 1271 | 1272 | describe('error handling', () => { 1273 | it('should suggest error handling for queries', () => { 1274 | context.config = { 1275 | operation: 'execute', 1276 | query: 'SELECT * FROM users' 1277 | }; 1278 | 1279 | NodeSpecificValidators.validateMySQL(context); 1280 | 1281 | expect(context.warnings).toContainEqual({ 1282 | type: 'best_practice', 1283 | property: 'errorHandling', 1284 | message: 'Database queries can fail due to connection issues', 1285 | suggestion: 'Add onError: "continueRegularOutput" and retryOnFail: true' 1286 | }); 1287 | }); 1288 | 1289 | it('should suggest error handling for modifications', () => { 1290 | context.config = { 1291 | operation: 'update', 1292 | table: 'users', 1293 | updateKey: 'id' 1294 | }; 1295 | 1296 | NodeSpecificValidators.validateMySQL(context); 1297 | 1298 | expect(context.warnings).toContainEqual({ 1299 | type: 'best_practice', 1300 | property: 'errorHandling', 1301 | message: 'Database modifications should handle errors carefully', 1302 | suggestion: 'Add onError: "stopWorkflow" with retryOnFail for transient failures' 1303 | }); 1304 | }); 1305 | }); 1306 | }); 1307 | 1308 | describe('validateHttpRequest', () => { 1309 | describe('URL validation', () => { 1310 | it('should require URL', () => { 1311 | context.config = { 1312 | method: 'GET' 1313 | }; 1314 | 1315 | NodeSpecificValidators.validateHttpRequest(context); 1316 | 1317 | expect(context.errors).toContainEqual({ 1318 | type: 'missing_required', 1319 | property: 'url', 1320 | message: 'URL is required for HTTP requests', 1321 | fix: 'Provide the full URL including protocol (https://...)' 1322 | }); 1323 | }); 1324 | 1325 | it('should warn about missing protocol', () => { 1326 | context.config = { 1327 | method: 'GET', 1328 | url: 'example.com/api' 1329 | }; 1330 | 1331 | NodeSpecificValidators.validateHttpRequest(context); 1332 | 1333 | expect(context.warnings).toContainEqual({ 1334 | type: 'invalid_value', 1335 | property: 'url', 1336 | message: 'URL should start with http:// or https://', 1337 | suggestion: 'Use https:// for secure connections' 1338 | }); 1339 | }); 1340 | 1341 | it('should accept URLs with expressions', () => { 1342 | context.config = { 1343 | method: 'GET', 1344 | url: '{{$node.Config.json.apiUrl}}/users' 1345 | }; 1346 | 1347 | NodeSpecificValidators.validateHttpRequest(context); 1348 | 1349 | const urlWarnings = context.warnings.filter(w => w.property === 'url'); 1350 | expect(urlWarnings).toHaveLength(0); 1351 | }); 1352 | }); 1353 | 1354 | describe('method-specific validation', () => { 1355 | it('should suggest body for POST requests', () => { 1356 | context.config = { 1357 | method: 'POST', 1358 | url: 'https://api.example.com/users' 1359 | }; 1360 | 1361 | NodeSpecificValidators.validateHttpRequest(context); 1362 | 1363 | expect(context.warnings).toContainEqual({ 1364 | type: 'missing_common', 1365 | property: 'sendBody', 1366 | message: 'POST requests typically include a body', 1367 | suggestion: 'Set sendBody: true and configure the body content' 1368 | }); 1369 | }); 1370 | 1371 | it('should suggest body for PUT requests', () => { 1372 | context.config = { 1373 | method: 'PUT', 1374 | url: 'https://api.example.com/users/1' 1375 | }; 1376 | 1377 | NodeSpecificValidators.validateHttpRequest(context); 1378 | 1379 | expect(context.warnings).toContainEqual({ 1380 | type: 'missing_common', 1381 | property: 'sendBody', 1382 | message: 'PUT requests typically include a body', 1383 | suggestion: 'Set sendBody: true and configure the body content' 1384 | }); 1385 | }); 1386 | 1387 | it('should suggest body for PATCH requests', () => { 1388 | context.config = { 1389 | method: 'PATCH', 1390 | url: 'https://api.example.com/users/1' 1391 | }; 1392 | 1393 | NodeSpecificValidators.validateHttpRequest(context); 1394 | 1395 | expect(context.warnings).toContainEqual({ 1396 | type: 'missing_common', 1397 | property: 'sendBody', 1398 | message: 'PATCH requests typically include a body', 1399 | suggestion: 'Set sendBody: true and configure the body content' 1400 | }); 1401 | }); 1402 | }); 1403 | 1404 | describe('error handling', () => { 1405 | it('should suggest error handling for HTTP requests', () => { 1406 | context.config = { 1407 | method: 'GET', 1408 | url: 'https://api.example.com/data' 1409 | }; 1410 | 1411 | NodeSpecificValidators.validateHttpRequest(context); 1412 | 1413 | expect(context.warnings).toContainEqual({ 1414 | type: 'best_practice', 1415 | property: 'errorHandling', 1416 | message: 'HTTP requests can fail due to network issues or server errors', 1417 | suggestion: 'Add onError: "continueRegularOutput" and retryOnFail: true for resilience' 1418 | }); 1419 | 1420 | expect(context.autofix).toMatchObject({ 1421 | onError: 'continueRegularOutput', 1422 | retryOnFail: true, 1423 | maxTries: 3, 1424 | waitBetweenTries: 1000 1425 | }); 1426 | }); 1427 | 1428 | it('should handle deprecated continueOnFail', () => { 1429 | context.config = { 1430 | method: 'GET', 1431 | url: 'https://api.example.com/data', 1432 | continueOnFail: true 1433 | }; 1434 | 1435 | NodeSpecificValidators.validateHttpRequest(context); 1436 | 1437 | expect(context.warnings).toContainEqual({ 1438 | type: 'deprecated', 1439 | property: 'continueOnFail', 1440 | message: 'continueOnFail is deprecated. Use onError instead', 1441 | suggestion: 'Replace with onError: "continueRegularOutput"' 1442 | }); 1443 | 1444 | expect(context.autofix.onError).toBe('continueRegularOutput'); 1445 | expect(context.autofix.continueOnFail).toBeUndefined(); 1446 | }); 1447 | 1448 | it('should handle continueOnFail false', () => { 1449 | context.config = { 1450 | method: 'GET', 1451 | url: 'https://api.example.com/data', 1452 | continueOnFail: false 1453 | }; 1454 | 1455 | NodeSpecificValidators.validateHttpRequest(context); 1456 | 1457 | expect(context.autofix.onError).toBe('stopWorkflow'); 1458 | }); 1459 | }); 1460 | 1461 | describe('retry configuration', () => { 1462 | it('should warn about retrying non-idempotent operations', () => { 1463 | context.config = { 1464 | method: 'POST', 1465 | url: 'https://api.example.com/orders', 1466 | retryOnFail: true, 1467 | maxTries: 5 1468 | }; 1469 | 1470 | NodeSpecificValidators.validateHttpRequest(context); 1471 | 1472 | expect(context.warnings).toContainEqual({ 1473 | type: 'best_practice', 1474 | property: 'maxTries', 1475 | message: 'POST requests might not be idempotent. Use fewer retries.', 1476 | suggestion: 'Set maxTries: 2 for non-idempotent operations' 1477 | }); 1478 | }); 1479 | 1480 | it('should suggest alwaysOutputData for debugging', () => { 1481 | context.config = { 1482 | method: 'GET', 1483 | url: 'https://api.example.com/data', 1484 | retryOnFail: true 1485 | }; 1486 | 1487 | NodeSpecificValidators.validateHttpRequest(context); 1488 | 1489 | expect(context.suggestions).toContain('Enable alwaysOutputData to capture error responses for debugging'); 1490 | expect(context.autofix.alwaysOutputData).toBe(true); 1491 | }); 1492 | }); 1493 | 1494 | describe('authentication and security', () => { 1495 | it('should warn about missing authentication for API endpoints', () => { 1496 | context.config = { 1497 | method: 'GET', 1498 | url: 'https://api.example.com/users' 1499 | }; 1500 | 1501 | NodeSpecificValidators.validateHttpRequest(context); 1502 | 1503 | expect(context.warnings).toContainEqual({ 1504 | type: 'security', 1505 | property: 'authentication', 1506 | message: 'API endpoints typically require authentication', 1507 | suggestion: 'Configure authentication method (Bearer token, API key, etc.)' 1508 | }); 1509 | }); 1510 | 1511 | it('should not warn about authentication for non-API URLs', () => { 1512 | context.config = { 1513 | method: 'GET', 1514 | url: 'https://example.com/public-page' 1515 | }; 1516 | 1517 | NodeSpecificValidators.validateHttpRequest(context); 1518 | 1519 | const authWarnings = context.warnings.filter(w => w.property === 'authentication'); 1520 | expect(authWarnings).toHaveLength(0); 1521 | }); 1522 | }); 1523 | 1524 | describe('timeout', () => { 1525 | it('should suggest timeout configuration', () => { 1526 | context.config = { 1527 | method: 'GET', 1528 | url: 'https://api.example.com/data' 1529 | }; 1530 | 1531 | NodeSpecificValidators.validateHttpRequest(context); 1532 | 1533 | expect(context.suggestions).toContain('Consider setting a timeout to prevent hanging requests'); 1534 | }); 1535 | }); 1536 | }); 1537 | 1538 | describe('validateWebhook', () => { 1539 | describe('path validation', () => { 1540 | it('should require webhook path', () => { 1541 | context.config = { 1542 | httpMethod: 'POST' 1543 | }; 1544 | 1545 | NodeSpecificValidators.validateWebhook(context); 1546 | 1547 | expect(context.errors).toContainEqual({ 1548 | type: 'missing_required', 1549 | property: 'path', 1550 | message: 'Webhook path is required', 1551 | fix: 'Provide a unique path like "my-webhook" or "github-events"' 1552 | }); 1553 | }); 1554 | 1555 | it('should warn about leading slash in path', () => { 1556 | context.config = { 1557 | path: '/my-webhook', 1558 | httpMethod: 'POST' 1559 | }; 1560 | 1561 | NodeSpecificValidators.validateWebhook(context); 1562 | 1563 | expect(context.warnings).toContainEqual({ 1564 | type: 'invalid_value', 1565 | property: 'path', 1566 | message: 'Webhook path should not start with /', 1567 | suggestion: 'Use "webhook-name" instead of "/webhook-name"' 1568 | }); 1569 | }); 1570 | }); 1571 | 1572 | describe('error handling', () => { 1573 | it('should suggest error handling for webhooks', () => { 1574 | context.config = { 1575 | path: 'my-webhook', 1576 | httpMethod: 'POST' 1577 | }; 1578 | 1579 | NodeSpecificValidators.validateWebhook(context); 1580 | 1581 | expect(context.warnings).toContainEqual({ 1582 | type: 'best_practice', 1583 | property: 'onError', 1584 | message: 'Webhooks should always send a response, even on error', 1585 | suggestion: 'Set onError: "continueRegularOutput" to ensure webhook responses' 1586 | }); 1587 | 1588 | expect(context.autofix.onError).toBe('continueRegularOutput'); 1589 | }); 1590 | 1591 | it('should handle deprecated continueOnFail', () => { 1592 | context.config = { 1593 | path: 'my-webhook', 1594 | httpMethod: 'POST', 1595 | continueOnFail: true 1596 | }; 1597 | 1598 | NodeSpecificValidators.validateWebhook(context); 1599 | 1600 | expect(context.warnings).toContainEqual({ 1601 | type: 'deprecated', 1602 | property: 'continueOnFail', 1603 | message: 'continueOnFail is deprecated. Use onError instead', 1604 | suggestion: 'Replace with onError: "continueRegularOutput"' 1605 | }); 1606 | 1607 | expect(context.autofix.onError).toBe('continueRegularOutput'); 1608 | expect(context.autofix.continueOnFail).toBeUndefined(); 1609 | }); 1610 | }); 1611 | 1612 | describe('response mode validation', () => { 1613 | it('should error on responseNode without error handling', () => { 1614 | context.config = { 1615 | path: 'my-webhook', 1616 | httpMethod: 'POST', 1617 | responseMode: 'responseNode' 1618 | }; 1619 | 1620 | NodeSpecificValidators.validateWebhook(context); 1621 | 1622 | expect(context.errors).toContainEqual({ 1623 | type: 'invalid_configuration', 1624 | property: 'responseMode', 1625 | message: 'responseNode mode requires onError: "continueRegularOutput"', 1626 | fix: 'Set onError to ensure response is always sent' 1627 | }); 1628 | }); 1629 | 1630 | it('should not error on responseNode with proper error handling', () => { 1631 | context.config = { 1632 | path: 'my-webhook', 1633 | httpMethod: 'POST', 1634 | responseMode: 'responseNode', 1635 | onError: 'continueRegularOutput' 1636 | }; 1637 | 1638 | NodeSpecificValidators.validateWebhook(context); 1639 | 1640 | const responseModeErrors = context.errors.filter(e => e.property === 'responseMode'); 1641 | expect(responseModeErrors).toHaveLength(0); 1642 | }); 1643 | }); 1644 | 1645 | describe('debugging and security', () => { 1646 | it('should suggest alwaysOutputData for debugging', () => { 1647 | context.config = { 1648 | path: 'my-webhook', 1649 | httpMethod: 'POST' 1650 | }; 1651 | 1652 | NodeSpecificValidators.validateWebhook(context); 1653 | 1654 | expect(context.suggestions).toContain('Enable alwaysOutputData to debug webhook payloads'); 1655 | expect(context.autofix.alwaysOutputData).toBe(true); 1656 | }); 1657 | 1658 | it('should suggest security measures', () => { 1659 | context.config = { 1660 | path: 'my-webhook', 1661 | httpMethod: 'POST' 1662 | }; 1663 | 1664 | NodeSpecificValidators.validateWebhook(context); 1665 | 1666 | expect(context.suggestions).toContain('Consider adding webhook validation (HMAC signature verification)'); 1667 | expect(context.suggestions).toContain('Implement rate limiting for public webhooks'); 1668 | }); 1669 | }); 1670 | }); 1671 | 1672 | describe('validateCode', () => { 1673 | describe('empty code validation', () => { 1674 | it('should error on empty JavaScript code', () => { 1675 | context.config = { 1676 | language: 'javaScript', 1677 | jsCode: '' 1678 | }; 1679 | 1680 | NodeSpecificValidators.validateCode(context); 1681 | 1682 | expect(context.errors).toContainEqual({ 1683 | type: 'missing_required', 1684 | property: 'jsCode', 1685 | message: 'Code cannot be empty', 1686 | fix: 'Add your code logic. Start with: return [{json: {result: "success"}}]' 1687 | }); 1688 | }); 1689 | 1690 | it('should error on whitespace-only code', () => { 1691 | context.config = { 1692 | language: 'javaScript', 1693 | jsCode: ' \n\t ' 1694 | }; 1695 | 1696 | NodeSpecificValidators.validateCode(context); 1697 | 1698 | expect(context.errors).toContainEqual({ 1699 | type: 'missing_required', 1700 | property: 'jsCode', 1701 | message: 'Code cannot be empty', 1702 | fix: 'Add your code logic. Start with: return [{json: {result: "success"}}]' 1703 | }); 1704 | }); 1705 | 1706 | it('should error on empty Python code', () => { 1707 | context.config = { 1708 | language: 'python', 1709 | pythonCode: '' 1710 | }; 1711 | 1712 | NodeSpecificValidators.validateCode(context); 1713 | 1714 | expect(context.errors).toContainEqual({ 1715 | type: 'missing_required', 1716 | property: 'pythonCode', 1717 | message: 'Code cannot be empty', 1718 | fix: 'Add your code logic. Start with: return [{json: {result: "success"}}]' 1719 | }); 1720 | }); 1721 | }); 1722 | 1723 | describe('JavaScript syntax validation', () => { 1724 | it('should detect duplicate const declarations', () => { 1725 | context.config = { 1726 | language: 'javaScript', 1727 | jsCode: 'const const x = 5; return [{json: {x}}];' 1728 | }; 1729 | 1730 | NodeSpecificValidators.validateCode(context); 1731 | 1732 | expect(context.errors).toContainEqual({ 1733 | type: 'invalid_value', 1734 | property: 'jsCode', 1735 | message: 'Syntax error: Duplicate const declaration', 1736 | fix: 'Check your JavaScript syntax' 1737 | }); 1738 | }); 1739 | 1740 | it('should warn about await in non-async function', () => { 1741 | context.config = { 1742 | language: 'javaScript', 1743 | jsCode: ` 1744 | function fetchData() { 1745 | const result = await fetch('https://api.example.com'); 1746 | return [{json: result}]; 1747 | } 1748 | ` 1749 | }; 1750 | 1751 | NodeSpecificValidators.validateCode(context); 1752 | 1753 | expect(context.warnings).toContainEqual({ 1754 | type: 'best_practice', 1755 | message: 'Using await inside a non-async function', 1756 | suggestion: 'Add async keyword to the function, or use top-level await (Code nodes support it)' 1757 | }); 1758 | }); 1759 | 1760 | it('should suggest async usage for $helpers.httpRequest', () => { 1761 | context.config = { 1762 | language: 'javaScript', 1763 | jsCode: 'const response = $helpers.httpRequest(...); return [{json: response}];' 1764 | }; 1765 | 1766 | NodeSpecificValidators.validateCode(context); 1767 | 1768 | expect(context.suggestions).toContain('$helpers.httpRequest is async - use: const response = await $helpers.httpRequest(...)'); 1769 | }); 1770 | 1771 | it('should warn about DateTime usage', () => { 1772 | context.config = { 1773 | language: 'javaScript', 1774 | jsCode: 'const now = DateTime(); return [{json: {now}}];' 1775 | }; 1776 | 1777 | NodeSpecificValidators.validateCode(context); 1778 | 1779 | expect(context.warnings).toContainEqual({ 1780 | type: 'best_practice', 1781 | message: 'DateTime is from Luxon library', 1782 | suggestion: 'Use DateTime.now() or DateTime.fromISO() for date operations' 1783 | }); 1784 | }); 1785 | }); 1786 | 1787 | describe('Python syntax validation', () => { 1788 | it('should warn about unnecessary main check', () => { 1789 | context.config = { 1790 | language: 'python', 1791 | pythonCode: ` 1792 | if __name__ == "__main__": 1793 | result = {"status": "ok"} 1794 | return [{"json": result}] 1795 | ` 1796 | }; 1797 | 1798 | NodeSpecificValidators.validateCode(context); 1799 | 1800 | expect(context.warnings).toContainEqual({ 1801 | type: 'inefficient', 1802 | message: 'if __name__ == "__main__" is not needed in Code nodes', 1803 | suggestion: 'Code node Python runs directly - remove the main check' 1804 | }); 1805 | }); 1806 | 1807 | it('should not warn about __name__ without __main__', () => { 1808 | context.config = { 1809 | language: 'python', 1810 | pythonCode: ` 1811 | module_name = __name__ 1812 | return [{"json": {"module": module_name}}] 1813 | ` 1814 | }; 1815 | 1816 | NodeSpecificValidators.validateCode(context); 1817 | 1818 | const mainWarnings = context.warnings.filter(w => w.message.includes('__main__')); 1819 | expect(mainWarnings).toHaveLength(0); 1820 | }); 1821 | 1822 | it('should error on unavailable imports', () => { 1823 | context.config = { 1824 | language: 'python', 1825 | pythonCode: 'import requests\nreturn [{"json": {"status": "ok"}}]' 1826 | }; 1827 | 1828 | NodeSpecificValidators.validateCode(context); 1829 | 1830 | expect(context.errors).toContainEqual({ 1831 | type: 'invalid_value', 1832 | property: 'pythonCode', 1833 | message: 'Module \'requests\' is not available in Code nodes', 1834 | fix: 'Use JavaScript Code node with $helpers.httpRequest for HTTP requests' 1835 | }); 1836 | }); 1837 | 1838 | it('should check indentation after colons', () => { 1839 | context.config = { 1840 | language: 'python', 1841 | pythonCode: ` 1842 | def process(): 1843 | result = "ok" 1844 | return [{"json": {"result": result}}] 1845 | ` 1846 | }; 1847 | 1848 | NodeSpecificValidators.validateCode(context); 1849 | 1850 | expect(context.errors).toContainEqual({ 1851 | type: 'invalid_value', 1852 | property: 'pythonCode', 1853 | message: 'Missing indentation after line 2', 1854 | fix: 'Indent the line after the colon' 1855 | }); 1856 | }); 1857 | }); 1858 | 1859 | describe('return statement validation', () => { 1860 | it('should error on missing return statement', () => { 1861 | context.config = { 1862 | language: 'javaScript', 1863 | jsCode: 'const result = {status: "ok"}; // missing return' 1864 | }; 1865 | 1866 | NodeSpecificValidators.validateCode(context); 1867 | 1868 | expect(context.errors).toContainEqual({ 1869 | type: 'missing_required', 1870 | property: 'jsCode', 1871 | message: 'Code must return data for the next node', 1872 | fix: 'Add: return [{json: {result: "success"}}]' 1873 | }); 1874 | }); 1875 | 1876 | it('should error on object return without array', () => { 1877 | context.config = { 1878 | language: 'javaScript', 1879 | jsCode: 'return {status: "ok"};' 1880 | }; 1881 | 1882 | NodeSpecificValidators.validateCode(context); 1883 | 1884 | expect(context.errors).toContainEqual({ 1885 | type: 'invalid_value', 1886 | property: 'jsCode', 1887 | message: 'Return value must be an array of objects', 1888 | fix: 'Wrap in array: return [{json: yourObject}]' 1889 | }); 1890 | }); 1891 | 1892 | it('should error on primitive return', () => { 1893 | context.config = { 1894 | language: 'javaScript', 1895 | jsCode: 'return "success";' 1896 | }; 1897 | 1898 | NodeSpecificValidators.validateCode(context); 1899 | 1900 | expect(context.errors).toContainEqual({ 1901 | type: 'invalid_value', 1902 | property: 'jsCode', 1903 | message: 'Cannot return primitive values directly', 1904 | fix: 'Return array of objects: return [{json: {value: yourData}}]' 1905 | }); 1906 | }); 1907 | 1908 | it('should error on Python primitive return', () => { 1909 | context.config = { 1910 | language: 'python', 1911 | pythonCode: 'return "success"' 1912 | }; 1913 | 1914 | NodeSpecificValidators.validateCode(context); 1915 | 1916 | expect(context.errors).toContainEqual({ 1917 | type: 'invalid_value', 1918 | property: 'pythonCode', 1919 | message: 'Cannot return primitive values directly', 1920 | fix: 'Return list of dicts: return [{"json": {"value": your_data}}]' 1921 | }); 1922 | }); 1923 | 1924 | it('should error on array of non-objects', () => { 1925 | context.config = { 1926 | language: 'javaScript', 1927 | jsCode: 'return ["item1", "item2"];' 1928 | }; 1929 | 1930 | NodeSpecificValidators.validateCode(context); 1931 | 1932 | expect(context.errors).toContainEqual({ 1933 | type: 'invalid_value', 1934 | property: 'jsCode', 1935 | message: 'Array items must be objects with json property', 1936 | fix: 'Use: return [{json: {value: "data"}}] not return ["data"]' 1937 | }); 1938 | }); 1939 | 1940 | it('should suggest proper items return format', () => { 1941 | context.config = { 1942 | language: 'javaScript', 1943 | jsCode: 'return items;' 1944 | }; 1945 | 1946 | NodeSpecificValidators.validateCode(context); 1947 | 1948 | expect(context.suggestions).toContain( 1949 | 'Returning items directly is fine if they already have {json: ...} structure. ' + 1950 | 'To modify: return items.map(item => ({json: {...item.json, newField: "value"}}))' 1951 | ); 1952 | }); 1953 | }); 1954 | 1955 | describe('n8n variable usage', () => { 1956 | it('should warn when code doesn\'t reference input data', () => { 1957 | context.config = { 1958 | language: 'javaScript', 1959 | jsCode: 'const result = Math.random(); return [{json: {result}}];' 1960 | }; 1961 | 1962 | NodeSpecificValidators.validateCode(context); 1963 | 1964 | expect(context.warnings).toContainEqual({ 1965 | type: 'missing_common', 1966 | message: 'Code doesn\'t reference input data', 1967 | suggestion: 'Access input with: items, $input.all(), or $json (single-item mode)' 1968 | }); 1969 | }); 1970 | 1971 | it('should error on expression syntax in code', () => { 1972 | context.config = { 1973 | language: 'javaScript', 1974 | jsCode: 'const name = {{$json.name}}; return [{json: {name}}];' 1975 | }; 1976 | 1977 | NodeSpecificValidators.validateCode(context); 1978 | 1979 | expect(context.errors).toContainEqual({ 1980 | type: 'invalid_value', 1981 | property: 'jsCode', 1982 | message: 'Expression syntax {{...}} is not valid in Code nodes', 1983 | fix: 'Use regular JavaScript/Python syntax without double curly braces' 1984 | }); 1985 | }); 1986 | 1987 | it('should warn about wrong $node syntax', () => { 1988 | context.config = { 1989 | language: 'javaScript', 1990 | jsCode: 'const data = $node[\'Previous Node\'].json; return [{json: data}];' 1991 | }; 1992 | 1993 | NodeSpecificValidators.validateCode(context); 1994 | 1995 | expect(context.warnings).toContainEqual({ 1996 | type: 'invalid_value', 1997 | property: 'jsCode', 1998 | message: 'Use $(\'Node Name\') instead of $node[\'Node Name\'] in Code nodes', 1999 | suggestion: 'Replace $node[\'NodeName\'] with $(\'NodeName\')' 2000 | }); 2001 | }); 2002 | 2003 | it('should warn about expression-only functions', () => { 2004 | context.config = { 2005 | language: 'javaScript', 2006 | jsCode: 'const now = $now(); return [{json: {now}}];' 2007 | }; 2008 | 2009 | NodeSpecificValidators.validateCode(context); 2010 | 2011 | expect(context.warnings).toContainEqual({ 2012 | type: 'invalid_value', 2013 | property: 'jsCode', 2014 | message: '$now() is an expression-only function not available in Code nodes', 2015 | suggestion: 'See Code node documentation for alternatives' 2016 | }); 2017 | }); 2018 | 2019 | it('should warn about invalid $ usage', () => { 2020 | context.config = { 2021 | language: 'javaScript', 2022 | jsCode: 'const value = $; return [{json: {value}}];' 2023 | }; 2024 | 2025 | NodeSpecificValidators.validateCode(context); 2026 | 2027 | expect(context.warnings).toContainEqual({ 2028 | type: 'best_practice', 2029 | message: 'Invalid $ usage detected', 2030 | suggestion: 'n8n variables start with $: $json, $input, $node, $workflow, $execution' 2031 | }); 2032 | }); 2033 | 2034 | it('should correct helpers usage', () => { 2035 | context.config = { 2036 | language: 'javaScript', 2037 | jsCode: 'const result = helpers.httpRequest(); return [{json: {result}}];' 2038 | }; 2039 | 2040 | NodeSpecificValidators.validateCode(context); 2041 | 2042 | expect(context.warnings).toContainEqual({ 2043 | type: 'invalid_value', 2044 | property: 'jsCode', 2045 | message: 'Use $helpers not helpers', 2046 | suggestion: 'Change helpers. to $helpers.' 2047 | }); 2048 | }); 2049 | 2050 | it('should warn about $helpers availability', () => { 2051 | context.config = { 2052 | language: 'javaScript', 2053 | jsCode: 'const result = await $helpers.httpRequest(); return [{json: {result}}];' 2054 | }; 2055 | 2056 | NodeSpecificValidators.validateCode(context); 2057 | 2058 | expect(context.warnings).toContainEqual({ 2059 | type: 'best_practice', 2060 | message: '$helpers availability varies by n8n version', 2061 | suggestion: 'Check availability first: if (typeof $helpers !== "undefined" && $helpers.httpRequest) { ... }' 2062 | }); 2063 | }); 2064 | 2065 | it('should error on incorrect getWorkflowStaticData usage', () => { 2066 | context.config = { 2067 | language: 'javaScript', 2068 | jsCode: 'const data = $helpers.getWorkflowStaticData(); return [{json: data}];' 2069 | }; 2070 | 2071 | NodeSpecificValidators.validateCode(context); 2072 | 2073 | expect(context.errors).toContainEqual({ 2074 | type: 'invalid_value', 2075 | property: 'jsCode', 2076 | message: '$helpers.getWorkflowStaticData() will cause "$helpers is not defined" error', 2077 | fix: 'Use $getWorkflowStaticData("global") or $getWorkflowStaticData("node") directly' 2078 | }); 2079 | }); 2080 | 2081 | it('should warn about wrong JMESPath parameter order', () => { 2082 | context.config = { 2083 | language: 'javaScript', 2084 | jsCode: 'const result = $jmespath("name", data); return [{json: {result}}];' 2085 | }; 2086 | 2087 | NodeSpecificValidators.validateCode(context); 2088 | 2089 | expect(context.warnings).toContainEqual({ 2090 | type: 'invalid_value', 2091 | property: 'jsCode', 2092 | message: 'Code node $jmespath has reversed parameter order: $jmespath(data, query)', 2093 | suggestion: 'Use: $jmespath(dataObject, "query.path") not $jmespath("query.path", dataObject)' 2094 | }); 2095 | }); 2096 | 2097 | it('should warn about webhook data access', () => { 2098 | context.config = { 2099 | language: 'javaScript', 2100 | jsCode: 'const payload = items[0].json.payload; return [{json: {payload}}];' 2101 | }; 2102 | 2103 | NodeSpecificValidators.validateCode(context); 2104 | 2105 | expect(context.warnings).toContainEqual({ 2106 | type: 'best_practice', 2107 | message: 'If processing webhook data, remember it\'s nested under .body', 2108 | suggestion: 'Webhook payloads are at items[0].json.body, not items[0].json' 2109 | }); 2110 | }); 2111 | 2112 | it('should warn about webhook data access when webhook node is referenced', () => { 2113 | context.config = { 2114 | language: 'javaScript', 2115 | jsCode: 'const webhookData = $("Webhook"); const data = items[0].json.someField; return [{json: {data}}];' 2116 | }; 2117 | 2118 | NodeSpecificValidators.validateCode(context); 2119 | 2120 | expect(context.warnings).toContainEqual({ 2121 | type: 'invalid_value', 2122 | property: 'jsCode', 2123 | message: 'Webhook data is nested under .body property', 2124 | suggestion: 'Use items[0].json.body.fieldName instead of items[0].json.fieldName for webhook data' 2125 | }); 2126 | }); 2127 | 2128 | it('should warn when code includes webhook string', () => { 2129 | context.config = { 2130 | language: 'javaScript', 2131 | jsCode: '// Process webhook response\nconst data = items[0].json.data; return [{json: {data}}];' 2132 | }; 2133 | 2134 | NodeSpecificValidators.validateCode(context); 2135 | 2136 | expect(context.warnings).toContainEqual({ 2137 | type: 'invalid_value', 2138 | property: 'jsCode', 2139 | message: 'Webhook data is nested under .body property', 2140 | suggestion: 'Use items[0].json.body.fieldName instead of items[0].json.fieldName for webhook data' 2141 | }); 2142 | }); 2143 | 2144 | it('should error on JMESPath numeric literals without backticks', () => { 2145 | context.config = { 2146 | language: 'javaScript', 2147 | jsCode: 'const filtered = $jmespath(data, "[?age >= 18]"); return [{json: {filtered}}];' 2148 | }; 2149 | 2150 | NodeSpecificValidators.validateCode(context); 2151 | 2152 | expect(context.errors).toContainEqual({ 2153 | type: 'invalid_value', 2154 | property: 'jsCode', 2155 | message: 'JMESPath numeric literal 18 must be wrapped in backticks', 2156 | fix: 'Change [?field >= 18] to [?field >= `18`]' 2157 | }); 2158 | }); 2159 | }); 2160 | 2161 | describe('code security', () => { 2162 | it('should warn about eval usage', () => { 2163 | context.config = { 2164 | language: 'javaScript', 2165 | jsCode: 'const result = eval("1 + 1"); return [{json: {result}}];' 2166 | }; 2167 | 2168 | NodeSpecificValidators.validateCode(context); 2169 | 2170 | expect(context.warnings).toContainEqual({ 2171 | type: 'security', 2172 | message: 'Avoid eval() - it\'s a security risk', 2173 | suggestion: 'Use safer alternatives or built-in functions' 2174 | }); 2175 | }); 2176 | 2177 | it('should warn about Function constructor', () => { 2178 | context.config = { 2179 | language: 'javaScript', 2180 | jsCode: 'const fn = new Function("return 1"); return [{json: {result: fn()}}];' 2181 | }; 2182 | 2183 | NodeSpecificValidators.validateCode(context); 2184 | 2185 | expect(context.warnings).toContainEqual({ 2186 | type: 'security', 2187 | message: 'Avoid Function constructor - use regular functions', 2188 | suggestion: 'Use safer alternatives or built-in functions' 2189 | }); 2190 | }); 2191 | 2192 | it('should warn about unavailable modules', () => { 2193 | context.config = { 2194 | language: 'javaScript', 2195 | jsCode: 'const axios = require("axios"); return [{json: {}}];' 2196 | }; 2197 | 2198 | NodeSpecificValidators.validateCode(context); 2199 | 2200 | expect(context.warnings).toContainEqual({ 2201 | type: 'security', 2202 | message: 'Cannot require(\'axios\') - only built-in Node.js modules are available', 2203 | suggestion: 'Available modules: crypto, util, querystring, url, buffer' 2204 | }); 2205 | }); 2206 | 2207 | it('should warn about dynamic require', () => { 2208 | context.config = { 2209 | language: 'javaScript', 2210 | jsCode: 'const module = require(moduleName); return [{json: {}}];' 2211 | }; 2212 | 2213 | NodeSpecificValidators.validateCode(context); 2214 | 2215 | expect(context.warnings).toContainEqual({ 2216 | type: 'security', 2217 | message: 'Dynamic require() not supported', 2218 | suggestion: 'Use static require with string literals: require("crypto")' 2219 | }); 2220 | }); 2221 | 2222 | it('should warn about crypto usage without require', () => { 2223 | context.config = { 2224 | language: 'javaScript', 2225 | jsCode: 'const hash = crypto.createHash("sha256"); return [{json: {hash}}];' 2226 | }; 2227 | 2228 | NodeSpecificValidators.validateCode(context); 2229 | 2230 | expect(context.warnings).toContainEqual({ 2231 | type: 'invalid_value', 2232 | message: 'Using crypto without require statement', 2233 | suggestion: 'Add: const crypto = require("crypto"); at the beginning (ignore editor warnings)' 2234 | }); 2235 | }); 2236 | 2237 | it('should warn about file system access', () => { 2238 | context.config = { 2239 | language: 'javaScript', 2240 | jsCode: 'const fs = require("fs"); return [{json: {}}];' 2241 | }; 2242 | 2243 | NodeSpecificValidators.validateCode(context); 2244 | 2245 | expect(context.warnings).toContainEqual({ 2246 | type: 'security', 2247 | message: 'File system and process access not available in Code nodes', 2248 | suggestion: 'Use other n8n nodes for file operations (e.g., Read/Write Files node)' 2249 | }); 2250 | }); 2251 | }); 2252 | 2253 | describe('mode-specific validation', () => { 2254 | it('should warn about items usage in single-item mode', () => { 2255 | context.config = { 2256 | mode: 'runOnceForEachItem', 2257 | language: 'javaScript', 2258 | jsCode: 'const allItems = items.length; return [{json: {count: allItems}}];' 2259 | }; 2260 | 2261 | NodeSpecificValidators.validateCode(context); 2262 | 2263 | expect(context.warnings).toContainEqual({ 2264 | type: 'best_practice', 2265 | message: 'In "Run Once for Each Item" mode, use $json instead of items array', 2266 | suggestion: 'Access current item data with $json.fieldName' 2267 | }); 2268 | }); 2269 | 2270 | it('should warn about $json usage without single-item mode', () => { 2271 | context.config = { 2272 | language: 'javaScript', 2273 | jsCode: 'const name = $json.name; return [{json: {name}}];' 2274 | }; 2275 | 2276 | NodeSpecificValidators.validateCode(context); 2277 | 2278 | expect(context.warnings).toContainEqual({ 2279 | type: 'best_practice', 2280 | message: '$json only works in "Run Once for Each Item" mode', 2281 | suggestion: 'Either set mode: "runOnceForEachItem" or use items[0].json' 2282 | }); 2283 | }); 2284 | }); 2285 | 2286 | describe('error handling', () => { 2287 | it('should suggest error handling for complex code', () => { 2288 | context.config = { 2289 | language: 'javaScript', 2290 | jsCode: 'a'.repeat(101) + '\nreturn [{json: {}}];' 2291 | }; 2292 | 2293 | NodeSpecificValidators.validateCode(context); 2294 | 2295 | expect(context.warnings).toContainEqual({ 2296 | type: 'best_practice', 2297 | property: 'errorHandling', 2298 | message: 'Code nodes can throw errors - consider error handling', 2299 | suggestion: 'Add onError: "continueRegularOutput" to handle errors gracefully' 2300 | }); 2301 | 2302 | expect(context.autofix.onError).toBe('continueRegularOutput'); 2303 | }); 2304 | }); 2305 | }); 2306 | }); ```