mirror of
https://github.com/n8n-io/n8n.git
synced 2026-09-21 12:51:16 +08:00
397 lines
11 KiB
TypeScript
397 lines
11 KiB
TypeScript
import { generateNanoId } from '@n8n/utils/generate-nano-id';
|
|
import type { INodeType } from 'n8n-workflow';
|
|
|
|
import {
|
|
shouldAssignExecuteMethod,
|
|
getAllKeyPaths,
|
|
isWorkflowIdValid,
|
|
setMicrosoftObservabilityDefaults,
|
|
containsExpression,
|
|
stripToolSuffix,
|
|
} from '../utils';
|
|
|
|
describe('stripToolSuffix', () => {
|
|
it.each([
|
|
['@n8n/n8n-nodes-langchain.openAi', '@n8n/n8n-nodes-langchain.openAi'],
|
|
['@n8n/n8n-nodes-langchain.openAiTool', '@n8n/n8n-nodes-langchain.openAi'],
|
|
['@n8n/n8n-nodes-langchain.openAiHitlTool', '@n8n/n8n-nodes-langchain.openAi'],
|
|
['@n8n/n8n-nodes-langchain.slackTool', '@n8n/n8n-nodes-langchain.slack'],
|
|
['plain', 'plain'],
|
|
['n8n-nodes-base.set', 'n8n-nodes-base.set'],
|
|
])('strips %s -> %s', (input, expected) => {
|
|
expect(stripToolSuffix(input)).toBe(expected);
|
|
});
|
|
});
|
|
|
|
describe('shouldAssignExecuteMethod', () => {
|
|
it('should return true when node has no execute, poll, trigger, webhook (unless declarative), or methods', () => {
|
|
const nodeType = {
|
|
description: { requestDefaults: {} }, // Declarative node
|
|
execute: undefined,
|
|
poll: undefined,
|
|
trigger: undefined,
|
|
webhook: undefined,
|
|
methods: undefined,
|
|
} as INodeType;
|
|
|
|
expect(shouldAssignExecuteMethod(nodeType)).toBe(true);
|
|
});
|
|
|
|
it('should return false when node has execute', () => {
|
|
const nodeType = {
|
|
execute: vi.fn(),
|
|
} as unknown as INodeType;
|
|
|
|
expect(shouldAssignExecuteMethod(nodeType)).toBe(false);
|
|
});
|
|
|
|
it('should return false when node has poll', () => {
|
|
const nodeType = {
|
|
poll: vi.fn(),
|
|
} as unknown as INodeType;
|
|
|
|
expect(shouldAssignExecuteMethod(nodeType)).toBe(false);
|
|
});
|
|
|
|
it('should return false when node has trigger', () => {
|
|
const nodeType = {
|
|
trigger: vi.fn(),
|
|
} as unknown as INodeType;
|
|
|
|
expect(shouldAssignExecuteMethod(nodeType)).toBe(false);
|
|
});
|
|
|
|
it('should return false when node has webhook and is not declarative', () => {
|
|
const nodeType = {
|
|
description: {},
|
|
webhook: vi.fn(),
|
|
} as unknown as INodeType;
|
|
|
|
expect(shouldAssignExecuteMethod(nodeType)).toBe(false);
|
|
});
|
|
|
|
it('should return true when node has webhook but is declarative', () => {
|
|
const nodeType = {
|
|
description: { requestDefaults: {} }, // Declarative node
|
|
webhook: vi.fn(),
|
|
} as unknown as INodeType;
|
|
|
|
expect(shouldAssignExecuteMethod(nodeType)).toBe(true);
|
|
});
|
|
|
|
it('should return false when node has methods and is not declarative', () => {
|
|
const nodeType = {
|
|
methods: {},
|
|
} as unknown as INodeType;
|
|
|
|
expect(shouldAssignExecuteMethod(nodeType)).toBe(false);
|
|
});
|
|
|
|
it('should return true when node has methods but is declarative', () => {
|
|
const nodeType = {
|
|
description: { requestDefaults: {} }, // Declarative node
|
|
methods: {},
|
|
} as unknown as INodeType;
|
|
|
|
expect(shouldAssignExecuteMethod(nodeType)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('getAllKeyPaths', () => {
|
|
const testCases = [
|
|
{
|
|
description: 'should return empty array for null or undefined',
|
|
obj: null,
|
|
valueFilter: (value: string) => value.includes('test'),
|
|
expected: [],
|
|
},
|
|
{
|
|
description: 'should return empty array for undefined',
|
|
obj: undefined,
|
|
valueFilter: (value: string) => value.includes('test'),
|
|
expected: [],
|
|
},
|
|
{
|
|
description: 'should find keys with matching string values in objects',
|
|
obj: {
|
|
name: 'test',
|
|
age: 25,
|
|
description: 'contains test data',
|
|
other: 'no match',
|
|
},
|
|
valueFilter: (value: string) => value.includes('test'),
|
|
expected: ['name', 'description'],
|
|
},
|
|
{
|
|
description: 'should recursively search nested objects',
|
|
obj: {
|
|
user: {
|
|
name: 'testuser',
|
|
profile: {
|
|
bio: 'test bio',
|
|
},
|
|
},
|
|
settings: {
|
|
theme: 'dark',
|
|
},
|
|
},
|
|
valueFilter: (value: string) => value.includes('test'),
|
|
expected: ['user.name', 'user.profile.bio'],
|
|
},
|
|
{
|
|
description: 'should search arrays',
|
|
obj: [{ name: 'test1' }, { name: 'other' }, { name: 'test2' }],
|
|
valueFilter: (value: string) => value.includes('test'),
|
|
expected: ['[0].name', '[2].name'],
|
|
},
|
|
{
|
|
description: 'should handle mixed arrays and objects',
|
|
obj: {
|
|
items: [{ label: 'test item' }, { label: 'normal item' }],
|
|
title: 'test title',
|
|
},
|
|
valueFilter: (value: string) => value.includes('test'),
|
|
expected: ['items[0].label', 'title'],
|
|
},
|
|
{
|
|
description: 'should handle non-string values by ignoring them',
|
|
obj: {
|
|
name: 'test',
|
|
count: 42,
|
|
active: true,
|
|
data: null,
|
|
},
|
|
valueFilter: (value: string) => value.includes('test'),
|
|
expected: ['name'],
|
|
},
|
|
];
|
|
|
|
it.each(testCases)('$description', ({ obj, valueFilter, expected }) => {
|
|
const result = getAllKeyPaths(obj, '', [], valueFilter);
|
|
expect(result).toEqual(expected);
|
|
});
|
|
});
|
|
|
|
describe('isWorkflowIdValid', () => {
|
|
describe('valid IDs', () => {
|
|
it('should accept n8n-generated workflow ID (16 characters)', () => {
|
|
const id = generateNanoId();
|
|
expect(id).toHaveLength(16);
|
|
expect(isWorkflowIdValid(id)).toBe(true);
|
|
});
|
|
|
|
it('should accept multiple generated workflow IDs', () => {
|
|
// Test 10 randomly generated IDs to ensure consistency
|
|
for (let i = 0; i < 10; i++) {
|
|
const id = generateNanoId();
|
|
expect(isWorkflowIdValid(id)).toBe(true);
|
|
}
|
|
});
|
|
|
|
it('should accept 21-character IDs (standard nanoid length)', () => {
|
|
const id = 'Gi4nXQITSnE_lj8ZA9hRa'; // 21 characters
|
|
expect(isWorkflowIdValid(id)).toBe(true);
|
|
});
|
|
|
|
it('should accept minimum length (1 character)', () => {
|
|
expect(isWorkflowIdValid('a')).toBe(true);
|
|
});
|
|
|
|
it('should accept maximum allowed length (21 characters)', () => {
|
|
const id = 'a'.repeat(21);
|
|
expect(isWorkflowIdValid(id)).toBe(true);
|
|
});
|
|
|
|
it('should accept various lengths between 1 and 21', () => {
|
|
[1, 8, 16, 20, 21].forEach((length) => {
|
|
const id = 'a'.repeat(length);
|
|
expect(isWorkflowIdValid(id)).toBe(true);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('invalid IDs', () => {
|
|
it('should reject null', () => {
|
|
expect(isWorkflowIdValid(null)).toBe(false);
|
|
});
|
|
|
|
it('should reject undefined', () => {
|
|
expect(isWorkflowIdValid(undefined)).toBe(false);
|
|
});
|
|
|
|
it('should reject empty string', () => {
|
|
expect(isWorkflowIdValid('')).toBe(false);
|
|
});
|
|
|
|
it('should reject IDs longer than maximum allowed length (22+ characters)', () => {
|
|
const tooLong = 'a'.repeat(22);
|
|
expect(isWorkflowIdValid(tooLong)).toBe(false);
|
|
});
|
|
|
|
it('should reject non-string values', () => {
|
|
expect(isWorkflowIdValid(123 as any)).toBe(false);
|
|
expect(isWorkflowIdValid({} as any)).toBe(false);
|
|
expect(isWorkflowIdValid([] as any)).toBe(false);
|
|
});
|
|
|
|
it('should reject very long strings', () => {
|
|
const veryLong = 'a'.repeat(100);
|
|
expect(isWorkflowIdValid(veryLong)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('edge cases', () => {
|
|
it('should handle special characters in valid length IDs', () => {
|
|
expect(isWorkflowIdValid('abc-def_123')).toBe(true);
|
|
expect(isWorkflowIdValid('workflow.id.test')).toBe(true);
|
|
});
|
|
|
|
it('should accept IDs with numbers only', () => {
|
|
expect(isWorkflowIdValid('1234567890')).toBe(true);
|
|
});
|
|
|
|
it('should accept IDs with mixed case', () => {
|
|
expect(isWorkflowIdValid('AbCdEfGhIjKlMnOp')).toBe(true);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('setMicrosoftObservabilityDefaults', () => {
|
|
const originalEnv = process.env;
|
|
|
|
beforeEach(() => {
|
|
vi.resetModules();
|
|
process.env = { ...originalEnv };
|
|
});
|
|
|
|
afterEach(() => {
|
|
process.env = originalEnv;
|
|
});
|
|
|
|
test('should set ENABLE_OBSERVABILITY to true when undefined', () => {
|
|
delete process.env.ENABLE_OBSERVABILITY;
|
|
|
|
setMicrosoftObservabilityDefaults();
|
|
|
|
expect(process.env.ENABLE_OBSERVABILITY).toBe('true');
|
|
});
|
|
|
|
test('should set ENABLE_OBSERVABILITY to true when empty string', () => {
|
|
process.env.ENABLE_OBSERVABILITY = '';
|
|
|
|
setMicrosoftObservabilityDefaults();
|
|
|
|
expect(process.env.ENABLE_OBSERVABILITY).toBe('true');
|
|
});
|
|
|
|
test('should not override ENABLE_OBSERVABILITY when already set', () => {
|
|
process.env.ENABLE_OBSERVABILITY = 'false';
|
|
|
|
setMicrosoftObservabilityDefaults();
|
|
|
|
expect(process.env.ENABLE_OBSERVABILITY).toBe('false');
|
|
});
|
|
|
|
test('should set ENABLE_A365_OBSERVABILITY_EXPORTER to true when undefined', () => {
|
|
delete process.env.ENABLE_A365_OBSERVABILITY_EXPORTER;
|
|
|
|
setMicrosoftObservabilityDefaults();
|
|
|
|
expect(process.env.ENABLE_A365_OBSERVABILITY_EXPORTER).toBe('true');
|
|
});
|
|
|
|
test('should set ENABLE_A365_OBSERVABILITY_EXPORTER to true when empty string', () => {
|
|
process.env.ENABLE_A365_OBSERVABILITY_EXPORTER = '';
|
|
|
|
setMicrosoftObservabilityDefaults();
|
|
|
|
expect(process.env.ENABLE_A365_OBSERVABILITY_EXPORTER).toBe('true');
|
|
});
|
|
|
|
test('should not override ENABLE_A365_OBSERVABILITY_EXPORTER when already set', () => {
|
|
process.env.ENABLE_A365_OBSERVABILITY_EXPORTER = 'false';
|
|
|
|
setMicrosoftObservabilityDefaults();
|
|
|
|
expect(process.env.ENABLE_A365_OBSERVABILITY_EXPORTER).toBe('false');
|
|
});
|
|
|
|
test('should set both environment variables when both are undefined', () => {
|
|
delete process.env.ENABLE_OBSERVABILITY;
|
|
delete process.env.ENABLE_A365_OBSERVABILITY_EXPORTER;
|
|
|
|
setMicrosoftObservabilityDefaults();
|
|
|
|
expect(process.env.ENABLE_OBSERVABILITY).toBe('true');
|
|
expect(process.env.ENABLE_A365_OBSERVABILITY_EXPORTER).toBe('true');
|
|
});
|
|
|
|
test('should set both environment variables when both are empty strings', () => {
|
|
process.env.ENABLE_OBSERVABILITY = '';
|
|
process.env.ENABLE_A365_OBSERVABILITY_EXPORTER = '';
|
|
|
|
setMicrosoftObservabilityDefaults();
|
|
|
|
expect(process.env.ENABLE_OBSERVABILITY).toBe('true');
|
|
expect(process.env.ENABLE_A365_OBSERVABILITY_EXPORTER).toBe('true');
|
|
});
|
|
|
|
test('should handle mixed scenarios correctly', () => {
|
|
process.env.ENABLE_OBSERVABILITY = 'custom-value';
|
|
delete process.env.ENABLE_A365_OBSERVABILITY_EXPORTER;
|
|
|
|
setMicrosoftObservabilityDefaults();
|
|
|
|
expect(process.env.ENABLE_OBSERVABILITY).toBe('custom-value');
|
|
expect(process.env.ENABLE_A365_OBSERVABILITY_EXPORTER).toBe('true');
|
|
});
|
|
});
|
|
|
|
describe('containsExpression', () => {
|
|
it('returns true for a simple expression with {{...}}', () => {
|
|
expect(containsExpression('={{value}}')).toBe(true);
|
|
});
|
|
|
|
it('returns true when {{...}} appears later in the string', () => {
|
|
expect(containsExpression('=hello {{world}}')).toBe(true);
|
|
});
|
|
|
|
it('returns true when there is content inside the braces (including spaces)', () => {
|
|
expect(containsExpression('={{ value }}')).toBe(true);
|
|
});
|
|
|
|
it('returns true when there is trailing content after the }}', () => {
|
|
expect(containsExpression('={{value}} + 1')).toBe(true);
|
|
});
|
|
|
|
it('returns false when the string does not start with "="', () => {
|
|
expect(containsExpression('hello {{world}}')).toBe(false);
|
|
});
|
|
|
|
it('returns false when the string starts with "=" but has no {{...}}', () => {
|
|
expect(containsExpression('=hello world')).toBe(false);
|
|
});
|
|
|
|
it('returns false for empty placeholder {{}}', () => {
|
|
expect(containsExpression('={{}}')).toBe(false);
|
|
});
|
|
|
|
it('returns false when the string uses single braces {..} instead of double {{..}}', () => {
|
|
expect(containsExpression('={value}')).toBe(false);
|
|
});
|
|
|
|
it('returns false when the braces are incomplete', () => {
|
|
expect(containsExpression('={{value}')).toBe(false);
|
|
expect(containsExpression('={value}}')).toBe(false);
|
|
expect(containsExpression('={{value')).toBe(false);
|
|
});
|
|
|
|
it('returns true when there are multiple placeholders and at least one matches', () => {
|
|
expect(containsExpression('=x {{a}} y {{b}}')).toBe(true);
|
|
});
|
|
|
|
it('returns false when the string is empty', () => {
|
|
expect(containsExpression('')).toBe(false);
|
|
});
|
|
});
|