Files
n8n-enterprise-unlocked/packages/frontend/editor-ui/src/composables/useWorkflowHelpers.test.ts
2025-04-28 14:29:32 +03:00

458 lines
15 KiB
TypeScript

import type { IWorkflowData, IWorkflowDataUpdate, IWorkflowDb } from '@/Interface';
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
import router from '@/router';
import { createTestingPinia } from '@pinia/testing';
import { setActivePinia } from 'pinia';
import { useWorkflowsStore } from '@/stores/workflows.store';
import { useWorkflowsEEStore } from '@/stores/workflows.ee.store';
import { useTagsStore } from '@/stores/tags.store';
import { useUIStore } from '@/stores/ui.store';
import { createTestWorkflow } from '@/__tests__/mocks';
import { WEBHOOK_NODE_TYPE, type AssignmentCollectionValue } from 'n8n-workflow';
import * as apiWebhooks from '../api/webhooks';
const getDuplicateTestWorkflow = (): IWorkflowDataUpdate => ({
name: 'Duplicate webhook test',
active: false,
nodes: [
{
parameters: {
path: '5340ae49-2c96-4492-9073-7744d2e52b8a',
options: {},
},
id: 'c1e1b6e7-df13-41b1-95f6-42903b85e438',
name: 'Webhook',
type: 'n8n-nodes-base.webhook',
typeVersion: 2,
position: [680, 20],
webhookId: '5340ae49-2c96-4492-9073-7744d2e52b8a',
},
{
parameters: {
path: 'aa5150d8-1d7d-4247-88d8-44c96fe3a37b',
options: {},
},
id: 'aa5150d8-1d7d-4247-88d8-44c96fe3a37b',
name: 'Webhook 2',
type: 'n8n-nodes-base.webhook',
typeVersion: 2,
position: [700, 40],
webhookId: 'aa5150d8-1d7d-4247-88d8-44c96fe3a37b',
},
{
parameters: {
resume: 'webhook',
options: {
webhookSuffix: '/test',
},
},
id: '979d8443-51b1-48e2-b239-acf399b66509',
name: 'Wait',
type: 'n8n-nodes-base.wait',
typeVersion: 1.1,
position: [900, 20],
webhookId: '5340ae49-2c96-4492-9073-7744d2e52b8a',
},
],
connections: {},
});
describe('useWorkflowHelpers', () => {
let workflowsStore: ReturnType<typeof useWorkflowsStore>;
let workflowsEEStore: ReturnType<typeof useWorkflowsEEStore>;
let tagsStore: ReturnType<typeof useTagsStore>;
let uiStore: ReturnType<typeof useUIStore>;
beforeAll(() => {
setActivePinia(createTestingPinia());
workflowsStore = useWorkflowsStore();
workflowsEEStore = useWorkflowsEEStore();
tagsStore = useTagsStore();
uiStore = useUIStore();
});
afterEach(() => {
vi.clearAllMocks();
});
describe('getNodeParametersWithResolvedExpressions', () => {
it('should correctly detect and resolve expressions in a regular node ', () => {
const nodeParameters = {
curlImport: '',
method: 'GET',
url: '={{ $json.name }}',
authentication: 'none',
provideSslCertificates: false,
sendQuery: false,
sendHeaders: false,
sendBody: false,
options: {},
infoMessage: '',
};
const workflowHelpers = useWorkflowHelpers({ router });
const resolvedParameters =
workflowHelpers.getNodeParametersWithResolvedExpressions(nodeParameters);
expect(resolvedParameters.url).toHaveProperty('resolvedExpressionValue');
});
it('should correctly detect and resolve expressions in a node with assignments (set node) ', () => {
const nodeParameters = {
mode: 'manual',
duplicateItem: false,
assignments: {
assignments: [
{
id: '25d2d012-089b-424d-bfc6-642982a0711f',
name: 'date',
value:
"={{ DateTime.fromFormat('2023-12-12', 'dd/MM/yyyy').toISODate().plus({7, 'days' }) }}",
type: 'number',
},
],
},
includeOtherFields: false,
options: {},
};
const workflowHelpers = useWorkflowHelpers({ router });
const resolvedParameters =
workflowHelpers.getNodeParametersWithResolvedExpressions(nodeParameters);
expect(resolvedParameters).toHaveProperty('assignments');
const assignments = resolvedParameters.assignments as AssignmentCollectionValue;
expect(assignments).toHaveProperty('assignments');
expect(assignments.assignments[0].value).toHaveProperty('resolvedExpressionValue');
});
it('should correctly detect and resolve expressions in a node with filter component', () => {
const nodeParameters = {
mode: 'rules',
rules: {
values: [
{
conditions: {
options: {
caseSensitive: true,
leftValue: '',
typeValidation: 'strict',
version: 2,
},
conditions: [
{
leftValue: "={{ $('Edit Fields 1').item.json.name }}",
rightValue: 12,
operator: {
type: 'number',
operation: 'equals',
},
},
],
combinator: 'and',
},
renameOutput: false,
},
],
},
looseTypeValidation: false,
options: {},
};
const workflowHelpers = useWorkflowHelpers({ router });
const resolvedParameters = workflowHelpers.getNodeParametersWithResolvedExpressions(
nodeParameters,
) as typeof nodeParameters;
expect(resolvedParameters).toHaveProperty('rules');
expect(resolvedParameters.rules).toHaveProperty('values');
expect(resolvedParameters.rules.values[0].conditions.conditions[0].leftValue).toHaveProperty(
'resolvedExpressionValue',
);
});
it('should correctly detect and resolve expressions in a node with resource locator component', () => {
const nodeParameters = {
authentication: 'oAuth2',
resource: 'sheet',
operation: 'read',
documentId: {
__rl: true,
value: "={{ $('Edit Fields').item.json.document }}",
mode: 'id',
},
sheetName: {
__rl: true,
value: "={{ $('Edit Fields').item.json.sheet }}",
mode: 'id',
},
filtersUI: {},
combineFilters: 'AND',
options: {},
};
const workflowHelpers = useWorkflowHelpers({ router });
const resolvedParameters = workflowHelpers.getNodeParametersWithResolvedExpressions(
nodeParameters,
) as typeof nodeParameters;
expect(resolvedParameters.documentId.value).toHaveProperty('resolvedExpressionValue');
expect(resolvedParameters.sheetName.value).toHaveProperty('resolvedExpressionValue');
});
it('should correctly detect and resolve expressions in a node with resource mapper component', () => {
const nodeParameters = {
authentication: 'oAuth2',
resource: 'sheet',
operation: 'read',
documentId: {
__rl: true,
value: '1BAjxEhlUu5tXDCMQcjqjguIZDFuct3FYkdo7flxl3yc',
mode: 'list',
cachedResultName: 'Mapping sheet',
cachedResultUrl:
'https://docs.google.com/spreadsheets/d/1BAjxEhlUu5tXDCMQcjqjguIZDFuct3FYkdo7flxl3yc/edit?usp=drivesdk',
},
sheetName: {
__rl: true,
value: 'gid=0',
mode: 'list',
cachedResultName: 'Users',
cachedResultUrl:
'https://docs.google.com/spreadsheets/d/1BAjxEhlUu5tXDCMQcjqjguIZDFuct3FYkdo7flxl3yc/edit#gid=0',
},
filtersUI: {
values: [
{
lookupColumn: 'First name',
lookupValue: "={{ $('Edit Fields 1').item.json.userName }}",
},
],
},
combineFilters: 'AND',
options: {},
};
const workflowHelpers = useWorkflowHelpers({ router });
const resolvedParameters = workflowHelpers.getNodeParametersWithResolvedExpressions(
nodeParameters,
) as typeof nodeParameters;
expect(resolvedParameters.filtersUI.values[0].lookupValue).toHaveProperty(
'resolvedExpressionValue',
);
});
});
describe('saveAsNewWorkflow', () => {
it('should respect `resetWebhookUrls: false` when duplicating workflows', async () => {
const workflow = getDuplicateTestWorkflow();
if (!workflow.nodes) {
throw new Error('Missing nodes in test workflow');
}
const { saveAsNewWorkflow } = useWorkflowHelpers({ router });
const webHookIdsPreSave = workflow.nodes.map((node) => node.webhookId);
const pathsPreSave = workflow.nodes.map((node) => node.parameters.path);
await saveAsNewWorkflow({
name: workflow.name,
resetWebhookUrls: false,
data: workflow,
});
const webHookIdsPostSave = workflow.nodes.map((node) => node.webhookId);
const pathsPostSave = workflow.nodes.map((node) => node.parameters.path);
// Expect webhookIds and paths to be the same as in the original workflow
expect(webHookIdsPreSave).toEqual(webHookIdsPostSave);
expect(pathsPreSave).toEqual(pathsPostSave);
});
it('should respect `resetWebhookUrls: true` when duplicating workflows', async () => {
const workflow = getDuplicateTestWorkflow();
if (!workflow.nodes) {
throw new Error('Missing nodes in test workflow');
}
const { saveAsNewWorkflow } = useWorkflowHelpers({ router });
const webHookIdsPreSave = workflow.nodes.map((node) => node.webhookId);
const pathsPreSave = workflow.nodes.map((node) => node.parameters.path);
await saveAsNewWorkflow({
name: workflow.name,
resetWebhookUrls: true,
data: workflow,
});
const webHookIdsPostSave = workflow.nodes.map((node) => node.webhookId);
const pathsPostSave = workflow.nodes.map((node) => node.parameters.path);
// Now, expect webhookIds and paths to be different
expect(webHookIdsPreSave).not.toEqual(webHookIdsPostSave);
expect(pathsPreSave).not.toEqual(pathsPostSave);
});
});
describe('initState', () => {
it('should initialize workflow state with provided data', () => {
const { initState } = useWorkflowHelpers({ router });
const workflowData = createTestWorkflow({
id: '1',
name: 'Test Workflow',
active: true,
pinData: {},
meta: {},
scopes: ['workflow:create'],
usedCredentials: [],
sharedWithProjects: [],
tags: [],
});
const addWorkflowSpy = vi.spyOn(workflowsStore, 'addWorkflow');
const setActiveSpy = vi.spyOn(workflowsStore, 'setActive');
const setWorkflowIdSpy = vi.spyOn(workflowsStore, 'setWorkflowId');
const setWorkflowNameSpy = vi.spyOn(workflowsStore, 'setWorkflowName');
const setWorkflowSettingsSpy = vi.spyOn(workflowsStore, 'setWorkflowSettings');
const setWorkflowPinDataSpy = vi.spyOn(workflowsStore, 'setWorkflowPinData');
const setWorkflowVersionIdSpy = vi.spyOn(workflowsStore, 'setWorkflowVersionId');
const setWorkflowMetadataSpy = vi.spyOn(workflowsStore, 'setWorkflowMetadata');
const setWorkflowScopesSpy = vi.spyOn(workflowsStore, 'setWorkflowScopes');
const setUsedCredentialsSpy = vi.spyOn(workflowsStore, 'setUsedCredentials');
const setWorkflowSharedWithSpy = vi.spyOn(workflowsEEStore, 'setWorkflowSharedWith');
const setWorkflowTagIdsSpy = vi.spyOn(workflowsStore, 'setWorkflowTagIds');
const upsertTagsSpy = vi.spyOn(tagsStore, 'upsertTags');
initState(workflowData);
expect(addWorkflowSpy).toHaveBeenCalledWith(workflowData);
expect(setActiveSpy).toHaveBeenCalledWith(true);
expect(setWorkflowIdSpy).toHaveBeenCalledWith('1');
expect(setWorkflowNameSpy).toHaveBeenCalledWith({
newName: 'Test Workflow',
setStateDirty: false,
});
expect(setWorkflowSettingsSpy).toHaveBeenCalledWith({
executionOrder: 'v1',
timezone: 'DEFAULT',
});
expect(setWorkflowPinDataSpy).toHaveBeenCalledWith({});
expect(setWorkflowVersionIdSpy).toHaveBeenCalledWith('1');
expect(setWorkflowMetadataSpy).toHaveBeenCalledWith({});
expect(setWorkflowScopesSpy).toHaveBeenCalledWith(['workflow:create']);
expect(setUsedCredentialsSpy).toHaveBeenCalledWith([]);
expect(setWorkflowSharedWithSpy).toHaveBeenCalledWith({
workflowId: '1',
sharedWithProjects: [],
});
expect(setWorkflowTagIdsSpy).toHaveBeenCalledWith([]);
expect(upsertTagsSpy).toHaveBeenCalledWith([]);
});
it('should handle missing `usedCredentials` and `sharedWithProjects` gracefully', () => {
const { initState } = useWorkflowHelpers({ router });
const workflowData = createTestWorkflow({
id: '1',
name: 'Test Workflow',
active: true,
pinData: {},
meta: {},
scopes: [],
tags: [],
});
const setUsedCredentialsSpy = vi.spyOn(workflowsStore, 'setUsedCredentials');
const setWorkflowSharedWithSpy = vi.spyOn(workflowsEEStore, 'setWorkflowSharedWith');
initState(workflowData);
expect(setUsedCredentialsSpy).not.toHaveBeenCalled();
expect(setWorkflowSharedWithSpy).not.toHaveBeenCalled();
});
it('should handle missing `tags` gracefully', () => {
const { initState } = useWorkflowHelpers({ router });
const workflowData = createTestWorkflow({
id: '1',
name: 'Test Workflow',
active: true,
pinData: {},
meta: {},
scopes: [],
});
const setWorkflowTagIdsSpy = vi.spyOn(workflowsStore, 'setWorkflowTagIds');
const upsertTagsSpy = vi.spyOn(tagsStore, 'upsertTags');
initState(workflowData);
expect(setWorkflowTagIdsSpy).toHaveBeenCalledWith([]);
expect(upsertTagsSpy).toHaveBeenCalledWith([]);
});
});
describe('checkConflictingWebhooks', () => {
it('should return null if no conflicts', async () => {
const workflowHelpers = useWorkflowHelpers({ router });
uiStore.stateIsDirty = false;
vi.spyOn(workflowsStore, 'fetchWorkflow').mockResolvedValue({
nodes: [],
} as unknown as IWorkflowDb);
expect(await workflowHelpers.checkConflictingWebhooks('12345')).toEqual(null);
});
it('should return conflicting webhook data and workflow id is different', async () => {
const workflowHelpers = useWorkflowHelpers({ router });
uiStore.stateIsDirty = false;
vi.spyOn(workflowsStore, 'fetchWorkflow').mockResolvedValue({
nodes: [
{
type: WEBHOOK_NODE_TYPE,
parameters: {
method: 'GET',
path: 'test-path',
},
},
],
} as unknown as IWorkflowDb);
vi.spyOn(apiWebhooks, 'findWebhook').mockResolvedValue({
method: 'GET',
webhookPath: 'test-path',
node: 'Webhook 1',
workflowId: '456',
});
expect(await workflowHelpers.checkConflictingWebhooks('123')).toEqual({
conflict: {
method: 'GET',
node: 'Webhook 1',
webhookPath: 'test-path',
workflowId: '456',
},
trigger: {
parameters: {
method: 'GET',
path: 'test-path',
},
type: 'n8n-nodes-base.webhook',
},
});
});
it('should return null if webhook already exist but workflow id is the same', async () => {
const workflowHelpers = useWorkflowHelpers({ router });
uiStore.stateIsDirty = false;
vi.spyOn(workflowsStore, 'fetchWorkflow').mockResolvedValue({
nodes: [
{
type: WEBHOOK_NODE_TYPE,
parameters: {
method: 'GET',
path: 'test-path',
},
},
],
} as unknown as IWorkflowDb);
vi.spyOn(apiWebhooks, 'findWebhook').mockResolvedValue({
method: 'GET',
webhookPath: 'test-path',
node: 'Webhook 1',
workflowId: '123',
});
expect(await workflowHelpers.checkConflictingWebhooks('123')).toEqual(null);
});
it('should call getWorkflowDataToSave if state is dirty', async () => {
const workflowHelpers = useWorkflowHelpers({ router });
uiStore.stateIsDirty = true;
vi.spyOn(workflowHelpers, 'getWorkflowDataToSave').mockResolvedValue({
nodes: [],
} as unknown as IWorkflowData);
expect(await workflowHelpers.checkConflictingWebhooks('12345')).toEqual(null);
});
});
});