diff --git a/src/App.ts b/src/App.ts index 07552bc8b..697630e95 100644 --- a/src/App.ts +++ b/src/App.ts @@ -54,7 +54,7 @@ import { SlashCommand, WorkflowStepEdit, } from './types'; -import { IncomingEventType, getTypeAndConversation, assertNever } from './helpers'; +import { IncomingEventType, getTypeAndConversation, assertNever, isBodyWithTypeEnterpriseInstall, isEventTypeToSkipAuthorize } from './helpers'; import { CodedError, asCodedError, AppInitializationError, MultipleListenerError, ErrorCode, InvalidCustomPropertyError } from './errors'; import { AllMiddlewareArgs, contextBuiltinKeys } from './types/middleware'; import { StringIndexed } from './types/helpers'; @@ -878,7 +878,7 @@ export default class App const source = buildSource(type, conversationId, bodyArg, isEnterpriseInstall); let authorizeResult: AuthorizeResult; - if (type === IncomingEventType.Event && isEventTypeToSkipAuthorize(event.body.event.type)) { + if (type === IncomingEventType.Event && isEventTypeToSkipAuthorize(event)) { authorizeResult = { enterpriseId: source.enterpriseId, teamId: source.teamId, @@ -1522,25 +1522,6 @@ function buildSource( }; } -function isBodyWithTypeEnterpriseInstall(body: AnyMiddlewareArgs['body'], type: IncomingEventType): boolean { - if (type === IncomingEventType.Event) { - const bodyAsEvent = body as SlackEventMiddlewareArgs['body']; - if (Array.isArray(bodyAsEvent.authorizations) && bodyAsEvent.authorizations[0] !== undefined) { - return !!bodyAsEvent.authorizations[0].is_enterprise_install; - } - } - // command payloads have this property set as a string - if (typeof body.is_enterprise_install === 'string') { - return body.is_enterprise_install === 'true'; - } - // all remaining types have a boolean property - if (body.is_enterprise_install !== undefined) { - return body.is_enterprise_install; - } - // as a fallback we assume it's a single team installation (but this should never happen) - return false; -} - function isBlockActionOrInteractiveMessageBody( body: SlackActionMiddlewareArgs['body'], ): body is SlackActionMiddlewareArgs['body'] { @@ -1562,13 +1543,6 @@ function buildRespondFn( }; } -// token revocation use cases -// https://github.com/slackapi/bolt-js/issues/674 -const eventTypesToSkipAuthorize = ['app_uninstalled', 'tokens_revoked']; -function isEventTypeToSkipAuthorize(eventType: string) { - return eventTypesToSkipAuthorize.includes(eventType); -} - function escapeHtml(input: string | undefined | null): string { if (input) { return input.replace(/&/g, '&') diff --git a/src/helpers.spec.ts b/src/helpers.spec.ts index f882fa788..9daab84ca 100644 --- a/src/helpers.spec.ts +++ b/src/helpers.spec.ts @@ -1,6 +1,7 @@ import 'mocha'; import { assert } from 'chai'; -import { getTypeAndConversation, IncomingEventType } from './helpers'; +import { isBodyWithTypeEnterpriseInstall, getTypeAndConversation, IncomingEventType, isEventTypeToSkipAuthorize } from './helpers'; +import { AnyMiddlewareArgs, ReceiverEvent, SlackEventMiddlewareArgs } from './types'; describe('Helpers', () => { describe('getTypeAndConversation()', () => { @@ -110,6 +111,118 @@ describe('Helpers', () => { }); }); }); + + describe(`${isBodyWithTypeEnterpriseInstall.name}()`, () => { + describe('with body of event type', () => { + // Arrange + const dummyEventBody: SlackEventMiddlewareArgs['body'] = { + token: '', + team_id: '', + api_app_id: '', + event_id: '', + event_time: 0, + type: 'event_callback', + event: { + type: 'app_home_opened', + user: '', + channel: '', + event_ts: '', + }, + authorizations: [{ + enterprise_id: '', + is_bot: true, + team_id: '', + user_id: '', + is_enterprise_install: true, + }], + }; + + it('should resolve the is_enterprise_install field', () => { + // Act + const isEnterpriseInstall = isBodyWithTypeEnterpriseInstall(dummyEventBody); + // Assert + assert(isEnterpriseInstall === true); + }); + + it('should resolve the is_enterprise_install with provided event type', () => { + // Act + const isEnterpriseInstall = isBodyWithTypeEnterpriseInstall(dummyEventBody, IncomingEventType.Event); + // Assert + assert(isEnterpriseInstall === true); + }); + }); + + describe('with is_enterprise_install as a string value', () => { + // Arrange + const dummyEventBody = { + is_enterprise_install: 'true', + } as AnyMiddlewareArgs['body']; + + it('should resolve is_enterprise_install as truthy', () => { + // Act + const isEnterpriseInstall = isBodyWithTypeEnterpriseInstall(dummyEventBody); + // Assert + assert(isEnterpriseInstall === true); + }); + }); + + describe('with is_enterprise_install as boolean value', () => { + // Arrange + const dummyEventBody = { + is_enterprise_install: true, + } as AnyMiddlewareArgs['body']; + + it('should resolve is_enterprise_install as truthy', () => { + // Act + const isEnterpriseInstall = isBodyWithTypeEnterpriseInstall(dummyEventBody); + // Assert + assert(isEnterpriseInstall === true); + }); + }); + + describe('with is_enterprise_install undefined', () => { + // Arrange + const dummyEventBody = {} as AnyMiddlewareArgs['body']; + + it('should resolve is_enterprise_install as falsy', () => { + // Act + const isEnterpriseInstall = isBodyWithTypeEnterpriseInstall(dummyEventBody); + // Assert + assert(isEnterpriseInstall === false); + }); + }); + }); + + describe(`${isEventTypeToSkipAuthorize.name}()`, () => { + describe('receiver events that can be skipped', () => { + it('should return truthy when event can be skipped', () => { + // Arrange + const dummyEventBody = { ack: async () => { }, body: { event: { type: 'app_uninstalled' } } } as ReceiverEvent; + // Act + const isEnterpriseInstall = isEventTypeToSkipAuthorize(dummyEventBody); + // Assert + assert(isEnterpriseInstall === true); + }); + + it('should return falsy when event can not be skipped', () => { + // Arrange + const dummyEventBody = { ack: async () => { }, body: { event: { type: '' } } } as ReceiverEvent; + // Act + const isEnterpriseInstall = isEventTypeToSkipAuthorize(dummyEventBody); + // Assert + assert(isEnterpriseInstall === false); + }); + + it('should return falsy when event is invalid', () => { + // Arrange + const dummyEventBody = { ack: async () => { }, body: {} } as ReceiverEvent; + // Act + const isEnterpriseInstall = isEventTypeToSkipAuthorize(dummyEventBody); + // Assert + assert(isEnterpriseInstall === false); + }); + }); + }); }); function createFakeActions(conversationId: string): any[] { diff --git a/src/helpers.ts b/src/helpers.ts index 559bfbf05..74889d698 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -8,6 +8,8 @@ import { SlackAction, OptionsSource, MessageShortcut, + AnyMiddlewareArgs, + ReceiverEvent, } from './types'; /** @@ -22,6 +24,11 @@ export enum IncomingEventType { Shortcut, } +// ---------------------------- +// For skipping authorize with event + +const eventTypesToSkipAuthorize = ['app_uninstalled', 'tokens_revoked']; + /** * Helper which finds the type and channel (if any) that any specific incoming event is related to. * @@ -101,6 +108,42 @@ export function getTypeAndConversation(body: any): { type?: IncomingEventType; c return {}; } +/** + * Helper which determines if the body of a request is enterprise install. + * + * Providing the type is optional but if you do the execution will be faster + */ +export function isBodyWithTypeEnterpriseInstall(body: AnyMiddlewareArgs['body'], type?: IncomingEventType): boolean { + const _type = type !== undefined ? type : getTypeAndConversation(body).type; + + if (_type === IncomingEventType.Event) { + const bodyAsEvent = body as SlackEventMiddlewareArgs['body']; + if (Array.isArray(bodyAsEvent.authorizations) && bodyAsEvent.authorizations[0] !== undefined) { + return !!bodyAsEvent.authorizations[0].is_enterprise_install; + } + } + // command payloads have this property set as a string + if (typeof body.is_enterprise_install === 'string') { + return body.is_enterprise_install === 'true'; + } + // all remaining types have a boolean property + if (body.is_enterprise_install !== undefined) { + return body.is_enterprise_install; + } + // as a fallback we assume it's a single team installation (but this should never happen) + return false; +} + +/** + * Helper which determines if the event type will skip Authorize. + * + * Token revocation use cases + * https://github.com/slackapi/bolt-js/issues/674 + */ +export function isEventTypeToSkipAuthorize(event: ReceiverEvent): boolean { + return eventTypesToSkipAuthorize.includes(event.body.event?.type); +} + /* istanbul ignore next */ /** Helper that should never be called, but is useful for exhaustiveness checking in conditional branches */