diff --git a/packages/cli/src/CredentialsHelper.ts b/packages/cli/src/CredentialsHelper.ts index 6806dd02943a9..995ce1af8746c 100644 --- a/packages/cli/src/CredentialsHelper.ts +++ b/packages/cli/src/CredentialsHelper.ts @@ -6,7 +6,8 @@ /* eslint-disable @typescript-eslint/no-unsafe-call */ import { Credentials, NodeExecuteFunctions } from 'n8n-core'; - +// eslint-disable-next-line import/no-extraneous-dependencies +import { get } from 'lodash'; import { NodeVersionedType } from 'n8n-nodes-base'; import { @@ -626,8 +627,10 @@ export class CredentialsHelper extends ICredentialsHelper { mode, ); + let response: INodeExecutionData[][] | null | undefined; + try { - await routingNode.runNode( + response = await routingNode.runNode( inputData, runIndex, nodeTypeCopy, @@ -676,6 +679,24 @@ export class CredentialsHelper extends ICredentialsHelper { }; } + if ( + credentialTestFunction.testRequest.rules && + Array.isArray(credentialTestFunction.testRequest.rules) + ) { + // Special testing rules are defined so check all in order + for (const rule of credentialTestFunction.testRequest.rules) { + if (rule.type === 'responseSuccessBody') { + const responseData = response![0][0].json; + if (get(responseData, rule.properties.key) === rule.properties.value) { + return { + status: 'Error', + message: rule.properties.message, + }; + } + } + } + } + return { status: 'OK', message: 'Connection successful!', diff --git a/packages/nodes-base/credentials/SlackApi.credentials.ts b/packages/nodes-base/credentials/SlackApi.credentials.ts index ae32dabfb9544..588c4b8cb6883 100644 --- a/packages/nodes-base/credentials/SlackApi.credentials.ts +++ b/packages/nodes-base/credentials/SlackApi.credentials.ts @@ -1,9 +1,11 @@ import { + IAuthenticateBearer, + IAuthenticateQueryAuth, + ICredentialTestRequest, ICredentialType, INodeProperties, } from 'n8n-workflow'; - export class SlackApi implements ICredentialType { name = 'slackApi'; displayName = 'Slack API'; @@ -17,4 +19,26 @@ export class SlackApi implements ICredentialType { required: true, }, ]; + authenticate: IAuthenticateBearer = { + type: 'bearer', + properties: { + tokenPropertyName: 'accessToken', + }, + }; + test: ICredentialTestRequest = { + request: { + baseURL: 'https://slack.com', + url: '/api/users.profile.get', + }, + rules: [ + { + type: 'responseSuccessBody', + properties: { + key: 'ok', + value: false, + message: 'Invalid access token', + }, + }, + ], + }; } diff --git a/packages/nodes-base/nodes/Slack/GenericFunctions.ts b/packages/nodes-base/nodes/Slack/GenericFunctions.ts index e9ee7711772a8..e27bc2741687f 100644 --- a/packages/nodes-base/nodes/Slack/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Slack/GenericFunctions.ts @@ -11,6 +11,7 @@ import { import { IDataObject, IOAuth2Options, + JsonObject, NodeApiError, NodeOperationError, } from 'n8n-workflow'; @@ -36,26 +37,16 @@ export async function slackApiRequest(this: IExecuteFunctions | IExecuteSingleFu if (Object.keys(query).length === 0) { delete options.qs; } - try { - let response: any; // tslint:disable-line:no-any - if (authenticationMethod === 'accessToken') { - const credentials = await this.getCredentials('slackApi'); - if (credentials === undefined) { - throw new NodeOperationError(this.getNode(), 'No credentials got returned!'); - } - options.headers!.Authorization = `Bearer ${credentials.accessToken}`; - //@ts-ignore - response = await this.helpers.request(options); - } else { + const oAuth2Options: IOAuth2Options = { + tokenType: 'Bearer', + property: 'authed_user.access_token', + }; - const oAuth2Options: IOAuth2Options = { - tokenType: 'Bearer', - property: 'authed_user.access_token', - }; - //@ts-ignore - response = await this.helpers.requestOAuth2.call(this, 'slackOAuth2Api', options, oAuth2Options); - } + try { + let response: any; // tslint:disable-line:no-any + const credentialType = authenticationMethod === 'accessToken' ? 'slackApi' : 'slackOAuth2Api'; + response = await this.helpers.requestWithAuthentication.call(this, credentialType, options, { oauth2: oAuth2Options }); if (response.ok === false) { if (response.error === 'paid_teams_only') { @@ -69,7 +60,7 @@ export async function slackApiRequest(this: IExecuteFunctions | IExecuteSingleFu return response; } catch (error) { - throw new NodeApiError(this.getNode(), error); + throw new NodeApiError(this.getNode(), error as JsonObject); } } diff --git a/packages/nodes-base/nodes/Slack/MessageDescription.ts b/packages/nodes-base/nodes/Slack/MessageDescription.ts index b2aeef41b7b3e..27e1c4aa8fbde 100644 --- a/packages/nodes-base/nodes/Slack/MessageDescription.ts +++ b/packages/nodes-base/nodes/Slack/MessageDescription.ts @@ -171,6 +171,97 @@ export const messageFields: INodeProperties[] = [ }, }, }, + { + displayName: 'Options', + name: 'otherOptions', + type: 'collection', + displayOptions: { + show: { + operation: [ + 'post', + 'postEphemeral', + ], + resource: [ + 'message', + ], + }, + }, + default: {}, + description: 'Other options to set', + placeholder: 'Add options', + options: [ + { + displayName: 'Icon Emoji', + name: 'icon_emoji', + type: 'string', + default: '', + description: 'Emoji to use as the icon for this message. Overrides icon_url.', + }, + { + displayName: 'Icon URL', + name: 'icon_url', + type: 'string', + default: '', + description: 'URL to an image to use as the icon for this message.', + }, + { + displayName: 'Link Names', + name: 'link_names', + type: 'boolean', + default: false, + description: 'Find and link channel names and usernames.', + }, + { + displayName: 'Make Reply', + name: 'thread_ts', + type: 'string', + default: '', + description: 'Provide another message\'s ts value to make this message a reply.', + }, + { + displayName: 'Markdown', + name: 'mrkdwn', + type: 'boolean', + default: true, + description: 'Use Slack Markdown parsing.', + }, + { + displayName: 'Reply Broadcast', + name: 'reply_broadcast', + type: 'boolean', + default: false, + description: 'Used in conjunction with thread_ts and indicates whether reply should be made visible to everyone in the channel or conversation.', + }, + { + displayName: 'Unfurl Links', + name: 'unfurl_links', + type: 'boolean', + default: false, + description: 'Pass true to enable unfurling of primarily text-based content.', + }, + { + displayName: 'Unfurl Media', + name: 'unfurl_media', + type: 'boolean', + default: true, + description: 'Pass false to disable unfurling of media content.', + }, + { + displayName: 'Send as User', + name: 'sendAsUser', + type: 'string', + displayOptions: { + show: { + '/authentication': [ + 'accessToken', + ], + }, + }, + default: '', + description: 'The message will be sent from this username (i.e. as if this individual sent the message).', + }, + ], + }, { displayName: 'Attachments', name: 'attachments', @@ -367,97 +458,6 @@ export const messageFields: INodeProperties[] = [ }, ], }, - { - displayName: 'Other Options', - name: 'otherOptions', - type: 'collection', - displayOptions: { - show: { - operation: [ - 'post', - 'postEphemeral', - ], - resource: [ - 'message', - ], - }, - }, - default: {}, - description: 'Other options to set', - placeholder: 'Add options', - options: [ - { - displayName: 'Icon Emoji', - name: 'icon_emoji', - type: 'string', - default: '', - description: 'Emoji to use as the icon for this message. Overrides icon_url.', - }, - { - displayName: 'Icon URL', - name: 'icon_url', - type: 'string', - default: '', - description: 'URL to an image to use as the icon for this message.', - }, - { - displayName: 'Link Names', - name: 'link_names', - type: 'boolean', - default: false, - description: 'Find and link channel names and usernames.', - }, - { - displayName: 'Make Reply', - name: 'thread_ts', - type: 'string', - default: '', - description: 'Provide another message\'s ts value to make this message a reply.', - }, - { - displayName: 'Markdown', - name: 'mrkdwn', - type: 'boolean', - default: true, - description: 'Use Slack Markdown parsing.', - }, - { - displayName: 'Reply Broadcast', - name: 'reply_broadcast', - type: 'boolean', - default: false, - description: 'Used in conjunction with thread_ts and indicates whether reply should be made visible to everyone in the channel or conversation.', - }, - { - displayName: 'Unfurl Links', - name: 'unfurl_links', - type: 'boolean', - default: false, - description: 'Pass true to enable unfurling of primarily text-based content.', - }, - { - displayName: 'Unfurl Media', - name: 'unfurl_media', - type: 'boolean', - default: true, - description: 'Pass false to disable unfurling of media content.', - }, - { - displayName: 'Send as User', - name: 'sendAsUser', - type: 'string', - displayOptions: { - show: { - '/authentication': [ - 'accessToken', - ], - }, - }, - default: '', - description: 'The message will be sent from this username (i.e. as if this individual sent the message).', - }, - ], - }, /* ----------------------------------------------------------------------- */ /* message:update */ @@ -583,8 +583,8 @@ export const messageFields: INodeProperties[] = [ ], }, { - displayName: 'Blocks', - name: 'blocksJson', + displayName: 'Attachments', + name: 'attachmentsJson', type: 'json', default: '', required: false, @@ -604,11 +604,11 @@ export const messageFields: INodeProperties[] = [ ], }, }, - description: 'The blocks to add', + description: 'The attachments to add', }, { - displayName: 'Attachments', - name: 'attachmentsJson', + displayName: 'Blocks', + name: 'blocksJson', type: 'json', default: '', required: false, @@ -628,7 +628,7 @@ export const messageFields: INodeProperties[] = [ ], }, }, - description: 'The attachments to add', + description: 'The blocks to add', }, { displayName: 'Blocks', diff --git a/packages/nodes-base/nodes/Slack/Slack.node.ts b/packages/nodes-base/nodes/Slack/Slack.node.ts index 260767473543b..acac54fb233b1 100644 --- a/packages/nodes-base/nodes/Slack/Slack.node.ts +++ b/packages/nodes-base/nodes/Slack/Slack.node.ts @@ -1,4 +1,6 @@ -import { IExecuteFunctions } from 'n8n-core'; +import { + IExecuteFunctions, +} from 'n8n-core'; import { ICredentialsDecrypted, @@ -131,7 +133,6 @@ export class Slack implements INodeType { ], }, }, - testedBy: 'testSlackTokenAuth', }, { name: 'slackOAuth2Api', @@ -288,41 +289,6 @@ export class Slack implements INodeType { return returnData; }, }, - credentialTest: { - async testSlackTokenAuth(this: ICredentialTestFunctions, credential: ICredentialsDecrypted): Promise { - - const options = { - method: 'POST', - headers: { - 'Content-Type': 'application/json; charset=utf-8', - Authorization: `Bearer ${credential.data!.accessToken}`, - }, - uri: 'https://slack.com/api/users.profile.get', - json: true, - }; - - try { - const response = await this.helpers.request(options); - - if (!response.ok) { - return { - status: 'Error', - message: `${response.error}`, - }; - } - } catch (err) { - return { - status: 'Error', - message: `${(err as JsonObject).message}`, - }; - } - - return { - status: 'OK', - message: 'Connection successful!', - }; - }, - }, }; async execute(this: IExecuteFunctions): Promise { diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts index 058b471c43375..4430c302da4ed 100644 --- a/packages/workflow/src/Interfaces.ts +++ b/packages/workflow/src/Interfaces.ts @@ -246,9 +246,17 @@ export interface IAuthenticateRuleResponseCode extends IAuthenticateRuleBase { }; } +export interface IAuthenticateRuleResponseSuccessBody extends IAuthenticateRuleBase { + type: 'responseSuccessBody'; + properties: { + message: string; + key: string; + value: any; + }; +} export interface ICredentialTestRequest { request: IHttpRequestOptions; - rules?: IAuthenticateRuleResponseCode[]; + rules?: IAuthenticateRuleResponseCode[] | IAuthenticateRuleResponseSuccessBody[]; } export interface ICredentialTestRequestData {