mirror of
https://github.com/czlonkowski/n8n-mcp.git
synced 2026-01-30 06:22:04 +00:00
* feat: n8n_deploy_template deploy-first with auto-fix (v2.27.2) Improved template deployment to deploy first, then automatically fix common issues. This dramatically improves deployment success rates for templates with expression format issues. Key Changes: - Deploy-first behavior: templates deployed before validation - Auto-fix runs automatically after deployment (configurable via `autoFix`) - Returns `fixesApplied` array showing all corrections made - Fixed expression validator "nested expressions" false positive - Fixed Zod schema missing `typeversion-upgrade` and `version-migration` fix types Testing: 87% deployment success rate across 15 diverse templates 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en * fix: address code review findings for deploy template Code review fixes: - CRITICAL: Update test schema to use `autoFix` instead of old `validate` parameter - WARNING: Add `AppliedFix` and `AutofixResultData` interfaces for type safety - WARNING: Add `autoFixStatus` field to response (success/failed/skipped) - WARNING: Report auto-fix failure in response message Changes: - tests/unit/mcp/handlers-deploy-template.test.ts: Fixed schema and test cases - src/mcp/handlers-n8n-manager.ts: Added type definitions, autoFixStatus tracking - src/mcp/tool-docs/workflow_management/n8n-deploy-template.ts: Updated docs 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
266 lines
8.6 KiB
TypeScript
266 lines
8.6 KiB
TypeScript
/**
|
|
* Unit tests for handleDeployTemplate handler - input validation and schema tests
|
|
*/
|
|
|
|
import { describe, it, expect } from 'vitest';
|
|
import { z } from 'zod';
|
|
|
|
// Test the schema directly without needing full API mocking
|
|
const deployTemplateSchema = z.object({
|
|
templateId: z.number().positive().int(),
|
|
name: z.string().optional(),
|
|
autoUpgradeVersions: z.boolean().default(true),
|
|
autoFix: z.boolean().default(true),
|
|
stripCredentials: z.boolean().default(true)
|
|
});
|
|
|
|
describe('handleDeployTemplate Schema Validation', () => {
|
|
describe('Input Schema', () => {
|
|
it('should require templateId as a positive integer', () => {
|
|
// Valid input
|
|
const validResult = deployTemplateSchema.safeParse({ templateId: 123 });
|
|
expect(validResult.success).toBe(true);
|
|
|
|
// Invalid: missing templateId
|
|
const missingResult = deployTemplateSchema.safeParse({});
|
|
expect(missingResult.success).toBe(false);
|
|
|
|
// Invalid: templateId as string
|
|
const stringResult = deployTemplateSchema.safeParse({ templateId: '123' });
|
|
expect(stringResult.success).toBe(false);
|
|
|
|
// Invalid: negative templateId
|
|
const negativeResult = deployTemplateSchema.safeParse({ templateId: -1 });
|
|
expect(negativeResult.success).toBe(false);
|
|
|
|
// Invalid: zero templateId
|
|
const zeroResult = deployTemplateSchema.safeParse({ templateId: 0 });
|
|
expect(zeroResult.success).toBe(false);
|
|
|
|
// Invalid: decimal templateId
|
|
const decimalResult = deployTemplateSchema.safeParse({ templateId: 123.5 });
|
|
expect(decimalResult.success).toBe(false);
|
|
});
|
|
|
|
it('should accept optional name parameter', () => {
|
|
const withName = deployTemplateSchema.safeParse({
|
|
templateId: 123,
|
|
name: 'Custom Name'
|
|
});
|
|
expect(withName.success).toBe(true);
|
|
if (withName.success) {
|
|
expect(withName.data.name).toBe('Custom Name');
|
|
}
|
|
|
|
const withoutName = deployTemplateSchema.safeParse({ templateId: 123 });
|
|
expect(withoutName.success).toBe(true);
|
|
if (withoutName.success) {
|
|
expect(withoutName.data.name).toBeUndefined();
|
|
}
|
|
});
|
|
|
|
it('should default autoUpgradeVersions to true', () => {
|
|
const result = deployTemplateSchema.safeParse({ templateId: 123 });
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.data.autoUpgradeVersions).toBe(true);
|
|
}
|
|
});
|
|
|
|
it('should allow setting autoUpgradeVersions to false', () => {
|
|
const result = deployTemplateSchema.safeParse({
|
|
templateId: 123,
|
|
autoUpgradeVersions: false
|
|
});
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.data.autoUpgradeVersions).toBe(false);
|
|
}
|
|
});
|
|
|
|
it('should default autoFix to true', () => {
|
|
const result = deployTemplateSchema.safeParse({ templateId: 123 });
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.data.autoFix).toBe(true);
|
|
}
|
|
});
|
|
|
|
it('should default stripCredentials to true', () => {
|
|
const result = deployTemplateSchema.safeParse({ templateId: 123 });
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.data.stripCredentials).toBe(true);
|
|
}
|
|
});
|
|
|
|
it('should accept all parameters together', () => {
|
|
const result = deployTemplateSchema.safeParse({
|
|
templateId: 2776,
|
|
name: 'My Deployed Workflow',
|
|
autoUpgradeVersions: false,
|
|
autoFix: false,
|
|
stripCredentials: false
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.data.templateId).toBe(2776);
|
|
expect(result.data.name).toBe('My Deployed Workflow');
|
|
expect(result.data.autoUpgradeVersions).toBe(false);
|
|
expect(result.data.autoFix).toBe(false);
|
|
expect(result.data.stripCredentials).toBe(false);
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('handleDeployTemplate Helper Functions', () => {
|
|
describe('Credential Extraction Logic', () => {
|
|
it('should extract credential types from node credentials object', () => {
|
|
const nodes = [
|
|
{
|
|
id: 'node-1',
|
|
name: 'Slack',
|
|
type: 'n8n-nodes-base.slack',
|
|
credentials: {
|
|
slackApi: { id: 'cred-1', name: 'My Slack' }
|
|
}
|
|
},
|
|
{
|
|
id: 'node-2',
|
|
name: 'Google Sheets',
|
|
type: 'n8n-nodes-base.googleSheets',
|
|
credentials: {
|
|
googleSheetsOAuth2Api: { id: 'cred-2', name: 'My Google' }
|
|
}
|
|
},
|
|
{
|
|
id: 'node-3',
|
|
name: 'Set',
|
|
type: 'n8n-nodes-base.set'
|
|
// No credentials
|
|
}
|
|
];
|
|
|
|
// Simulate the credential extraction logic from the handler
|
|
const requiredCredentials: Array<{
|
|
nodeType: string;
|
|
nodeName: string;
|
|
credentialType: string;
|
|
}> = [];
|
|
|
|
for (const node of nodes) {
|
|
if (node.credentials && typeof node.credentials === 'object') {
|
|
for (const [credType] of Object.entries(node.credentials)) {
|
|
requiredCredentials.push({
|
|
nodeType: node.type,
|
|
nodeName: node.name,
|
|
credentialType: credType
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
expect(requiredCredentials).toHaveLength(2);
|
|
expect(requiredCredentials[0]).toEqual({
|
|
nodeType: 'n8n-nodes-base.slack',
|
|
nodeName: 'Slack',
|
|
credentialType: 'slackApi'
|
|
});
|
|
expect(requiredCredentials[1]).toEqual({
|
|
nodeType: 'n8n-nodes-base.googleSheets',
|
|
nodeName: 'Google Sheets',
|
|
credentialType: 'googleSheetsOAuth2Api'
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Credential Stripping Logic', () => {
|
|
it('should remove credentials property from nodes', () => {
|
|
const nodes = [
|
|
{
|
|
id: 'node-1',
|
|
name: 'Slack',
|
|
type: 'n8n-nodes-base.slack',
|
|
typeVersion: 2,
|
|
position: [250, 300],
|
|
parameters: { channel: '#general' },
|
|
credentials: {
|
|
slackApi: { id: 'cred-1', name: 'My Slack' }
|
|
}
|
|
}
|
|
];
|
|
|
|
// Simulate the credential stripping logic from the handler
|
|
const strippedNodes = nodes.map((node: any) => {
|
|
const { credentials, ...rest } = node;
|
|
return rest;
|
|
});
|
|
|
|
expect(strippedNodes[0].credentials).toBeUndefined();
|
|
expect(strippedNodes[0].id).toBe('node-1');
|
|
expect(strippedNodes[0].name).toBe('Slack');
|
|
expect(strippedNodes[0].parameters).toEqual({ channel: '#general' });
|
|
});
|
|
});
|
|
|
|
describe('Trigger Type Detection Logic', () => {
|
|
it('should identify trigger nodes', () => {
|
|
const testCases = [
|
|
{ type: 'n8n-nodes-base.scheduleTrigger', expected: 'scheduleTrigger' },
|
|
{ type: 'n8n-nodes-base.webhook', expected: 'webhook' },
|
|
{ type: 'n8n-nodes-base.emailReadImapTrigger', expected: 'emailReadImapTrigger' },
|
|
{ type: 'n8n-nodes-base.googleDriveTrigger', expected: 'googleDriveTrigger' }
|
|
];
|
|
|
|
for (const { type, expected } of testCases) {
|
|
const nodes = [{ type, name: 'Trigger' }];
|
|
|
|
// Simulate the trigger detection logic from the handler
|
|
const triggerNode = nodes.find((n: any) =>
|
|
n.type?.includes('Trigger') ||
|
|
n.type?.includes('webhook') ||
|
|
n.type === 'n8n-nodes-base.webhook'
|
|
);
|
|
const triggerType = triggerNode?.type?.split('.').pop() || 'manual';
|
|
|
|
expect(triggerType).toBe(expected);
|
|
}
|
|
});
|
|
|
|
it('should return manual for workflows without trigger', () => {
|
|
const nodes = [
|
|
{ type: 'n8n-nodes-base.set', name: 'Set' },
|
|
{ type: 'n8n-nodes-base.httpRequest', name: 'HTTP Request' }
|
|
];
|
|
|
|
const triggerNode = nodes.find((n: any) =>
|
|
n.type?.includes('Trigger') ||
|
|
n.type?.includes('webhook') ||
|
|
n.type === 'n8n-nodes-base.webhook'
|
|
);
|
|
const triggerType = triggerNode?.type?.split('.').pop() || 'manual';
|
|
|
|
expect(triggerType).toBe('manual');
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Tool Definition Validation', () => {
|
|
it('should have correct tool name', () => {
|
|
// This tests that the tool is properly exported
|
|
const toolName = 'n8n_deploy_template';
|
|
expect(toolName).toBe('n8n_deploy_template');
|
|
});
|
|
|
|
it('should have required parameter templateId in schema', () => {
|
|
// Validate that the schema correctly requires templateId
|
|
const validResult = deployTemplateSchema.safeParse({ templateId: 123 });
|
|
const invalidResult = deployTemplateSchema.safeParse({});
|
|
|
|
expect(validResult.success).toBe(true);
|
|
expect(invalidResult.success).toBe(false);
|
|
});
|
|
});
|