- Add unit tests for ConfigValidator with 44 test cases (95.21% coverage) - Create test templates for 7 major services: - PropertyFilter (23 tests) - ExampleGenerator (35 tests) - TaskTemplates (36 tests) - PropertyDependencies (21 tests) - EnhancedConfigValidator (8 tests) - ExpressionValidator (11 tests) - WorkflowValidator (9 tests) - Fix service implementations to handle edge cases discovered during testing - Add comprehensive testing documentation: - Phase 3 testing plan with priorities and timeline - Context documentation for quick implementation - Mocking strategy for complex dependencies - All 262 tests now passing (up from 75) 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1076 lines
32 KiB
TypeScript
1076 lines
32 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
import { ConfigValidator } from '@/services/config-validator';
|
|
import { slackNodeFactory } from '@tests/fixtures/factories/node.factory';
|
|
import type { ValidationResult, ValidationError, ValidationWarning } from '@/services/config-validator';
|
|
|
|
// Mock the database
|
|
vi.mock('better-sqlite3');
|
|
|
|
describe('ConfigValidator', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
describe('validate', () => {
|
|
it('should validate required fields for Slack message post', () => {
|
|
const nodeType = 'nodes-base.slack';
|
|
const config = {
|
|
resource: 'message',
|
|
operation: 'post'
|
|
// Missing required 'channel' field
|
|
};
|
|
const properties = [
|
|
{
|
|
name: 'resource',
|
|
type: 'options',
|
|
required: true,
|
|
default: 'message',
|
|
options: [
|
|
{ name: 'Message', value: 'message' },
|
|
{ name: 'Channel', value: 'channel' }
|
|
]
|
|
},
|
|
{
|
|
name: 'operation',
|
|
type: 'options',
|
|
required: true,
|
|
default: 'post',
|
|
displayOptions: {
|
|
show: { resource: ['message'] }
|
|
},
|
|
options: [
|
|
{ name: 'Post', value: 'post' },
|
|
{ name: 'Update', value: 'update' }
|
|
]
|
|
},
|
|
{
|
|
name: 'channel',
|
|
type: 'string',
|
|
required: true,
|
|
displayOptions: {
|
|
show: {
|
|
resource: ['message'],
|
|
operation: ['post']
|
|
}
|
|
}
|
|
}
|
|
];
|
|
|
|
const result = ConfigValidator.validate(nodeType, config, properties);
|
|
|
|
expect(result.valid).toBe(false);
|
|
expect(result.errors).toHaveLength(1);
|
|
expect(result.errors[0]).toMatchObject({
|
|
type: 'missing_required',
|
|
property: 'channel',
|
|
message: "Required property 'channel' is missing",
|
|
fix: 'Add channel to your configuration'
|
|
});
|
|
});
|
|
|
|
it('should validate successfully with all required fields', () => {
|
|
const nodeType = 'nodes-base.slack';
|
|
const config = {
|
|
resource: 'message',
|
|
operation: 'post',
|
|
channel: '#general',
|
|
text: 'Hello, Slack!'
|
|
};
|
|
const properties = [
|
|
{
|
|
name: 'resource',
|
|
type: 'options',
|
|
required: true,
|
|
default: 'message',
|
|
options: [
|
|
{ name: 'Message', value: 'message' },
|
|
{ name: 'Channel', value: 'channel' }
|
|
]
|
|
},
|
|
{
|
|
name: 'operation',
|
|
type: 'options',
|
|
required: true,
|
|
default: 'post',
|
|
displayOptions: {
|
|
show: { resource: ['message'] }
|
|
},
|
|
options: [
|
|
{ name: 'Post', value: 'post' },
|
|
{ name: 'Update', value: 'update' }
|
|
]
|
|
},
|
|
{
|
|
name: 'channel',
|
|
type: 'string',
|
|
required: true,
|
|
displayOptions: {
|
|
show: {
|
|
resource: ['message'],
|
|
operation: ['post']
|
|
}
|
|
}
|
|
},
|
|
{
|
|
name: 'text',
|
|
type: 'string',
|
|
default: '',
|
|
displayOptions: {
|
|
show: {
|
|
resource: ['message'],
|
|
operation: ['post']
|
|
}
|
|
}
|
|
}
|
|
];
|
|
|
|
const result = ConfigValidator.validate(nodeType, config, properties);
|
|
|
|
expect(result.valid).toBe(true);
|
|
expect(result.errors).toHaveLength(0);
|
|
expect(result.visibleProperties).toContain('channel');
|
|
expect(result.visibleProperties).toContain('text');
|
|
});
|
|
|
|
it('should handle unknown node types gracefully', () => {
|
|
const nodeType = 'nodes-base.unknown';
|
|
const config = { someField: 'value' };
|
|
const properties: any[] = [];
|
|
|
|
const result = ConfigValidator.validate(nodeType, config, properties);
|
|
|
|
expect(result.valid).toBe(true);
|
|
expect(result.errors).toHaveLength(0);
|
|
// Unknown node types may still generate warnings for configured properties
|
|
// This is expected behavior
|
|
});
|
|
|
|
it('should validate property types', () => {
|
|
const nodeType = 'nodes-base.test';
|
|
const config = {
|
|
stringField: 123, // Should be string
|
|
numberField: '456', // Should be number
|
|
booleanField: 'true' // Should be boolean
|
|
};
|
|
const properties = [
|
|
{ name: 'stringField', type: 'string' },
|
|
{ name: 'numberField', type: 'number' },
|
|
{ name: 'booleanField', type: 'boolean' }
|
|
];
|
|
|
|
const result = ConfigValidator.validate(nodeType, config, properties);
|
|
|
|
expect(result.valid).toBe(false);
|
|
expect(result.errors).toHaveLength(3);
|
|
expect(result.errors[0].type).toBe('invalid_type');
|
|
expect(result.errors[0].property).toBe('stringField');
|
|
expect(result.errors[1].type).toBe('invalid_type');
|
|
expect(result.errors[1].property).toBe('numberField');
|
|
expect(result.errors[2].type).toBe('invalid_type');
|
|
expect(result.errors[2].property).toBe('booleanField');
|
|
});
|
|
|
|
it('should validate option values', () => {
|
|
const nodeType = 'nodes-base.test';
|
|
const config = {
|
|
resource: 'invalid_option'
|
|
};
|
|
const properties = [
|
|
{
|
|
name: 'resource',
|
|
type: 'options',
|
|
options: [
|
|
{ name: 'User', value: 'user' },
|
|
{ name: 'Post', value: 'post' }
|
|
]
|
|
}
|
|
];
|
|
|
|
const result = ConfigValidator.validate(nodeType, config, properties);
|
|
|
|
expect(result.valid).toBe(false);
|
|
expect(result.errors).toHaveLength(1);
|
|
expect(result.errors[0]).toMatchObject({
|
|
type: 'invalid_value',
|
|
property: 'resource',
|
|
message: "Invalid value for 'resource'. Must be one of: user, post"
|
|
});
|
|
});
|
|
|
|
it('should check property visibility based on displayOptions', () => {
|
|
const nodeType = 'nodes-base.test';
|
|
const config = {
|
|
resource: 'user',
|
|
userField: 'visible'
|
|
};
|
|
const properties = [
|
|
{
|
|
name: 'resource',
|
|
type: 'options',
|
|
options: [
|
|
{ name: 'User', value: 'user' },
|
|
{ name: 'Post', value: 'post' }
|
|
]
|
|
},
|
|
{
|
|
name: 'userField',
|
|
type: 'string',
|
|
displayOptions: {
|
|
show: { resource: ['user'] }
|
|
}
|
|
},
|
|
{
|
|
name: 'postField',
|
|
type: 'string',
|
|
displayOptions: {
|
|
show: { resource: ['post'] }
|
|
}
|
|
}
|
|
];
|
|
|
|
const result = ConfigValidator.validate(nodeType, config, properties);
|
|
|
|
expect(result.visibleProperties).toContain('resource');
|
|
expect(result.visibleProperties).toContain('userField');
|
|
expect(result.hiddenProperties).toContain('postField');
|
|
});
|
|
|
|
it('should perform HTTP Request specific validation', () => {
|
|
const nodeType = 'nodes-base.httpRequest';
|
|
const config = {
|
|
method: 'POST',
|
|
url: 'invalid-url', // Missing protocol
|
|
sendBody: false
|
|
};
|
|
const properties = [
|
|
{ name: 'method', type: 'options' },
|
|
{ name: 'url', type: 'string' },
|
|
{ name: 'sendBody', type: 'boolean' }
|
|
];
|
|
|
|
const result = ConfigValidator.validate(nodeType, config, properties);
|
|
|
|
expect(result.valid).toBe(false);
|
|
expect(result.errors).toHaveLength(1);
|
|
expect(result.errors[0]).toMatchObject({
|
|
type: 'invalid_value',
|
|
property: 'url',
|
|
message: 'URL must start with http:// or https://'
|
|
});
|
|
expect(result.warnings).toHaveLength(1);
|
|
expect(result.warnings[0]).toMatchObject({
|
|
type: 'missing_common',
|
|
property: 'sendBody',
|
|
message: 'POST requests typically send a body'
|
|
});
|
|
expect(result.autofix).toMatchObject({
|
|
sendBody: true,
|
|
contentType: 'json'
|
|
});
|
|
});
|
|
|
|
it('should perform security checks for hardcoded credentials', () => {
|
|
const nodeType = 'nodes-base.test';
|
|
const config = {
|
|
api_key: 'sk-1234567890abcdef',
|
|
password: 'my-secret-password',
|
|
token: 'hardcoded-token'
|
|
};
|
|
const properties = [
|
|
{ name: 'api_key', type: 'string' },
|
|
{ name: 'password', type: 'string' },
|
|
{ name: 'token', type: 'string' }
|
|
];
|
|
|
|
const result = ConfigValidator.validate(nodeType, config, properties);
|
|
|
|
expect(result.warnings.filter(w => w.type === 'security')).toHaveLength(3);
|
|
expect(result.warnings.some(w => w.property === 'api_key')).toBe(true);
|
|
expect(result.warnings.some(w => w.property === 'password')).toBe(true);
|
|
expect(result.warnings.some(w => w.property === 'token')).toBe(true);
|
|
});
|
|
|
|
it('should validate Code node configurations', () => {
|
|
const nodeType = 'nodes-base.code';
|
|
const config = {
|
|
language: 'javascript',
|
|
jsCode: '' // Empty code
|
|
};
|
|
const properties = [
|
|
{ name: 'language', type: 'options' },
|
|
{ name: 'jsCode', type: 'string' }
|
|
];
|
|
|
|
const result = ConfigValidator.validate(nodeType, config, properties);
|
|
|
|
expect(result.valid).toBe(false);
|
|
expect(result.errors).toHaveLength(1);
|
|
expect(result.errors[0]).toMatchObject({
|
|
type: 'missing_required',
|
|
property: 'jsCode',
|
|
message: 'Code cannot be empty'
|
|
});
|
|
});
|
|
|
|
it('should validate JavaScript syntax in Code node', () => {
|
|
const nodeType = 'nodes-base.code';
|
|
const config = {
|
|
language: 'javascript',
|
|
jsCode: `
|
|
const data = { foo: "bar" };
|
|
if (data.foo { // Missing closing parenthesis
|
|
return [{json: data}];
|
|
}
|
|
`
|
|
};
|
|
const properties = [
|
|
{ name: 'language', type: 'options' },
|
|
{ name: 'jsCode', type: 'string' }
|
|
];
|
|
|
|
const result = ConfigValidator.validate(nodeType, config, properties);
|
|
|
|
expect(result.errors.some(e => e.message.includes('Unbalanced')));
|
|
expect(result.warnings).toHaveLength(1);
|
|
});
|
|
|
|
it('should validate n8n-specific patterns in Code node', () => {
|
|
const nodeType = 'nodes-base.code';
|
|
const config = {
|
|
language: 'javascript',
|
|
jsCode: `
|
|
// Process data without returning
|
|
const processedData = items.map(item => ({
|
|
...item.json,
|
|
processed: true
|
|
}));
|
|
// No output provided
|
|
`
|
|
};
|
|
const properties = [
|
|
{ name: 'language', type: 'options' },
|
|
{ name: 'jsCode', type: 'string' }
|
|
];
|
|
|
|
const result = ConfigValidator.validate(nodeType, config, properties);
|
|
|
|
// The warning should be about missing return statement
|
|
expect(result.warnings.some(w => w.type === 'missing_common' && w.message.includes('No return statement found'))).toBe(true);
|
|
});
|
|
|
|
it('should validate database query security', () => {
|
|
const nodeType = 'nodes-base.postgres';
|
|
const config = {
|
|
query: 'DELETE FROM users;' // Missing WHERE clause
|
|
};
|
|
const properties = [
|
|
{ name: 'query', type: 'string' }
|
|
];
|
|
|
|
const result = ConfigValidator.validate(nodeType, config, properties);
|
|
|
|
expect(result.warnings.some(w =>
|
|
w.type === 'security' &&
|
|
w.message.includes('DELETE query without WHERE clause')
|
|
)).toBe(true);
|
|
});
|
|
|
|
it('should check for SQL injection vulnerabilities', () => {
|
|
const nodeType = 'nodes-base.mysql';
|
|
const config = {
|
|
query: 'SELECT * FROM users WHERE id = ${userId}'
|
|
};
|
|
const properties = [
|
|
{ name: 'query', type: 'string' }
|
|
];
|
|
|
|
const result = ConfigValidator.validate(nodeType, config, properties);
|
|
|
|
expect(result.warnings.some(w =>
|
|
w.type === 'security' &&
|
|
w.message.includes('SQL injection')
|
|
)).toBe(true);
|
|
});
|
|
|
|
it('should warn about inefficient configured but hidden properties', () => {
|
|
const nodeType = 'nodes-base.test'; // Changed from Code node
|
|
const config = {
|
|
mode: 'manual',
|
|
automaticField: 'This will not be used'
|
|
};
|
|
const properties = [
|
|
{
|
|
name: 'mode',
|
|
type: 'options',
|
|
options: [
|
|
{ name: 'Manual', value: 'manual' },
|
|
{ name: 'Automatic', value: 'automatic' }
|
|
]
|
|
},
|
|
{
|
|
name: 'automaticField',
|
|
type: 'string',
|
|
displayOptions: {
|
|
show: { mode: ['automatic'] }
|
|
}
|
|
}
|
|
];
|
|
|
|
const result = ConfigValidator.validate(nodeType, config, properties);
|
|
|
|
expect(result.warnings.some(w =>
|
|
w.type === 'inefficient' &&
|
|
w.property === 'automaticField'
|
|
)).toBe(true);
|
|
});
|
|
|
|
it('should suggest commonly used properties', () => {
|
|
const nodeType = 'nodes-base.test';
|
|
const config = {
|
|
url: 'https://api.example.com'
|
|
};
|
|
const properties = [
|
|
{ name: 'url', type: 'string' },
|
|
{ name: 'authentication', type: 'options' },
|
|
{ name: 'timeout', type: 'number' }
|
|
];
|
|
|
|
const result = ConfigValidator.validate(nodeType, config, properties);
|
|
|
|
expect(result.suggestions.some(s => s.includes('authentication'))).toBe(true);
|
|
expect(result.suggestions.some(s => s.includes('timeout'))).toBe(true);
|
|
});
|
|
|
|
it('should handle webhook-specific validation', () => {
|
|
const nodeType = 'nodes-base.webhook';
|
|
const config = {
|
|
responseMode: 'responseNode'
|
|
// Missing responseData when using responseNode mode
|
|
};
|
|
const properties = [
|
|
{ name: 'responseMode', type: 'options' },
|
|
{ name: 'responseData', type: 'string' }
|
|
];
|
|
|
|
const result = ConfigValidator.validate(nodeType, config, properties);
|
|
|
|
expect(result.suggestions.some(s =>
|
|
s.includes('Respond to Webhook')
|
|
)).toBe(true);
|
|
});
|
|
|
|
it('should validate Python code syntax', () => {
|
|
const nodeType = 'nodes-base.code';
|
|
const config = {
|
|
language: 'python',
|
|
pythonCode: `
|
|
def process_items():
|
|
for item in items # Missing colon
|
|
processed = item.get('json', {})
|
|
return [{"json": processed}]
|
|
`
|
|
};
|
|
const properties = [
|
|
{ name: 'language', type: 'options' },
|
|
{ name: 'pythonCode', type: 'string' }
|
|
];
|
|
|
|
const result = ConfigValidator.validate(nodeType, config, properties);
|
|
|
|
expect(result.warnings.some(w =>
|
|
w.message.includes('Missing colon after control structure')
|
|
)).toBe(true);
|
|
});
|
|
|
|
it('should detect mixed indentation in Python code', () => {
|
|
const nodeType = 'nodes-base.code';
|
|
const config = {
|
|
language: 'python',
|
|
pythonCode: `
|
|
def process():
|
|
if True:
|
|
\t\treturn True # Mixed tabs and spaces
|
|
`
|
|
};
|
|
const properties = [
|
|
{ name: 'language', type: 'options' },
|
|
{ name: 'pythonCode', type: 'string' }
|
|
];
|
|
|
|
const result = ConfigValidator.validate(nodeType, config, properties);
|
|
|
|
expect(result.errors.some(e =>
|
|
e.message.includes('Mixed tabs and spaces')
|
|
)).toBe(true);
|
|
});
|
|
|
|
it('should warn about incorrect n8n return patterns', () => {
|
|
const nodeType = 'nodes-base.code';
|
|
const config = {
|
|
language: 'javascript',
|
|
jsCode: `
|
|
const data = items.map(item => item.json);
|
|
const processed = data.reduce((acc, item) => ({ ...acc, ...item }), {});
|
|
return {result: processed};
|
|
`
|
|
};
|
|
const properties = [
|
|
{ name: 'language', type: 'options' },
|
|
{ name: 'jsCode', type: 'string' }
|
|
];
|
|
|
|
const result = ConfigValidator.validate(nodeType, config, properties);
|
|
|
|
expect(result.warnings.some(w =>
|
|
w.message.includes('Return value must be an array')
|
|
)).toBe(true);
|
|
});
|
|
|
|
it('should validate JSON in HTTP Request body', () => {
|
|
const nodeType = 'nodes-base.httpRequest';
|
|
const config = {
|
|
method: 'POST',
|
|
url: 'https://api.example.com',
|
|
sendBody: true,
|
|
contentType: 'json',
|
|
jsonBody: '{ invalid json' // Invalid JSON
|
|
};
|
|
const properties = [
|
|
{ name: 'method', type: 'options' },
|
|
{ name: 'url', type: 'string' },
|
|
{ name: 'sendBody', type: 'boolean' },
|
|
{ name: 'contentType', type: 'options' },
|
|
{ name: 'jsonBody', type: 'string' }
|
|
];
|
|
|
|
const result = ConfigValidator.validate(nodeType, config, properties);
|
|
|
|
expect(result.errors.some(e =>
|
|
e.property === 'jsonBody' &&
|
|
e.message.includes('invalid JSON')
|
|
)).toBe(true);
|
|
});
|
|
|
|
it('should handle empty properties array', () => {
|
|
const nodeType = 'nodes-base.test';
|
|
const config = { someField: 'value' };
|
|
const properties: any[] = [];
|
|
|
|
const result = ConfigValidator.validate(nodeType, config, properties);
|
|
|
|
expect(result.valid).toBe(true);
|
|
expect(result.errors).toHaveLength(0);
|
|
});
|
|
|
|
it('should handle missing displayOptions gracefully', () => {
|
|
const nodeType = 'nodes-base.test';
|
|
const config = { field1: 'value1' };
|
|
const properties = [
|
|
{ name: 'field1', type: 'string' },
|
|
{ name: 'field2', type: 'string' } // No displayOptions
|
|
];
|
|
|
|
const result = ConfigValidator.validate(nodeType, config, properties);
|
|
|
|
expect(result.visibleProperties).toContain('field1');
|
|
expect(result.visibleProperties).toContain('field2');
|
|
});
|
|
|
|
it('should validate options with string format', () => {
|
|
const nodeType = 'nodes-base.test';
|
|
const config = { resource: 'user' };
|
|
const properties = [
|
|
{
|
|
name: 'resource',
|
|
type: 'options',
|
|
options: ['user', 'post', 'comment'] // String array format
|
|
}
|
|
];
|
|
|
|
const result = ConfigValidator.validate(nodeType, config, properties);
|
|
|
|
expect(result.valid).toBe(true);
|
|
expect(result.errors).toHaveLength(0);
|
|
});
|
|
|
|
it('should warn about security issues with eval/exec', () => {
|
|
const nodeType = 'nodes-base.code';
|
|
const config = {
|
|
language: 'javascript',
|
|
jsCode: `
|
|
const userInput = items[0].json.code;
|
|
const result = eval(userInput); // Security risk
|
|
return [{json: {result}}];
|
|
`
|
|
};
|
|
const properties = [
|
|
{ name: 'language', type: 'options' },
|
|
{ name: 'jsCode', type: 'string' }
|
|
];
|
|
|
|
const result = ConfigValidator.validate(nodeType, config, properties);
|
|
|
|
expect(result.warnings.some(w =>
|
|
w.type === 'security' &&
|
|
w.message.includes('eval/exec')
|
|
)).toBe(true);
|
|
});
|
|
|
|
it('should detect infinite loops', () => {
|
|
const nodeType = 'nodes-base.code';
|
|
const config = {
|
|
language: 'javascript',
|
|
jsCode: `
|
|
while (true) {
|
|
// Infinite loop
|
|
processData();
|
|
}
|
|
`
|
|
};
|
|
const properties = [
|
|
{ name: 'language', type: 'options' },
|
|
{ name: 'jsCode', type: 'string' }
|
|
];
|
|
|
|
const result = ConfigValidator.validate(nodeType, config, properties);
|
|
|
|
expect(result.warnings.some(w =>
|
|
w.type === 'security' &&
|
|
w.message.includes('Infinite loop')
|
|
)).toBe(true);
|
|
});
|
|
|
|
it('should suggest error handling for complex code', () => {
|
|
const nodeType = 'nodes-base.code';
|
|
const config = {
|
|
language: 'javascript',
|
|
jsCode: `
|
|
// Complex code without error handling (over 200 chars)
|
|
const data = items.map(item => {
|
|
const processed = item.json;
|
|
processed.timestamp = new Date().toISOString();
|
|
processed.status = 'processed';
|
|
return processed;
|
|
});
|
|
const filtered = data.filter(d => d.status === 'processed');
|
|
return filtered.map(item => ({json: item}));
|
|
`
|
|
};
|
|
const properties = [
|
|
{ name: 'language', type: 'options' },
|
|
{ name: 'jsCode', type: 'string' }
|
|
];
|
|
|
|
const result = ConfigValidator.validate(nodeType, config, properties);
|
|
|
|
expect(result.warnings.some(w =>
|
|
w.type === 'best_practice' &&
|
|
w.message.includes('error handling')
|
|
)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('edge cases and additional coverage', () => {
|
|
it('should handle null and undefined config values', () => {
|
|
const nodeType = 'nodes-base.test';
|
|
const config = {
|
|
stringField: null,
|
|
numberField: undefined,
|
|
booleanField: null
|
|
};
|
|
const properties = [
|
|
{ name: 'stringField', type: 'string', required: true },
|
|
{ name: 'numberField', type: 'number' },
|
|
{ name: 'booleanField', type: 'boolean' }
|
|
];
|
|
|
|
const result = ConfigValidator.validate(nodeType, config, properties);
|
|
|
|
expect(result.errors).toHaveLength(3);
|
|
expect(result.errors.every(e => e.type === 'invalid_type')).toBe(true);
|
|
});
|
|
|
|
it('should validate nested displayOptions conditions', () => {
|
|
const nodeType = 'nodes-base.test';
|
|
const config = {
|
|
mode: 'advanced',
|
|
resource: 'user',
|
|
operation: 'create'
|
|
};
|
|
const properties = [
|
|
{ name: 'mode', type: 'options' },
|
|
{ name: 'resource', type: 'options' },
|
|
{ name: 'operation', type: 'options' },
|
|
{
|
|
name: 'advancedField',
|
|
type: 'string',
|
|
displayOptions: {
|
|
show: {
|
|
mode: ['advanced'],
|
|
resource: ['user'],
|
|
operation: ['create', 'update']
|
|
}
|
|
}
|
|
}
|
|
];
|
|
|
|
const result = ConfigValidator.validate(nodeType, config, properties);
|
|
|
|
expect(result.visibleProperties).toContain('advancedField');
|
|
});
|
|
|
|
it('should handle hide conditions in displayOptions', () => {
|
|
const nodeType = 'nodes-base.test';
|
|
const config = {
|
|
mode: 'simple',
|
|
showAdvanced: true
|
|
};
|
|
const properties = [
|
|
{ name: 'mode', type: 'options' },
|
|
{ name: 'showAdvanced', type: 'boolean' },
|
|
{
|
|
name: 'hiddenInSimpleMode',
|
|
type: 'string',
|
|
displayOptions: {
|
|
hide: {
|
|
mode: ['simple']
|
|
}
|
|
}
|
|
}
|
|
];
|
|
|
|
const result = ConfigValidator.validate(nodeType, config, properties);
|
|
|
|
expect(result.hiddenProperties).toContain('hiddenInSimpleMode');
|
|
});
|
|
|
|
it('should validate Code node with $helpers usage', () => {
|
|
const nodeType = 'nodes-base.code';
|
|
const config = {
|
|
language: 'javascript',
|
|
jsCode: `
|
|
const response = await $helpers.httpRequest({
|
|
method: 'GET',
|
|
url: 'https://api.example.com/data'
|
|
});
|
|
return [{json: response}];
|
|
`
|
|
};
|
|
const properties = [
|
|
{ name: 'language', type: 'options' },
|
|
{ name: 'jsCode', type: 'string' }
|
|
];
|
|
|
|
const result = ConfigValidator.validate(nodeType, config, properties);
|
|
|
|
expect(result.warnings.some(w =>
|
|
w.type === 'best_practice' &&
|
|
w.message.includes('$helpers availability')
|
|
)).toBe(true);
|
|
});
|
|
|
|
it('should detect incorrect $helpers.getWorkflowStaticData usage', () => {
|
|
const nodeType = 'nodes-base.code';
|
|
const config = {
|
|
language: 'javascript',
|
|
jsCode: `
|
|
const staticData = $helpers.getWorkflowStaticData();
|
|
staticData.counter = (staticData.counter || 0) + 1;
|
|
return [{json: {count: staticData.counter}}];
|
|
`
|
|
};
|
|
const properties = [
|
|
{ name: 'language', type: 'options' },
|
|
{ name: 'jsCode', type: 'string' }
|
|
];
|
|
|
|
const result = ConfigValidator.validate(nodeType, config, properties);
|
|
|
|
expect(result.warnings.some(w =>
|
|
w.type === 'invalid_value' &&
|
|
w.message.includes('$helpers.getWorkflowStaticData() is incorrect')
|
|
)).toBe(true);
|
|
});
|
|
|
|
it('should warn about using external libraries in Python code', () => {
|
|
const nodeType = 'nodes-base.code';
|
|
const config = {
|
|
language: 'python',
|
|
pythonCode: `
|
|
import pandas as pd
|
|
import requests
|
|
|
|
df = pd.DataFrame(items)
|
|
response = requests.get('https://api.example.com')
|
|
return [{"json": {"data": response.json()}}]
|
|
`
|
|
};
|
|
const properties = [
|
|
{ name: 'language', type: 'options' },
|
|
{ name: 'pythonCode', type: 'string' }
|
|
];
|
|
|
|
const result = ConfigValidator.validate(nodeType, config, properties);
|
|
|
|
expect(result.warnings.some(w =>
|
|
w.type === 'invalid_value' &&
|
|
w.message.includes('External libraries not available')
|
|
)).toBe(true);
|
|
});
|
|
|
|
it('should validate crypto module usage', () => {
|
|
const nodeType = 'nodes-base.code';
|
|
const config = {
|
|
language: 'javascript',
|
|
jsCode: `
|
|
const uuid = crypto.randomUUID();
|
|
return [{json: {id: uuid}}];
|
|
`
|
|
};
|
|
const properties = [
|
|
{ name: 'language', type: 'options' },
|
|
{ name: 'jsCode', type: 'string' }
|
|
];
|
|
|
|
const result = ConfigValidator.validate(nodeType, config, properties);
|
|
|
|
expect(result.warnings.some(w =>
|
|
w.type === 'invalid_value' &&
|
|
w.message.includes('Using crypto without require')
|
|
)).toBe(true);
|
|
});
|
|
|
|
it('should validate HTTP Request with authentication in API URLs', () => {
|
|
const nodeType = 'nodes-base.httpRequest';
|
|
const config = {
|
|
method: 'GET',
|
|
url: 'https://api.github.com/user/repos',
|
|
authentication: 'none'
|
|
};
|
|
const properties = [
|
|
{ name: 'method', type: 'options' },
|
|
{ name: 'url', type: 'string' },
|
|
{ name: 'authentication', type: 'options' }
|
|
];
|
|
|
|
const result = ConfigValidator.validate(nodeType, config, properties);
|
|
|
|
expect(result.warnings.some(w =>
|
|
w.type === 'security' &&
|
|
w.message.includes('API endpoints typically require authentication')
|
|
)).toBe(true);
|
|
});
|
|
|
|
it('should validate SQL SELECT * performance warning', () => {
|
|
const nodeType = 'nodes-base.postgres';
|
|
const config = {
|
|
query: 'SELECT * FROM large_table WHERE status = "active"'
|
|
};
|
|
const properties = [
|
|
{ name: 'query', type: 'string' }
|
|
];
|
|
|
|
const result = ConfigValidator.validate(nodeType, config, properties);
|
|
|
|
expect(result.suggestions.some(s =>
|
|
s.includes('Consider selecting specific columns')
|
|
)).toBe(true);
|
|
});
|
|
|
|
it('should handle empty code in Code node', () => {
|
|
const nodeType = 'nodes-base.code';
|
|
const config = {
|
|
language: 'javascript',
|
|
jsCode: ' \n \t \n ' // Just whitespace
|
|
};
|
|
const properties = [
|
|
{ name: 'language', type: 'options' },
|
|
{ name: 'jsCode', type: 'string' }
|
|
];
|
|
|
|
const result = ConfigValidator.validate(nodeType, config, properties);
|
|
|
|
expect(result.valid).toBe(false);
|
|
expect(result.errors.some(e =>
|
|
e.type === 'missing_required' &&
|
|
e.message.includes('Code cannot be empty')
|
|
)).toBe(true);
|
|
});
|
|
|
|
it('should validate complex return patterns in Code node', () => {
|
|
const nodeType = 'nodes-base.code';
|
|
const config = {
|
|
language: 'javascript',
|
|
jsCode: `
|
|
return ["string1", "string2", "string3"];
|
|
`
|
|
};
|
|
const properties = [
|
|
{ name: 'language', type: 'options' },
|
|
{ name: 'jsCode', type: 'string' }
|
|
];
|
|
|
|
const result = ConfigValidator.validate(nodeType, config, properties);
|
|
|
|
expect(result.warnings.some(w =>
|
|
w.type === 'invalid_value' &&
|
|
w.message.includes('Items must be objects with json property')
|
|
)).toBe(true);
|
|
});
|
|
|
|
it('should validate console.log usage', () => {
|
|
const nodeType = 'nodes-base.code';
|
|
const config = {
|
|
language: 'javascript',
|
|
jsCode: `
|
|
console.log('Debug info:', items);
|
|
return items;
|
|
`
|
|
};
|
|
const properties = [
|
|
{ name: 'language', type: 'options' },
|
|
{ name: 'jsCode', type: 'string' }
|
|
];
|
|
|
|
const result = ConfigValidator.validate(nodeType, config, properties);
|
|
|
|
expect(result.warnings.some(w =>
|
|
w.type === 'best_practice' &&
|
|
w.message.includes('console.log output appears in n8n execution logs')
|
|
)).toBe(true);
|
|
});
|
|
|
|
it('should validate $json usage warning', () => {
|
|
const nodeType = 'nodes-base.code';
|
|
const config = {
|
|
language: 'javascript',
|
|
jsCode: `
|
|
const data = $json.myField;
|
|
return [{json: {processed: data}}];
|
|
`
|
|
};
|
|
const properties = [
|
|
{ name: 'language', type: 'options' },
|
|
{ name: 'jsCode', type: 'string' }
|
|
];
|
|
|
|
const result = ConfigValidator.validate(nodeType, config, properties);
|
|
|
|
expect(result.warnings.some(w =>
|
|
w.type === 'best_practice' &&
|
|
w.message.includes('$json only works in "Run Once for Each Item" mode')
|
|
)).toBe(true);
|
|
});
|
|
|
|
it('should not warn about properties for Code nodes', () => {
|
|
const nodeType = 'nodes-base.code';
|
|
const config = {
|
|
language: 'javascript',
|
|
jsCode: 'return items;',
|
|
unusedProperty: 'this should not generate a warning for Code nodes'
|
|
};
|
|
const properties = [
|
|
{ name: 'language', type: 'options' },
|
|
{ name: 'jsCode', type: 'string' }
|
|
];
|
|
|
|
const result = ConfigValidator.validate(nodeType, config, properties);
|
|
|
|
// Code nodes should skip the common issues check that warns about unused properties
|
|
expect(result.warnings.some(w =>
|
|
w.type === 'inefficient' &&
|
|
w.property === 'unusedProperty'
|
|
)).toBe(false);
|
|
});
|
|
|
|
it('should handle internal properties that start with underscore', () => {
|
|
const nodeType = 'nodes-base.test';
|
|
const config = {
|
|
'@version': 1,
|
|
'_internalField': 'value',
|
|
normalField: 'value'
|
|
};
|
|
const properties = [
|
|
{ name: 'normalField', type: 'string' }
|
|
];
|
|
|
|
const result = ConfigValidator.validate(nodeType, config, properties);
|
|
|
|
// Should not warn about @version or _internalField
|
|
expect(result.warnings.some(w =>
|
|
w.property === '@version' || w.property === '_internalField'
|
|
)).toBe(false);
|
|
});
|
|
|
|
it('should validate Python code with print statements', () => {
|
|
const nodeType = 'nodes-base.code';
|
|
const config = {
|
|
language: 'python',
|
|
pythonCode: `
|
|
print("Processing items:", len(items))
|
|
processed = []
|
|
for item in items:
|
|
print(f"Processing: {item}")
|
|
processed.append({"json": item["json"]})
|
|
return processed
|
|
`
|
|
};
|
|
const properties = [
|
|
{ name: 'language', type: 'options' },
|
|
{ name: 'pythonCode', type: 'string' }
|
|
];
|
|
|
|
const result = ConfigValidator.validate(nodeType, config, properties);
|
|
|
|
expect(result.warnings.some(w =>
|
|
w.type === 'best_practice' &&
|
|
w.message.includes('print() output appears in n8n execution logs')
|
|
)).toBe(true);
|
|
});
|
|
|
|
it('should suggest error handling for non-trivial code', () => {
|
|
const nodeType = 'nodes-base.code';
|
|
const config = {
|
|
language: 'javascript',
|
|
jsCode: 'return items;' // Trivial code, should not suggest error handling
|
|
};
|
|
const properties = [
|
|
{ name: 'language', type: 'options' },
|
|
{ name: 'jsCode', type: 'string' }
|
|
];
|
|
|
|
const result = ConfigValidator.validate(nodeType, config, properties);
|
|
|
|
// Should not suggest error handling for trivial code
|
|
expect(result.warnings.some(w =>
|
|
w.type === 'best_practice' &&
|
|
w.message.includes('No error handling found')
|
|
)).toBe(false);
|
|
});
|
|
|
|
it('should validate async operations without await', () => {
|
|
const nodeType = 'nodes-base.code';
|
|
const config = {
|
|
language: 'javascript',
|
|
jsCode: `
|
|
const promises = items.map(async item => {
|
|
const result = processItem(item);
|
|
return {json: result};
|
|
});
|
|
return Promise.all(promises);
|
|
`
|
|
};
|
|
const properties = [
|
|
{ name: 'language', type: 'options' },
|
|
{ name: 'jsCode', type: 'string' }
|
|
];
|
|
|
|
const result = ConfigValidator.validate(nodeType, config, properties);
|
|
|
|
expect(result.warnings.some(w =>
|
|
w.type === 'best_practice' &&
|
|
w.message.includes('Using async operations without await')
|
|
)).toBe(true);
|
|
});
|
|
});
|
|
}); |