first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,371 @@
|
||||
import type { INode, INodeType, IConnections, INodeTypeDescription } from '../src/interfaces';
|
||||
import {
|
||||
validateNodeCredentials,
|
||||
isNodeConnected,
|
||||
isTriggerLikeNode,
|
||||
type NodeCredentialIssue,
|
||||
} from '../src/node-validation';
|
||||
|
||||
describe('node-validation', () => {
|
||||
describe('validateNodeCredentials', () => {
|
||||
const createNode = (
|
||||
credentials?: Record<string, { id: string }>,
|
||||
parameters?: Record<string, unknown>,
|
||||
): INode => ({
|
||||
name: 'Test Node',
|
||||
type: 'n8n-nodes-base.test',
|
||||
id: 'node-1',
|
||||
typeVersion: 1,
|
||||
position: [0, 0],
|
||||
credentials: credentials as INode['credentials'],
|
||||
parameters: (parameters || {}) as INode['parameters'],
|
||||
});
|
||||
|
||||
const createNodeType = (credentials?: INodeTypeDescription['credentials']): INodeType => ({
|
||||
description: {
|
||||
displayName: 'Test Node',
|
||||
name: 'test',
|
||||
group: ['transform'],
|
||||
version: 1,
|
||||
description: 'Test node',
|
||||
defaults: { name: 'Test Node' },
|
||||
inputs: ['main'],
|
||||
outputs: ['main'],
|
||||
properties: [],
|
||||
credentials: credentials || [],
|
||||
},
|
||||
});
|
||||
|
||||
it('should return no issues when node has all required credentials', () => {
|
||||
const node = createNode({ testCredential: { id: 'cred-1' } });
|
||||
const nodeType = createNodeType([
|
||||
{
|
||||
name: 'testCredential',
|
||||
displayName: 'Test Credential',
|
||||
required: true,
|
||||
},
|
||||
]);
|
||||
|
||||
const issues = validateNodeCredentials(node, nodeType);
|
||||
|
||||
expect(issues).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return missing issue when required credential is not set', () => {
|
||||
const node = createNode();
|
||||
const nodeType = createNodeType([
|
||||
{
|
||||
name: 'testCredential',
|
||||
displayName: 'Test Credential',
|
||||
required: true,
|
||||
},
|
||||
]);
|
||||
|
||||
const issues = validateNodeCredentials(node, nodeType);
|
||||
|
||||
expect(issues).toEqual([
|
||||
{
|
||||
type: 'missing',
|
||||
displayName: 'Test Credential',
|
||||
credentialName: 'testCredential',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return not-configured issue when credential has no ID', () => {
|
||||
const node = createNode({ testCredential: { id: '' } });
|
||||
const nodeType = createNodeType([
|
||||
{
|
||||
name: 'testCredential',
|
||||
displayName: 'Test Credential',
|
||||
required: true,
|
||||
},
|
||||
]);
|
||||
|
||||
const issues = validateNodeCredentials(node, nodeType);
|
||||
|
||||
expect(issues).toEqual([
|
||||
{
|
||||
type: 'not-configured',
|
||||
displayName: 'Test Credential',
|
||||
credentialName: 'testCredential',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should skip optional credentials', () => {
|
||||
const node = createNode();
|
||||
const nodeType = createNodeType([
|
||||
{
|
||||
name: 'optionalCredential',
|
||||
displayName: 'Optional Credential',
|
||||
required: false,
|
||||
},
|
||||
]);
|
||||
|
||||
const issues = validateNodeCredentials(node, nodeType);
|
||||
|
||||
expect(issues).toEqual([]);
|
||||
});
|
||||
|
||||
it('should respect displayOptions and skip hidden credentials', () => {
|
||||
const node = createNode(undefined, { authentication: 'none' });
|
||||
const nodeType = createNodeType([
|
||||
{
|
||||
name: 'basicAuth',
|
||||
displayName: 'Basic Auth',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
authentication: ['basicAuth'],
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const issues = validateNodeCredentials(node, nodeType);
|
||||
|
||||
// Should be empty because basicAuth is hidden when authentication='none'
|
||||
expect(issues).toEqual([]);
|
||||
});
|
||||
|
||||
it('should validate credentials when displayOptions match', () => {
|
||||
const node = createNode(undefined, { authentication: 'basicAuth' });
|
||||
const nodeType = createNodeType([
|
||||
{
|
||||
name: 'basicAuth',
|
||||
displayName: 'Basic Auth',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
authentication: ['basicAuth'],
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const issues = validateNodeCredentials(node, nodeType);
|
||||
|
||||
// Should have issue because basicAuth is shown but not set
|
||||
expect(issues).toEqual([
|
||||
{
|
||||
type: 'missing',
|
||||
displayName: 'Basic Auth',
|
||||
credentialName: 'basicAuth',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return multiple issues for multiple missing credentials', () => {
|
||||
const node = createNode();
|
||||
const nodeType = createNodeType([
|
||||
{
|
||||
name: 'credential1',
|
||||
displayName: 'Credential 1',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'credential2',
|
||||
displayName: 'Credential 2',
|
||||
required: true,
|
||||
},
|
||||
]);
|
||||
|
||||
const issues = validateNodeCredentials(node, nodeType);
|
||||
|
||||
expect(issues).toHaveLength(2);
|
||||
expect(issues).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ credentialName: 'credential1' }),
|
||||
expect.objectContaining({ credentialName: 'credential2' }),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should use credential name as displayName fallback', () => {
|
||||
const node = createNode();
|
||||
const nodeType = createNodeType([
|
||||
{
|
||||
name: 'testCredential',
|
||||
required: true,
|
||||
// No displayName provided
|
||||
} as any,
|
||||
]);
|
||||
|
||||
const issues = validateNodeCredentials(node, nodeType);
|
||||
|
||||
expect(issues[0].displayName).toBe('testCredential');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isNodeConnected', () => {
|
||||
it('should return true when node has outgoing connections', () => {
|
||||
const connections: IConnections = {
|
||||
'Node A': {
|
||||
main: [[{ node: 'Node B', type: 'main', index: 0 }]],
|
||||
},
|
||||
};
|
||||
const connectionsByDestination: IConnections = {};
|
||||
|
||||
const result = isNodeConnected('Node A', connections, connectionsByDestination);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when node has incoming connections', () => {
|
||||
const connections: IConnections = {};
|
||||
const connectionsByDestination: IConnections = {
|
||||
'Node B': {
|
||||
main: [[{ node: 'Node A', type: 'main', index: 0 }]],
|
||||
},
|
||||
};
|
||||
|
||||
const result = isNodeConnected('Node B', connections, connectionsByDestination);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when node has both incoming and outgoing connections', () => {
|
||||
const connections: IConnections = {
|
||||
'Node B': {
|
||||
main: [[{ node: 'Node C', type: 'main', index: 0 }]],
|
||||
},
|
||||
};
|
||||
const connectionsByDestination: IConnections = {
|
||||
'Node B': {
|
||||
main: [[{ node: 'Node A', type: 'main', index: 0 }]],
|
||||
},
|
||||
};
|
||||
|
||||
const result = isNodeConnected('Node B', connections, connectionsByDestination);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when node has no connections', () => {
|
||||
const connections: IConnections = {
|
||||
'Node A': {
|
||||
main: [[{ node: 'Node B', type: 'main', index: 0 }]],
|
||||
},
|
||||
};
|
||||
const connectionsByDestination: IConnections = {};
|
||||
|
||||
const result = isNodeConnected('Node C', connections, connectionsByDestination);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when node exists but has empty connections', () => {
|
||||
const connections: IConnections = {
|
||||
'Node A': {},
|
||||
};
|
||||
const connectionsByDestination: IConnections = {
|
||||
'Node A': {},
|
||||
};
|
||||
|
||||
const result = isNodeConnected('Node A', connections, connectionsByDestination);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isTriggerLikeNode', () => {
|
||||
it('should return true for node with trigger function', () => {
|
||||
const nodeType: INodeType = {
|
||||
description: {
|
||||
displayName: 'Trigger Node',
|
||||
name: 'trigger',
|
||||
group: ['trigger'],
|
||||
version: 1,
|
||||
description: 'Test trigger',
|
||||
defaults: { name: 'Trigger' },
|
||||
inputs: [],
|
||||
outputs: ['main'],
|
||||
properties: [],
|
||||
},
|
||||
trigger: async () => ({
|
||||
closeFunction: async () => {},
|
||||
manualTriggerFunction: async () => {},
|
||||
}),
|
||||
};
|
||||
|
||||
expect(isTriggerLikeNode(nodeType)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for node with webhook function', () => {
|
||||
const nodeType: INodeType = {
|
||||
description: {
|
||||
displayName: 'Webhook Node',
|
||||
name: 'webhook',
|
||||
group: ['trigger'],
|
||||
version: 1,
|
||||
description: 'Test webhook',
|
||||
defaults: { name: 'Webhook' },
|
||||
inputs: [],
|
||||
outputs: ['main'],
|
||||
properties: [],
|
||||
},
|
||||
webhook: async () => ({ workflowData: [[]] }),
|
||||
};
|
||||
|
||||
expect(isTriggerLikeNode(nodeType)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for node with poll function', () => {
|
||||
const nodeType: INodeType = {
|
||||
description: {
|
||||
displayName: 'Poll Node',
|
||||
name: 'poll',
|
||||
group: ['trigger'],
|
||||
version: 1,
|
||||
description: 'Test poll',
|
||||
defaults: { name: 'Poll' },
|
||||
inputs: [],
|
||||
outputs: ['main'],
|
||||
properties: [],
|
||||
},
|
||||
poll: async () => [[]],
|
||||
};
|
||||
|
||||
expect(isTriggerLikeNode(nodeType)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for regular node', () => {
|
||||
const nodeType: INodeType = {
|
||||
description: {
|
||||
displayName: 'Regular Node',
|
||||
name: 'regular',
|
||||
group: ['transform'],
|
||||
version: 1,
|
||||
description: 'Test regular node',
|
||||
defaults: { name: 'Regular' },
|
||||
inputs: ['main'],
|
||||
outputs: ['main'],
|
||||
properties: [],
|
||||
},
|
||||
execute: async () => [[]],
|
||||
};
|
||||
|
||||
expect(isTriggerLikeNode(nodeType)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for node with only execute function', () => {
|
||||
const nodeType: INodeType = {
|
||||
description: {
|
||||
displayName: 'Execute Node',
|
||||
name: 'execute',
|
||||
group: ['transform'],
|
||||
version: 1,
|
||||
description: 'Test execute node',
|
||||
defaults: { name: 'Execute' },
|
||||
inputs: ['main'],
|
||||
outputs: ['main'],
|
||||
properties: [],
|
||||
},
|
||||
execute: async () => [[]],
|
||||
};
|
||||
|
||||
expect(isTriggerLikeNode(nodeType)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user