diff --git a/packages/nodes-base/nodes/Slack/Slack.node.ts b/packages/nodes-base/nodes/Slack/Slack.node.ts index 1c17ce6e3a0..7487ffcf3cb 100644 --- a/packages/nodes-base/nodes/Slack/Slack.node.ts +++ b/packages/nodes-base/nodes/Slack/Slack.node.ts @@ -13,7 +13,7 @@ export class Slack extends VersionedNodeType { group: ['output'], subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', description: 'Consume Slack API', - defaultVersion: 2.4, + defaultVersion: 2.5, }; const nodeVersions: IVersionedNodeType['nodeVersions'] = { @@ -23,6 +23,7 @@ export class Slack extends VersionedNodeType { 2.2: new SlackV2(baseDescription), 2.3: new SlackV2(baseDescription), 2.4: new SlackV2(baseDescription), + 2.5: new SlackV2(baseDescription), }; super(nodeVersions, baseDescription); diff --git a/packages/nodes-base/nodes/Slack/V2/SlackV2.node.ts b/packages/nodes-base/nodes/Slack/V2/SlackV2.node.ts index 5bcb513b583..4b2022b78fd 100644 --- a/packages/nodes-base/nodes/Slack/V2/SlackV2.node.ts +++ b/packages/nodes-base/nodes/Slack/V2/SlackV2.node.ts @@ -58,7 +58,7 @@ export class SlackV2 implements INodeType { constructor(baseDescription: INodeTypeBaseDescription) { this.description = { ...baseDescription, - version: [2, 2.1, 2.2, 2.3, 2.4], + version: [2, 2.1, 2.2, 2.3, 2.4, 2.5], defaults: { name: 'Slack', }, @@ -380,12 +380,19 @@ export class SlackV2 implements INodeType { const instanceId = this.getInstanceId(); if (resource === 'message' && operation === SEND_AND_WAIT_OPERATION) { - await slackApiRequest.call( - this, - 'POST', - '/chat.postMessage', - createSendAndWaitMessageBody(this), - ); + try { + await slackApiRequest.call( + this, + 'POST', + '/chat.postMessage', + createSendAndWaitMessageBody(this), + ); + } catch (error) { + if (this.continueOnFail()) { + return [[{ json: { error: (error as JsonObject).message } }]]; + } + throw error; + } const waitTill = configureWaitTillDate(this); diff --git a/packages/nodes-base/nodes/Slack/test/v2/node/message/sendAndWait.test.ts b/packages/nodes-base/nodes/Slack/test/v2/node/message/sendAndWait.test.ts new file mode 100644 index 00000000000..845b053a2a5 --- /dev/null +++ b/packages/nodes-base/nodes/Slack/test/v2/node/message/sendAndWait.test.ts @@ -0,0 +1,117 @@ +import type { MockProxy } from 'jest-mock-extended'; +import { mock } from 'jest-mock-extended'; +import { type INode, SEND_AND_WAIT_OPERATION, type IExecuteFunctions } from 'n8n-workflow'; + +import { SlackV2 } from '../../../../V2/SlackV2.node'; +import * as GenericFunctions from '../../../../V2/GenericFunctions'; + +describe('Test SlackV2, message => sendAndWait', () => { + let slack: SlackV2; + let mockExecuteFunctions: MockProxy; + let slackApiRequestSpy: jest.SpyInstance; + + const mockNode: INode = { + id: 'test-node-id', + name: 'Slack', + type: 'n8n-nodes-base.slack', + typeVersion: 2.3, + position: [0, 0], + parameters: {}, + }; + + beforeEach(() => { + slack = new SlackV2({ + name: 'Slack', + displayName: 'Slack', + description: 'Slack node', + group: ['output'], + }); + mockExecuteFunctions = mock(); + slackApiRequestSpy = jest.spyOn(GenericFunctions, 'slackApiRequest'); + + mockExecuteFunctions.getNode.mockReturnValue(mockNode); + mockExecuteFunctions.getInstanceId.mockReturnValue('instanceId'); + mockExecuteFunctions.getInputData.mockReturnValue([{ json: { data: 'test' } }]); + mockExecuteFunctions.continueOnFail.mockReturnValue(false); + mockExecuteFunctions.putExecutionToWait.mockImplementation(async () => {}); + mockExecuteFunctions.getSignedResumeUrl.mockReturnValue( + 'http://localhost/waiting-webhook/nodeID?approved=true&token=abc', + ); + + mockExecuteFunctions.getNodeParameter.mockImplementation((key: string) => { + if (key === 'authentication') return 'accessToken'; + if (key === 'resource') return 'message'; + if (key === 'operation') return SEND_AND_WAIT_OPERATION; + if (key === 'select') return 'channel'; + if (key === 'channelId') return 'C123456789'; + if (key === 'message') return 'test message'; + if (key === 'subject') return ''; + if (key === 'approvalOptions.values') return {}; + if (key === 'options') return {}; + if (key === 'options.limitWaitTime.values') return {}; + if (key === 'responseType') return 'approval'; + return undefined; + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should send message and put execution to wait', async () => { + slackApiRequestSpy.mockResolvedValue({ ok: true }); + + const result = await slack.execute.call(mockExecuteFunctions); + + expect(result).toEqual([[{ json: { data: 'test' } }]]); + expect(slackApiRequestSpy).toHaveBeenCalledTimes(1); + expect(mockExecuteFunctions.putExecutionToWait).toHaveBeenCalledTimes(1); + + expect(slackApiRequestSpy).toHaveBeenCalledWith('POST', '/chat.postMessage', { + channel: 'C123456789', + blocks: [ + { type: 'divider' }, + { + type: 'section', + text: { + type: 'mrkdwn', + text: 'test message', + }, + }, + { + type: 'section', + text: { type: 'plain_text', text: ' ' }, + }, + { type: 'divider' }, + { + type: 'actions', + elements: [ + { + type: 'button', + style: 'primary', + text: { type: 'plain_text', text: 'Approve', emoji: true }, + url: 'http://localhost/waiting-webhook/nodeID?approved=true&token=abc', + }, + ], + }, + ], + }); + }); + + it('should route API errors to error output when continueOnFail is true', async () => { + slackApiRequestSpy.mockRejectedValueOnce(new Error('channel_not_found')); + mockExecuteFunctions.continueOnFail.mockReturnValue(true); + + const result = await slack.execute.call(mockExecuteFunctions); + + expect(result).toEqual([[{ json: { error: 'channel_not_found' } }]]); + expect(mockExecuteFunctions.putExecutionToWait).not.toHaveBeenCalled(); + }); + + it('should throw error when continueOnFail is false', async () => { + slackApiRequestSpy.mockRejectedValueOnce(new Error('channel_not_found')); + + await expect(slack.execute.call(mockExecuteFunctions)).rejects.toThrow('channel_not_found'); + expect(mockExecuteFunctions.putExecutionToWait).not.toHaveBeenCalled(); + }); +});