test: implement comprehensive testing improvements from PR #104 review
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>
This commit is contained in:
@@ -40,7 +40,7 @@ describe('NodeParser', () => {
|
||||
});
|
||||
|
||||
describe('parse method', () => {
|
||||
it('should parse a basic programmatic node', () => {
|
||||
it('should parse correctly when node is programmatic', () => {
|
||||
const nodeDefinition = programmaticNodeFactory.build();
|
||||
const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
|
||||
|
||||
@@ -66,7 +66,7 @@ describe('NodeParser', () => {
|
||||
expect(mockPropertyExtractor.extractCredentials).toHaveBeenCalledWith(NodeClass);
|
||||
});
|
||||
|
||||
it('should parse a declarative node', () => {
|
||||
it('should parse correctly when node is declarative', () => {
|
||||
const nodeDefinition = declarativeNodeFactory.build();
|
||||
const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
|
||||
|
||||
@@ -76,7 +76,7 @@ describe('NodeParser', () => {
|
||||
expect(result.nodeType).toBe(`nodes-base.${nodeDefinition.name}`);
|
||||
});
|
||||
|
||||
it('should handle node type with package prefix already included', () => {
|
||||
it('should preserve type when package prefix is already included', () => {
|
||||
const nodeDefinition = programmaticNodeFactory.build({
|
||||
name: 'nodes-base.slack'
|
||||
});
|
||||
@@ -87,7 +87,7 @@ describe('NodeParser', () => {
|
||||
expect(result.nodeType).toBe('nodes-base.slack');
|
||||
});
|
||||
|
||||
it('should detect trigger nodes', () => {
|
||||
it('should set isTrigger flag when node is a trigger', () => {
|
||||
const nodeDefinition = triggerNodeFactory.build();
|
||||
const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
|
||||
|
||||
@@ -96,7 +96,7 @@ describe('NodeParser', () => {
|
||||
expect(result.isTrigger).toBe(true);
|
||||
});
|
||||
|
||||
it('should detect webhook nodes', () => {
|
||||
it('should set isWebhook flag when node is a webhook', () => {
|
||||
const nodeDefinition = webhookNodeFactory.build();
|
||||
const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
|
||||
|
||||
@@ -105,7 +105,7 @@ describe('NodeParser', () => {
|
||||
expect(result.isWebhook).toBe(true);
|
||||
});
|
||||
|
||||
it('should detect AI tool capability', () => {
|
||||
it('should set isAITool flag when node has AI capability', () => {
|
||||
const nodeDefinition = aiToolNodeFactory.build();
|
||||
const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
|
||||
|
||||
@@ -116,7 +116,7 @@ describe('NodeParser', () => {
|
||||
expect(result.isAITool).toBe(true);
|
||||
});
|
||||
|
||||
it('should parse versioned nodes with VersionedNodeType class', () => {
|
||||
it('should parse correctly when node uses VersionedNodeType class', () => {
|
||||
// Create a simple versioned node class without modifying function properties
|
||||
const VersionedNodeClass = class VersionedNodeType {
|
||||
baseDescription = {
|
||||
@@ -144,7 +144,7 @@ describe('NodeParser', () => {
|
||||
expect(result.nodeType).toBe('nodes-base.versionedNode');
|
||||
});
|
||||
|
||||
it('should handle versioned nodes with nodeVersions property', () => {
|
||||
it('should parse correctly when node has nodeVersions property', () => {
|
||||
const versionedDef = versionedNodeClassFactory.build();
|
||||
const NodeClass = class {
|
||||
nodeVersions = versionedDef.nodeVersions;
|
||||
@@ -157,7 +157,7 @@ describe('NodeParser', () => {
|
||||
expect(result.version).toBe('2');
|
||||
});
|
||||
|
||||
it('should handle nodes with version array', () => {
|
||||
it('should use max version when version is an array', () => {
|
||||
const nodeDefinition = programmaticNodeFactory.build({
|
||||
version: [1, 1.1, 1.2, 2]
|
||||
});
|
||||
@@ -169,14 +169,14 @@ describe('NodeParser', () => {
|
||||
expect(result.version).toBe('2'); // Should return max version
|
||||
});
|
||||
|
||||
it('should throw error for nodes without name property', () => {
|
||||
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 handle nodes that fail to instantiate', () => {
|
||||
it('should use static description when instantiation fails', () => {
|
||||
const NodeClass = class {
|
||||
static description = programmaticNodeFactory.build();
|
||||
constructor() {
|
||||
@@ -189,7 +189,7 @@ describe('NodeParser', () => {
|
||||
expect(result.displayName).toBe(NodeClass.description.displayName);
|
||||
});
|
||||
|
||||
it('should extract category from different property names', () => {
|
||||
it('should extract category when using different property names', () => {
|
||||
const testCases = [
|
||||
{ group: ['transform'], expected: 'transform' },
|
||||
{ categories: ['output'], expected: 'output' },
|
||||
@@ -211,7 +211,7 @@ describe('NodeParser', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should detect polling trigger nodes', () => {
|
||||
it('should set isTrigger flag when node has polling property', () => {
|
||||
const nodeDefinition = programmaticNodeFactory.build({
|
||||
polling: true
|
||||
});
|
||||
@@ -222,7 +222,7 @@ describe('NodeParser', () => {
|
||||
expect(result.isTrigger).toBe(true);
|
||||
});
|
||||
|
||||
it('should detect event trigger nodes', () => {
|
||||
it('should set isTrigger flag when node has eventTrigger property', () => {
|
||||
const nodeDefinition = programmaticNodeFactory.build({
|
||||
eventTrigger: true
|
||||
});
|
||||
@@ -233,7 +233,7 @@ describe('NodeParser', () => {
|
||||
expect(result.isTrigger).toBe(true);
|
||||
});
|
||||
|
||||
it('should detect trigger nodes by name', () => {
|
||||
it('should set isTrigger flag when node name contains trigger', () => {
|
||||
const nodeDefinition = programmaticNodeFactory.build({
|
||||
name: 'myTrigger'
|
||||
});
|
||||
@@ -244,7 +244,7 @@ describe('NodeParser', () => {
|
||||
expect(result.isTrigger).toBe(true);
|
||||
});
|
||||
|
||||
it('should detect webhook nodes by name', () => {
|
||||
it('should set isWebhook flag when node name contains webhook', () => {
|
||||
const nodeDefinition = programmaticNodeFactory.build({
|
||||
name: 'customWebhook'
|
||||
});
|
||||
@@ -255,7 +255,7 @@ describe('NodeParser', () => {
|
||||
expect(result.isWebhook).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle instance-based nodes', () => {
|
||||
it('should parse correctly when node is an instance object', () => {
|
||||
const nodeDefinition = programmaticNodeFactory.build();
|
||||
const nodeInstance = {
|
||||
description: nodeDefinition
|
||||
|
||||
@@ -226,7 +226,7 @@ describe('PropertyExtractor', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should extract operations from programmatic node properties', () => {
|
||||
it('should extract operations when node has programmatic properties', () => {
|
||||
const operationProp = operationPropertyFactory.build();
|
||||
const NodeClass = nodeClassFactory.build({
|
||||
description: {
|
||||
@@ -247,7 +247,7 @@ describe('PropertyExtractor', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should extract operations from routing.operations structure', () => {
|
||||
it('should extract operations when routing.operations structure exists', () => {
|
||||
const NodeClass = nodeClassFactory.build({
|
||||
description: {
|
||||
name: 'test',
|
||||
@@ -268,7 +268,7 @@ describe('PropertyExtractor', () => {
|
||||
expect(operations).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should handle programmatic nodes with resource-based operations', () => {
|
||||
it('should handle operations when programmatic nodes have resource-based structure', () => {
|
||||
const resourceProp = resourcePropertyFactory.build();
|
||||
const operationProp = {
|
||||
displayName: 'Operation',
|
||||
@@ -309,7 +309,7 @@ describe('PropertyExtractor', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle nodes without operations', () => {
|
||||
it('should return empty array when node has no operations', () => {
|
||||
const NodeClass = nodeClassFactory.build({
|
||||
description: {
|
||||
name: 'test',
|
||||
@@ -322,7 +322,7 @@ describe('PropertyExtractor', () => {
|
||||
expect(operations).toEqual([]);
|
||||
});
|
||||
|
||||
it('should extract from versioned nodes', () => {
|
||||
it('should extract operations when node has version structure', () => {
|
||||
const NodeClass = class {
|
||||
nodeVersions = {
|
||||
1: {
|
||||
@@ -364,7 +364,7 @@ describe('PropertyExtractor', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle action property name as well as operation', () => {
|
||||
it('should handle extraction when property is named action instead of operation', () => {
|
||||
const actionProp = {
|
||||
displayName: 'Action',
|
||||
name: 'action',
|
||||
@@ -390,7 +390,7 @@ describe('PropertyExtractor', () => {
|
||||
});
|
||||
|
||||
describe('detectAIToolCapability', () => {
|
||||
it('should detect direct usableAsTool property', () => {
|
||||
it('should detect AI capability when usableAsTool property is true', () => {
|
||||
const NodeClass = nodeClassFactory.build({
|
||||
description: {
|
||||
name: 'test',
|
||||
@@ -403,7 +403,7 @@ describe('PropertyExtractor', () => {
|
||||
expect(isAITool).toBe(true);
|
||||
});
|
||||
|
||||
it('should detect usableAsTool in actions for declarative nodes', () => {
|
||||
it('should detect AI capability when actions contain usableAsTool', () => {
|
||||
const NodeClass = nodeClassFactory.build({
|
||||
description: {
|
||||
name: 'test',
|
||||
@@ -419,7 +419,7 @@ describe('PropertyExtractor', () => {
|
||||
expect(isAITool).toBe(true);
|
||||
});
|
||||
|
||||
it('should detect AI tools in versioned nodes', () => {
|
||||
it('should detect AI capability when versioned node has usableAsTool', () => {
|
||||
const NodeClass = {
|
||||
nodeVersions: {
|
||||
1: {
|
||||
@@ -436,7 +436,7 @@ describe('PropertyExtractor', () => {
|
||||
expect(isAITool).toBe(true);
|
||||
});
|
||||
|
||||
it('should detect AI tools by node name', () => {
|
||||
it('should detect AI capability when node name contains AI-related terms', () => {
|
||||
const aiNodeNames = ['openai', 'anthropic', 'huggingface', 'cohere', 'myai'];
|
||||
|
||||
aiNodeNames.forEach(name => {
|
||||
@@ -450,7 +450,7 @@ describe('PropertyExtractor', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should not detect non-AI nodes as AI tools', () => {
|
||||
it('should return false when node is not AI-related', () => {
|
||||
const NodeClass = nodeClassFactory.build({
|
||||
description: {
|
||||
name: 'slack',
|
||||
@@ -463,7 +463,7 @@ describe('PropertyExtractor', () => {
|
||||
expect(isAITool).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle nodes without description', () => {
|
||||
it('should return false when node has no description', () => {
|
||||
const NodeClass = class {};
|
||||
|
||||
const isAITool = extractor.detectAIToolCapability(NodeClass);
|
||||
@@ -473,7 +473,7 @@ describe('PropertyExtractor', () => {
|
||||
});
|
||||
|
||||
describe('extractCredentials', () => {
|
||||
it('should extract credentials from node description', () => {
|
||||
it('should extract credentials when node description contains them', () => {
|
||||
const credentials = [
|
||||
{ name: 'apiKey', required: true },
|
||||
{ name: 'oauth2', required: false }
|
||||
@@ -491,7 +491,7 @@ describe('PropertyExtractor', () => {
|
||||
expect(extracted).toEqual(credentials);
|
||||
});
|
||||
|
||||
it('should extract credentials from versioned nodes', () => {
|
||||
it('should extract credentials when node has version structure', () => {
|
||||
const NodeClass = class {
|
||||
nodeVersions = {
|
||||
1: {
|
||||
@@ -517,7 +517,7 @@ describe('PropertyExtractor', () => {
|
||||
expect(credentials[1].name).toBe('apiKey');
|
||||
});
|
||||
|
||||
it('should return empty array when no credentials', () => {
|
||||
it('should return empty array when node has no credentials', () => {
|
||||
const NodeClass = nodeClassFactory.build({
|
||||
description: {
|
||||
name: 'test'
|
||||
@@ -530,7 +530,7 @@ describe('PropertyExtractor', () => {
|
||||
expect(credentials).toEqual([]);
|
||||
});
|
||||
|
||||
it('should extract from baseDescription', () => {
|
||||
it('should extract credentials when only baseDescription has them', () => {
|
||||
const NodeClass = class {
|
||||
baseDescription = {
|
||||
credentials: [{ name: 'token', required: true }]
|
||||
@@ -543,7 +543,7 @@ describe('PropertyExtractor', () => {
|
||||
expect(credentials[0].name).toBe('token');
|
||||
});
|
||||
|
||||
it('should handle instance-level credentials', () => {
|
||||
it('should extract credentials when they are defined at instance level', () => {
|
||||
const NodeClass = class {
|
||||
constructor() {
|
||||
(this as any).description = {
|
||||
@@ -560,7 +560,7 @@ describe('PropertyExtractor', () => {
|
||||
expect(credentials[0].name).toBe('jwt');
|
||||
});
|
||||
|
||||
it('should handle failed instantiation gracefully', () => {
|
||||
it('should return empty array when instantiation fails', () => {
|
||||
const NodeClass = class {
|
||||
constructor() {
|
||||
throw new Error('Cannot instantiate');
|
||||
@@ -574,7 +574,7 @@ describe('PropertyExtractor', () => {
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle deeply nested properties', () => {
|
||||
it('should handle extraction when properties are deeply nested', () => {
|
||||
const deepProperty = {
|
||||
displayName: 'Deep Options',
|
||||
name: 'deepOptions',
|
||||
@@ -612,7 +612,7 @@ describe('PropertyExtractor', () => {
|
||||
expect(properties[0].options[0].options[0].options).toBeDefined();
|
||||
});
|
||||
|
||||
it('should handle circular references in node structure', () => {
|
||||
it('should not throw when node structure has circular references', () => {
|
||||
const NodeClass = class {
|
||||
description: any = { name: 'test' };
|
||||
constructor() {
|
||||
@@ -632,7 +632,7 @@ describe('PropertyExtractor', () => {
|
||||
expect(properties).toBeDefined();
|
||||
});
|
||||
|
||||
it('should handle mixed operation extraction scenarios', () => {
|
||||
it('should extract from all sources when multiple operation types exist', () => {
|
||||
const NodeClass = nodeClassFactory.build({
|
||||
description: {
|
||||
name: 'test',
|
||||
|
||||
Reference in New Issue
Block a user