Major improvements based on comprehensive test suite review: Test Fixes: - Fix all 78 failing tests across logger, MSW, and validator tests - Fix console spy management in logger tests with proper DEBUG env handling - Fix MSW test environment restoration in session-management.test.ts - Fix workflow validator tests by adding proper node connections - Fix mock setup issues in edge case tests Test Organization: - Split large config-validator.test.ts (1,075 lines) into 4 focused files - Rename 63+ tests to follow "should X when Y" naming convention - Add comprehensive edge case test files for all major validators - Create tests/README.md with testing guidelines and best practices New Features: - Add ConfigValidator.validateBatch() method for bulk validation - Add edge case coverage for null/undefined, boundaries, invalid data - Add CI-aware performance test timeouts - Add JSDoc comments to test utilities and factories - Add workflow duplicate node name validation tests Results: - All tests passing: 1,356 passed, 19 skipped - Test coverage: 85.34% statements, 85.3% branches - From 78 failures to 0 failures 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
468 lines
16 KiB
TypeScript
468 lines
16 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
import { NodeParser } from '@/parsers/node-parser';
|
|
import { PropertyExtractor } from '@/parsers/property-extractor';
|
|
import {
|
|
programmaticNodeFactory,
|
|
declarativeNodeFactory,
|
|
triggerNodeFactory,
|
|
webhookNodeFactory,
|
|
aiToolNodeFactory,
|
|
versionedNodeClassFactory,
|
|
versionedNodeTypeClassFactory,
|
|
malformedNodeFactory,
|
|
nodeClassFactory,
|
|
propertyFactory,
|
|
stringPropertyFactory,
|
|
optionsPropertyFactory
|
|
} from '@tests/fixtures/factories/parser-node.factory';
|
|
|
|
// Mock PropertyExtractor
|
|
vi.mock('@/parsers/property-extractor');
|
|
|
|
describe('NodeParser', () => {
|
|
let parser: NodeParser;
|
|
let mockPropertyExtractor: any;
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
|
|
// Setup mock property extractor
|
|
mockPropertyExtractor = {
|
|
extractProperties: vi.fn().mockReturnValue([]),
|
|
extractCredentials: vi.fn().mockReturnValue([]),
|
|
detectAIToolCapability: vi.fn().mockReturnValue(false),
|
|
extractOperations: vi.fn().mockReturnValue([])
|
|
};
|
|
|
|
(PropertyExtractor as any).mockImplementation(() => mockPropertyExtractor);
|
|
|
|
parser = new NodeParser();
|
|
});
|
|
|
|
describe('parse method', () => {
|
|
it('should parse correctly when node is programmatic', () => {
|
|
const nodeDefinition = programmaticNodeFactory.build();
|
|
const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
|
|
|
|
mockPropertyExtractor.extractProperties.mockReturnValue(nodeDefinition.properties);
|
|
mockPropertyExtractor.extractCredentials.mockReturnValue(nodeDefinition.credentials);
|
|
|
|
const result = parser.parse(NodeClass, 'n8n-nodes-base');
|
|
|
|
expect(result).toMatchObject({
|
|
style: 'programmatic',
|
|
nodeType: `nodes-base.${nodeDefinition.name}`,
|
|
displayName: nodeDefinition.displayName,
|
|
description: nodeDefinition.description,
|
|
category: nodeDefinition.group?.[0] || 'misc',
|
|
packageName: 'n8n-nodes-base'
|
|
});
|
|
|
|
// Check specific properties separately to avoid strict matching
|
|
expect(result.isVersioned).toBe(false);
|
|
expect(result.version).toBe(nodeDefinition.version?.toString() || '1');
|
|
|
|
expect(mockPropertyExtractor.extractProperties).toHaveBeenCalledWith(NodeClass);
|
|
expect(mockPropertyExtractor.extractCredentials).toHaveBeenCalledWith(NodeClass);
|
|
});
|
|
|
|
it('should parse correctly when node is declarative', () => {
|
|
const nodeDefinition = declarativeNodeFactory.build();
|
|
const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
|
|
|
|
const result = parser.parse(NodeClass, 'n8n-nodes-base');
|
|
|
|
expect(result.style).toBe('declarative');
|
|
expect(result.nodeType).toBe(`nodes-base.${nodeDefinition.name}`);
|
|
});
|
|
|
|
it('should preserve type when package prefix is already included', () => {
|
|
const nodeDefinition = programmaticNodeFactory.build({
|
|
name: 'nodes-base.slack'
|
|
});
|
|
const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
|
|
|
|
const result = parser.parse(NodeClass, 'n8n-nodes-base');
|
|
|
|
expect(result.nodeType).toBe('nodes-base.slack');
|
|
});
|
|
|
|
it('should set isTrigger flag when node is a trigger', () => {
|
|
const nodeDefinition = triggerNodeFactory.build();
|
|
const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
|
|
|
|
const result = parser.parse(NodeClass, 'n8n-nodes-base');
|
|
|
|
expect(result.isTrigger).toBe(true);
|
|
});
|
|
|
|
it('should set isWebhook flag when node is a webhook', () => {
|
|
const nodeDefinition = webhookNodeFactory.build();
|
|
const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
|
|
|
|
const result = parser.parse(NodeClass, 'n8n-nodes-base');
|
|
|
|
expect(result.isWebhook).toBe(true);
|
|
});
|
|
|
|
it('should set isAITool flag when node has AI capability', () => {
|
|
const nodeDefinition = aiToolNodeFactory.build();
|
|
const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
|
|
|
|
mockPropertyExtractor.detectAIToolCapability.mockReturnValue(true);
|
|
|
|
const result = parser.parse(NodeClass, 'n8n-nodes-base');
|
|
|
|
expect(result.isAITool).toBe(true);
|
|
});
|
|
|
|
it('should parse correctly when node uses VersionedNodeType class', () => {
|
|
// Create a simple versioned node class without modifying function properties
|
|
const VersionedNodeClass = class VersionedNodeType {
|
|
baseDescription = {
|
|
name: 'versionedNode',
|
|
displayName: 'Versioned Node',
|
|
description: 'A versioned node',
|
|
defaultVersion: 2
|
|
};
|
|
nodeVersions = {
|
|
1: { description: { properties: [] } },
|
|
2: { description: { properties: [] } }
|
|
};
|
|
currentVersion = 2;
|
|
};
|
|
|
|
mockPropertyExtractor.extractProperties.mockReturnValue([
|
|
propertyFactory.build(),
|
|
propertyFactory.build()
|
|
]);
|
|
|
|
const result = parser.parse(VersionedNodeClass, 'n8n-nodes-base');
|
|
|
|
expect(result.isVersioned).toBe(true);
|
|
expect(result.version).toBe('2');
|
|
expect(result.nodeType).toBe('nodes-base.versionedNode');
|
|
});
|
|
|
|
it('should parse correctly when node has nodeVersions property', () => {
|
|
const versionedDef = versionedNodeClassFactory.build();
|
|
const NodeClass = class {
|
|
nodeVersions = versionedDef.nodeVersions;
|
|
baseDescription = versionedDef.baseDescription;
|
|
};
|
|
|
|
const result = parser.parse(NodeClass, 'n8n-nodes-base');
|
|
|
|
expect(result.isVersioned).toBe(true);
|
|
expect(result.version).toBe('2');
|
|
});
|
|
|
|
it('should use max version when version is an array', () => {
|
|
const nodeDefinition = programmaticNodeFactory.build({
|
|
version: [1, 1.1, 1.2, 2]
|
|
});
|
|
const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
|
|
|
|
const result = parser.parse(NodeClass, 'n8n-nodes-base');
|
|
|
|
expect(result.isVersioned).toBe(true);
|
|
expect(result.version).toBe('2'); // Should return max version
|
|
});
|
|
|
|
it('should throw error when node is missing name property', () => {
|
|
const nodeDefinition = malformedNodeFactory.build();
|
|
const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
|
|
|
|
expect(() => parser.parse(NodeClass, 'n8n-nodes-base')).toThrow('Node is missing name property');
|
|
});
|
|
|
|
it('should use static description when instantiation fails', () => {
|
|
const NodeClass = class {
|
|
static description = programmaticNodeFactory.build();
|
|
constructor() {
|
|
throw new Error('Cannot instantiate');
|
|
}
|
|
};
|
|
|
|
const result = parser.parse(NodeClass, 'n8n-nodes-base');
|
|
|
|
expect(result.displayName).toBe(NodeClass.description.displayName);
|
|
});
|
|
|
|
it('should extract category when using different property names', () => {
|
|
const testCases = [
|
|
{ group: ['transform'], expected: 'transform' },
|
|
{ categories: ['output'], expected: 'output' },
|
|
{ category: 'trigger', expected: 'trigger' },
|
|
{ /* no category */ expected: 'misc' }
|
|
];
|
|
|
|
testCases.forEach(({ group, categories, category, expected }) => {
|
|
const nodeDefinition = programmaticNodeFactory.build({
|
|
group,
|
|
categories,
|
|
category
|
|
} as any);
|
|
const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
|
|
|
|
const result = parser.parse(NodeClass, 'n8n-nodes-base');
|
|
|
|
expect(result.category).toBe(expected);
|
|
});
|
|
});
|
|
|
|
it('should set isTrigger flag when node has polling property', () => {
|
|
const nodeDefinition = programmaticNodeFactory.build({
|
|
polling: true
|
|
});
|
|
const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
|
|
|
|
const result = parser.parse(NodeClass, 'n8n-nodes-base');
|
|
|
|
expect(result.isTrigger).toBe(true);
|
|
});
|
|
|
|
it('should set isTrigger flag when node has eventTrigger property', () => {
|
|
const nodeDefinition = programmaticNodeFactory.build({
|
|
eventTrigger: true
|
|
});
|
|
const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
|
|
|
|
const result = parser.parse(NodeClass, 'n8n-nodes-base');
|
|
|
|
expect(result.isTrigger).toBe(true);
|
|
});
|
|
|
|
it('should set isTrigger flag when node name contains trigger', () => {
|
|
const nodeDefinition = programmaticNodeFactory.build({
|
|
name: 'myTrigger'
|
|
});
|
|
const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
|
|
|
|
const result = parser.parse(NodeClass, 'n8n-nodes-base');
|
|
|
|
expect(result.isTrigger).toBe(true);
|
|
});
|
|
|
|
it('should set isWebhook flag when node name contains webhook', () => {
|
|
const nodeDefinition = programmaticNodeFactory.build({
|
|
name: 'customWebhook'
|
|
});
|
|
const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
|
|
|
|
const result = parser.parse(NodeClass, 'n8n-nodes-base');
|
|
|
|
expect(result.isWebhook).toBe(true);
|
|
});
|
|
|
|
it('should parse correctly when node is an instance object', () => {
|
|
const nodeDefinition = programmaticNodeFactory.build();
|
|
const nodeInstance = {
|
|
description: nodeDefinition
|
|
};
|
|
|
|
mockPropertyExtractor.extractProperties.mockReturnValue(nodeDefinition.properties);
|
|
|
|
const result = parser.parse(nodeInstance, 'n8n-nodes-base');
|
|
|
|
expect(result.displayName).toBe(nodeDefinition.displayName);
|
|
});
|
|
|
|
it('should handle different package name formats', () => {
|
|
const nodeDefinition = programmaticNodeFactory.build();
|
|
const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
|
|
|
|
const testCases = [
|
|
{ packageName: '@n8n/n8n-nodes-langchain', expectedPrefix: 'nodes-langchain' },
|
|
{ packageName: 'n8n-nodes-custom', expectedPrefix: 'nodes-custom' },
|
|
{ packageName: 'custom-package', expectedPrefix: 'custom-package' }
|
|
];
|
|
|
|
testCases.forEach(({ packageName, expectedPrefix }) => {
|
|
const result = parser.parse(NodeClass, packageName);
|
|
expect(result.nodeType).toBe(`${expectedPrefix}.${nodeDefinition.name}`);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('version extraction', () => {
|
|
it('should extract version from baseDescription.defaultVersion', () => {
|
|
const NodeClass = class {
|
|
baseDescription = {
|
|
name: 'test',
|
|
displayName: 'Test',
|
|
defaultVersion: 3
|
|
};
|
|
};
|
|
|
|
const result = parser.parse(NodeClass, 'n8n-nodes-base');
|
|
|
|
expect(result.version).toBe('3');
|
|
});
|
|
|
|
it('should extract version from nodeVersions keys', () => {
|
|
const NodeClass = class {
|
|
description = { name: 'test', displayName: 'Test' };
|
|
nodeVersions = {
|
|
1: { description: {} },
|
|
2: { description: {} },
|
|
3: { description: {} }
|
|
};
|
|
};
|
|
|
|
const result = parser.parse(NodeClass, 'n8n-nodes-base');
|
|
|
|
expect(result.version).toBe('3');
|
|
});
|
|
|
|
it('should extract version from instance nodeVersions', () => {
|
|
const NodeClass = class {
|
|
description = { name: 'test', displayName: 'Test' };
|
|
|
|
constructor() {
|
|
(this as any).nodeVersions = {
|
|
1: { description: {} },
|
|
2: { description: {} },
|
|
4: { description: {} }
|
|
};
|
|
}
|
|
};
|
|
|
|
const result = parser.parse(NodeClass, 'n8n-nodes-base');
|
|
|
|
expect(result.version).toBe('4');
|
|
});
|
|
|
|
it('should handle version as number in description', () => {
|
|
const nodeDefinition = programmaticNodeFactory.build({
|
|
version: 2
|
|
});
|
|
const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
|
|
|
|
const result = parser.parse(NodeClass, 'n8n-nodes-base');
|
|
|
|
expect(result.version).toBe('2');
|
|
});
|
|
|
|
it('should handle version as string in description', () => {
|
|
const nodeDefinition = programmaticNodeFactory.build({
|
|
version: '1.5' as any
|
|
});
|
|
const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
|
|
|
|
const result = parser.parse(NodeClass, 'n8n-nodes-base');
|
|
|
|
expect(result.version).toBe('1.5');
|
|
});
|
|
|
|
it('should default to version 1 when no version found', () => {
|
|
const nodeDefinition = programmaticNodeFactory.build();
|
|
delete (nodeDefinition as any).version;
|
|
const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
|
|
|
|
const result = parser.parse(NodeClass, 'n8n-nodes-base');
|
|
|
|
expect(result.version).toBe('1');
|
|
});
|
|
});
|
|
|
|
describe('versioned node detection', () => {
|
|
it('should detect versioned nodes with nodeVersions', () => {
|
|
const NodeClass = class {
|
|
description = { name: 'test', displayName: 'Test' };
|
|
nodeVersions = { 1: {}, 2: {} };
|
|
};
|
|
|
|
const result = parser.parse(NodeClass, 'n8n-nodes-base');
|
|
|
|
expect(result.isVersioned).toBe(true);
|
|
});
|
|
|
|
it('should detect versioned nodes with defaultVersion', () => {
|
|
const NodeClass = class {
|
|
baseDescription = {
|
|
name: 'test',
|
|
displayName: 'Test',
|
|
defaultVersion: 2
|
|
};
|
|
};
|
|
|
|
const result = parser.parse(NodeClass, 'n8n-nodes-base');
|
|
|
|
expect(result.isVersioned).toBe(true);
|
|
});
|
|
|
|
it('should detect versioned nodes with version array in instance', () => {
|
|
const NodeClass = class {
|
|
description = {
|
|
name: 'test',
|
|
displayName: 'Test',
|
|
version: [1, 1.1, 2]
|
|
};
|
|
};
|
|
|
|
const result = parser.parse(NodeClass, 'n8n-nodes-base');
|
|
|
|
expect(result.isVersioned).toBe(true);
|
|
});
|
|
|
|
it('should not detect non-versioned nodes as versioned', () => {
|
|
const nodeDefinition = programmaticNodeFactory.build({
|
|
version: 1
|
|
});
|
|
const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
|
|
|
|
const result = parser.parse(NodeClass, 'n8n-nodes-base');
|
|
|
|
expect(result.isVersioned).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('edge cases', () => {
|
|
it('should handle null/undefined description gracefully', () => {
|
|
const NodeClass = class {
|
|
description = null;
|
|
};
|
|
|
|
expect(() => parser.parse(NodeClass, 'n8n-nodes-base')).toThrow();
|
|
});
|
|
|
|
it('should handle empty routing object for declarative nodes', () => {
|
|
const nodeDefinition = declarativeNodeFactory.build({
|
|
routing: {} as any
|
|
});
|
|
const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
|
|
|
|
const result = parser.parse(NodeClass, 'n8n-nodes-base');
|
|
|
|
expect(result.style).toBe('declarative');
|
|
});
|
|
|
|
it('should handle complex nested versioned structure', () => {
|
|
const NodeClass = class VersionedNodeType {
|
|
constructor() {
|
|
(this as any).baseDescription = {
|
|
name: 'complex',
|
|
displayName: 'Complex Node',
|
|
defaultVersion: 3
|
|
};
|
|
(this as any).nodeVersions = {
|
|
1: { description: { properties: [] } },
|
|
2: { description: { properties: [] } },
|
|
3: { description: { properties: [] } }
|
|
};
|
|
}
|
|
};
|
|
|
|
// Override constructor name check
|
|
Object.defineProperty(NodeClass.prototype.constructor, 'name', {
|
|
value: 'VersionedNodeType'
|
|
});
|
|
|
|
const result = parser.parse(NodeClass, 'n8n-nodes-base');
|
|
|
|
expect(result.isVersioned).toBe(true);
|
|
expect(result.version).toBe('3');
|
|
});
|
|
});
|
|
}); |