diff --git a/packages/nodes-base/nodes/Slack/V1/GenericFunctions.ts b/packages/nodes-base/nodes/Slack/V1/GenericFunctions.ts index 42f618eddf6..74a31d5b922 100644 --- a/packages/nodes-base/nodes/Slack/V1/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Slack/V1/GenericFunctions.ts @@ -10,6 +10,26 @@ import type { } from 'n8n-workflow'; import { NodeApiError, NodeOperationError } from 'n8n-workflow'; +// When an expression is wrapped in surrounding text/whitespace, n8n switches to +// string interpolation and a multiOptions array is coerced to a comma-joined +// string. Accept both shapes so the Slack node degrades gracefully. +export function toMultiOptionsCsv(value: unknown): string { + if (Array.isArray(value)) { + return value + .map((entry) => String(entry).trim()) + .filter((entry) => entry.length > 0) + .join(','); + } + if (typeof value === 'string') { + return value + .split(',') + .map((entry) => entry.trim()) + .filter((entry) => entry.length > 0) + .join(','); + } + return ''; +} + export async function slackApiRequest( this: IExecuteFunctions | ILoadOptionsFunctions, method: IHttpRequestMethods, diff --git a/packages/nodes-base/nodes/Slack/V1/SlackV1.node.ts b/packages/nodes-base/nodes/Slack/V1/SlackV1.node.ts index 1e68d5705fd..f87c8352b48 100644 --- a/packages/nodes-base/nodes/Slack/V1/SlackV1.node.ts +++ b/packages/nodes-base/nodes/Slack/V1/SlackV1.node.ts @@ -16,7 +16,12 @@ import { oldVersionNotice } from '@utils/descriptions'; import { channelFields, channelOperations } from './ChannelDescription'; import { fileFields, fileOperations } from './FileDescription'; -import { slackApiRequest, slackApiRequestAllItems, validateJSON } from './GenericFunctions'; +import { + slackApiRequest, + slackApiRequestAllItems, + toMultiOptionsCsv, + validateJSON, +} from './GenericFunctions'; import { messageFields, messageOperations } from './MessageDescription'; import type { IAttachment } from './MessageInterface'; import { reactionFields, reactionOperations } from './ReactionDescription'; @@ -367,7 +372,7 @@ export class SlackV1 implements INodeType { const returnAll = this.getNodeParameter('returnAll', i); const filters = this.getNodeParameter('filters', i); if (filters.types) { - qs.types = (filters.types as string[]).join(','); + qs.types = toMultiOptionsCsv(filters.types); } if (filters.excludeArchived) { qs.exclude_archived = filters.excludeArchived as boolean; @@ -426,7 +431,7 @@ export class SlackV1 implements INodeType { //https://api.slack.com/methods/conversations.invite if (operation === 'invite') { const channel = this.getNodeParameter('channelId', i) as string; - const userIds = (this.getNodeParameter('userIds', i) as string[]).join(','); + const userIds = toMultiOptionsCsv(this.getNodeParameter('userIds', i)); const body: IDataObject = { channel, users: userIds, @@ -507,7 +512,7 @@ export class SlackV1 implements INodeType { body.return_im = options.returnIm as boolean; } if (options.users) { - body.users = (options.users as string[]).join(','); + body.users = toMultiOptionsCsv(options.users); } responseData = await slackApiRequest.call( this, @@ -1078,7 +1083,7 @@ export class SlackV1 implements INodeType { const options = this.getNodeParameter('options', i); const body: IDataObject = {}; if (options.channelIds) { - body.channels = (options.channelIds as string[]).join(','); + body.channels = toMultiOptionsCsv(options.channelIds); } if (options.fileName) { body.filename = options.fileName as string; @@ -1148,7 +1153,7 @@ export class SlackV1 implements INodeType { qs.ts_to = filters.tsTo as string; } if (filters.types) { - qs.types = (filters.types as string[]).join(','); + qs.types = toMultiOptionsCsv(filters.types); } if (filters.userId) { qs.user = filters.userId as string; diff --git a/packages/nodes-base/nodes/Slack/V2/GenericFunctions.ts b/packages/nodes-base/nodes/Slack/V2/GenericFunctions.ts index cb8f33b793b..8d78ec7e259 100644 --- a/packages/nodes-base/nodes/Slack/V2/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Slack/V2/GenericFunctions.ts @@ -35,6 +35,26 @@ function isDefined(value: T | undefined | null | ''): value is NonNullable return value !== undefined && value !== null && value !== ''; } +// When an expression is wrapped in surrounding text/whitespace, n8n switches to +// string interpolation and a multiOptions array is coerced to a comma-joined +// string. Accept both shapes so the Slack node degrades gracefully. +export function toMultiOptionsCsv(value: unknown): string { + if (Array.isArray(value)) { + return value + .map((entry) => String(entry).trim()) + .filter((entry) => entry.length > 0) + .join(','); + } + if (typeof value === 'string') { + return value + .split(',') + .map((entry) => entry.trim()) + .filter((entry) => entry.length > 0) + .join(','); + } + return ''; +} + export async function slackApiRequest( this: IExecuteFunctions | ILoadOptionsFunctions | IWebhookFunctions, method: IHttpRequestMethods, diff --git a/packages/nodes-base/nodes/Slack/V2/SlackV2.node.ts b/packages/nodes-base/nodes/Slack/V2/SlackV2.node.ts index 4b2022b78fd..52ff3a61f3c 100644 --- a/packages/nodes-base/nodes/Slack/V2/SlackV2.node.ts +++ b/packages/nodes-base/nodes/Slack/V2/SlackV2.node.ts @@ -31,6 +31,7 @@ import { createSendAndWaitMessageBody, processThreadOptions, slackApiRequestAllItemsWithRateLimit, + toMultiOptionsCsv, } from './GenericFunctions'; import { channelRLC, @@ -526,7 +527,7 @@ export class SlackV2 implements INodeType { const returnAll = this.getNodeParameter('returnAll', i); const filters = this.getNodeParameter('filters', i); if (filters.types) { - qs.types = (filters.types as string[]).join(','); + qs.types = toMultiOptionsCsv(filters.types); } if (filters.excludeArchived) { qs.exclude_archived = filters.excludeArchived as boolean; @@ -601,7 +602,7 @@ export class SlackV2 implements INodeType { {}, { extractValue: true }, ) as string; - const userIds = (this.getNodeParameter('userIds', i) as string[]).join(','); + const userIds = toMultiOptionsCsv(this.getNodeParameter('userIds', i)); const body: IDataObject = { channel, users: userIds, @@ -692,7 +693,7 @@ export class SlackV2 implements INodeType { body.return_im = options.returnIm as boolean; } if (options.users) { - body.users = (options.users as string[]).join(','); + body.users = toMultiOptionsCsv(options.users); } responseData = await slackApiRequest.call( this, @@ -1100,7 +1101,7 @@ export class SlackV2 implements INodeType { const fileBody: IDataObject = {}; if (options.channelIds) { - body.channels = (options.channelIds as string[]).join(','); + body.channels = toMultiOptionsCsv(options.channelIds); } if (options.channelId) { body.channel_id = options.channelId as string; @@ -1225,7 +1226,7 @@ export class SlackV2 implements INodeType { qs.ts_to = filters.tsTo as string; } if (filters.types) { - qs.types = (filters.types as string[]).join(','); + qs.types = toMultiOptionsCsv(filters.types); } if (filters.userId) { qs.user = filters.userId as string; diff --git a/packages/nodes-base/nodes/Slack/test/v1/GenericFunctions.test.ts b/packages/nodes-base/nodes/Slack/test/v1/GenericFunctions.test.ts index 7ab20958b80..3941584a401 100644 --- a/packages/nodes-base/nodes/Slack/test/v1/GenericFunctions.test.ts +++ b/packages/nodes-base/nodes/Slack/test/v1/GenericFunctions.test.ts @@ -1,6 +1,11 @@ import type { IExecuteFunctions } from 'n8n-workflow'; -import { slackApiRequest, slackApiRequestAllItems, validateJSON } from '../../V1/GenericFunctions'; +import { + slackApiRequest, + slackApiRequestAllItems, + toMultiOptionsCsv, + validateJSON, +} from '../../V1/GenericFunctions'; jest.mock('n8n-workflow', () => ({ ...jest.requireActual('n8n-workflow'), @@ -147,6 +152,31 @@ describe('Slack V1 > GenericFunctions', () => { }); }); + describe('toMultiOptionsCsv', () => { + it('joins array values', () => { + expect(toMultiOptionsCsv(['U123', 'U456'])).toBe('U123,U456'); + }); + + it('accepts a comma-joined string and trims trailing whitespace', () => { + expect(toMultiOptionsCsv('U123,U456 ')).toBe('U123,U456'); + }); + + it('trims whitespace around each entry in a comma-string', () => { + expect(toMultiOptionsCsv(' U123 , U456 ')).toBe('U123,U456'); + }); + + it('drops empty entries', () => { + expect(toMultiOptionsCsv(['U123', '', ' ', 'U456'])).toBe('U123,U456'); + }); + + it('returns empty string for undefined/null/empty', () => { + expect(toMultiOptionsCsv(undefined)).toBe(''); + expect(toMultiOptionsCsv(null)).toBe(''); + expect(toMultiOptionsCsv('')).toBe(''); + expect(toMultiOptionsCsv([])).toBe(''); + }); + }); + describe('validateJSON', () => { it('should return undefined for invalid JSON', () => { const result = validateJSON('{invalid:json}'); diff --git a/packages/nodes-base/nodes/Slack/test/v1/SlackV1.node.test.ts b/packages/nodes-base/nodes/Slack/test/v1/SlackV1.node.test.ts new file mode 100644 index 00000000000..40159366bde --- /dev/null +++ b/packages/nodes-base/nodes/Slack/test/v1/SlackV1.node.test.ts @@ -0,0 +1,281 @@ +import { mockDeep } from 'jest-mock-extended'; +import type { IExecuteFunctions, INode } from 'n8n-workflow'; + +import * as GenericFunctions from '../../V1/GenericFunctions'; +import { SlackV1 } from '../../V1/SlackV1.node'; + +describe('SlackV1 — multiOptions parameter normalization', () => { + let node: SlackV1; + let mockExecuteFunctions: jest.Mocked; + let slackApiRequestSpy: jest.SpyInstance; + let slackApiRequestAllItemsSpy: jest.SpyInstance; + + const mockNode: INode = { + id: 'test-node-id', + name: 'Slack Test', + type: 'n8n-nodes-base.slack', + typeVersion: 1, + position: [0, 0], + parameters: {}, + }; + + beforeEach(() => { + node = new SlackV1({ + name: 'Slack', + displayName: 'Slack', + description: 'Slack V1 node test', + group: ['input'], + }); + + mockExecuteFunctions = mockDeep(); + slackApiRequestSpy = jest.spyOn(GenericFunctions, 'slackApiRequest'); + slackApiRequestAllItemsSpy = jest.spyOn(GenericFunctions, 'slackApiRequestAllItems'); + + jest.clearAllMocks(); + + mockExecuteFunctions.getInputData.mockReturnValue([{ json: {} }]); + mockExecuteFunctions.getNode.mockReturnValue(mockNode); + mockExecuteFunctions.continueOnFail.mockReturnValue(false); + (mockExecuteFunctions.helpers.constructExecutionMetaData as jest.Mock).mockImplementation( + (data: any, options: any) => { + return data.map((item: any, index: number) => ({ + ...item, + pairedItem: { item: options?.itemData?.item ?? index }, + })); + }, + ); + (mockExecuteFunctions.helpers.returnJsonArray as jest.Mock).mockImplementation((data: any) => { + return [{ json: data }]; + }); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + const mockParams = (params: Record) => { + mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => { + return params[paramName]; + }); + }; + + describe('channel: getAll — filters.types', () => { + it('joins an array of types', async () => { + mockParams({ + authentication: 'accessToken', + resource: 'channel', + operation: 'getAll', + returnAll: false, + limit: 10, + filters: { types: ['public_channel', 'private_channel'] }, + }); + slackApiRequestSpy.mockResolvedValue({ channels: [] }); + + await node.execute.call(mockExecuteFunctions); + + expect(slackApiRequestSpy).toHaveBeenCalledWith( + 'GET', + '/conversations.list', + {}, + expect.objectContaining({ types: 'public_channel,private_channel' }), + ); + }); + + it('normalizes a comma-joined string with trailing whitespace', async () => { + mockParams({ + authentication: 'accessToken', + resource: 'channel', + operation: 'getAll', + returnAll: false, + limit: 10, + filters: { types: 'public_channel,private_channel ' }, + }); + slackApiRequestSpy.mockResolvedValue({ channels: [] }); + + await node.execute.call(mockExecuteFunctions); + + expect(slackApiRequestSpy).toHaveBeenCalledWith( + 'GET', + '/conversations.list', + {}, + expect.objectContaining({ types: 'public_channel,private_channel' }), + ); + }); + }); + + describe('channel: invite — userIds', () => { + it('joins an array of user IDs', async () => { + mockParams({ + authentication: 'accessToken', + resource: 'channel', + operation: 'invite', + channelId: 'C123456789', + userIds: ['U111111111', 'U222222222'], + }); + slackApiRequestSpy.mockResolvedValue({ channel: { id: 'C123456789' } }); + + await node.execute.call(mockExecuteFunctions); + + expect(slackApiRequestSpy).toHaveBeenCalledWith( + 'POST', + '/conversations.invite', + { channel: 'C123456789', users: 'U111111111,U222222222' }, + {}, + ); + }); + + it('normalizes a comma-joined string with trailing whitespace (whitespace-coerced expression)', async () => { + mockParams({ + authentication: 'accessToken', + resource: 'channel', + operation: 'invite', + channelId: 'C123456789', + userIds: 'U111111111,U222222222 ', + }); + slackApiRequestSpy.mockResolvedValue({ channel: { id: 'C123456789' } }); + + await node.execute.call(mockExecuteFunctions); + + expect(slackApiRequestSpy).toHaveBeenCalledWith( + 'POST', + '/conversations.invite', + { channel: 'C123456789', users: 'U111111111,U222222222' }, + {}, + ); + }); + }); + + describe('channel: open — options.users', () => { + it('joins an array of users', async () => { + mockParams({ + authentication: 'accessToken', + resource: 'channel', + operation: 'open', + options: { users: ['U111111111', 'U222222222'], returnIm: true }, + }); + slackApiRequestSpy.mockResolvedValue({ channel: { id: 'D1' } }); + + await node.execute.call(mockExecuteFunctions); + + expect(slackApiRequestSpy).toHaveBeenCalledWith( + 'POST', + '/conversations.open', + { users: 'U111111111,U222222222', return_im: true }, + {}, + ); + }); + + it('normalizes a comma-joined string for users option', async () => { + mockParams({ + authentication: 'accessToken', + resource: 'channel', + operation: 'open', + options: { users: 'U111111111,U222222222 ' }, + }); + slackApiRequestSpy.mockResolvedValue({ channel: { id: 'D1' } }); + + await node.execute.call(mockExecuteFunctions); + + expect(slackApiRequestSpy).toHaveBeenCalledWith( + 'POST', + '/conversations.open', + { users: 'U111111111,U222222222' }, + {}, + ); + }); + }); + + describe('file: upload — options.channelIds', () => { + it('joins an array of channel IDs', async () => { + mockParams({ + authentication: 'accessToken', + resource: 'file', + operation: 'upload', + binaryData: false, + fileContent: 'hello', + options: { channelIds: ['C111111111', 'C222222222'] }, + }); + slackApiRequestSpy.mockResolvedValue({ file: { id: 'F1' } }); + + await node.execute.call(mockExecuteFunctions); + + expect(slackApiRequestSpy).toHaveBeenCalledWith( + 'POST', + '/files.upload', + expect.objectContaining({ channels: 'C111111111,C222222222' }), + {}, + { 'Content-Type': 'application/x-www-form-urlencoded' }, + expect.objectContaining({ + form: expect.objectContaining({ channels: 'C111111111,C222222222' }), + }), + ); + }); + + it('normalizes a comma-joined string for channelIds option', async () => { + mockParams({ + authentication: 'accessToken', + resource: 'file', + operation: 'upload', + binaryData: false, + fileContent: 'hello', + options: { channelIds: 'C111111111,C222222222 ' }, + }); + slackApiRequestSpy.mockResolvedValue({ file: { id: 'F1' } }); + + await node.execute.call(mockExecuteFunctions); + + expect(slackApiRequestSpy).toHaveBeenCalledWith( + 'POST', + '/files.upload', + expect.objectContaining({ channels: 'C111111111,C222222222' }), + {}, + { 'Content-Type': 'application/x-www-form-urlencoded' }, + expect.anything(), + ); + }); + }); + + describe('file: getAll — filters.types', () => { + it('joins an array of file types', async () => { + mockParams({ + authentication: 'accessToken', + resource: 'file', + operation: 'getAll', + returnAll: true, + filters: { types: ['images', 'pdfs'] }, + }); + slackApiRequestAllItemsSpy.mockResolvedValue([]); + + await node.execute.call(mockExecuteFunctions); + + expect(slackApiRequestAllItemsSpy).toHaveBeenCalledWith( + 'files', + 'GET', + '/files.list', + {}, + expect.objectContaining({ types: 'images,pdfs' }), + ); + }); + + it('normalizes a comma-joined string for file types', async () => { + mockParams({ + authentication: 'accessToken', + resource: 'file', + operation: 'getAll', + returnAll: true, + filters: { types: 'images,pdfs ' }, + }); + slackApiRequestAllItemsSpy.mockResolvedValue([]); + + await node.execute.call(mockExecuteFunctions); + + expect(slackApiRequestAllItemsSpy).toHaveBeenCalledWith( + 'files', + 'GET', + '/files.list', + {}, + expect.objectContaining({ types: 'images,pdfs' }), + ); + }); + }); +}); diff --git a/packages/nodes-base/nodes/Slack/test/v2/Slack.node.test.ts b/packages/nodes-base/nodes/Slack/test/v2/Slack.node.test.ts index 82b7a9224be..25f6c9e13f2 100644 --- a/packages/nodes-base/nodes/Slack/test/v2/Slack.node.test.ts +++ b/packages/nodes-base/nodes/Slack/test/v2/Slack.node.test.ts @@ -151,6 +151,56 @@ describe('SlackV2', () => { }); }); + describe('Channel List Operations - multiOptions types filter', () => { + it('should send types as CSV when filters.types is an array', async () => { + mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => { + const params: Record = { + resource: 'channel', + operation: 'getAll', + returnAll: false, + limit: 50, + filters: { types: ['public_channel', 'private_channel'] }, + }; + return params[paramName]; + }); + + slackApiRequestSpy.mockResolvedValue({ channels: [] }); + + await node.execute.call(mockExecuteFunctions); + + expect(slackApiRequestSpy).toHaveBeenCalledWith( + 'GET', + '/conversations.list', + {}, + expect.objectContaining({ types: 'public_channel,private_channel' }), + ); + }); + + it('should normalize types when filters.types is a comma-joined string', async () => { + mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => { + const params: Record = { + resource: 'channel', + operation: 'getAll', + returnAll: false, + limit: 50, + filters: { types: 'public_channel,private_channel ' }, + }; + return params[paramName]; + }); + + slackApiRequestSpy.mockResolvedValue({ channels: [] }); + + await node.execute.call(mockExecuteFunctions); + + expect(slackApiRequestSpy).toHaveBeenCalledWith( + 'GET', + '/conversations.list', + {}, + expect.objectContaining({ types: 'public_channel,private_channel' }), + ); + }); + }); + describe('Channel Operations - History, Invite, Leave, Members, etc.', () => { it('should get channel history with pagination', async () => { mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => { @@ -318,6 +368,32 @@ describe('SlackV2', () => { ]); }); + it('should invite users when userIds is a comma-joined string (whitespace-coerced expression)', async () => { + mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => { + const params: Record = { + resource: 'channel', + operation: 'invite', + channelId: 'C123456789', + userIds: 'U111111111,U222222222 ', + }; + return params[paramName]; + }); + + slackApiRequestSpy.mockResolvedValue({ channel: { id: 'C123456789' } }); + + await node.execute.call(mockExecuteFunctions); + + expect(slackApiRequestSpy).toHaveBeenCalledWith( + 'POST', + '/conversations.invite', + { + channel: 'C123456789', + users: 'U111111111,U222222222', + }, + {}, + ); + }); + it('should leave channel', async () => { mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => { const params: Record = { @@ -1416,6 +1492,56 @@ describe('SlackV2', () => { }); }); + describe('File List Operations - multiOptions types filter', () => { + it('should send types as CSV when filters.types is an array', async () => { + mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => { + const params: Record = { + resource: 'file', + operation: 'getAll', + returnAll: false, + limit: 50, + filters: { types: ['images', 'pdfs'] }, + }; + return params[paramName]; + }); + + slackApiRequestSpy.mockResolvedValue({ files: [{ id: 'F1' }] }); + + await node.execute.call(mockExecuteFunctions); + + expect(slackApiRequestSpy).toHaveBeenCalledWith( + 'GET', + '/files.list', + {}, + expect.objectContaining({ types: 'images,pdfs' }), + ); + }); + + it('should normalize types when filters.types is a comma-joined string with trailing whitespace', async () => { + mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => { + const params: Record = { + resource: 'file', + operation: 'getAll', + returnAll: false, + limit: 50, + filters: { types: 'images,pdfs ' }, + }; + return params[paramName]; + }); + + slackApiRequestSpy.mockResolvedValue({ files: [] }); + + await node.execute.call(mockExecuteFunctions); + + expect(slackApiRequestSpy).toHaveBeenCalledWith( + 'GET', + '/files.list', + {}, + expect.objectContaining({ types: 'images,pdfs' }), + ); + }); + }); + describe('Error Handling with continueOnFail', () => { it('should continue execution on API error when continueOnFail is true', async () => { mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => { diff --git a/packages/nodes-base/nodes/Slack/test/v2/utils.test.ts b/packages/nodes-base/nodes/Slack/test/v2/utils.test.ts index 46dc377a2e1..68f3d225a88 100644 --- a/packages/nodes-base/nodes/Slack/test/v2/utils.test.ts +++ b/packages/nodes-base/nodes/Slack/test/v2/utils.test.ts @@ -1,7 +1,11 @@ import { type MockProxy, mock } from 'jest-mock-extended'; import type { IExecuteFunctions } from 'n8n-workflow'; -import { getTarget, createSendAndWaitMessageBody } from '../../V2/GenericFunctions'; +import { + getTarget, + createSendAndWaitMessageBody, + toMultiOptionsCsv, +} from '../../V2/GenericFunctions'; describe('Slack Utility Functions', () => { let mockExecuteFunctions: MockProxy; @@ -18,6 +22,43 @@ describe('Slack Utility Functions', () => { jest.clearAllMocks(); }); + describe('toMultiOptionsCsv', () => { + it('joins array values', () => { + expect(toMultiOptionsCsv(['U123', 'U456'])).toBe('U123,U456'); + }); + + it('trims entries inside an array (interpolated array elements)', () => { + expect(toMultiOptionsCsv(['U123 ', ' U456'])).toBe('U123,U456'); + }); + + it('drops empty array entries', () => { + expect(toMultiOptionsCsv(['U123', '', ' ', 'U456'])).toBe('U123,U456'); + }); + + it('coerces non-string array entries via String()', () => { + expect(toMultiOptionsCsv([1, 2, 3])).toBe('1,2,3'); + }); + + it('accepts a comma-joined string (the whitespace-expression coercion case)', () => { + expect(toMultiOptionsCsv('U123,U456')).toBe('U123,U456'); + }); + + it('trims surrounding whitespace on a string value (trailing-space expression bug)', () => { + expect(toMultiOptionsCsv('U123,U456 ')).toBe('U123,U456'); + }); + + it('trims whitespace around each entry in a comma-string', () => { + expect(toMultiOptionsCsv(' U123 , U456 ')).toBe('U123,U456'); + }); + + it('returns empty string for undefined/null/empty', () => { + expect(toMultiOptionsCsv(undefined)).toBe(''); + expect(toMultiOptionsCsv(null)).toBe(''); + expect(toMultiOptionsCsv('')).toBe(''); + expect(toMultiOptionsCsv([])).toBe(''); + }); + }); + describe('getTarget', () => { it('should return correct target id', () => { mockExecuteFunctions.getNodeParameter.mockImplementation((parameterName: string) => {