- 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
410 lines
12 KiB
TypeScript
410 lines
12 KiB
TypeScript
/**
|
|
* 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');
|
|
});
|
|
});
|
|
}); |