fix: address critical code review issues for validation improvements

- Fix type safety vulnerability in enhanced-config-validator.ts
  - Added proper type checking before string operations
  - Return early when nodeType is invalid instead of using empty string

- Improve error handling robustness in MCP server
  - Wrapped validation in try-catch to handle unexpected errors
  - Properly re-throw ValidationError instances
  - Add user-friendly error messages for internal errors

- Write comprehensive CHANGELOG entry for v2.10.3
  - Document fixes for issues #58, #68, #70, #73
  - Detail new validation system features
  - List all enhancements and test coverage

Addressed HIGH priority issues from code review:
- Type safety holes in config validator
- Missing error handling for validation system failures
- Consistent error types across validation tools
This commit is contained in:
czlonkowski
2025-08-07 20:05:57 +02:00
parent 48986263bf
commit 13c1663489
12 changed files with 1106 additions and 128 deletions

View File

@@ -223,7 +223,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
it('should error when nodes array is missing', async () => {
const workflow = { connections: {} } as any;
const result = await validator.validateWorkflow(workflow);
const result = await validator.validateWorkflow(workflow as any);
expect(result.valid).toBe(false);
expect(result.errors.some(e => e.message === 'Workflow must have a nodes array')).toBe(true);
@@ -232,7 +232,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
it('should error when connections object is missing', async () => {
const workflow = { nodes: [] } as any;
const result = await validator.validateWorkflow(workflow);
const result = await validator.validateWorkflow(workflow as any);
expect(result.valid).toBe(false);
expect(result.errors.some(e => e.message === 'Workflow must have a connections object')).toBe(true);
@@ -241,7 +241,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
it('should warn when workflow has no nodes', async () => {
const workflow = { nodes: [], connections: {} } as any;
const result = await validator.validateWorkflow(workflow);
const result = await validator.validateWorkflow(workflow as any);
expect(result.valid).toBe(true); // Empty workflows are valid but get a warning
expect(result.warnings).toHaveLength(1);
@@ -260,7 +260,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
connections: {}
} as any;
const result = await validator.validateWorkflow(workflow);
const result = await validator.validateWorkflow(workflow as any);
expect(result.valid).toBe(false);
expect(result.errors.some(e => e.message.includes('Single-node workflows are only valid for webhook endpoints'))).toBe(true);
@@ -279,7 +279,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
connections: {}
} as any;
const result = await validator.validateWorkflow(workflow);
const result = await validator.validateWorkflow(workflow as any);
expect(result.valid).toBe(true);
expect(result.warnings.some(w => w.message.includes('Webhook node has no connections'))).toBe(true);
@@ -306,7 +306,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
connections: {}
} as any;
const result = await validator.validateWorkflow(workflow);
const result = await validator.validateWorkflow(workflow as any);
expect(result.valid).toBe(false);
expect(result.errors.some(e => e.message.includes('Multi-node workflow has no connections'))).toBe(true);
@@ -333,7 +333,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
connections: {}
} as any;
const result = await validator.validateWorkflow(workflow);
const result = await validator.validateWorkflow(workflow as any);
expect(result.errors.some(e => e.message.includes('Duplicate node name: "Webhook"'))).toBe(true);
});
@@ -359,7 +359,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
connections: {}
} as any;
const result = await validator.validateWorkflow(workflow);
const result = await validator.validateWorkflow(workflow as any);
expect(result.errors.some(e => e.message.includes('Duplicate node ID: "1"'))).toBe(true);
});
@@ -392,7 +392,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
connections: {}
} as any;
const result = await validator.validateWorkflow(workflow);
const result = await validator.validateWorkflow(workflow as any);
expect(result.statistics.triggerNodes).toBe(3);
});
@@ -422,7 +422,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
}
} as any;
const result = await validator.validateWorkflow(workflow);
const result = await validator.validateWorkflow(workflow as any);
expect(result.warnings.some(w => w.message.includes('Workflow has no trigger nodes'))).toBe(true);
});
@@ -449,7 +449,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
connections: {}
} as any;
const result = await validator.validateWorkflow(workflow);
const result = await validator.validateWorkflow(workflow as any);
expect(result.statistics.totalNodes).toBe(2);
expect(result.statistics.enabledNodes).toBe(1);
@@ -472,7 +472,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
connections: {}
} as any;
const result = await validator.validateWorkflow(workflow);
const result = await validator.validateWorkflow(workflow as any);
expect(mockNodeRepository.getNode).not.toHaveBeenCalled();
});
@@ -491,7 +491,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
connections: {}
} as any;
const result = await validator.validateWorkflow(workflow);
const result = await validator.validateWorkflow(workflow as any);
expect(result.valid).toBe(false);
expect(result.errors.some(e => e.message.includes('Invalid node type: "nodes-base.webhook"'))).toBe(true);
@@ -512,7 +512,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
connections: {}
} as any;
const result = await validator.validateWorkflow(workflow);
const result = await validator.validateWorkflow(workflow as any);
expect(result.valid).toBe(false);
expect(result.errors.some(e => e.message.includes('Unknown node type: "httpRequest"'))).toBe(true);
@@ -533,7 +533,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
connections: {}
} as any;
const result = await validator.validateWorkflow(workflow);
const result = await validator.validateWorkflow(workflow as any);
expect(mockNodeRepository.getNode).toHaveBeenCalledWith('n8n-nodes-base.webhook');
expect(mockNodeRepository.getNode).toHaveBeenCalledWith('nodes-base.webhook');
@@ -553,7 +553,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
connections: {}
} as any;
const result = await validator.validateWorkflow(workflow);
const result = await validator.validateWorkflow(workflow as any);
expect(mockNodeRepository.getNode).toHaveBeenCalledWith('@n8n/n8n-nodes-langchain.agent');
expect(mockNodeRepository.getNode).toHaveBeenCalledWith('nodes-langchain.agent');
@@ -574,7 +574,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
connections: {}
} as any;
const result = await validator.validateWorkflow(workflow);
const result = await validator.validateWorkflow(workflow as any);
expect(result.errors.some(e => e.message.includes('Missing required property \'typeVersion\''))).toBe(true);
});
@@ -594,7 +594,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
connections: {}
} as any;
const result = await validator.validateWorkflow(workflow);
const result = await validator.validateWorkflow(workflow as any);
expect(result.errors.some(e => e.message.includes('Invalid typeVersion: invalid'))).toBe(true);
});
@@ -614,7 +614,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
connections: {}
} as any;
const result = await validator.validateWorkflow(workflow);
const result = await validator.validateWorkflow(workflow as any);
expect(result.warnings.some(w => w.message.includes('Outdated typeVersion: 1. Latest is 2'))).toBe(true);
});
@@ -634,7 +634,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
connections: {}
} as any;
const result = await validator.validateWorkflow(workflow);
const result = await validator.validateWorkflow(workflow as any);
expect(result.errors.some(e => e.message.includes('typeVersion 10 exceeds maximum supported version 2'))).toBe(true);
});
@@ -664,7 +664,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
connections: {}
} as any;
const result = await validator.validateWorkflow(workflow);
const result = await validator.validateWorkflow(workflow as any);
expect(result.errors.some(e => e.message.includes('Missing required field: url'))).toBe(true);
expect(result.warnings.some(w => w.message.includes('Consider using HTTPS'))).toBe(true);
@@ -689,7 +689,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
connections: {}
} as any;
const result = await validator.validateWorkflow(workflow);
const result = await validator.validateWorkflow(workflow as any);
expect(result.errors.some(e => e.message.includes('Failed to validate node: Validation error'))).toBe(true);
});
@@ -721,7 +721,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
}
} as any;
const result = await validator.validateWorkflow(workflow);
const result = await validator.validateWorkflow(workflow as any);
expect(result.statistics.validConnections).toBe(1);
expect(result.statistics.invalidConnections).toBe(0);
@@ -745,7 +745,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
}
} as any;
const result = await validator.validateWorkflow(workflow);
const result = await validator.validateWorkflow(workflow as any);
expect(result.errors.some(e => e.message.includes('Connection from non-existent node: "NonExistent"'))).toBe(true);
expect(result.statistics.invalidConnections).toBe(1);
@@ -776,7 +776,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
}
} as any;
const result = await validator.validateWorkflow(workflow);
const result = await validator.validateWorkflow(workflow as any);
expect(result.errors.some(e => e.message.includes('Connection uses node ID \'webhook-id\' instead of node name \'Webhook\''))).toBe(true);
});
@@ -799,7 +799,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
}
} as any;
const result = await validator.validateWorkflow(workflow);
const result = await validator.validateWorkflow(workflow as any);
expect(result.errors.some(e => e.message.includes('Connection to non-existent node: "NonExistent"'))).toBe(true);
expect(result.statistics.invalidConnections).toBe(1);
@@ -830,7 +830,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
}
} as any;
const result = await validator.validateWorkflow(workflow);
const result = await validator.validateWorkflow(workflow as any);
expect(result.errors.some(e => e.message.includes('Connection target uses node ID \'set-id\' instead of node name \'Set\''))).toBe(true);
});
@@ -861,7 +861,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
}
} as any;
const result = await validator.validateWorkflow(workflow);
const result = await validator.validateWorkflow(workflow as any);
expect(result.warnings.some(w => w.message.includes('Connection to disabled node: "Set"'))).toBe(true);
});
@@ -891,7 +891,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
}
} as any;
const result = await validator.validateWorkflow(workflow);
const result = await validator.validateWorkflow(workflow as any);
expect(result.statistics.validConnections).toBe(1);
});
@@ -921,7 +921,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
}
} as any;
const result = await validator.validateWorkflow(workflow);
const result = await validator.validateWorkflow(workflow as any);
expect(result.statistics.validConnections).toBe(1);
});
@@ -953,7 +953,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
}
} as any;
const result = await validator.validateWorkflow(workflow);
const result = await validator.validateWorkflow(workflow as any);
expect(result.warnings.some(w => w.message.includes('Community node "CustomTool" is being used as an AI tool'))).toBe(true);
});
@@ -990,7 +990,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
}
} as any;
const result = await validator.validateWorkflow(workflow);
const result = await validator.validateWorkflow(workflow as any);
expect(result.warnings.some(w => w.message.includes('Node is not connected to any other nodes') && w.nodeName === 'Orphaned')).toBe(true);
});
@@ -1033,7 +1033,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
}
} as any;
const result = await validator.validateWorkflow(workflow);
const result = await validator.validateWorkflow(workflow as any);
expect(result.errors.some(e => e.message.includes('Workflow contains a cycle'))).toBe(true);
});
@@ -1068,7 +1068,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
}
} as any;
const result = await validator.validateWorkflow(workflow);
const result = await validator.validateWorkflow(workflow as any);
expect(result.statistics.validConnections).toBe(1);
expect(result.valid).toBe(true);
@@ -1110,7 +1110,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
}
} as any;
const result = await validator.validateWorkflow(workflow);
const result = await validator.validateWorkflow(workflow as any);
expect(ExpressionValidator.validateNodeExpressions).toHaveBeenCalledWith(
expect.objectContaining({ values: expect.any(Object) }),
@@ -1146,7 +1146,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
connections: {}
} as any;
const result = await validator.validateWorkflow(workflow);
const result = await validator.validateWorkflow(workflow as any);
expect(result.errors.some(e => e.message.includes('Expression error: Invalid expression syntax'))).toBe(true);
expect(result.warnings.some(w => w.message.includes('Expression warning: Deprecated variable usage'))).toBe(true);
@@ -1170,7 +1170,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
connections: {}
} as any;
const result = await validator.validateWorkflow(workflow);
const result = await validator.validateWorkflow(workflow as any);
expect(ExpressionValidator.validateNodeExpressions).not.toHaveBeenCalled();
});
@@ -1187,7 +1187,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
const workflow = builder.build() as any;
const result = await validator.validateWorkflow(workflow);
const result = await validator.validateWorkflow(workflow as any);
expect(result.warnings.some(w => w.message.includes('Consider adding error handling'))).toBe(true);
});
@@ -1208,7 +1208,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
const workflow = builder.build() as any;
const result = await validator.validateWorkflow(workflow);
const result = await validator.validateWorkflow(workflow as any);
expect(result.warnings.some(w => w.message.includes('Long linear chain detected'))).toBe(true);
});
@@ -1230,7 +1230,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
connections: {}
} as any;
const result = await validator.validateWorkflow(workflow);
const result = await validator.validateWorkflow(workflow as any);
expect(result.warnings.some(w => w.message.includes('Missing credentials configuration for slackApi'))).toBe(true);
});
@@ -1249,7 +1249,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
connections: {}
} as any;
const result = await validator.validateWorkflow(workflow);
const result = await validator.validateWorkflow(workflow as any);
expect(result.warnings.some(w => w.message.includes('AI Agent has no tools connected'))).toBe(true);
});
@@ -1279,7 +1279,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
}
} as any;
const result = await validator.validateWorkflow(workflow);
const result = await validator.validateWorkflow(workflow as any);
expect(result.suggestions.some(s => s.includes('N8N_COMMUNITY_PACKAGES_ALLOW_TOOL_USAGE'))).toBe(true);
});
@@ -1306,7 +1306,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
connections: {}
} as any;
const result = await validator.validateWorkflow(workflow);
const result = await validator.validateWorkflow(workflow as any);
expect(result.errors.some(e => e.message.includes('Node-level properties onError, retryOnFail, credentials are in the wrong location'))).toBe(true);
expect(result.errors.some(e => e.details?.fix?.includes('Move these properties from node.parameters to the node level'))).toBe(true);
@@ -1327,7 +1327,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
connections: {}
} as any;
const result = await validator.validateWorkflow(workflow);
const result = await validator.validateWorkflow(workflow as any);
expect(result.errors.some(e => e.message.includes('Invalid onError value: "invalidValue"'))).toBe(true);
});
@@ -1347,7 +1347,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
connections: {}
} as any;
const result = await validator.validateWorkflow(workflow);
const result = await validator.validateWorkflow(workflow as any);
expect(result.warnings.some(w => w.message.includes('Using deprecated "continueOnFail: true"'))).toBe(true);
});
@@ -1368,7 +1368,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
connections: {}
} as any;
const result = await validator.validateWorkflow(workflow);
const result = await validator.validateWorkflow(workflow as any);
expect(result.errors.some(e => e.message.includes('Cannot use both "continueOnFail" and "onError" properties'))).toBe(true);
});
@@ -1390,7 +1390,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
connections: {}
} as any;
const result = await validator.validateWorkflow(workflow);
const result = await validator.validateWorkflow(workflow as any);
expect(result.errors.some(e => e.message.includes('maxTries must be a positive number'))).toBe(true);
expect(result.errors.some(e => e.message.includes('waitBetweenTries must be a non-negative number'))).toBe(true);
@@ -1413,7 +1413,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
connections: {}
} as any;
const result = await validator.validateWorkflow(workflow);
const result = await validator.validateWorkflow(workflow as any);
expect(result.warnings.some(w => w.message.includes('maxTries is set to 15'))).toBe(true);
expect(result.warnings.some(w => w.message.includes('waitBetweenTries is set to 400000ms'))).toBe(true);
@@ -1434,7 +1434,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
connections: {}
} as any;
const result = await validator.validateWorkflow(workflow);
const result = await validator.validateWorkflow(workflow as any);
expect(result.warnings.some(w => w.message.includes('retryOnFail is enabled but maxTries is not specified'))).toBe(true);
});
@@ -1459,7 +1459,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
connections: {}
} as any;
const result = await validator.validateWorkflow(workflow);
const result = await validator.validateWorkflow(workflow as any);
expect(result.errors.some(e => e.message.includes('alwaysOutputData must be a boolean'))).toBe(true);
@@ -1484,7 +1484,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
connections: {}
} as any;
const result = await validator.validateWorkflow(workflow);
const result = await validator.validateWorkflow(workflow as any);
expect(result.warnings.some(w => w.message.includes('executeOnce is enabled'))).toBe(true);
});
@@ -1512,7 +1512,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
connections: {}
} as any;
const result = await validator.validateWorkflow(workflow);
const result = await validator.validateWorkflow(workflow as any);
expect(result.warnings.some(w => w.message.includes(nodeInfo.message) && w.message.includes('without error handling'))).toBe(true);
}
@@ -1534,7 +1534,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
connections: {}
} as any;
const result = await validator.validateWorkflow(workflow);
const result = await validator.validateWorkflow(workflow as any);
expect(result.warnings.some(w => w.message.includes('Both continueOnFail and retryOnFail are enabled'))).toBe(true);
});
@@ -1554,7 +1554,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
connections: {}
} as any;
const result = await validator.validateWorkflow(workflow);
const result = await validator.validateWorkflow(workflow as any);
expect(result.suggestions.some(s => s.includes('Consider enabling alwaysOutputData'))).toBe(true);
});
@@ -1569,7 +1569,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
const workflow = builder.build() as any;
const result = await validator.validateWorkflow(workflow);
const result = await validator.validateWorkflow(workflow as any);
expect(result.suggestions.some(s => s.includes('Most nodes lack error handling'))).toBe(true);
});
@@ -1589,7 +1589,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
connections: {}
} as any;
const result = await validator.validateWorkflow(workflow);
const result = await validator.validateWorkflow(workflow as any);
expect(result.suggestions.some(s => s.includes('Replace "continueOnFail: true" with "onError:'))).toBe(true);
});
@@ -1610,7 +1610,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
connections: {}
} as any;
const result = await validator.validateWorkflow(workflow);
const result = await validator.validateWorkflow(workflow as any);
expect(result.suggestions.some(s => s.includes('Add a trigger node'))).toBe(true);
});
@@ -1636,7 +1636,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
connections: {} // Missing connections
} as any;
const result = await validator.validateWorkflow(workflow);
const result = await validator.validateWorkflow(workflow as any);
expect(result.suggestions.some(s => s.includes('Example connection structure'))).toBe(true);
expect(result.suggestions.some(s => s.includes('Use node NAMES (not IDs) in connections'))).toBe(true);
@@ -1667,7 +1667,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
}
} as any;
const result = await validator.validateWorkflow(workflow);
const result = await validator.validateWorkflow(workflow as any);
expect(result.suggestions.some(s => s.includes('Add error handling'))).toBe(true);
});
@@ -1682,7 +1682,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
const workflow = builder.build() as any;
const result = await validator.validateWorkflow(workflow);
const result = await validator.validateWorkflow(workflow as any);
expect(result.suggestions.some(s => s.includes('Consider breaking this workflow into smaller sub-workflows'))).toBe(true);
});
@@ -1708,7 +1708,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
connections: {}
} as any;
const result = await validator.validateWorkflow(workflow);
const result = await validator.validateWorkflow(workflow as any);
expect(result.suggestions.some(s => s.includes('Consider using a Code node for complex data transformations'))).toBe(true);
});
@@ -1727,7 +1727,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
connections: {}
} as any;
const result = await validator.validateWorkflow(workflow);
const result = await validator.validateWorkflow(workflow as any);
expect(result.suggestions.some(s => s.includes('A minimal workflow needs'))).toBe(true);
});
@@ -1756,7 +1756,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
connections: {}
} as any;
const result = await validator.validateWorkflow(workflow);
const result = await validator.validateWorkflow(workflow as any);
expect(result.errors.some(e => e.message.includes(`Did you mean`) && e.message.includes(testCase.suggestion))).toBe(true);
}
@@ -1848,7 +1848,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
}
} as any;
const result = await validator.validateWorkflow(workflow);
const result = await validator.validateWorkflow(workflow as any);
// Should have multiple errors
expect(result.valid).toBe(false);
@@ -1940,7 +1940,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
}
} as any;
const result = await validator.validateWorkflow(workflow);
const result = await validator.validateWorkflow(workflow as any);
expect(result.valid).toBe(true);
expect(result.errors).toHaveLength(0);

View File

@@ -157,7 +157,7 @@ describe('WorkflowValidator - Edge Cases', () => {
nodes: [],
connections: {}
};
const result = await validator.validateWorkflow(workflow);
const result = await validator.validateWorkflow(workflow as any);
expect(result.valid).toBe(true);
expect(result.warnings.some(w => w.message.includes('empty'))).toBe(true);
});
@@ -181,7 +181,7 @@ describe('WorkflowValidator - Edge Cases', () => {
const workflow = { nodes, connections };
const start = Date.now();
const result = await validator.validateWorkflow(workflow);
const result = await validator.validateWorkflow(workflow as any);
const duration = Date.now() - start;
expect(result).toBeDefined();
@@ -207,7 +207,7 @@ describe('WorkflowValidator - Edge Cases', () => {
}
};
const result = await validator.validateWorkflow(workflow);
const result = await validator.validateWorkflow(workflow as any);
expect(result.statistics.invalidConnections).toBe(0);
});
@@ -228,7 +228,7 @@ describe('WorkflowValidator - Edge Cases', () => {
}
};
const result = await validator.validateWorkflow(workflow);
const result = await validator.validateWorkflow(workflow as any);
expect(result.valid).toBe(true);
});
});
@@ -264,7 +264,7 @@ describe('WorkflowValidator - Edge Cases', () => {
connections: {}
};
const result = await validator.validateWorkflow(workflow);
const result = await validator.validateWorkflow(workflow as any);
expect(result.errors.length).toBeGreaterThan(0);
});
@@ -292,7 +292,7 @@ describe('WorkflowValidator - Edge Cases', () => {
}
};
const result = await validator.validateWorkflow(workflow);
const result = await validator.validateWorkflow(workflow as any);
expect(result.warnings.some(w => w.message.includes('self-referencing'))).toBe(true);
});
@@ -308,7 +308,7 @@ describe('WorkflowValidator - Edge Cases', () => {
}
};
const result = await validator.validateWorkflow(workflow);
const result = await validator.validateWorkflow(workflow as any);
expect(result.errors.some(e => e.message.includes('non-existent'))).toBe(true);
});
@@ -324,7 +324,7 @@ describe('WorkflowValidator - Edge Cases', () => {
}
};
const result = await validator.validateWorkflow(workflow);
const result = await validator.validateWorkflow(workflow as any);
expect(result.errors.length).toBeGreaterThan(0);
});
@@ -341,7 +341,7 @@ describe('WorkflowValidator - Edge Cases', () => {
} as any
};
const result = await validator.validateWorkflow(workflow);
const result = await validator.validateWorkflow(workflow as any);
// Should still work as type and index can have defaults
expect(result.statistics.validConnections).toBeGreaterThan(0);
});
@@ -359,7 +359,7 @@ describe('WorkflowValidator - Edge Cases', () => {
}
};
const result = await validator.validateWorkflow(workflow);
const result = await validator.validateWorkflow(workflow as any);
expect(result.errors.some(e => e.message.includes('Invalid'))).toBe(true);
});
});
@@ -382,7 +382,7 @@ describe('WorkflowValidator - Edge Cases', () => {
}
};
const result = await validator.validateWorkflow(workflow);
const result = await validator.validateWorkflow(workflow as any);
expect(result.valid).toBe(true);
});
@@ -395,7 +395,7 @@ describe('WorkflowValidator - Edge Cases', () => {
connections: {}
};
const result = await validator.validateWorkflow(workflow);
const result = await validator.validateWorkflow(workflow as any);
expect(result.warnings.some(w => w.message.includes('very long'))).toBe(true);
});
});
@@ -479,7 +479,7 @@ describe('WorkflowValidator - Edge Cases', () => {
}
};
const result = await validator.validateWorkflow(workflow);
const result = await validator.validateWorkflow(workflow as any);
expect(result.statistics.validConnections).toBeGreaterThan(0);
});
});
@@ -499,7 +499,7 @@ describe('WorkflowValidator - Edge Cases', () => {
}
};
const result = await validator.validateWorkflow(workflow);
const result = await validator.validateWorkflow(workflow as any);
expect(result.errors.length).toBeGreaterThan(0);
expect(result.statistics.validConnections).toBeGreaterThan(0);
});

View File

@@ -77,7 +77,7 @@ describe('WorkflowValidator - Simple Unit Tests', () => {
};
// Act
const result = await validator.validateWorkflow(workflow);
const result = await validator.validateWorkflow(workflow as any);
// Assert
expect(result.valid).toBe(true);
@@ -113,7 +113,7 @@ describe('WorkflowValidator - Simple Unit Tests', () => {
};
// Act
const result = await validator.validateWorkflow(workflow);
const result = await validator.validateWorkflow(workflow as any);
// Assert
expect(result.valid).toBe(false);
@@ -154,7 +154,7 @@ describe('WorkflowValidator - Simple Unit Tests', () => {
};
// Act
const result = await validator.validateWorkflow(workflow);
const result = await validator.validateWorkflow(workflow as any);
// Assert
expect(result.valid).toBe(false);
@@ -229,7 +229,7 @@ describe('WorkflowValidator - Simple Unit Tests', () => {
};
// Act
const result = await validator.validateWorkflow(workflow);
const result = await validator.validateWorkflow(workflow as any);
// Assert
expect(result.valid).toBe(true);
@@ -297,7 +297,7 @@ describe('WorkflowValidator - Simple Unit Tests', () => {
};
// Act
const result = await validator.validateWorkflow(workflow);
const result = await validator.validateWorkflow(workflow as any);
// Assert
expect(result.valid).toBe(false);
@@ -386,7 +386,7 @@ describe('WorkflowValidator - Simple Unit Tests', () => {
};
// Act
const result = await validator.validateWorkflow(workflow);
const result = await validator.validateWorkflow(workflow as any);
// Assert
expect(result.valid).toBe(false);
@@ -438,7 +438,7 @@ describe('WorkflowValidator - Simple Unit Tests', () => {
};
// Act
const result = await validator.validateWorkflow(workflow);
const result = await validator.validateWorkflow(workflow as any);
// Assert
expect(result.warnings.some(w => w.message.includes('Outdated typeVersion'))).toBe(true);
@@ -471,7 +471,7 @@ describe('WorkflowValidator - Simple Unit Tests', () => {
};
// Act
const result = await validator.validateWorkflow(workflow);
const result = await validator.validateWorkflow(workflow as any);
// Assert
expect(result.valid).toBe(false);

View File

@@ -0,0 +1,410 @@
/**
* Test suite for validation system fixes
* Covers issues #58, #68, #70, #73
*/
import { describe, test, expect, beforeAll, afterAll } from 'vitest';
import { WorkflowValidator } from '../../src/services/workflow-validator';
import { EnhancedConfigValidator } from '../../src/services/enhanced-config-validator';
import { ToolValidation, Validator, ValidationError } from '../../src/utils/validation-schemas';
describe('Validation System Fixes', () => {
let workflowValidator: WorkflowValidator;
let mockNodeRepository: any;
beforeAll(async () => {
// Initialize test environment
process.env.NODE_ENV = 'test';
// Mock repository for testing
mockNodeRepository = {
getNode: (nodeType: string) => {
if (nodeType === 'nodes-base.webhook' || nodeType === 'n8n-nodes-base.webhook') {
return {
nodeType: 'nodes-base.webhook',
displayName: 'Webhook',
properties: [
{ name: 'path', required: true, displayName: 'Path' },
{ name: 'httpMethod', required: true, displayName: 'HTTP Method' }
]
};
}
if (nodeType === 'nodes-base.set' || nodeType === 'n8n-nodes-base.set') {
return {
nodeType: 'nodes-base.set',
displayName: 'Set',
properties: [
{ name: 'values', required: false, displayName: 'Values' }
]
};
}
return null;
}
} as any;
workflowValidator = new WorkflowValidator(mockNodeRepository, EnhancedConfigValidator);
});
afterAll(() => {
delete process.env.NODE_ENV;
});
describe('Issue #73: validate_node_minimal crashes without input validation', () => {
test('should handle empty config in validation schemas', () => {
// Test the validation schema handles empty config
const result = ToolValidation.validateNodeMinimal({
nodeType: 'nodes-base.webhook',
config: undefined
});
expect(result).toBeDefined();
expect(result.valid).toBe(false);
expect(result.errors.length).toBeGreaterThan(0);
expect(result.errors[0].field).toBe('config');
});
test('should handle null config in validation schemas', () => {
const result = ToolValidation.validateNodeMinimal({
nodeType: 'nodes-base.webhook',
config: null
});
expect(result).toBeDefined();
expect(result.valid).toBe(false);
expect(result.errors.length).toBeGreaterThan(0);
expect(result.errors[0].field).toBe('config');
});
test('should accept valid config object', () => {
const result = ToolValidation.validateNodeMinimal({
nodeType: 'nodes-base.webhook',
config: { path: '/webhook', httpMethod: 'POST' }
});
expect(result).toBeDefined();
expect(result.valid).toBe(true);
expect(result.errors).toHaveLength(0);
});
});
describe('Issue #58: validate_node_operation crashes on nested input', () => {
test('should handle invalid nodeType gracefully', () => {
expect(() => {
EnhancedConfigValidator.validateWithMode(
undefined as any,
{ resource: 'channel', operation: 'create' },
[],
'operation',
'ai-friendly'
);
}).toThrow(Error);
});
test('should handle null nodeType gracefully', () => {
expect(() => {
EnhancedConfigValidator.validateWithMode(
null as any,
{ resource: 'channel', operation: 'create' },
[],
'operation',
'ai-friendly'
);
}).toThrow(Error);
});
test('should handle non-string nodeType gracefully', () => {
expect(() => {
EnhancedConfigValidator.validateWithMode(
{ type: 'nodes-base.slack' } as any,
{ resource: 'channel', operation: 'create' },
[],
'operation',
'ai-friendly'
);
}).toThrow(Error);
});
test('should handle valid nodeType properly', () => {
const result = EnhancedConfigValidator.validateWithMode(
'nodes-base.set',
{ values: {} },
[],
'operation',
'ai-friendly'
);
expect(result).toBeDefined();
expect(typeof result.valid).toBe('boolean');
});
});
describe('Issue #70: Profile settings not respected', () => {
test('should pass profile parameter to all validation phases', async () => {
const workflow = {
nodes: [
{
id: '1',
name: 'Webhook',
type: 'n8n-nodes-base.webhook',
position: [100, 200],
parameters: { path: '/test', httpMethod: 'POST' },
typeVersion: 1
},
{
id: '2',
name: 'Set',
type: 'n8n-nodes-base.set',
position: [300, 200],
parameters: { values: {} },
typeVersion: 1
}
],
connections: {
'Webhook': {
main: [[{ node: 'Set', type: 'main', index: 0 }]]
}
}
};
const result = await workflowValidator.validateWorkflow(workflow, {
validateNodes: true,
validateConnections: true,
validateExpressions: true,
profile: 'minimal'
});
expect(result).toBeDefined();
expect(result.valid).toBe(true);
// In minimal profile, should have fewer warnings/errors - just check it's reasonable
expect(result.warnings.length).toBeLessThanOrEqual(5);
});
test('should filter out sticky notes from validation', async () => {
const workflow = {
nodes: [
{
id: '1',
name: 'Webhook',
type: 'n8n-nodes-base.webhook',
position: [100, 200],
parameters: { path: '/test', httpMethod: 'POST' },
typeVersion: 1
},
{
id: '2',
name: 'Sticky Note',
type: 'n8n-nodes-base.stickyNote',
position: [300, 100],
parameters: { content: 'This is a note' },
typeVersion: 1
}
],
connections: {}
};
const result = await workflowValidator.validateWorkflow(workflow);
expect(result).toBeDefined();
expect(result.statistics.totalNodes).toBe(1); // Only webhook, sticky note excluded
expect(result.statistics.enabledNodes).toBe(1);
});
test('should allow legitimate loops in cycle detection', async () => {
const workflow = {
nodes: [
{
id: '1',
name: 'Manual Trigger',
type: 'n8n-nodes-base.manualTrigger',
position: [100, 200],
parameters: {},
typeVersion: 1
},
{
id: '2',
name: 'SplitInBatches',
type: 'n8n-nodes-base.splitInBatches',
position: [300, 200],
parameters: { batchSize: 1 },
typeVersion: 1
},
{
id: '3',
name: 'Set',
type: 'n8n-nodes-base.set',
position: [500, 200],
parameters: { values: {} },
typeVersion: 1
}
],
connections: {
'Manual Trigger': {
main: [[{ node: 'SplitInBatches', type: 'main', index: 0 }]]
},
'SplitInBatches': {
main: [
[{ node: 'Set', type: 'main', index: 0 }], // Done output
[{ node: 'Set', type: 'main', index: 0 }] // Loop output
]
},
'Set': {
main: [[{ node: 'SplitInBatches', type: 'main', index: 0 }]] // Loop back
}
}
};
const result = await workflowValidator.validateWorkflow(workflow);
expect(result).toBeDefined();
// Should not report cycle error for legitimate SplitInBatches loop
const cycleErrors = result.errors.filter(e => e.message.includes('cycle'));
expect(cycleErrors).toHaveLength(0);
});
});
describe('Issue #68: Better error recovery suggestions', () => {
test('should provide recovery suggestions for invalid node types', async () => {
const workflow = {
nodes: [
{
id: '1',
name: 'Invalid Node',
type: 'invalid-node-type',
position: [100, 200],
parameters: {},
typeVersion: 1
}
],
connections: {}
};
const result = await workflowValidator.validateWorkflow(workflow);
expect(result).toBeDefined();
expect(result.valid).toBe(false);
expect(result.suggestions.length).toBeGreaterThan(0);
// Should contain recovery suggestions
const recoveryStarted = result.suggestions.some(s => s.includes('🔧 RECOVERY'));
expect(recoveryStarted).toBe(true);
});
test('should provide recovery suggestions for connection errors', async () => {
const workflow = {
nodes: [
{
id: '1',
name: 'Webhook',
type: 'n8n-nodes-base.webhook',
position: [100, 200],
parameters: { path: '/test', httpMethod: 'POST' },
typeVersion: 1
}
],
connections: {
'Webhook': {
main: [[{ node: 'NonExistentNode', type: 'main', index: 0 }]]
}
}
};
const result = await workflowValidator.validateWorkflow(workflow);
expect(result).toBeDefined();
expect(result.valid).toBe(false);
expect(result.suggestions.length).toBeGreaterThan(0);
// Should contain connection recovery suggestions
const connectionRecovery = result.suggestions.some(s =>
s.includes('Connection errors detected') || s.includes('connection')
);
expect(connectionRecovery).toBe(true);
});
test('should provide workflow for multiple errors', async () => {
const workflow = {
nodes: [
{
id: '1',
name: 'Invalid Node 1',
type: 'invalid-type-1',
position: [100, 200],
parameters: {}
// Missing typeVersion
},
{
id: '2',
name: 'Invalid Node 2',
type: 'invalid-type-2',
position: [300, 200],
parameters: {}
// Missing typeVersion
},
{
id: '3',
name: 'Invalid Node 3',
type: 'invalid-type-3',
position: [500, 200],
parameters: {}
// Missing typeVersion
}
],
connections: {
'Invalid Node 1': {
main: [[{ node: 'NonExistent', type: 'main', index: 0 }]]
}
}
};
const result = await workflowValidator.validateWorkflow(workflow);
expect(result).toBeDefined();
expect(result.valid).toBe(false);
expect(result.errors.length).toBeGreaterThan(3);
// Should provide step-by-step recovery workflow
const workflowSuggestion = result.suggestions.some(s =>
s.includes('SUGGESTED WORKFLOW') && s.includes('Too many errors detected')
);
expect(workflowSuggestion).toBe(true);
});
});
describe('Enhanced Input Validation', () => {
test('should validate tool parameters with schemas', () => {
// Test validate_node_operation parameters
const validationResult = ToolValidation.validateNodeOperation({
nodeType: 'nodes-base.webhook',
config: { path: '/test' },
profile: 'ai-friendly'
});
expect(validationResult.valid).toBe(true);
expect(validationResult.errors).toHaveLength(0);
});
test('should reject invalid parameters', () => {
const validationResult = ToolValidation.validateNodeOperation({
nodeType: 123, // Invalid type
config: 'not an object', // Invalid type
profile: 'invalid-profile' // Invalid enum value
});
expect(validationResult.valid).toBe(false);
expect(validationResult.errors.length).toBeGreaterThan(0);
});
test('should format validation errors properly', () => {
const validationResult = ToolValidation.validateNodeOperation({
nodeType: null,
config: null
});
const errorMessage = Validator.formatErrors(validationResult, 'validate_node_operation');
expect(errorMessage).toContain('validate_node_operation: Validation failed:');
expect(errorMessage).toContain('nodeType');
expect(errorMessage).toContain('config');
});
});
});