mirror of
https://github.com/czlonkowski/n8n-mcp.git
synced 2026-03-28 05:03:07 +00:00
Remove duplicate, low-value, and fragmented test files while preserving all meaningful coverage. Enable parallel test execution and remove the entire benchmark infrastructure. Key changes: - Consolidate workflow-validator tests (13 files -> 3) - Consolidate config-validator tests (9 files -> 3) - Consolidate telemetry tests (11 files -> 6) - Merge AI validator tests (2 files -> 1) - Remove example/demo test files, mock-testing files, and already-skipped tests - Remove benchmark infrastructure (10 files, CI workflow, 4 npm scripts) - Enable parallel test execution (remove singleThread: true) - Remove retry:2 that was masking flaky tests - Slim CI publish-results job Results: 224 -> 191 test files, 4690 -> 4303 tests, 121K -> 106K lines Local runtime: 319s -> 27s (11.9x speedup) Conceived by Romuald Członkowski - www.aiadvisors.pl/en Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1682 lines
70 KiB
TypeScript
1682 lines
70 KiB
TypeScript
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
import { EnhancedConfigValidator, ValidationMode, ValidationProfile } from '@/services/enhanced-config-validator';
|
|
import { ValidationError } from '@/services/config-validator';
|
|
import { NodeSpecificValidators } from '@/services/node-specific-validators';
|
|
import { ResourceSimilarityService } from '@/services/resource-similarity-service';
|
|
import { OperationSimilarityService } from '@/services/operation-similarity-service';
|
|
import { NodeRepository } from '@/database/node-repository';
|
|
import { nodeFactory } from '@tests/fixtures/factories/node.factory';
|
|
import { createTestDatabase } from '@tests/utils/database-utils';
|
|
|
|
// Mock similarity services
|
|
vi.mock('@/services/resource-similarity-service');
|
|
vi.mock('@/services/operation-similarity-service');
|
|
|
|
// Mock node-specific validators
|
|
vi.mock('@/services/node-specific-validators', () => ({
|
|
NodeSpecificValidators: {
|
|
validateSlack: vi.fn(),
|
|
validateGoogleSheets: vi.fn(),
|
|
validateCode: vi.fn(),
|
|
validateOpenAI: vi.fn(),
|
|
validateMongoDB: vi.fn(),
|
|
validateWebhook: vi.fn(),
|
|
validatePostgres: vi.fn(),
|
|
validateMySQL: vi.fn(),
|
|
validateAIAgent: vi.fn(),
|
|
validateSet: vi.fn()
|
|
}
|
|
}));
|
|
|
|
describe('EnhancedConfigValidator', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
describe('validateWithMode', () => {
|
|
it('should validate config with operation awareness', () => {
|
|
const nodeType = 'nodes-base.slack';
|
|
const config = {
|
|
resource: 'message',
|
|
operation: 'send',
|
|
channel: '#general',
|
|
text: 'Hello World'
|
|
};
|
|
const properties = [
|
|
{ name: 'resource', type: 'options', required: true },
|
|
{ name: 'operation', type: 'options', required: true },
|
|
{ name: 'channel', type: 'string', required: true },
|
|
{ name: 'text', type: 'string', required: true }
|
|
];
|
|
|
|
const result = EnhancedConfigValidator.validateWithMode(
|
|
nodeType,
|
|
config,
|
|
properties,
|
|
'operation',
|
|
'ai-friendly'
|
|
);
|
|
|
|
expect(result).toMatchObject({
|
|
valid: true,
|
|
mode: 'operation',
|
|
profile: 'ai-friendly',
|
|
operation: {
|
|
resource: 'message',
|
|
operation: 'send'
|
|
}
|
|
});
|
|
});
|
|
|
|
it('should extract operation context from config', () => {
|
|
const config = {
|
|
resource: 'channel',
|
|
operation: 'create',
|
|
action: 'archive'
|
|
};
|
|
|
|
const context = EnhancedConfigValidator['extractOperationContext'](config);
|
|
|
|
expect(context).toEqual({
|
|
resource: 'channel',
|
|
operation: 'create',
|
|
action: 'archive'
|
|
});
|
|
});
|
|
|
|
it('should filter properties based on operation context', () => {
|
|
const properties = [
|
|
{
|
|
name: 'channel',
|
|
displayOptions: {
|
|
show: {
|
|
resource: ['message'],
|
|
operation: ['send']
|
|
}
|
|
}
|
|
},
|
|
{
|
|
name: 'user',
|
|
displayOptions: {
|
|
show: {
|
|
resource: ['user'],
|
|
operation: ['get']
|
|
}
|
|
}
|
|
}
|
|
];
|
|
|
|
// Mock isPropertyVisible to return true
|
|
vi.spyOn(EnhancedConfigValidator as any, 'isPropertyVisible').mockReturnValue(true);
|
|
|
|
const result = EnhancedConfigValidator['filterPropertiesByMode'](
|
|
properties,
|
|
{ resource: 'message', operation: 'send' },
|
|
'operation',
|
|
{ resource: 'message', operation: 'send' }
|
|
);
|
|
|
|
expect(result.properties).toHaveLength(1);
|
|
expect(result.properties[0].name).toBe('channel');
|
|
});
|
|
|
|
it('should handle minimal validation mode', () => {
|
|
const result = EnhancedConfigValidator.validateWithMode(
|
|
'nodes-base.httpRequest',
|
|
{ url: 'https://api.example.com' },
|
|
[{ name: 'url', required: true }],
|
|
'minimal'
|
|
);
|
|
|
|
expect(result.mode).toBe('minimal');
|
|
expect(result.errors).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
describe('validation profiles', () => {
|
|
it('should apply strict profile with all checks', () => {
|
|
const config = {};
|
|
const properties = [
|
|
{ name: 'required', required: true },
|
|
{ name: 'optional', required: false }
|
|
];
|
|
|
|
const result = EnhancedConfigValidator.validateWithMode(
|
|
'nodes-base.webhook',
|
|
config,
|
|
properties,
|
|
'full',
|
|
'strict'
|
|
);
|
|
|
|
expect(result.profile).toBe('strict');
|
|
expect(result.errors.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('should apply runtime profile focusing on critical errors', () => {
|
|
const result = EnhancedConfigValidator.validateWithMode(
|
|
'nodes-base.function',
|
|
{ functionCode: 'return items;' },
|
|
[],
|
|
'operation',
|
|
'runtime'
|
|
);
|
|
|
|
expect(result.profile).toBe('runtime');
|
|
expect(result.valid).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('enhanced validation features', () => {
|
|
it('should provide examples for common errors', () => {
|
|
const config = { resource: 'message' };
|
|
const properties = [
|
|
{ name: 'resource', required: true },
|
|
{ name: 'operation', required: true }
|
|
];
|
|
|
|
const result = EnhancedConfigValidator.validateWithMode(
|
|
'nodes-base.slack',
|
|
config,
|
|
properties
|
|
);
|
|
|
|
// Examples are not implemented in the current code, just ensure the field exists
|
|
expect(result.examples).toBeDefined();
|
|
expect(Array.isArray(result.examples)).toBe(true);
|
|
});
|
|
|
|
it('should suggest next steps for incomplete configurations', () => {
|
|
const config = { url: 'https://api.example.com' };
|
|
|
|
const result = EnhancedConfigValidator.validateWithMode(
|
|
'nodes-base.httpRequest',
|
|
config,
|
|
[]
|
|
);
|
|
|
|
expect(result.nextSteps).toBeDefined();
|
|
expect(result.nextSteps?.length).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
|
|
describe('deduplicateErrors', () => {
|
|
it('should remove duplicate errors for the same property and type', () => {
|
|
const errors = [
|
|
{ type: 'missing_required', property: 'channel', message: 'Short message' },
|
|
{ type: 'missing_required', property: 'channel', message: 'Much longer and more detailed message with specific fix' },
|
|
{ type: 'invalid_type', property: 'channel', message: 'Different type error' }
|
|
];
|
|
|
|
const deduplicated = EnhancedConfigValidator['deduplicateErrors'](errors as ValidationError[]);
|
|
|
|
expect(deduplicated).toHaveLength(2);
|
|
// Should keep the longer message
|
|
expect(deduplicated.find(e => e.type === 'missing_required')?.message).toContain('longer');
|
|
});
|
|
|
|
it('should prefer errors with fix information over those without', () => {
|
|
const errors = [
|
|
{ type: 'missing_required', property: 'url', message: 'URL is required' },
|
|
{ type: 'missing_required', property: 'url', message: 'URL is required', fix: 'Add a valid URL like https://api.example.com' }
|
|
];
|
|
|
|
const deduplicated = EnhancedConfigValidator['deduplicateErrors'](errors as ValidationError[]);
|
|
|
|
expect(deduplicated).toHaveLength(1);
|
|
expect(deduplicated[0].fix).toBeDefined();
|
|
});
|
|
|
|
it('should handle empty error arrays', () => {
|
|
const deduplicated = EnhancedConfigValidator['deduplicateErrors']([]);
|
|
expect(deduplicated).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
describe('applyProfileFilters - strict profile', () => {
|
|
it('should add suggestions for error-free configurations in strict mode', () => {
|
|
const result: any = {
|
|
errors: [],
|
|
warnings: [],
|
|
suggestions: [],
|
|
operation: { resource: 'httpRequest' }
|
|
};
|
|
|
|
EnhancedConfigValidator['applyProfileFilters'](result, 'strict');
|
|
|
|
expect(result.suggestions).toContain('Consider adding error handling with onError property and timeout configuration');
|
|
expect(result.suggestions).toContain('Add authentication if connecting to external services');
|
|
});
|
|
|
|
it('should enforce error handling for external service nodes in strict mode', () => {
|
|
const result: any = {
|
|
errors: [],
|
|
warnings: [],
|
|
suggestions: [],
|
|
operation: { resource: 'slack' }
|
|
};
|
|
|
|
EnhancedConfigValidator['applyProfileFilters'](result, 'strict');
|
|
|
|
// Should have warning about error handling
|
|
const errorHandlingWarning = result.warnings.find((w: any) => w.property === 'errorHandling');
|
|
expect(errorHandlingWarning).toBeDefined();
|
|
expect(errorHandlingWarning.message).toContain('External service nodes should have error handling');
|
|
});
|
|
|
|
it('should keep all errors, warnings, and suggestions in strict mode', () => {
|
|
const result: any = {
|
|
errors: [
|
|
{ type: 'missing_required', property: 'test' },
|
|
{ type: 'invalid_type', property: 'test2' }
|
|
],
|
|
warnings: [
|
|
{ type: 'security', property: 'auth' },
|
|
{ type: 'inefficient', property: 'query' }
|
|
],
|
|
suggestions: ['existing suggestion'],
|
|
operation: { resource: 'message' }
|
|
};
|
|
|
|
EnhancedConfigValidator['applyProfileFilters'](result, 'strict');
|
|
|
|
expect(result.errors).toHaveLength(2);
|
|
// The 'message' resource is not in the errorProneTypes list, so no error handling warning
|
|
expect(result.warnings).toHaveLength(2); // Just the original warnings
|
|
// When there are errors, no additional suggestions are added
|
|
expect(result.suggestions).toHaveLength(1); // Just the existing suggestion
|
|
});
|
|
});
|
|
|
|
describe('enforceErrorHandlingForProfile', () => {
|
|
it('should add error handling warning for external service nodes', () => {
|
|
// Test the actual behavior of the implementation
|
|
// The errorProneTypes array has mixed case 'httpRequest' but nodeType is lowercased before checking
|
|
// This appears to be a bug in the implementation - it should use all lowercase in errorProneTypes
|
|
|
|
// Test with node types that will actually match
|
|
const workingCases = [
|
|
'SlackNode', // 'slacknode'.includes('slack') = true
|
|
'WebhookTrigger', // 'webhooktrigger'.includes('webhook') = true
|
|
'DatabaseQuery', // 'databasequery'.includes('database') = true
|
|
'APICall', // 'apicall'.includes('api') = true
|
|
'EmailSender', // 'emailsender'.includes('email') = true
|
|
'OpenAIChat' // 'openaichat'.includes('openai') = true
|
|
];
|
|
|
|
workingCases.forEach(resource => {
|
|
const result: any = {
|
|
errors: [],
|
|
warnings: [],
|
|
suggestions: [],
|
|
operation: { resource }
|
|
};
|
|
|
|
EnhancedConfigValidator['enforceErrorHandlingForProfile'](result, 'strict');
|
|
|
|
const warning = result.warnings.find((w: any) => w.property === 'errorHandling');
|
|
expect(warning).toBeDefined();
|
|
expect(warning.type).toBe('best_practice');
|
|
expect(warning.message).toContain('External service nodes should have error handling');
|
|
});
|
|
});
|
|
|
|
it('should not add warning for non-error-prone nodes', () => {
|
|
const result: any = {
|
|
errors: [],
|
|
warnings: [],
|
|
suggestions: [],
|
|
operation: { resource: 'setVariable' }
|
|
};
|
|
|
|
EnhancedConfigValidator['enforceErrorHandlingForProfile'](result, 'strict');
|
|
|
|
expect(result.warnings).toHaveLength(0);
|
|
});
|
|
|
|
it('should not match httpRequest due to case sensitivity bug', () => {
|
|
// This test documents the current behavior - 'httpRequest' in errorProneTypes doesn't match
|
|
// because nodeType is lowercased to 'httprequest' which doesn't include 'httpRequest'
|
|
const result: any = {
|
|
errors: [],
|
|
warnings: [],
|
|
suggestions: [],
|
|
operation: { resource: 'HTTPRequest' }
|
|
};
|
|
|
|
EnhancedConfigValidator['enforceErrorHandlingForProfile'](result, 'strict');
|
|
|
|
// Due to the bug, this won't match
|
|
const warning = result.warnings.find((w: any) => w.property === 'errorHandling');
|
|
expect(warning).toBeUndefined();
|
|
});
|
|
|
|
it('should only enforce for strict profile', () => {
|
|
const result: any = {
|
|
errors: [],
|
|
warnings: [],
|
|
suggestions: [],
|
|
operation: { resource: 'httpRequest' }
|
|
};
|
|
|
|
EnhancedConfigValidator['enforceErrorHandlingForProfile'](result, 'runtime');
|
|
|
|
expect(result.warnings).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
describe('addErrorHandlingSuggestions', () => {
|
|
it('should add network error handling suggestions when URL errors exist', () => {
|
|
const result: any = {
|
|
errors: [
|
|
{ type: 'missing_required', property: 'url', message: 'URL is required' }
|
|
],
|
|
warnings: [],
|
|
suggestions: [],
|
|
operation: {}
|
|
};
|
|
|
|
EnhancedConfigValidator['addErrorHandlingSuggestions'](result);
|
|
|
|
const suggestion = result.suggestions.find((s: string) => s.includes('onError: "continueRegularOutput"'));
|
|
expect(suggestion).toBeDefined();
|
|
expect(suggestion).toContain('retryOnFail: true');
|
|
});
|
|
|
|
it('should add webhook-specific suggestions', () => {
|
|
const result: any = {
|
|
errors: [],
|
|
warnings: [],
|
|
suggestions: [],
|
|
operation: { resource: 'webhook' }
|
|
};
|
|
|
|
EnhancedConfigValidator['addErrorHandlingSuggestions'](result);
|
|
|
|
const suggestion = result.suggestions.find((s: string) => s.includes('Webhooks should use'));
|
|
expect(suggestion).toBeDefined();
|
|
expect(suggestion).toContain('continueRegularOutput');
|
|
});
|
|
|
|
it('should detect webhook from error messages', () => {
|
|
const result: any = {
|
|
errors: [
|
|
{ type: 'missing_required', property: 'path', message: 'Webhook path is required' }
|
|
],
|
|
warnings: [],
|
|
suggestions: [],
|
|
operation: {}
|
|
};
|
|
|
|
EnhancedConfigValidator['addErrorHandlingSuggestions'](result);
|
|
|
|
const suggestion = result.suggestions.find((s: string) => s.includes('Webhooks should use'));
|
|
expect(suggestion).toBeDefined();
|
|
});
|
|
|
|
it('should not add duplicate suggestions', () => {
|
|
const result: any = {
|
|
errors: [
|
|
{ type: 'missing_required', property: 'url', message: 'URL is required' },
|
|
{ type: 'invalid_value', property: 'endpoint', message: 'Invalid API endpoint' }
|
|
],
|
|
warnings: [],
|
|
suggestions: [],
|
|
operation: {}
|
|
};
|
|
|
|
EnhancedConfigValidator['addErrorHandlingSuggestions'](result);
|
|
|
|
// Should only add one network error suggestion
|
|
const networkSuggestions = result.suggestions.filter((s: string) =>
|
|
s.includes('For API calls')
|
|
);
|
|
expect(networkSuggestions).toHaveLength(1);
|
|
});
|
|
});
|
|
|
|
describe('filterPropertiesByOperation - real implementation', () => {
|
|
it('should filter properties based on operation context matching', () => {
|
|
const properties = [
|
|
{
|
|
name: 'messageChannel',
|
|
displayOptions: {
|
|
show: {
|
|
resource: ['message'],
|
|
operation: ['send']
|
|
}
|
|
}
|
|
},
|
|
{
|
|
name: 'userEmail',
|
|
displayOptions: {
|
|
show: {
|
|
resource: ['user'],
|
|
operation: ['get']
|
|
}
|
|
}
|
|
},
|
|
{
|
|
name: 'sharedProperty',
|
|
displayOptions: {
|
|
show: {
|
|
resource: ['message', 'user']
|
|
}
|
|
}
|
|
}
|
|
];
|
|
|
|
// Remove the mock to test real implementation
|
|
vi.restoreAllMocks();
|
|
|
|
const result = EnhancedConfigValidator['filterPropertiesByMode'](
|
|
properties,
|
|
{ resource: 'message', operation: 'send' },
|
|
'operation',
|
|
{ resource: 'message', operation: 'send' }
|
|
);
|
|
|
|
// Should include messageChannel and sharedProperty, but not userEmail
|
|
expect(result.properties).toHaveLength(2);
|
|
expect(result.properties.map(p => p.name)).toContain('messageChannel');
|
|
expect(result.properties.map(p => p.name)).toContain('sharedProperty');
|
|
});
|
|
|
|
it('should handle properties without displayOptions in operation mode', () => {
|
|
const properties = [
|
|
{ name: 'alwaysVisible', required: true },
|
|
{
|
|
name: 'conditionalProperty',
|
|
displayOptions: {
|
|
show: {
|
|
resource: ['message']
|
|
}
|
|
}
|
|
}
|
|
];
|
|
|
|
vi.restoreAllMocks();
|
|
|
|
const result = EnhancedConfigValidator['filterPropertiesByMode'](
|
|
properties,
|
|
{ resource: 'user' },
|
|
'operation',
|
|
{ resource: 'user' }
|
|
);
|
|
|
|
// Should include property without displayOptions
|
|
expect(result.properties.map(p => p.name)).toContain('alwaysVisible');
|
|
// Should not include conditionalProperty (wrong resource)
|
|
expect(result.properties.map(p => p.name)).not.toContain('conditionalProperty');
|
|
});
|
|
});
|
|
|
|
describe('isPropertyRelevantToOperation', () => {
|
|
it('should handle action field in operation context', () => {
|
|
const prop = {
|
|
name: 'archiveChannel',
|
|
displayOptions: {
|
|
show: {
|
|
resource: ['channel'],
|
|
action: ['archive']
|
|
}
|
|
}
|
|
};
|
|
|
|
const config = { resource: 'channel', action: 'archive' };
|
|
const operation = { resource: 'channel', action: 'archive' };
|
|
|
|
const isRelevant = EnhancedConfigValidator['isPropertyRelevantToOperation'](
|
|
prop,
|
|
config,
|
|
operation
|
|
);
|
|
|
|
expect(isRelevant).toBe(true);
|
|
});
|
|
|
|
it('should return false when action does not match', () => {
|
|
const prop = {
|
|
name: 'deleteChannel',
|
|
displayOptions: {
|
|
show: {
|
|
resource: ['channel'],
|
|
action: ['delete']
|
|
}
|
|
}
|
|
};
|
|
|
|
const config = { resource: 'channel', action: 'archive' };
|
|
const operation = { resource: 'channel', action: 'archive' };
|
|
|
|
const isRelevant = EnhancedConfigValidator['isPropertyRelevantToOperation'](
|
|
prop,
|
|
config,
|
|
operation
|
|
);
|
|
|
|
expect(isRelevant).toBe(false);
|
|
});
|
|
|
|
it('should handle arrays in displayOptions', () => {
|
|
const prop = {
|
|
name: 'multiOperation',
|
|
displayOptions: {
|
|
show: {
|
|
operation: ['create', 'update', 'upsert']
|
|
}
|
|
}
|
|
};
|
|
|
|
const config = { operation: 'update' };
|
|
const operation = { operation: 'update' };
|
|
|
|
const isRelevant = EnhancedConfigValidator['isPropertyRelevantToOperation'](
|
|
prop,
|
|
config,
|
|
operation
|
|
);
|
|
|
|
expect(isRelevant).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('operation-specific enhancements', () => {
|
|
it('should enhance MongoDB validation', () => {
|
|
const mockValidateMongoDB = vi.mocked(NodeSpecificValidators.validateMongoDB);
|
|
|
|
const config = { collection: 'users', operation: 'insert' };
|
|
const properties: any[] = [];
|
|
|
|
const result = EnhancedConfigValidator.validateWithMode(
|
|
'nodes-base.mongoDb',
|
|
config,
|
|
properties,
|
|
'operation'
|
|
);
|
|
|
|
expect(mockValidateMongoDB).toHaveBeenCalled();
|
|
const context = mockValidateMongoDB.mock.calls[0][0];
|
|
expect(context.config).toEqual(config);
|
|
});
|
|
|
|
it('should enhance MySQL validation', () => {
|
|
const mockValidateMySQL = vi.mocked(NodeSpecificValidators.validateMySQL);
|
|
|
|
const config = { table: 'users', operation: 'insert' };
|
|
const properties: any[] = [];
|
|
|
|
const result = EnhancedConfigValidator.validateWithMode(
|
|
'nodes-base.mysql',
|
|
config,
|
|
properties,
|
|
'operation'
|
|
);
|
|
|
|
expect(mockValidateMySQL).toHaveBeenCalled();
|
|
});
|
|
|
|
it('should enhance Postgres validation', () => {
|
|
const mockValidatePostgres = vi.mocked(NodeSpecificValidators.validatePostgres);
|
|
|
|
const config = { table: 'users', operation: 'select' };
|
|
const properties: any[] = [];
|
|
|
|
const result = EnhancedConfigValidator.validateWithMode(
|
|
'nodes-base.postgres',
|
|
config,
|
|
properties,
|
|
'operation'
|
|
);
|
|
|
|
expect(mockValidatePostgres).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('generateNextSteps', () => {
|
|
it('should generate steps for different error types', () => {
|
|
const result: any = {
|
|
errors: [
|
|
{ type: 'missing_required', property: 'url' },
|
|
{ type: 'missing_required', property: 'method' },
|
|
{ type: 'invalid_type', property: 'headers', fix: 'object' },
|
|
{ type: 'invalid_value', property: 'timeout' }
|
|
],
|
|
warnings: [],
|
|
suggestions: []
|
|
};
|
|
|
|
const steps = EnhancedConfigValidator['generateNextSteps'](result);
|
|
|
|
expect(steps).toContain('Add required fields: url, method');
|
|
expect(steps).toContain('Fix type mismatches: headers should be object');
|
|
expect(steps).toContain('Correct invalid values: timeout');
|
|
expect(steps).toContain('Fix the errors above following the provided suggestions');
|
|
});
|
|
|
|
it('should suggest addressing warnings when no errors exist', () => {
|
|
const result: any = {
|
|
errors: [],
|
|
warnings: [{ type: 'security', property: 'auth' }],
|
|
suggestions: []
|
|
};
|
|
|
|
const steps = EnhancedConfigValidator['generateNextSteps'](result);
|
|
|
|
expect(steps).toContain('Consider addressing warnings for better reliability');
|
|
});
|
|
});
|
|
|
|
describe('minimal validation mode edge cases', () => {
|
|
it('should only validate visible required properties in minimal mode', () => {
|
|
const properties = [
|
|
{ name: 'visible', required: true },
|
|
{ name: 'hidden', required: true, displayOptions: { hide: { always: [true] } } },
|
|
{ name: 'optional', required: false }
|
|
];
|
|
|
|
// Mock isPropertyVisible to return false for hidden property
|
|
const isVisibleSpy = vi.spyOn(EnhancedConfigValidator as any, 'isPropertyVisible');
|
|
isVisibleSpy.mockImplementation((prop: any) => prop.name !== 'hidden');
|
|
|
|
const result = EnhancedConfigValidator.validateWithMode(
|
|
'nodes-base.test',
|
|
{},
|
|
properties,
|
|
'minimal'
|
|
);
|
|
|
|
// Should only validate the visible required property
|
|
expect(result.errors).toHaveLength(1);
|
|
expect(result.errors[0].property).toBe('visible');
|
|
|
|
isVisibleSpy.mockRestore();
|
|
});
|
|
});
|
|
|
|
describe('complex operation contexts', () => {
|
|
it('should handle all operation context fields (resource, operation, action, mode)', () => {
|
|
const config = {
|
|
resource: 'database',
|
|
operation: 'query',
|
|
action: 'execute',
|
|
mode: 'advanced'
|
|
};
|
|
|
|
const result = EnhancedConfigValidator.validateWithMode(
|
|
'nodes-base.database',
|
|
config,
|
|
[],
|
|
'operation'
|
|
);
|
|
|
|
expect(result.operation).toEqual({
|
|
resource: 'database',
|
|
operation: 'query',
|
|
action: 'execute',
|
|
mode: 'advanced'
|
|
});
|
|
});
|
|
|
|
it('should validate Google Sheets append operation with range warning', () => {
|
|
const config = {
|
|
operation: 'append', // This is what gets checked in enhanceGoogleSheetsValidation
|
|
range: 'A1:B10' // Missing sheet name
|
|
};
|
|
|
|
const result = EnhancedConfigValidator.validateWithMode(
|
|
'nodes-base.googleSheets',
|
|
config,
|
|
[],
|
|
'operation'
|
|
);
|
|
|
|
// Check if the custom validation was applied
|
|
expect(vi.mocked(NodeSpecificValidators.validateGoogleSheets)).toHaveBeenCalled();
|
|
|
|
// If there's a range warning from the enhanced validation
|
|
const enhancedWarning = result.warnings.find(w =>
|
|
w.property === 'range' && w.message.includes('sheet name')
|
|
);
|
|
|
|
if (enhancedWarning) {
|
|
expect(enhancedWarning.type).toBe('inefficient');
|
|
expect(enhancedWarning.suggestion).toContain('SheetName!A1:B10');
|
|
} else {
|
|
// At least verify the validation was triggered
|
|
expect(result.warnings.length).toBeGreaterThanOrEqual(0);
|
|
}
|
|
});
|
|
|
|
it('should enhance Slack message send validation', () => {
|
|
const config = {
|
|
resource: 'message',
|
|
operation: 'send',
|
|
text: 'Hello'
|
|
// Missing channel
|
|
};
|
|
|
|
const properties = [
|
|
{ name: 'channel', required: true },
|
|
{ name: 'text', required: true }
|
|
];
|
|
|
|
const result = EnhancedConfigValidator.validateWithMode(
|
|
'nodes-base.slack',
|
|
config,
|
|
properties,
|
|
'operation'
|
|
);
|
|
|
|
const channelError = result.errors.find(e => e.property === 'channel');
|
|
expect(channelError?.message).toContain('To send a Slack message');
|
|
expect(channelError?.fix).toContain('#general');
|
|
});
|
|
});
|
|
|
|
describe('profile-specific edge cases', () => {
|
|
it('should filter internal warnings in ai-friendly profile', () => {
|
|
const result: any = {
|
|
errors: [],
|
|
warnings: [
|
|
{ type: 'inefficient', property: '_internal' },
|
|
{ type: 'inefficient', property: 'publicProperty' },
|
|
{ type: 'security', property: 'auth' }
|
|
],
|
|
suggestions: [],
|
|
operation: {}
|
|
};
|
|
|
|
EnhancedConfigValidator['applyProfileFilters'](result, 'ai-friendly');
|
|
|
|
// Should filter out _internal but keep others
|
|
expect(result.warnings).toHaveLength(2);
|
|
expect(result.warnings.find((w: any) => w.property === '_internal')).toBeUndefined();
|
|
});
|
|
|
|
it('should handle undefined message in runtime profile filtering', () => {
|
|
const result: any = {
|
|
errors: [
|
|
{ type: 'invalid_type', property: 'test', message: 'Value is undefined' },
|
|
{ type: 'invalid_type', property: 'test2', message: '' } // Empty message
|
|
],
|
|
warnings: [],
|
|
suggestions: [],
|
|
operation: {}
|
|
};
|
|
|
|
EnhancedConfigValidator['applyProfileFilters'](result, 'runtime');
|
|
|
|
// Should keep the one with undefined in message
|
|
expect(result.errors).toHaveLength(1);
|
|
expect(result.errors[0].property).toBe('test');
|
|
});
|
|
});
|
|
|
|
describe('enhanceHttpRequestValidation', () => {
|
|
it('should suggest alwaysOutputData for HTTP Request nodes', () => {
|
|
const nodeType = 'nodes-base.httpRequest';
|
|
const config = {
|
|
url: 'https://api.example.com/data',
|
|
method: 'GET'
|
|
};
|
|
const properties = [
|
|
{ name: 'url', type: 'string', required: true },
|
|
{ name: 'method', type: 'options', required: false }
|
|
];
|
|
|
|
const result = EnhancedConfigValidator.validateWithMode(
|
|
nodeType,
|
|
config,
|
|
properties,
|
|
'operation',
|
|
'ai-friendly'
|
|
);
|
|
|
|
expect(result.valid).toBe(true);
|
|
expect(result.suggestions).toContainEqual(
|
|
expect.stringContaining('alwaysOutputData: true at node level')
|
|
);
|
|
expect(result.suggestions).toContainEqual(
|
|
expect.stringContaining('ensures the node produces output even when HTTP requests fail')
|
|
);
|
|
});
|
|
|
|
it('should suggest responseFormat for API endpoint URLs', () => {
|
|
const nodeType = 'nodes-base.httpRequest';
|
|
const config = {
|
|
url: 'https://api.example.com/data',
|
|
method: 'GET',
|
|
options: {} // Empty options, no responseFormat
|
|
};
|
|
const properties = [
|
|
{ name: 'url', type: 'string', required: true },
|
|
{ name: 'method', type: 'options', required: false },
|
|
{ name: 'options', type: 'collection', required: false }
|
|
];
|
|
|
|
const result = EnhancedConfigValidator.validateWithMode(
|
|
nodeType,
|
|
config,
|
|
properties,
|
|
'operation',
|
|
'ai-friendly'
|
|
);
|
|
|
|
expect(result.valid).toBe(true);
|
|
expect(result.suggestions).toContainEqual(
|
|
expect.stringContaining('responseFormat')
|
|
);
|
|
expect(result.suggestions).toContainEqual(
|
|
expect.stringContaining('options.response.response.responseFormat')
|
|
);
|
|
});
|
|
|
|
it('should suggest responseFormat for Supabase URLs', () => {
|
|
const nodeType = 'nodes-base.httpRequest';
|
|
const config = {
|
|
url: 'https://xxciwnthnnywanbplqwg.supabase.co/rest/v1/messages',
|
|
method: 'GET',
|
|
options: {}
|
|
};
|
|
const properties = [
|
|
{ name: 'url', type: 'string', required: true }
|
|
];
|
|
|
|
const result = EnhancedConfigValidator.validateWithMode(
|
|
nodeType,
|
|
config,
|
|
properties,
|
|
'operation',
|
|
'ai-friendly'
|
|
);
|
|
|
|
expect(result.suggestions).toContainEqual(
|
|
expect.stringContaining('responseFormat')
|
|
);
|
|
});
|
|
|
|
it('should NOT suggest responseFormat when already configured', () => {
|
|
const nodeType = 'nodes-base.httpRequest';
|
|
const config = {
|
|
url: 'https://api.example.com/data',
|
|
method: 'GET',
|
|
options: {
|
|
response: {
|
|
response: {
|
|
responseFormat: 'json'
|
|
}
|
|
}
|
|
}
|
|
};
|
|
const properties = [
|
|
{ name: 'url', type: 'string', required: true },
|
|
{ name: 'options', type: 'collection', required: false }
|
|
];
|
|
|
|
const result = EnhancedConfigValidator.validateWithMode(
|
|
nodeType,
|
|
config,
|
|
properties,
|
|
'operation',
|
|
'ai-friendly'
|
|
);
|
|
|
|
const responseFormatSuggestion = result.suggestions.find(
|
|
(s: string) => s.includes('responseFormat')
|
|
);
|
|
expect(responseFormatSuggestion).toBeUndefined();
|
|
});
|
|
|
|
it('should warn about missing protocol in expression-based URLs', () => {
|
|
const nodeType = 'nodes-base.httpRequest';
|
|
const config = {
|
|
url: '=www.{{ $json.domain }}.com',
|
|
method: 'GET'
|
|
};
|
|
const properties = [
|
|
{ name: 'url', type: 'string', required: true }
|
|
];
|
|
|
|
const result = EnhancedConfigValidator.validateWithMode(
|
|
nodeType,
|
|
config,
|
|
properties,
|
|
'operation',
|
|
'ai-friendly'
|
|
);
|
|
|
|
expect(result.warnings).toContainEqual(
|
|
expect.objectContaining({
|
|
type: 'invalid_value',
|
|
property: 'url',
|
|
message: expect.stringContaining('missing http:// or https://')
|
|
})
|
|
);
|
|
});
|
|
|
|
it('should warn about missing protocol in expressions with template markers', () => {
|
|
const nodeType = 'nodes-base.httpRequest';
|
|
const config = {
|
|
url: '={{ $json.domain }}/api/data',
|
|
method: 'GET'
|
|
};
|
|
const properties = [
|
|
{ name: 'url', type: 'string', required: true }
|
|
];
|
|
|
|
const result = EnhancedConfigValidator.validateWithMode(
|
|
nodeType,
|
|
config,
|
|
properties,
|
|
'operation',
|
|
'ai-friendly'
|
|
);
|
|
|
|
expect(result.warnings).toContainEqual(
|
|
expect.objectContaining({
|
|
type: 'invalid_value',
|
|
property: 'url',
|
|
message: expect.stringContaining('missing http:// or https://')
|
|
})
|
|
);
|
|
});
|
|
|
|
it('should NOT warn when expression includes http protocol', () => {
|
|
const nodeType = 'nodes-base.httpRequest';
|
|
const config = {
|
|
url: '={{ "https://" + $json.domain + ".com" }}',
|
|
method: 'GET'
|
|
};
|
|
const properties = [
|
|
{ name: 'url', type: 'string', required: true }
|
|
];
|
|
|
|
const result = EnhancedConfigValidator.validateWithMode(
|
|
nodeType,
|
|
config,
|
|
properties,
|
|
'operation',
|
|
'ai-friendly'
|
|
);
|
|
|
|
const urlWarning = result.warnings.find(
|
|
(w: any) => w.property === 'url' && w.message.includes('protocol')
|
|
);
|
|
expect(urlWarning).toBeUndefined();
|
|
});
|
|
|
|
it('should NOT suggest responseFormat for non-API URLs', () => {
|
|
const nodeType = 'nodes-base.httpRequest';
|
|
const config = {
|
|
url: 'https://example.com/page.html',
|
|
method: 'GET',
|
|
options: {}
|
|
};
|
|
const properties = [
|
|
{ name: 'url', type: 'string', required: true }
|
|
];
|
|
|
|
const result = EnhancedConfigValidator.validateWithMode(
|
|
nodeType,
|
|
config,
|
|
properties,
|
|
'operation',
|
|
'ai-friendly'
|
|
);
|
|
|
|
const responseFormatSuggestion = result.suggestions.find(
|
|
(s: string) => s.includes('responseFormat')
|
|
);
|
|
expect(responseFormatSuggestion).toBeUndefined();
|
|
});
|
|
|
|
it('should detect missing protocol in expressions with uppercase HTTP', () => {
|
|
const nodeType = 'nodes-base.httpRequest';
|
|
const config = {
|
|
url: '={{ "HTTP://" + $json.domain + ".com" }}',
|
|
method: 'GET'
|
|
};
|
|
const properties = [
|
|
{ name: 'url', type: 'string', required: true }
|
|
];
|
|
|
|
const result = EnhancedConfigValidator.validateWithMode(
|
|
nodeType,
|
|
config,
|
|
properties,
|
|
'operation',
|
|
'ai-friendly'
|
|
);
|
|
|
|
// Should NOT warn because HTTP:// is present (case-insensitive)
|
|
expect(result.warnings).toHaveLength(0);
|
|
});
|
|
|
|
it('should NOT suggest responseFormat for false positive URLs', () => {
|
|
const nodeType = 'nodes-base.httpRequest';
|
|
const testUrls = [
|
|
'https://example.com/therapist-directory',
|
|
'https://restaurant-bookings.com/reserve',
|
|
'https://forest-management.org/data'
|
|
];
|
|
|
|
testUrls.forEach(url => {
|
|
const config = {
|
|
url,
|
|
method: 'GET',
|
|
options: {}
|
|
};
|
|
const properties = [
|
|
{ name: 'url', type: 'string', required: true }
|
|
];
|
|
|
|
const result = EnhancedConfigValidator.validateWithMode(
|
|
nodeType,
|
|
config,
|
|
properties,
|
|
'operation',
|
|
'ai-friendly'
|
|
);
|
|
|
|
const responseFormatSuggestion = result.suggestions.find(
|
|
(s: string) => s.includes('responseFormat')
|
|
);
|
|
expect(responseFormatSuggestion).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
it('should suggest responseFormat for case-insensitive API paths', () => {
|
|
const nodeType = 'nodes-base.httpRequest';
|
|
const testUrls = [
|
|
'https://example.com/API/users',
|
|
'https://example.com/Rest/data',
|
|
'https://example.com/REST/v1/items'
|
|
];
|
|
|
|
testUrls.forEach(url => {
|
|
const config = {
|
|
url,
|
|
method: 'GET',
|
|
options: {}
|
|
};
|
|
const properties = [
|
|
{ name: 'url', type: 'string', required: true }
|
|
];
|
|
|
|
const result = EnhancedConfigValidator.validateWithMode(
|
|
nodeType,
|
|
config,
|
|
properties,
|
|
'operation',
|
|
'ai-friendly'
|
|
);
|
|
|
|
expect(result.suggestions).toContainEqual(
|
|
expect.stringContaining('responseFormat')
|
|
);
|
|
});
|
|
});
|
|
|
|
it('should handle null and undefined URLs gracefully', () => {
|
|
const nodeType = 'nodes-base.httpRequest';
|
|
const testConfigs = [
|
|
{ url: null, method: 'GET' },
|
|
{ url: undefined, method: 'GET' },
|
|
{ url: '', method: 'GET' }
|
|
];
|
|
|
|
testConfigs.forEach(config => {
|
|
const properties = [
|
|
{ name: 'url', type: 'string', required: true }
|
|
];
|
|
|
|
expect(() => {
|
|
EnhancedConfigValidator.validateWithMode(
|
|
nodeType,
|
|
config,
|
|
properties,
|
|
'operation',
|
|
'ai-friendly'
|
|
);
|
|
}).not.toThrow();
|
|
});
|
|
});
|
|
|
|
describe('AI Agent node validation', () => {
|
|
it('should call validateAIAgent for AI Agent nodes', () => {
|
|
const nodeType = 'nodes-langchain.agent';
|
|
const config = {
|
|
promptType: 'define',
|
|
text: 'You are a helpful assistant'
|
|
};
|
|
const properties = [
|
|
{ name: 'promptType', type: 'options', required: true },
|
|
{ name: 'text', type: 'string', required: false }
|
|
];
|
|
|
|
EnhancedConfigValidator.validateWithMode(
|
|
nodeType,
|
|
config,
|
|
properties,
|
|
'operation',
|
|
'ai-friendly'
|
|
);
|
|
|
|
// Verify the validator was called (fix for issue where it wasn't being called at all)
|
|
expect(NodeSpecificValidators.validateAIAgent).toHaveBeenCalledTimes(1);
|
|
|
|
// Verify it was called with a context object containing our config
|
|
const callArgs = (NodeSpecificValidators.validateAIAgent as any).mock.calls[0][0];
|
|
expect(callArgs).toHaveProperty('config');
|
|
expect(callArgs.config).toEqual(config);
|
|
expect(callArgs).toHaveProperty('errors');
|
|
expect(callArgs).toHaveProperty('warnings');
|
|
expect(callArgs).toHaveProperty('suggestions');
|
|
expect(callArgs).toHaveProperty('autofix');
|
|
});
|
|
});
|
|
});
|
|
|
|
// ─── Type Structure Validation (from enhanced-config-validator-type-structures) ───
|
|
|
|
describe('type structure validation', () => {
|
|
describe('Filter Type Validation', () => {
|
|
it('should validate valid filter configuration', () => {
|
|
const config = {
|
|
conditions: {
|
|
combinator: 'and',
|
|
conditions: [{ id: '1', leftValue: '{{ $json.name }}', operator: { type: 'string', operation: 'equals' }, rightValue: 'John' }],
|
|
},
|
|
};
|
|
const properties = [{ name: 'conditions', type: 'filter', required: true, displayName: 'Conditions', default: {} }];
|
|
const result = EnhancedConfigValidator.validateWithMode('nodes-base.filter', config, properties, 'operation', 'ai-friendly');
|
|
expect(result.valid).toBe(true);
|
|
expect(result.errors).toHaveLength(0);
|
|
});
|
|
|
|
it('should validate filter with multiple conditions', () => {
|
|
const config = {
|
|
conditions: {
|
|
combinator: 'or',
|
|
conditions: [
|
|
{ id: '1', leftValue: '{{ $json.age }}', operator: { type: 'number', operation: 'gt' }, rightValue: 18 },
|
|
{ id: '2', leftValue: '{{ $json.country }}', operator: { type: 'string', operation: 'equals' }, rightValue: 'US' },
|
|
],
|
|
},
|
|
};
|
|
const properties = [{ name: 'conditions', type: 'filter', required: true }];
|
|
const result = EnhancedConfigValidator.validateWithMode('nodes-base.filter', config, properties, 'operation', 'ai-friendly');
|
|
expect(result.valid).toBe(true);
|
|
});
|
|
|
|
it('should detect missing combinator in filter', () => {
|
|
const config = {
|
|
conditions: {
|
|
conditions: [{ id: '1', operator: { type: 'string', operation: 'equals' }, leftValue: 'test', rightValue: 'value' }],
|
|
},
|
|
};
|
|
const properties = [{ name: 'conditions', type: 'filter', required: true }];
|
|
const result = EnhancedConfigValidator.validateWithMode('nodes-base.filter', config, properties, 'operation', 'ai-friendly');
|
|
expect(result.valid).toBe(false);
|
|
expect(result.errors).toContainEqual(expect.objectContaining({ property: expect.stringMatching(/conditions/), type: 'invalid_configuration' }));
|
|
});
|
|
|
|
it('should detect invalid combinator value', () => {
|
|
const config = {
|
|
conditions: {
|
|
combinator: 'invalid',
|
|
conditions: [{ id: '1', operator: { type: 'string', operation: 'equals' }, leftValue: 'test', rightValue: 'value' }],
|
|
},
|
|
};
|
|
const properties = [{ name: 'conditions', type: 'filter', required: true }];
|
|
const result = EnhancedConfigValidator.validateWithMode('nodes-base.filter', config, properties, 'operation', 'ai-friendly');
|
|
expect(result.valid).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('Filter Operation Validation', () => {
|
|
it('should validate string operations correctly', () => {
|
|
for (const operation of ['equals', 'notEquals', 'contains', 'notContains', 'startsWith', 'endsWith', 'regex']) {
|
|
const config = { conditions: { combinator: 'and', conditions: [{ id: '1', operator: { type: 'string', operation }, leftValue: 'test', rightValue: 'value' }] } };
|
|
const result = EnhancedConfigValidator.validateWithMode('nodes-base.filter', config, [{ name: 'conditions', type: 'filter', required: true }], 'operation', 'ai-friendly');
|
|
expect(result.valid).toBe(true);
|
|
}
|
|
});
|
|
|
|
it('should reject invalid operation for string type', () => {
|
|
const config = { conditions: { combinator: 'and', conditions: [{ id: '1', operator: { type: 'string', operation: 'gt' }, leftValue: 'test', rightValue: 'value' }] } };
|
|
const result = EnhancedConfigValidator.validateWithMode('nodes-base.filter', config, [{ name: 'conditions', type: 'filter', required: true }], 'operation', 'ai-friendly');
|
|
expect(result.valid).toBe(false);
|
|
expect(result.errors).toContainEqual(expect.objectContaining({ property: expect.stringContaining('operator.operation'), message: expect.stringContaining('not valid for type') }));
|
|
});
|
|
|
|
it('should validate number operations correctly', () => {
|
|
for (const operation of ['equals', 'notEquals', 'gt', 'lt', 'gte', 'lte']) {
|
|
const config = { conditions: { combinator: 'and', conditions: [{ id: '1', operator: { type: 'number', operation }, leftValue: 10, rightValue: 20 }] } };
|
|
const result = EnhancedConfigValidator.validateWithMode('nodes-base.filter', config, [{ name: 'conditions', type: 'filter', required: true }], 'operation', 'ai-friendly');
|
|
expect(result.valid).toBe(true);
|
|
}
|
|
});
|
|
|
|
it('should reject string operations for number type', () => {
|
|
const config = { conditions: { combinator: 'and', conditions: [{ id: '1', operator: { type: 'number', operation: 'contains' }, leftValue: 10, rightValue: 20 }] } };
|
|
const result = EnhancedConfigValidator.validateWithMode('nodes-base.filter', config, [{ name: 'conditions', type: 'filter', required: true }], 'operation', 'ai-friendly');
|
|
expect(result.valid).toBe(false);
|
|
});
|
|
|
|
it('should validate boolean operations', () => {
|
|
const config = { conditions: { combinator: 'and', conditions: [{ id: '1', operator: { type: 'boolean', operation: 'true' }, leftValue: '{{ $json.isActive }}' }] } };
|
|
const result = EnhancedConfigValidator.validateWithMode('nodes-base.filter', config, [{ name: 'conditions', type: 'filter', required: true }], 'operation', 'ai-friendly');
|
|
expect(result.valid).toBe(true);
|
|
});
|
|
|
|
it('should validate dateTime operations', () => {
|
|
const config = { conditions: { combinator: 'and', conditions: [{ id: '1', operator: { type: 'dateTime', operation: 'after' }, leftValue: '{{ $json.createdAt }}', rightValue: '2024-01-01' }] } };
|
|
const result = EnhancedConfigValidator.validateWithMode('nodes-base.filter', config, [{ name: 'conditions', type: 'filter', required: true }], 'operation', 'ai-friendly');
|
|
expect(result.valid).toBe(true);
|
|
});
|
|
|
|
it('should validate array operations', () => {
|
|
const config = { conditions: { combinator: 'and', conditions: [{ id: '1', operator: { type: 'array', operation: 'contains' }, leftValue: '{{ $json.tags }}', rightValue: 'urgent' }] } };
|
|
const result = EnhancedConfigValidator.validateWithMode('nodes-base.filter', config, [{ name: 'conditions', type: 'filter', required: true }], 'operation', 'ai-friendly');
|
|
expect(result.valid).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('ResourceMapper Type Validation', () => {
|
|
it('should validate valid resourceMapper configuration', () => {
|
|
const config = { mapping: { mappingMode: 'defineBelow', value: { name: '{{ $json.fullName }}', email: '{{ $json.emailAddress }}', status: 'active' } } };
|
|
const result = EnhancedConfigValidator.validateWithMode('nodes-base.httpRequest', config, [{ name: 'mapping', type: 'resourceMapper', required: true }], 'operation', 'ai-friendly');
|
|
expect(result.valid).toBe(true);
|
|
});
|
|
|
|
it('should validate autoMapInputData mode', () => {
|
|
const config = { mapping: { mappingMode: 'autoMapInputData', value: {} } };
|
|
const result = EnhancedConfigValidator.validateWithMode('nodes-base.httpRequest', config, [{ name: 'mapping', type: 'resourceMapper', required: true }], 'operation', 'ai-friendly');
|
|
expect(result.valid).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('AssignmentCollection Type Validation', () => {
|
|
it('should validate valid assignmentCollection configuration', () => {
|
|
const config = { assignments: { assignments: [{ id: '1', name: 'userName', value: '{{ $json.name }}', type: 'string' }, { id: '2', name: 'userAge', value: 30, type: 'number' }] } };
|
|
const result = EnhancedConfigValidator.validateWithMode('nodes-base.set', config, [{ name: 'assignments', type: 'assignmentCollection', required: true }], 'operation', 'ai-friendly');
|
|
expect(result.valid).toBe(true);
|
|
});
|
|
|
|
it('should detect missing assignments array', () => {
|
|
const config = { assignments: {} };
|
|
const result = EnhancedConfigValidator.validateWithMode('nodes-base.set', config, [{ name: 'assignments', type: 'assignmentCollection', required: true }], 'operation', 'ai-friendly');
|
|
expect(result.valid).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('ResourceLocator Type Validation', () => {
|
|
it.skip('should validate valid resourceLocator by ID', () => {
|
|
const config = { resource: { mode: 'id', value: 'abc123' } };
|
|
const result = EnhancedConfigValidator.validateWithMode('nodes-base.googleSheets', config, [{ name: 'resource', type: 'resourceLocator', required: true, displayName: 'Resource', default: { mode: 'list', value: '' } }], 'operation', 'ai-friendly');
|
|
expect(result.valid).toBe(true);
|
|
});
|
|
|
|
it.skip('should validate resourceLocator by URL', () => {
|
|
const config = { resource: { mode: 'url', value: 'https://example.com/resource/123' } };
|
|
const result = EnhancedConfigValidator.validateWithMode('nodes-base.googleSheets', config, [{ name: 'resource', type: 'resourceLocator', required: true, displayName: 'Resource', default: { mode: 'list', value: '' } }], 'operation', 'ai-friendly');
|
|
expect(result.valid).toBe(true);
|
|
});
|
|
|
|
it.skip('should validate resourceLocator by list', () => {
|
|
const config = { resource: { mode: 'list', value: 'item-from-dropdown' } };
|
|
const result = EnhancedConfigValidator.validateWithMode('nodes-base.googleSheets', config, [{ name: 'resource', type: 'resourceLocator', required: true, displayName: 'Resource', default: { mode: 'list', value: '' } }], 'operation', 'ai-friendly');
|
|
expect(result.valid).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('Type Structure Edge Cases', () => {
|
|
it('should handle null values gracefully', () => {
|
|
const result = EnhancedConfigValidator.validateWithMode('nodes-base.filter', { conditions: null }, [{ name: 'conditions', type: 'filter', required: false }], 'operation', 'ai-friendly');
|
|
expect(result.valid).toBe(true);
|
|
});
|
|
|
|
it('should handle undefined values gracefully', () => {
|
|
const result = EnhancedConfigValidator.validateWithMode('nodes-base.filter', {}, [{ name: 'conditions', type: 'filter', required: false }], 'operation', 'ai-friendly');
|
|
expect(result.valid).toBe(true);
|
|
});
|
|
|
|
it('should handle multiple special types in same config', () => {
|
|
const config = {
|
|
conditions: { combinator: 'and', conditions: [{ id: '1', operator: { type: 'string', operation: 'equals' }, leftValue: 'test', rightValue: 'value' }] },
|
|
assignments: { assignments: [{ id: '1', name: 'result', value: 'processed', type: 'string' }] },
|
|
};
|
|
const properties = [{ name: 'conditions', type: 'filter', required: true }, { name: 'assignments', type: 'assignmentCollection', required: true }];
|
|
const result = EnhancedConfigValidator.validateWithMode('nodes-base.custom', config, properties, 'operation', 'ai-friendly');
|
|
expect(result.valid).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('Validation Profiles for Type Structures', () => {
|
|
it('should respect strict profile for type validation', () => {
|
|
const config = { conditions: { combinator: 'and', conditions: [{ id: '1', operator: { type: 'string', operation: 'gt' }, leftValue: 'test', rightValue: 'value' }] } };
|
|
const result = EnhancedConfigValidator.validateWithMode('nodes-base.filter', config, [{ name: 'conditions', type: 'filter', required: true }], 'operation', 'strict');
|
|
expect(result.valid).toBe(false);
|
|
expect(result.profile).toBe('strict');
|
|
});
|
|
|
|
it('should respect minimal profile (less strict)', () => {
|
|
const config = { conditions: { combinator: 'and', conditions: [] } };
|
|
const result = EnhancedConfigValidator.validateWithMode('nodes-base.filter', config, [{ name: 'conditions', type: 'filter', required: true }], 'operation', 'minimal');
|
|
expect(result.profile).toBe('minimal');
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
// ─── Integration Tests (from enhanced-config-validator-integration) ─────────
|
|
|
|
describe('EnhancedConfigValidator - Integration Tests', () => {
|
|
let mockResourceService: any;
|
|
let mockOperationService: any;
|
|
let mockRepository: any;
|
|
|
|
beforeEach(() => {
|
|
mockRepository = {
|
|
getNode: vi.fn(),
|
|
getNodeOperations: vi.fn().mockReturnValue([]),
|
|
getNodeResources: vi.fn().mockReturnValue([]),
|
|
getOperationsForResource: vi.fn().mockReturnValue([]),
|
|
getDefaultOperationForResource: vi.fn().mockReturnValue(undefined),
|
|
getNodePropertyDefaults: vi.fn().mockReturnValue({})
|
|
};
|
|
|
|
mockResourceService = { findSimilarResources: vi.fn().mockReturnValue([]) };
|
|
mockOperationService = { findSimilarOperations: vi.fn().mockReturnValue([]) };
|
|
|
|
vi.mocked(ResourceSimilarityService).mockImplementation(() => mockResourceService);
|
|
vi.mocked(OperationSimilarityService).mockImplementation(() => mockOperationService);
|
|
|
|
EnhancedConfigValidator.initializeSimilarityServices(mockRepository);
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
describe('similarity service integration', () => {
|
|
it('should initialize similarity services when initializeSimilarityServices is called', () => {
|
|
expect(ResourceSimilarityService).toHaveBeenCalled();
|
|
expect(OperationSimilarityService).toHaveBeenCalled();
|
|
});
|
|
|
|
it('should use resource similarity service for invalid resource errors', () => {
|
|
mockResourceService.findSimilarResources.mockReturnValue([{ value: 'message', confidence: 0.8, reason: 'Similar resource name', availableOperations: ['send', 'update'] }]);
|
|
const result = EnhancedConfigValidator.validateWithMode('nodes-base.slack', { resource: 'invalidResource', operation: 'send' }, [{ name: 'resource', type: 'options', required: true, options: [{ value: 'message', name: 'Message' }, { value: 'channel', name: 'Channel' }] }, { name: 'operation', type: 'options', required: true, displayOptions: { show: { resource: ['message'] } }, options: [{ value: 'send', name: 'Send Message' }] }], 'operation', 'ai-friendly');
|
|
expect(mockResourceService.findSimilarResources).toHaveBeenCalledWith('nodes-base.slack', 'invalidResource', expect.any(Number));
|
|
expect(result.suggestions.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('should use operation similarity service for invalid operation errors', () => {
|
|
mockOperationService.findSimilarOperations.mockReturnValue([{ value: 'send', confidence: 0.9, reason: 'Very similar - likely a typo', resource: 'message' }]);
|
|
const result = EnhancedConfigValidator.validateWithMode('nodes-base.slack', { resource: 'message', operation: 'invalidOperation' }, [{ name: 'resource', type: 'options', required: true, options: [{ value: 'message', name: 'Message' }] }, { name: 'operation', type: 'options', required: true, displayOptions: { show: { resource: ['message'] } }, options: [{ value: 'send', name: 'Send Message' }, { value: 'update', name: 'Update Message' }] }], 'operation', 'ai-friendly');
|
|
expect(mockOperationService.findSimilarOperations).toHaveBeenCalledWith('nodes-base.slack', 'invalidOperation', 'message', expect.any(Number));
|
|
expect(result.suggestions.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('should handle similarity service errors gracefully', () => {
|
|
mockResourceService.findSimilarResources.mockImplementation(() => { throw new Error('Service error'); });
|
|
const result = EnhancedConfigValidator.validateWithMode('nodes-base.slack', { resource: 'invalidResource', operation: 'send' }, [{ name: 'resource', type: 'options', required: true, options: [{ value: 'message', name: 'Message' }] }], 'operation', 'ai-friendly');
|
|
expect(result).toBeDefined();
|
|
expect(result.valid).toBe(false);
|
|
expect(result.errors.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('should not call similarity services for valid configurations', () => {
|
|
mockRepository.getNodeResources.mockReturnValue([{ value: 'message', name: 'Message' }, { value: 'channel', name: 'Channel' }]);
|
|
mockRepository.getNodeOperations.mockReturnValue([{ value: 'send', name: 'Send Message' }]);
|
|
const result = EnhancedConfigValidator.validateWithMode('nodes-base.slack', { resource: 'message', operation: 'send', channel: '#general', text: 'Test message' }, [{ name: 'resource', type: 'options', required: true, options: [{ value: 'message', name: 'Message' }] }, { name: 'operation', type: 'options', required: true, displayOptions: { show: { resource: ['message'] } }, options: [{ value: 'send', name: 'Send Message' }] }], 'operation', 'ai-friendly');
|
|
expect(mockResourceService.findSimilarResources).not.toHaveBeenCalled();
|
|
expect(mockOperationService.findSimilarOperations).not.toHaveBeenCalled();
|
|
expect(result.valid).toBe(true);
|
|
});
|
|
|
|
it('should limit suggestion count when calling similarity services', () => {
|
|
EnhancedConfigValidator.validateWithMode('nodes-base.slack', { resource: 'invalidResource' }, [{ name: 'resource', type: 'options', required: true, options: [{ value: 'message', name: 'Message' }] }], 'operation', 'ai-friendly');
|
|
expect(mockResourceService.findSimilarResources).toHaveBeenCalledWith('nodes-base.slack', 'invalidResource', 3);
|
|
});
|
|
});
|
|
|
|
describe('error enhancement with suggestions', () => {
|
|
it('should enhance resource validation errors with suggestions', () => {
|
|
mockResourceService.findSimilarResources.mockReturnValue([{ value: 'message', confidence: 0.85, reason: 'Very similar - likely a typo', availableOperations: ['send', 'update', 'delete'] }]);
|
|
const result = EnhancedConfigValidator.validateWithMode('nodes-base.slack', { resource: 'msgs' }, [{ name: 'resource', type: 'options', required: true, options: [{ value: 'message', name: 'Message' }, { value: 'channel', name: 'Channel' }] }], 'operation', 'ai-friendly');
|
|
const resourceError = result.errors.find(e => e.property === 'resource');
|
|
expect(resourceError).toBeDefined();
|
|
expect(resourceError!.suggestion).toBeDefined();
|
|
expect(resourceError!.suggestion).toContain('message');
|
|
});
|
|
|
|
it('should enhance operation validation errors with suggestions', () => {
|
|
mockOperationService.findSimilarOperations.mockReturnValue([{ value: 'send', confidence: 0.9, reason: 'Almost exact match - likely a typo', resource: 'message', description: 'Send Message' }]);
|
|
const result = EnhancedConfigValidator.validateWithMode('nodes-base.slack', { resource: 'message', operation: 'sned' }, [{ name: 'resource', type: 'options', required: true, options: [{ value: 'message', name: 'Message' }] }, { name: 'operation', type: 'options', required: true, displayOptions: { show: { resource: ['message'] } }, options: [{ value: 'send', name: 'Send Message' }, { value: 'update', name: 'Update Message' }] }], 'operation', 'ai-friendly');
|
|
const operationError = result.errors.find(e => e.property === 'operation');
|
|
expect(operationError).toBeDefined();
|
|
expect(operationError!.suggestion).toBeDefined();
|
|
expect(operationError!.suggestion).toContain('send');
|
|
});
|
|
|
|
it('should not enhance errors when no good suggestions are available', () => {
|
|
mockResourceService.findSimilarResources.mockReturnValue([{ value: 'message', confidence: 0.2, reason: 'Possibly related resource' }]);
|
|
const result = EnhancedConfigValidator.validateWithMode('nodes-base.slack', { resource: 'completelyWrongValue' }, [{ name: 'resource', type: 'options', required: true, options: [{ value: 'message', name: 'Message' }] }], 'operation', 'ai-friendly');
|
|
const resourceError = result.errors.find(e => e.property === 'resource');
|
|
expect(resourceError).toBeDefined();
|
|
expect(resourceError!.suggestion).toBeUndefined();
|
|
});
|
|
|
|
it('should provide multiple operation suggestions when resource is known', () => {
|
|
mockOperationService.findSimilarOperations.mockReturnValue([{ value: 'send', confidence: 0.7, reason: 'Similar operation' }, { value: 'update', confidence: 0.6, reason: 'Similar operation' }, { value: 'delete', confidence: 0.5, reason: 'Similar operation' }]);
|
|
const result = EnhancedConfigValidator.validateWithMode('nodes-base.slack', { resource: 'message', operation: 'invalidOp' }, [{ name: 'resource', type: 'options', required: true, options: [{ value: 'message', name: 'Message' }] }, { name: 'operation', type: 'options', required: true, displayOptions: { show: { resource: ['message'] } }, options: [{ value: 'send', name: 'Send Message' }, { value: 'update', name: 'Update Message' }, { value: 'delete', name: 'Delete Message' }] }], 'operation', 'ai-friendly');
|
|
expect(result.suggestions.length).toBeGreaterThan(2);
|
|
expect(result.suggestions.filter(s => s.includes('send') || s.includes('update') || s.includes('delete')).length).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
|
|
describe('confidence thresholds and filtering', () => {
|
|
it('should only use high confidence resource suggestions', () => {
|
|
mockResourceService.findSimilarResources.mockReturnValue([{ value: 'message1', confidence: 0.9, reason: 'High confidence' }, { value: 'message2', confidence: 0.4, reason: 'Low confidence' }, { value: 'message3', confidence: 0.7, reason: 'Medium confidence' }]);
|
|
const result = EnhancedConfigValidator.validateWithMode('nodes-base.slack', { resource: 'invalidResource' }, [{ name: 'resource', type: 'options', required: true, options: [{ value: 'message', name: 'Message' }] }], 'operation', 'ai-friendly');
|
|
const resourceError = result.errors.find(e => e.property === 'resource');
|
|
expect(resourceError?.suggestion).toBeDefined();
|
|
expect(resourceError!.suggestion).toContain('message1');
|
|
});
|
|
|
|
it('should only use high confidence operation suggestions', () => {
|
|
mockOperationService.findSimilarOperations.mockReturnValue([{ value: 'send', confidence: 0.95, reason: 'Very high confidence' }, { value: 'post', confidence: 0.3, reason: 'Low confidence' }]);
|
|
const result = EnhancedConfigValidator.validateWithMode('nodes-base.slack', { resource: 'message', operation: 'invalidOperation' }, [{ name: 'resource', type: 'options', required: true, options: [{ value: 'message', name: 'Message' }] }, { name: 'operation', type: 'options', required: true, displayOptions: { show: { resource: ['message'] } }, options: [{ value: 'send', name: 'Send Message' }] }], 'operation', 'ai-friendly');
|
|
const operationError = result.errors.find(e => e.property === 'operation');
|
|
expect(operationError?.suggestion).toBeDefined();
|
|
expect(operationError!.suggestion).toContain('send');
|
|
expect(operationError!.suggestion).not.toContain('post');
|
|
});
|
|
});
|
|
|
|
describe('integration with existing validation logic', () => {
|
|
it('should work with minimal validation mode', () => {
|
|
mockRepository.getNodeResources.mockReturnValue([]);
|
|
mockResourceService.findSimilarResources.mockReturnValue([{ value: 'message', confidence: 0.8, reason: 'Similar' }]);
|
|
const result = EnhancedConfigValidator.validateWithMode('nodes-base.slack', { resource: 'invalidResource' }, [{ name: 'resource', type: 'options', required: true, options: [{ value: 'message', name: 'Message' }] }], 'minimal', 'ai-friendly');
|
|
expect(mockResourceService.findSimilarResources).toHaveBeenCalled();
|
|
expect(result.errors.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('should work with strict validation profile', () => {
|
|
mockRepository.getNodeResources.mockReturnValue([{ value: 'message', name: 'Message' }]);
|
|
mockRepository.getOperationsForResource.mockReturnValue([]);
|
|
mockOperationService.findSimilarOperations.mockReturnValue([{ value: 'send', confidence: 0.8, reason: 'Similar' }]);
|
|
const result = EnhancedConfigValidator.validateWithMode('nodes-base.slack', { resource: 'message', operation: 'invalidOp' }, [{ name: 'resource', type: 'options', required: true, options: [{ value: 'message', name: 'Message' }] }, { name: 'operation', type: 'options', required: true, displayOptions: { show: { resource: ['message'] } }, options: [{ value: 'send', name: 'Send Message' }] }], 'operation', 'strict');
|
|
expect(mockOperationService.findSimilarOperations).toHaveBeenCalled();
|
|
const operationError = result.errors.find(e => e.property === 'operation');
|
|
expect(operationError?.suggestion).toBeDefined();
|
|
});
|
|
|
|
it('should preserve original error properties when enhancing', () => {
|
|
mockResourceService.findSimilarResources.mockReturnValue([{ value: 'message', confidence: 0.8, reason: 'Similar' }]);
|
|
const result = EnhancedConfigValidator.validateWithMode('nodes-base.slack', { resource: 'invalidResource' }, [{ name: 'resource', type: 'options', required: true, options: [{ value: 'message', name: 'Message' }] }], 'operation', 'ai-friendly');
|
|
const resourceError = result.errors.find(e => e.property === 'resource');
|
|
expect(resourceError?.type).toBeDefined();
|
|
expect(resourceError?.property).toBe('resource');
|
|
expect(resourceError?.message).toBeDefined();
|
|
expect(resourceError?.suggestion).toBeDefined();
|
|
});
|
|
});
|
|
});
|
|
|
|
// ─── Operation and Resource Validation (from enhanced-config-validator-operations) ───
|
|
|
|
describe('EnhancedConfigValidator - Operation and Resource Validation', () => {
|
|
let repository: NodeRepository;
|
|
let testDb: any;
|
|
|
|
beforeEach(async () => {
|
|
testDb = await createTestDatabase();
|
|
repository = testDb.nodeRepository;
|
|
|
|
// Configure mocked similarity services to return empty arrays by default
|
|
vi.mocked(ResourceSimilarityService).mockImplementation(() => ({
|
|
findSimilarResources: vi.fn().mockReturnValue([])
|
|
}) as any);
|
|
vi.mocked(OperationSimilarityService).mockImplementation(() => ({
|
|
findSimilarOperations: vi.fn().mockReturnValue([])
|
|
}) as any);
|
|
|
|
EnhancedConfigValidator.initializeSimilarityServices(repository);
|
|
|
|
repository.saveNode({
|
|
nodeType: 'nodes-base.googleDrive', packageName: 'n8n-nodes-base', displayName: 'Google Drive', description: 'Access Google Drive', category: 'transform', style: 'declarative' as const, isAITool: false, isTrigger: false, isWebhook: false, isVersioned: true, version: '1',
|
|
properties: [
|
|
{ name: 'resource', type: 'options', required: true, options: [{ value: 'file', name: 'File' }, { value: 'folder', name: 'Folder' }, { value: 'fileFolder', name: 'File & Folder' }] },
|
|
{ name: 'operation', type: 'options', required: true, displayOptions: { show: { resource: ['file'] } }, options: [{ value: 'copy', name: 'Copy' }, { value: 'delete', name: 'Delete' }, { value: 'download', name: 'Download' }, { value: 'list', name: 'List' }, { value: 'share', name: 'Share' }, { value: 'update', name: 'Update' }, { value: 'upload', name: 'Upload' }] },
|
|
{ name: 'operation', type: 'options', required: true, displayOptions: { show: { resource: ['folder'] } }, options: [{ value: 'create', name: 'Create' }, { value: 'delete', name: 'Delete' }, { value: 'share', name: 'Share' }] },
|
|
{ name: 'operation', type: 'options', required: true, displayOptions: { show: { resource: ['fileFolder'] } }, options: [{ value: 'search', name: 'Search' }] }
|
|
],
|
|
operations: [], credentials: []
|
|
});
|
|
|
|
repository.saveNode({
|
|
nodeType: 'nodes-base.slack', packageName: 'n8n-nodes-base', displayName: 'Slack', description: 'Send messages to Slack', category: 'communication', style: 'declarative' as const, isAITool: false, isTrigger: false, isWebhook: false, isVersioned: true, version: '2',
|
|
properties: [
|
|
{ name: 'resource', type: 'options', required: true, options: [{ value: 'channel', name: 'Channel' }, { value: 'message', name: 'Message' }, { value: 'user', name: 'User' }] },
|
|
{ name: 'operation', type: 'options', required: true, displayOptions: { show: { resource: ['message'] } }, options: [{ value: 'send', name: 'Send' }, { value: 'update', name: 'Update' }, { value: 'delete', name: 'Delete' }] }
|
|
],
|
|
operations: [], credentials: []
|
|
});
|
|
});
|
|
|
|
afterEach(async () => {
|
|
if (testDb) { await testDb.cleanup(); }
|
|
});
|
|
|
|
describe('Invalid Operations', () => {
|
|
it('should detect invalid operation for Google Drive fileFolder resource', () => {
|
|
const node = repository.getNode('nodes-base.googleDrive');
|
|
const result = EnhancedConfigValidator.validateWithMode('nodes-base.googleDrive', { resource: 'fileFolder', operation: 'listFiles' }, node.properties, 'operation', 'ai-friendly');
|
|
expect(result.valid).toBe(false);
|
|
const operationError = result.errors.find(e => e.property === 'operation');
|
|
expect(operationError).toBeDefined();
|
|
expect(operationError!.message).toContain('listFiles');
|
|
});
|
|
|
|
it('should detect typos in operations', () => {
|
|
const node = repository.getNode('nodes-base.googleDrive');
|
|
const result = EnhancedConfigValidator.validateWithMode('nodes-base.googleDrive', { resource: 'file', operation: 'downlod' }, node.properties, 'operation', 'ai-friendly');
|
|
expect(result.valid).toBe(false);
|
|
const operationError = result.errors.find(e => e.property === 'operation');
|
|
expect(operationError).toBeDefined();
|
|
});
|
|
|
|
it('should list valid operations for the resource', () => {
|
|
const node = repository.getNode('nodes-base.googleDrive');
|
|
const result = EnhancedConfigValidator.validateWithMode('nodes-base.googleDrive', { resource: 'folder', operation: 'upload' }, node.properties, 'operation', 'ai-friendly');
|
|
expect(result.valid).toBe(false);
|
|
const operationError = result.errors.find(e => e.property === 'operation');
|
|
expect(operationError).toBeDefined();
|
|
expect(operationError!.fix).toContain('Valid operations for resource "folder"');
|
|
expect(operationError!.fix).toContain('create');
|
|
expect(operationError!.fix).toContain('delete');
|
|
expect(operationError!.fix).toContain('share');
|
|
});
|
|
});
|
|
|
|
describe('Invalid Resources', () => {
|
|
it('should detect invalid plural resource "files"', () => {
|
|
const node = repository.getNode('nodes-base.googleDrive');
|
|
const result = EnhancedConfigValidator.validateWithMode('nodes-base.googleDrive', { resource: 'files', operation: 'list' }, node.properties, 'operation', 'ai-friendly');
|
|
expect(result.valid).toBe(false);
|
|
const resourceError = result.errors.find(e => e.property === 'resource');
|
|
expect(resourceError).toBeDefined();
|
|
expect(resourceError!.message).toContain('files');
|
|
});
|
|
|
|
it('should detect typos in resources', () => {
|
|
const node = repository.getNode('nodes-base.googleDrive');
|
|
const result = EnhancedConfigValidator.validateWithMode('nodes-base.googleDrive', { resource: 'flie', operation: 'download' }, node.properties, 'operation', 'ai-friendly');
|
|
expect(result.valid).toBe(false);
|
|
const resourceError = result.errors.find(e => e.property === 'resource');
|
|
expect(resourceError).toBeDefined();
|
|
});
|
|
|
|
it('should list valid resources when no match found', () => {
|
|
const node = repository.getNode('nodes-base.googleDrive');
|
|
const result = EnhancedConfigValidator.validateWithMode('nodes-base.googleDrive', { resource: 'document', operation: 'create' }, node.properties, 'operation', 'ai-friendly');
|
|
expect(result.valid).toBe(false);
|
|
const resourceError = result.errors.find(e => e.property === 'resource');
|
|
expect(resourceError).toBeDefined();
|
|
expect(resourceError!.fix).toContain('Valid resources:');
|
|
expect(resourceError!.fix).toContain('file');
|
|
expect(resourceError!.fix).toContain('folder');
|
|
});
|
|
});
|
|
|
|
describe('Combined Resource and Operation Validation', () => {
|
|
it('should validate both resource and operation together', () => {
|
|
const node = repository.getNode('nodes-base.googleDrive');
|
|
const result = EnhancedConfigValidator.validateWithMode('nodes-base.googleDrive', { resource: 'files', operation: 'listFiles' }, node.properties, 'operation', 'ai-friendly');
|
|
expect(result.valid).toBe(false);
|
|
expect(result.errors.length).toBeGreaterThanOrEqual(2);
|
|
expect(result.errors.find(e => e.property === 'resource')).toBeDefined();
|
|
expect(result.errors.find(e => e.property === 'operation')).toBeDefined();
|
|
});
|
|
});
|
|
|
|
describe('Slack Node Validation', () => {
|
|
it('should detect invalid operation "sendMessage" for Slack', () => {
|
|
const node = repository.getNode('nodes-base.slack');
|
|
const result = EnhancedConfigValidator.validateWithMode('nodes-base.slack', { resource: 'message', operation: 'sendMessage' }, node.properties, 'operation', 'ai-friendly');
|
|
expect(result.valid).toBe(false);
|
|
const operationError = result.errors.find(e => e.property === 'operation');
|
|
expect(operationError).toBeDefined();
|
|
});
|
|
|
|
it('should detect invalid plural resource "channels" for Slack', () => {
|
|
const node = repository.getNode('nodes-base.slack');
|
|
const result = EnhancedConfigValidator.validateWithMode('nodes-base.slack', { resource: 'channels', operation: 'create' }, node.properties, 'operation', 'ai-friendly');
|
|
expect(result.valid).toBe(false);
|
|
const resourceError = result.errors.find(e => e.property === 'resource');
|
|
expect(resourceError).toBeDefined();
|
|
});
|
|
});
|
|
|
|
describe('Valid Configurations', () => {
|
|
it('should accept valid Google Drive configuration', () => {
|
|
const node = repository.getNode('nodes-base.googleDrive');
|
|
const result = EnhancedConfigValidator.validateWithMode('nodes-base.googleDrive', { resource: 'file', operation: 'download' }, node.properties, 'operation', 'ai-friendly');
|
|
expect(result.errors.find(e => e.property === 'resource')).toBeUndefined();
|
|
expect(result.errors.find(e => e.property === 'operation')).toBeUndefined();
|
|
});
|
|
|
|
it('should accept valid Slack configuration', () => {
|
|
const node = repository.getNode('nodes-base.slack');
|
|
const result = EnhancedConfigValidator.validateWithMode('nodes-base.slack', { resource: 'message', operation: 'send' }, node.properties, 'operation', 'ai-friendly');
|
|
expect(result.errors.find(e => e.property === 'resource')).toBeUndefined();
|
|
expect(result.errors.find(e => e.property === 'operation')).toBeUndefined();
|
|
});
|
|
});
|
|
}); |