mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-20 03:12:15 +00:00
feat(editor): Refactor the executable node logic (no-changelog) (#16848)
This commit is contained in:
@@ -1,13 +1,23 @@
|
||||
import { setActivePinia } from 'pinia';
|
||||
import type { INode, INodeTypeDescription, Workflow } from 'n8n-workflow';
|
||||
import {
|
||||
NodeConnectionTypes,
|
||||
NodeHelpers,
|
||||
type INode,
|
||||
type INodeTypeDescription,
|
||||
type Workflow,
|
||||
} from 'n8n-workflow';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { useNodeHelpers } from '@/composables/useNodeHelpers';
|
||||
import { createTestNode } from '@/__tests__/mocks';
|
||||
import { createTestNode, createMockEnterpriseSettings } from '@/__tests__/mocks';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import { CUSTOM_API_CALL_KEY } from '@/constants';
|
||||
import { useSettingsStore } from '@/stores/settings.store';
|
||||
import { CUSTOM_API_CALL_KEY, EnterpriseEditionFeature } from '@/constants';
|
||||
import { mockedStore } from '@/__tests__/utils';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
import type { ExecutionStatus, IRunData } from 'n8n-workflow';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import type { INodeUi, IUsedCredential } from '@/Interface';
|
||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||
|
||||
describe('useNodeHelpers()', () => {
|
||||
beforeAll(() => {
|
||||
@@ -18,6 +28,293 @@ describe('useNodeHelpers()', () => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('isNodeExecutable()', () => {
|
||||
it('should return true if the node is null but explicitly executable', () => {
|
||||
const { isNodeExecutable } = useNodeHelpers();
|
||||
|
||||
const result = isNodeExecutable(null, true, []);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if node has no Main input and is not trigger or tool', () => {
|
||||
const { isNodeExecutable } = useNodeHelpers();
|
||||
|
||||
const node: INodeUi = {
|
||||
id: 'node-id',
|
||||
name: 'Code',
|
||||
type: 'n8n-nodes-base.code',
|
||||
typeVersion: 1,
|
||||
position: [0, 0],
|
||||
parameters: {},
|
||||
};
|
||||
|
||||
const mockWorkflow = {
|
||||
id: 'workflow-id',
|
||||
getNode: () => node,
|
||||
};
|
||||
|
||||
mockedStore(useWorkflowsStore).getCurrentWorkflow = vi.fn().mockReturnValue(mockWorkflow);
|
||||
mockedStore(useNodeTypesStore).getNodeType = vi.fn().mockReturnValue({});
|
||||
mockedStore(useNodeTypesStore).isTriggerNode = vi.fn().mockReturnValue(false);
|
||||
mockedStore(useNodeTypesStore).isToolNode = vi.fn().mockReturnValue(false);
|
||||
vi.spyOn(NodeHelpers, 'getNodeInputs').mockReturnValue([]);
|
||||
vi.spyOn(NodeHelpers, 'getConnectionTypes').mockReturnValue([NodeConnectionTypes.AiDocument]);
|
||||
|
||||
const result = isNodeExecutable(node, true, []);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true if node has Main input and is marked executable', () => {
|
||||
const { isNodeExecutable } = useNodeHelpers();
|
||||
|
||||
const node: INodeUi = {
|
||||
id: 'node-id',
|
||||
name: 'Code',
|
||||
type: 'n8n-nodes-base.code',
|
||||
typeVersion: 1,
|
||||
position: [0, 0],
|
||||
parameters: {},
|
||||
};
|
||||
|
||||
const mockWorkflow = {
|
||||
getNode: () => node,
|
||||
};
|
||||
|
||||
mockedStore(useWorkflowsStore).getCurrentWorkflow = vi.fn().mockReturnValue(mockWorkflow);
|
||||
mockedStore(useNodeTypesStore).getNodeType = vi.fn().mockReturnValue({});
|
||||
mockedStore(useNodeTypesStore).isTriggerNode = vi.fn().mockReturnValue(false);
|
||||
mockedStore(useNodeTypesStore).isToolNode = vi.fn().mockReturnValue(false);
|
||||
vi.spyOn(NodeHelpers, 'getNodeInputs').mockReturnValue([]);
|
||||
vi.spyOn(NodeHelpers, 'getConnectionTypes').mockReturnValue([NodeConnectionTypes.Main]);
|
||||
|
||||
const result = isNodeExecutable(node, true, []);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true if node has foreign credentials even if not marked executable', () => {
|
||||
const { isNodeExecutable } = useNodeHelpers();
|
||||
|
||||
const node: INodeUi = {
|
||||
id: 'node-id',
|
||||
name: 'Code',
|
||||
type: 'n8n-nodes-base.code',
|
||||
typeVersion: 1,
|
||||
position: [0, 0],
|
||||
parameters: {},
|
||||
};
|
||||
|
||||
const mockWorkflow = {
|
||||
getNode: () => node,
|
||||
};
|
||||
|
||||
mockedStore(useWorkflowsStore).getCurrentWorkflow = vi.fn().mockReturnValue(mockWorkflow);
|
||||
mockedStore(useNodeTypesStore).getNodeType = vi.fn().mockReturnValue({});
|
||||
mockedStore(useNodeTypesStore).isTriggerNode = vi.fn().mockReturnValue(false);
|
||||
mockedStore(useNodeTypesStore).isToolNode = vi.fn().mockReturnValue(false);
|
||||
vi.spyOn(NodeHelpers, 'getNodeInputs').mockReturnValue([]);
|
||||
vi.spyOn(NodeHelpers, 'getConnectionTypes').mockReturnValue([NodeConnectionTypes.Main]);
|
||||
|
||||
const result = isNodeExecutable(node, false, ['foreign-cred-id']);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for trigger nodes regardless of inputs', () => {
|
||||
const { isNodeExecutable } = useNodeHelpers();
|
||||
|
||||
const triggerNode: INodeUi = {
|
||||
id: 'node-id',
|
||||
name: 'Manual Trigger',
|
||||
type: 'n8n-nodes-base.manualTrigger',
|
||||
typeVersion: 1,
|
||||
position: [0, 0],
|
||||
parameters: {},
|
||||
};
|
||||
|
||||
const mockWorkflow = {
|
||||
getNode: () => triggerNode,
|
||||
};
|
||||
|
||||
mockedStore(useWorkflowsStore).getCurrentWorkflow = vi.fn().mockReturnValue(mockWorkflow);
|
||||
mockedStore(useNodeTypesStore).getNodeType = vi.fn().mockReturnValue({});
|
||||
mockedStore(useNodeTypesStore).isTriggerNode = vi.fn().mockReturnValue(true);
|
||||
mockedStore(useNodeTypesStore).isToolNode = vi.fn().mockReturnValue(false);
|
||||
vi.spyOn(NodeHelpers, 'getNodeInputs').mockReturnValue([]);
|
||||
vi.spyOn(NodeHelpers, 'getConnectionTypes').mockReturnValue([]);
|
||||
|
||||
const result = isNodeExecutable(triggerNode, true, []);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for tool nodes regardless of inputs', () => {
|
||||
const { isNodeExecutable } = useNodeHelpers();
|
||||
|
||||
const toolNode: INodeUi = {
|
||||
id: 'node-id',
|
||||
name: 'Tool Node',
|
||||
type: 'n8n-nodes-base.ai-tool',
|
||||
typeVersion: 1,
|
||||
position: [0, 0],
|
||||
parameters: {},
|
||||
};
|
||||
|
||||
const mockWorkflow = {
|
||||
getNode: () => toolNode,
|
||||
};
|
||||
|
||||
mockedStore(useWorkflowsStore).getCurrentWorkflow = vi.fn().mockReturnValue(mockWorkflow);
|
||||
mockedStore(useNodeTypesStore).getNodeType = vi.fn().mockReturnValue({});
|
||||
mockedStore(useNodeTypesStore).isTriggerNode = vi.fn().mockReturnValue(false);
|
||||
mockedStore(useNodeTypesStore).isToolNode = vi.fn().mockReturnValue(true);
|
||||
vi.spyOn(NodeHelpers, 'getNodeInputs').mockReturnValue([]);
|
||||
vi.spyOn(NodeHelpers, 'getConnectionTypes').mockReturnValue([]);
|
||||
|
||||
const result = isNodeExecutable(toolNode, true, []);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true if node is structurally valid and has foreign credentials, even if not executable', () => {
|
||||
const { isNodeExecutable } = useNodeHelpers();
|
||||
|
||||
const node: INodeUi = {
|
||||
id: 'node-id',
|
||||
name: 'Code',
|
||||
type: 'n8n-nodes-base.code',
|
||||
typeVersion: 1,
|
||||
position: [0, 0],
|
||||
parameters: {},
|
||||
};
|
||||
|
||||
const mockWorkflow = {
|
||||
getNode: () => node,
|
||||
};
|
||||
|
||||
mockedStore(useWorkflowsStore).getCurrentWorkflow = vi.fn().mockReturnValue(mockWorkflow);
|
||||
mockedStore(useNodeTypesStore).getNodeType = vi.fn().mockReturnValue({});
|
||||
mockedStore(useNodeTypesStore).isTriggerNode = vi.fn().mockReturnValue(false);
|
||||
mockedStore(useNodeTypesStore).isToolNode = vi.fn().mockReturnValue(false);
|
||||
vi.spyOn(NodeHelpers, 'getNodeInputs').mockReturnValue([]);
|
||||
vi.spyOn(NodeHelpers, 'getConnectionTypes').mockReturnValue([NodeConnectionTypes.Main]);
|
||||
|
||||
const result = isNodeExecutable(node, false, ['cred-1']);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getForeignCredentialsIfSharingEnabled()', () => {
|
||||
it('should return an empty array when user has the wrong license', () => {
|
||||
const { getForeignCredentialsIfSharingEnabled } = useNodeHelpers();
|
||||
|
||||
const credentialWithoutAccess: IUsedCredential = {
|
||||
id: faker.string.alphanumeric(10),
|
||||
credentialType: 'generic',
|
||||
name: faker.lorem.words(2),
|
||||
currentUserHasAccess: false,
|
||||
};
|
||||
|
||||
mockedStore(useSettingsStore).isEnterpriseFeatureEnabled = createMockEnterpriseSettings({
|
||||
[EnterpriseEditionFeature.Sharing]: false,
|
||||
});
|
||||
mockedStore(useWorkflowsStore).usedCredentials = {
|
||||
[credentialWithoutAccess.id]: credentialWithoutAccess,
|
||||
};
|
||||
|
||||
const result = getForeignCredentialsIfSharingEnabled({
|
||||
[credentialWithoutAccess.id]: {
|
||||
id: credentialWithoutAccess.id,
|
||||
name: credentialWithoutAccess.name,
|
||||
},
|
||||
});
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return an empty array when credentials are undefined', () => {
|
||||
const { getForeignCredentialsIfSharingEnabled } = useNodeHelpers();
|
||||
|
||||
mockedStore(useSettingsStore).isEnterpriseFeatureEnabled = createMockEnterpriseSettings({
|
||||
[EnterpriseEditionFeature.Sharing]: true,
|
||||
});
|
||||
|
||||
const result = getForeignCredentialsIfSharingEnabled(undefined);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return an empty array when user has access to all credentials', () => {
|
||||
const { getForeignCredentialsIfSharingEnabled } = useNodeHelpers();
|
||||
|
||||
const credentialWithAccess1: IUsedCredential = {
|
||||
id: faker.string.alphanumeric(10),
|
||||
credentialType: 'generic',
|
||||
name: faker.lorem.words(2),
|
||||
currentUserHasAccess: true,
|
||||
};
|
||||
|
||||
const credentialWithAccess2: IUsedCredential = {
|
||||
id: faker.string.alphanumeric(10),
|
||||
credentialType: 'generic',
|
||||
name: faker.lorem.words(2),
|
||||
currentUserHasAccess: true,
|
||||
};
|
||||
|
||||
mockedStore(useSettingsStore).isEnterpriseFeatureEnabled = createMockEnterpriseSettings({
|
||||
[EnterpriseEditionFeature.Sharing]: true,
|
||||
});
|
||||
mockedStore(useWorkflowsStore).usedCredentials = {
|
||||
[credentialWithAccess1.id]: credentialWithAccess1,
|
||||
[credentialWithAccess2.id]: credentialWithAccess2,
|
||||
};
|
||||
|
||||
const result = getForeignCredentialsIfSharingEnabled({
|
||||
[credentialWithAccess1.id]: {
|
||||
id: credentialWithAccess1.id,
|
||||
name: credentialWithAccess1.name,
|
||||
},
|
||||
[credentialWithAccess2.id]: {
|
||||
id: credentialWithAccess2.id,
|
||||
name: credentialWithAccess2.name,
|
||||
},
|
||||
});
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return an array of foreign credentials', () => {
|
||||
const { getForeignCredentialsIfSharingEnabled } = useNodeHelpers();
|
||||
|
||||
const credentialWithAccess: IUsedCredential = {
|
||||
id: faker.string.alphanumeric(10),
|
||||
credentialType: 'generic',
|
||||
name: faker.lorem.words(2),
|
||||
currentUserHasAccess: true,
|
||||
};
|
||||
|
||||
const credentialWithoutAccess: IUsedCredential = {
|
||||
id: faker.string.alphanumeric(10),
|
||||
credentialType: 'generic',
|
||||
name: faker.lorem.words(2),
|
||||
currentUserHasAccess: false,
|
||||
};
|
||||
|
||||
mockedStore(useSettingsStore).isEnterpriseFeatureEnabled = createMockEnterpriseSettings({
|
||||
[EnterpriseEditionFeature.Sharing]: true,
|
||||
});
|
||||
mockedStore(useWorkflowsStore).usedCredentials = {
|
||||
[credentialWithAccess.id]: credentialWithAccess,
|
||||
[credentialWithoutAccess.id]: credentialWithoutAccess,
|
||||
};
|
||||
|
||||
const result = getForeignCredentialsIfSharingEnabled({
|
||||
[credentialWithAccess.id]: {
|
||||
id: credentialWithAccess.id,
|
||||
name: credentialWithAccess.name,
|
||||
},
|
||||
[credentialWithoutAccess.id]: {
|
||||
id: credentialWithoutAccess.id,
|
||||
name: credentialWithoutAccess.name,
|
||||
},
|
||||
});
|
||||
expect(result).toEqual([credentialWithoutAccess.id]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isCustomApiCallSelected', () => {
|
||||
test('should return `true` when resource includes `CUSTOM_API_CALL_KEY`', () => {
|
||||
const nodeValues = {
|
||||
|
||||
Reference in New Issue
Block a user