From 9edef5be30f5c8670ece2b7f1d8b89d8785e146e Mon Sep 17 00:00:00 2001 From: Kazuhiro Sera Date: Fri, 4 Feb 2022 11:56:24 +0900 Subject: [PATCH 1/7] Fix #248 by adding a new option deferInitialization --- src/App.spec.ts | 36 +++++++ src/App.ts | 275 +++++++++++++++++++++++++++++++++++------------- 2 files changed, 235 insertions(+), 76 deletions(-) diff --git a/src/App.spec.ts b/src/App.spec.ts index 3ce6c4a9c..3c7b19a64 100644 --- a/src/App.spec.ts +++ b/src/App.spec.ts @@ -417,6 +417,42 @@ describe('App', () => { // Assert assert.instanceOf(app, MockApp); }); + + it('should fail in await App#init()', async () => { + // Arrange + const fakeConstructor = sinon.fake(); + const overrides = mergeOverrides(withNoopAppMetadata(), { + '@slack/web-api': { + WebClient: class { + public constructor() { + fakeConstructor(...arguments); // eslint-disable-line prefer-rest-params + } + + public auth = { + test: () => { + throw new Error('Failing for init() test!'); + }, + }; + }, + }, + }); + + const MockApp = await importApp(overrides); + const app = new MockApp({ + token: 'xoxb-completely-invalid-token', + signingSecret: 'invalid-one', + deferInitialization: true, + }); + // Assert + assert.instanceOf(app, MockApp); + try { + await app.init(); + assert.fail('The init() method should fail here'); + } catch (err: any) { + assert.equal(err.message, 'Failing for init() test!'); + } + }); + // TODO: tests for ignoreSelf option // TODO: tests for logger and logLevel option // TODO: tests for providing botId and botUserId options diff --git a/src/App.ts b/src/App.ts index 9e932603d..569334667 100644 --- a/src/App.ts +++ b/src/App.ts @@ -105,6 +105,7 @@ export interface AppOptions { socketMode?: boolean; developerMode?: boolean; tokenVerificationEnabled?: boolean; + deferInitialization?: boolean; extendedErrorHandler?: boolean; } @@ -241,6 +242,17 @@ export default class App { private hasCustomErrorHandler: boolean; + // used for the deferred initialization + private argToken?: string; + + // used for the deferred initialization + private argAuthorize?: Authorize; + + // used for the deferred initialization + private argAuthorization?: Authorization; + + private tokenVerificationEnabled: boolean; + public constructor({ signingSecret = undefined, endpoints = undefined, @@ -272,6 +284,7 @@ export default class App { developerMode = false, tokenVerificationEnabled = true, extendedErrorHandler = false, + deferInitialization = false, }: AppOptions = {}) { // this.logLevel = logLevel; @@ -369,61 +382,68 @@ export default class App { } /* --------------------- Initialize receiver ---------------------- */ - if (receiver !== undefined) { - // Custom receiver supplied - if (this.socketMode === true) { - // socketMode = true should result in SocketModeReceiver being used as receiver - // TODO: Add case for when socketMode = true and receiver = SocketModeReceiver - // as this should not result in an error - throw new AppInitializationError('You cannot supply a custom receiver when socketMode is set to true.'); - } - this.receiver = receiver; - } else if (this.socketMode === true) { - if (appToken === undefined) { - throw new AppInitializationError('You must provide an appToken when socketMode is set to true. To generate an appToken see: https://api.slack.com/apis/connections/socket#token'); - } - this.logger.debug('Initializing SocketModeReceiver'); - this.receiver = new SocketModeReceiver({ - appToken, - clientId, - clientSecret, - stateSecret, - redirectUri, - installationStore, - scopes, - logger, - logLevel: this.logLevel, - installerOptions: this.installerOptions, - customRoutes, - }); - } else if (signatureVerification === true && signingSecret === undefined) { - // Using default receiver HTTPReceiver, signature verification enabled, missing signingSecret - throw new AppInitializationError( - 'signingSecret is required to initialize the default receiver. Set signingSecret or use a ' + - 'custom receiver. You can find your Signing Secret in your Slack App Settings.', - ); + this.receiver = this.initReceiver( + receiver, + signingSecret, + endpoints, + port, + customRoutes, + processBeforeResponse, + signatureVerification, + clientId, + clientSecret, + stateSecret, + redirectUri, + installationStore, + scopes, + appToken, + logger, + ); + + /* ------------------------ Set authorize ----------------------------- */ + this.tokenVerificationEnabled = tokenVerificationEnabled; + let argAuthorization: Authorization | undefined; + if (token !== undefined) { + argAuthorization = { + botId, + botUserId, + botToken: token, + }; + } + if (deferInitialization) { + this.argToken = token; + this.argAuthorize = authorize; + this.argAuthorization = argAuthorization; + // You need to run `await app.init();` on your own } else { - this.logger.debug('Initializing HTTPReceiver'); - this.receiver = new HTTPReceiver({ - signingSecret: signingSecret || '', - endpoints, - port, - customRoutes, - processBeforeResponse, - signatureVerification, - clientId, - clientSecret, - stateSecret, - redirectUri, - installationStore, - scopes, - logger, - logLevel: this.logLevel, - installerOptions: this.installerOptions, - }); + this.initInConstructor( + token, + authorize, + argAuthorization, + ); } - /* ------------------------ Set authorize ----------------------------- */ + // Conditionally use a global middleware that ignores events (including messages) that are sent from this app + if (ignoreSelf) { + this.use(ignoreSelfMiddleware()); + } + + // Use conversation state global middleware + if (convoStore !== false) { + // Use the memory store by default, or another store if provided + const store: ConversationStore = convoStore === undefined ? new MemoryStore() : convoStore; + this.use(conversationContext(store)); + } + + /* ------------------------ Initialize receiver ------------------------ */ + // Should be last to avoid exposing partially initialized app + this.receiver.init(this); + } + + public initAuthorizeIfNoTokenIsGiven( + token?: string, + authorize?: Authorize, + ): void { let usingOauth = false; const httpReceiver = (this.receiver as HTTPReceiver); if ( @@ -434,23 +454,17 @@ export default class App { // and theoretically, doing a fully custom (non express) receiver that implements OAuth usingOauth = true; } + if (token !== undefined) { - // If a token is supplied, the app is installed in at least one workspace if (usingOauth || authorize !== undefined) { throw new AppInitializationError( `You cannot provide a token along with either oauth installer options or authorize. ${tokenUsage}`, ); } - this.authorize = singleAuthorization( - this.client, - { - botId, - botUserId, - botToken: token, - }, - tokenVerificationEnabled, - ); - } else if (authorize === undefined && !usingOauth) { + return; + } + + if (authorize === undefined && !usingOauth) { throw new AppInitializationError( `${tokenUsage} \n\nSince you have not provided a token or authorize, you might be missing one or more required oauth installer options. See https://slack.dev/bolt-js/concepts#authenticating-oauth for these required fields.\n`, ); @@ -460,27 +474,136 @@ export default class App { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion this.authorize = httpReceiver.installer!.authorize; } else if (authorize !== undefined && !usingOauth) { - this.authorize = authorize; + this.authorize = authorize as Authorize; + } + } + + public initInConstructor( + token?: string, + authorize?: Authorize, + authorization?: Authorization, + ): void { + this.initAuthorizeIfNoTokenIsGiven( + token, + authorize, + ); + if (this.authorize !== undefined) { + return; + } + if (token !== undefined && authorization !== undefined) { + this.authorize = singleAuthorization( + this.client, + authorization, + this.tokenVerificationEnabled, + ); } else { this.logger.error('Something has gone wrong. Please report this issue to the maintainers. https://github.com/slackapi/bolt-js/issues'); assertNever(); } + } - // Conditionally use a global middleware that ignores events (including messages) that are sent from this app - if (ignoreSelf) { - this.use(ignoreSelfMiddleware()); + public async init(): Promise { + this.initAuthorizeIfNoTokenIsGiven( + this.argToken, + this.argAuthorize, + ); + if (this.authorize !== undefined) { + return; } - - // Use conversation state global middleware - if (convoStore !== false) { - // Use the memory store by default, or another store if provided - const store: ConversationStore = convoStore === undefined ? new MemoryStore() : convoStore; - this.use(conversationContext(store)); + if (this.argToken !== undefined && this.argAuthorization !== undefined) { + let authorization = this.argAuthorization; + if (this.tokenVerificationEnabled) { + const authTestResult = await this.client.auth.test({ token: this.argToken }); + if (authTestResult.ok) { + authorization = { + botUserId: authTestResult.user_id as string, + botId: authTestResult.bot_id as string, + botToken: this.argToken, + }; + } + } + this.authorize = singleAuthorization( + this.client, + authorization, + this.tokenVerificationEnabled, + ); + } else { + this.logger.error('Something has gone wrong. Please report this issue to the maintainers. https://github.com/slackapi/bolt-js/issues'); + assertNever(); } + } - /* ------------------------ Initialize receiver ------------------------ */ - // Should be last to avoid exposing partially initialized app - this.receiver.init(this); + private initReceiver( + receiver?: Receiver, + signingSecret?: HTTPReceiverOptions['signingSecret'], + endpoints?: HTTPReceiverOptions['endpoints'], + port?: HTTPReceiverOptions['port'], + customRoutes?: HTTPReceiverOptions['customRoutes'], + processBeforeResponse?: HTTPReceiverOptions['processBeforeResponse'], + signatureVerification?: HTTPReceiverOptions['signatureVerification'], + clientId?: HTTPReceiverOptions['clientId'], + clientSecret?: HTTPReceiverOptions['clientSecret'], + stateSecret?: HTTPReceiverOptions['stateSecret'], + redirectUri?: HTTPReceiverOptions['redirectUri'], + installationStore?: HTTPReceiverOptions['installationStore'], + scopes?: HTTPReceiverOptions['scopes'], + appToken?: string, + logger?: Logger, + ): Receiver { + if (receiver !== undefined) { + // Custom receiver supplied + if (this.socketMode === true) { + // socketMode = true should result in SocketModeReceiver being used as receiver + // TODO: Add case for when socketMode = true and receiver = SocketModeReceiver + // as this should not result in an error + throw new AppInitializationError('You cannot supply a custom receiver when socketMode is set to true.'); + } + return receiver; + } + if (this.socketMode === true) { + if (appToken === undefined) { + throw new AppInitializationError('You must provide an appToken when socketMode is set to true. To generate an appToken see: https://api.slack.com/apis/connections/socket#token'); + } + this.logger.debug('Initializing SocketModeReceiver'); + return new SocketModeReceiver({ + appToken, + clientId, + clientSecret, + stateSecret, + redirectUri, + installationStore, + scopes, + logger, + logLevel: this.logLevel, + installerOptions: this.installerOptions, + customRoutes, + }); + } + if (signatureVerification === true && signingSecret === undefined) { + // Using default receiver HTTPReceiver, signature verification enabled, missing signingSecret + throw new AppInitializationError( + 'signingSecret is required to initialize the default receiver. Set signingSecret or use a ' + + 'custom receiver. You can find your Signing Secret in your Slack App Settings.', + ); + } + this.logger.debug('Initializing HTTPReceiver'); + return new HTTPReceiver({ + signingSecret: signingSecret || '', + endpoints, + port, + customRoutes, + processBeforeResponse, + signatureVerification, + clientId, + clientSecret, + stateSecret, + redirectUri, + installationStore, + scopes, + logger, + logLevel: this.logLevel, + installerOptions: this.installerOptions, + }); } /** From 0a672213ca1fb6afe30b3ed3a581e3b5f102b39d Mon Sep 17 00:00:00 2001 From: Kazuhiro Sera Date: Fri, 4 Feb 2022 12:00:48 +0900 Subject: [PATCH 2/7] Move private methods in code --- src/App.ts | 274 +++++++++++++++++++++++++++-------------------------- 1 file changed, 139 insertions(+), 135 deletions(-) diff --git a/src/App.ts b/src/App.ts index 569334667..7c4d272d3 100644 --- a/src/App.ts +++ b/src/App.ts @@ -440,68 +440,6 @@ export default class App { this.receiver.init(this); } - public initAuthorizeIfNoTokenIsGiven( - token?: string, - authorize?: Authorize, - ): void { - let usingOauth = false; - const httpReceiver = (this.receiver as HTTPReceiver); - if ( - httpReceiver.installer !== undefined && - httpReceiver.installer.authorize !== undefined - ) { - // This supports using the built in HTTPReceiver, declaring your own HTTPReceiver - // and theoretically, doing a fully custom (non express) receiver that implements OAuth - usingOauth = true; - } - - if (token !== undefined) { - if (usingOauth || authorize !== undefined) { - throw new AppInitializationError( - `You cannot provide a token along with either oauth installer options or authorize. ${tokenUsage}`, - ); - } - return; - } - - if (authorize === undefined && !usingOauth) { - throw new AppInitializationError( - `${tokenUsage} \n\nSince you have not provided a token or authorize, you might be missing one or more required oauth installer options. See https://slack.dev/bolt-js/concepts#authenticating-oauth for these required fields.\n`, - ); - } else if (authorize !== undefined && usingOauth) { - throw new AppInitializationError(`You cannot provide both authorize and oauth installer options. ${tokenUsage}`); - } else if (authorize === undefined && usingOauth) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - this.authorize = httpReceiver.installer!.authorize; - } else if (authorize !== undefined && !usingOauth) { - this.authorize = authorize as Authorize; - } - } - - public initInConstructor( - token?: string, - authorize?: Authorize, - authorization?: Authorization, - ): void { - this.initAuthorizeIfNoTokenIsGiven( - token, - authorize, - ); - if (this.authorize !== undefined) { - return; - } - if (token !== undefined && authorization !== undefined) { - this.authorize = singleAuthorization( - this.client, - authorization, - this.tokenVerificationEnabled, - ); - } else { - this.logger.error('Something has gone wrong. Please report this issue to the maintainers. https://github.com/slackapi/bolt-js/issues'); - assertNever(); - } - } - public async init(): Promise { this.initAuthorizeIfNoTokenIsGiven( this.argToken, @@ -533,79 +471,6 @@ export default class App { } } - private initReceiver( - receiver?: Receiver, - signingSecret?: HTTPReceiverOptions['signingSecret'], - endpoints?: HTTPReceiverOptions['endpoints'], - port?: HTTPReceiverOptions['port'], - customRoutes?: HTTPReceiverOptions['customRoutes'], - processBeforeResponse?: HTTPReceiverOptions['processBeforeResponse'], - signatureVerification?: HTTPReceiverOptions['signatureVerification'], - clientId?: HTTPReceiverOptions['clientId'], - clientSecret?: HTTPReceiverOptions['clientSecret'], - stateSecret?: HTTPReceiverOptions['stateSecret'], - redirectUri?: HTTPReceiverOptions['redirectUri'], - installationStore?: HTTPReceiverOptions['installationStore'], - scopes?: HTTPReceiverOptions['scopes'], - appToken?: string, - logger?: Logger, - ): Receiver { - if (receiver !== undefined) { - // Custom receiver supplied - if (this.socketMode === true) { - // socketMode = true should result in SocketModeReceiver being used as receiver - // TODO: Add case for when socketMode = true and receiver = SocketModeReceiver - // as this should not result in an error - throw new AppInitializationError('You cannot supply a custom receiver when socketMode is set to true.'); - } - return receiver; - } - if (this.socketMode === true) { - if (appToken === undefined) { - throw new AppInitializationError('You must provide an appToken when socketMode is set to true. To generate an appToken see: https://api.slack.com/apis/connections/socket#token'); - } - this.logger.debug('Initializing SocketModeReceiver'); - return new SocketModeReceiver({ - appToken, - clientId, - clientSecret, - stateSecret, - redirectUri, - installationStore, - scopes, - logger, - logLevel: this.logLevel, - installerOptions: this.installerOptions, - customRoutes, - }); - } - if (signatureVerification === true && signingSecret === undefined) { - // Using default receiver HTTPReceiver, signature verification enabled, missing signingSecret - throw new AppInitializationError( - 'signingSecret is required to initialize the default receiver. Set signingSecret or use a ' + - 'custom receiver. You can find your Signing Secret in your Slack App Settings.', - ); - } - this.logger.debug('Initializing HTTPReceiver'); - return new HTTPReceiver({ - signingSecret: signingSecret || '', - endpoints, - port, - customRoutes, - processBeforeResponse, - signatureVerification, - clientId, - clientSecret, - stateSecret, - redirectUri, - installationStore, - scopes, - logger, - logLevel: this.logLevel, - installerOptions: this.installerOptions, - }); - } - /** * Register a new middleware, processed in the order registered. * @@ -1163,6 +1028,145 @@ export default class App { this.errorHandler({ error: asCodedError(error), ...rest }) : this.errorHandler(asCodedError(error)); } + + // --------------------- + // Private methods for initialization + // --------------------- + + private initReceiver( + receiver?: Receiver, + signingSecret?: HTTPReceiverOptions['signingSecret'], + endpoints?: HTTPReceiverOptions['endpoints'], + port?: HTTPReceiverOptions['port'], + customRoutes?: HTTPReceiverOptions['customRoutes'], + processBeforeResponse?: HTTPReceiverOptions['processBeforeResponse'], + signatureVerification?: HTTPReceiverOptions['signatureVerification'], + clientId?: HTTPReceiverOptions['clientId'], + clientSecret?: HTTPReceiverOptions['clientSecret'], + stateSecret?: HTTPReceiverOptions['stateSecret'], + redirectUri?: HTTPReceiverOptions['redirectUri'], + installationStore?: HTTPReceiverOptions['installationStore'], + scopes?: HTTPReceiverOptions['scopes'], + appToken?: string, + logger?: Logger, + ): Receiver { + if (receiver !== undefined) { + // Custom receiver supplied + if (this.socketMode === true) { + // socketMode = true should result in SocketModeReceiver being used as receiver + // TODO: Add case for when socketMode = true and receiver = SocketModeReceiver + // as this should not result in an error + throw new AppInitializationError('You cannot supply a custom receiver when socketMode is set to true.'); + } + return receiver; + } + if (this.socketMode === true) { + if (appToken === undefined) { + throw new AppInitializationError('You must provide an appToken when socketMode is set to true. To generate an appToken see: https://api.slack.com/apis/connections/socket#token'); + } + this.logger.debug('Initializing SocketModeReceiver'); + return new SocketModeReceiver({ + appToken, + clientId, + clientSecret, + stateSecret, + redirectUri, + installationStore, + scopes, + logger, + logLevel: this.logLevel, + installerOptions: this.installerOptions, + customRoutes, + }); + } + if (signatureVerification === true && signingSecret === undefined) { + // Using default receiver HTTPReceiver, signature verification enabled, missing signingSecret + throw new AppInitializationError( + 'signingSecret is required to initialize the default receiver. Set signingSecret or use a ' + + 'custom receiver. You can find your Signing Secret in your Slack App Settings.', + ); + } + this.logger.debug('Initializing HTTPReceiver'); + return new HTTPReceiver({ + signingSecret: signingSecret || '', + endpoints, + port, + customRoutes, + processBeforeResponse, + signatureVerification, + clientId, + clientSecret, + stateSecret, + redirectUri, + installationStore, + scopes, + logger, + logLevel: this.logLevel, + installerOptions: this.installerOptions, + }); + } + + private initAuthorizeIfNoTokenIsGiven( + token?: string, + authorize?: Authorize, + ): void { + let usingOauth = false; + const httpReceiver = (this.receiver as HTTPReceiver); + if ( + httpReceiver.installer !== undefined && + httpReceiver.installer.authorize !== undefined + ) { + // This supports using the built in HTTPReceiver, declaring your own HTTPReceiver + // and theoretically, doing a fully custom (non express) receiver that implements OAuth + usingOauth = true; + } + + if (token !== undefined) { + if (usingOauth || authorize !== undefined) { + throw new AppInitializationError( + `You cannot provide a token along with either oauth installer options or authorize. ${tokenUsage}`, + ); + } + return; + } + + if (authorize === undefined && !usingOauth) { + throw new AppInitializationError( + `${tokenUsage} \n\nSince you have not provided a token or authorize, you might be missing one or more required oauth installer options. See https://slack.dev/bolt-js/concepts#authenticating-oauth for these required fields.\n`, + ); + } else if (authorize !== undefined && usingOauth) { + throw new AppInitializationError(`You cannot provide both authorize and oauth installer options. ${tokenUsage}`); + } else if (authorize === undefined && usingOauth) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.authorize = httpReceiver.installer!.authorize; + } else if (authorize !== undefined && !usingOauth) { + this.authorize = authorize as Authorize; + } + } + + private initInConstructor( + token?: string, + authorize?: Authorize, + authorization?: Authorization, + ): void { + this.initAuthorizeIfNoTokenIsGiven( + token, + authorize, + ); + if (this.authorize !== undefined) { + return; + } + if (token !== undefined && authorization !== undefined) { + this.authorize = singleAuthorization( + this.client, + authorization, + this.tokenVerificationEnabled, + ); + } else { + this.logger.error('Something has gone wrong. Please report this issue to the maintainers. https://github.com/slackapi/bolt-js/issues'); + assertNever(); + } + } } function defaultErrorHandler(logger: Logger): ErrorHandler { From cd576458cfd2e53e2fb2dfde463b5f546ea2ddf4 Mon Sep 17 00:00:00 2001 From: Kazuhiro Sera Date: Sat, 5 Feb 2022 08:09:03 +0900 Subject: [PATCH 3/7] Improve a lot --- src/App.spec.ts | 7 +++++++ src/App.ts | 53 +++++++++++++++++++++++++++++-------------------- 2 files changed, 39 insertions(+), 21 deletions(-) diff --git a/src/App.spec.ts b/src/App.spec.ts index 3c7b19a64..34b9f3477 100644 --- a/src/App.spec.ts +++ b/src/App.spec.ts @@ -445,6 +445,13 @@ describe('App', () => { }); // Assert assert.instanceOf(app, MockApp); + try { + // call #start() before #init() + await app.start(); + assert.fail('The start() method should fail befofe init() call'); + } catch (err: any) { + assert.equal(err.message, 'This App instance is not yet initialized. Call `await App#init()` before starting the app.'); + } try { await app.init(); assert.fail('The init() method should fail here'); diff --git a/src/App.ts b/src/App.ts index 7c4d272d3..3d78aa836 100644 --- a/src/App.ts +++ b/src/App.ts @@ -242,17 +242,19 @@ export default class App { private hasCustomErrorHandler: boolean; - // used for the deferred initialization + // used when deferInitialization is true private argToken?: string; - // used for the deferred initialization + // used when deferInitialization is true private argAuthorize?: Authorize; - // used for the deferred initialization + // used when deferInitialization is true private argAuthorization?: Authorization; private tokenVerificationEnabled: boolean; + private initialized: boolean; + public constructor({ signingSecret = undefined, endpoints = undefined, @@ -381,7 +383,6 @@ export default class App { }; } - /* --------------------- Initialize receiver ---------------------- */ this.receiver = this.initReceiver( receiver, signingSecret, @@ -414,13 +415,15 @@ export default class App { this.argToken = token; this.argAuthorize = authorize; this.argAuthorization = argAuthorization; + this.initialized = false; // You need to run `await app.init();` on your own } else { - this.initInConstructor( + this.authorize = this.initAuthorizeInConstructor( token, authorize, argAuthorization, ); + this.initialized = true; } // Conditionally use a global middleware that ignores events (including messages) that are sent from this app @@ -441,11 +444,12 @@ export default class App { } public async init(): Promise { - this.initAuthorizeIfNoTokenIsGiven( + const initializedAuthorize = this.initAuthorizeIfNoTokenIsGiven( this.argToken, this.argAuthorize, ); - if (this.authorize !== undefined) { + if (initializedAuthorize !== undefined) { + this.authorize = initializedAuthorize; return; } if (this.argToken !== undefined && this.argAuthorization !== undefined) { @@ -502,6 +506,11 @@ export default class App { public start( ...args: Parameters ): ReturnType { + if (!this.initialized) { + throw new AppInitializationError( + 'This App instance is not yet initialized. Call `await App#init()` before starting the app.', + ); + } // TODO: HTTPReceiver['start'] should be the actual receiver's return type return this.receiver.start(...args) as ReturnType; } @@ -1109,7 +1118,7 @@ export default class App { private initAuthorizeIfNoTokenIsGiven( token?: string, authorize?: Authorize, - ): void { + ): Authorize | undefined { let usingOauth = false; const httpReceiver = (this.receiver as HTTPReceiver); if ( @@ -1127,7 +1136,7 @@ export default class App { `You cannot provide a token along with either oauth installer options or authorize. ${tokenUsage}`, ); } - return; + return undefined; } if (authorize === undefined && !usingOauth) { @@ -1138,34 +1147,36 @@ export default class App { throw new AppInitializationError(`You cannot provide both authorize and oauth installer options. ${tokenUsage}`); } else if (authorize === undefined && usingOauth) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - this.authorize = httpReceiver.installer!.authorize; + return httpReceiver.installer!.authorize; } else if (authorize !== undefined && !usingOauth) { - this.authorize = authorize as Authorize; + return authorize as Authorize; } + return undefined; } - private initInConstructor( + private initAuthorizeInConstructor( token?: string, - authorize?: Authorize, + authorize?: Authorize, authorization?: Authorization, - ): void { - this.initAuthorizeIfNoTokenIsGiven( + ): Authorize { + const initializedAuthorize = this.initAuthorizeIfNoTokenIsGiven( token, authorize, ); - if (this.authorize !== undefined) { - return; + if (initializedAuthorize !== undefined) { + return initializedAuthorize; } if (token !== undefined && authorization !== undefined) { - this.authorize = singleAuthorization( + return singleAuthorization( this.client, authorization, this.tokenVerificationEnabled, ); - } else { - this.logger.error('Something has gone wrong. Please report this issue to the maintainers. https://github.com/slackapi/bolt-js/issues'); - assertNever(); } + const hasToken = token !== undefined && token.length > 0; + const errorMessage = `Something has gone wrong in #initAuthorizeInConstructor method (hasToken: ${hasToken}, authorize: ${authorize}). Please report this issue to the maintainers. https://github.com/slackapi/bolt-js/issues`; + this.logger.error(errorMessage); + throw new Error(errorMessage); } } From e837905e237fa06981294c32404e7a17af27195f Mon Sep 17 00:00:00 2001 From: Kazuhiro Sera Date: Sat, 5 Feb 2022 08:13:18 +0900 Subject: [PATCH 4/7] Update more --- src/App.ts | 60 +++++++++++++++++++++++++++++++----------------------- 1 file changed, 34 insertions(+), 26 deletions(-) diff --git a/src/App.ts b/src/App.ts index 3d78aa836..4c91e168d 100644 --- a/src/App.ts +++ b/src/App.ts @@ -444,34 +444,42 @@ export default class App { } public async init(): Promise { - const initializedAuthorize = this.initAuthorizeIfNoTokenIsGiven( - this.argToken, - this.argAuthorize, - ); - if (initializedAuthorize !== undefined) { - this.authorize = initializedAuthorize; - return; - } - if (this.argToken !== undefined && this.argAuthorization !== undefined) { - let authorization = this.argAuthorization; - if (this.tokenVerificationEnabled) { - const authTestResult = await this.client.auth.test({ token: this.argToken }); - if (authTestResult.ok) { - authorization = { - botUserId: authTestResult.user_id as string, - botId: authTestResult.bot_id as string, - botToken: this.argToken, - }; + this.initialized = true; + try { + const initializedAuthorize = this.initAuthorizeIfNoTokenIsGiven( + this.argToken, + this.argAuthorize, + ); + if (initializedAuthorize !== undefined) { + this.authorize = initializedAuthorize; + return; + } + if (this.argToken !== undefined && this.argAuthorization !== undefined) { + let authorization = this.argAuthorization; + if (this.tokenVerificationEnabled) { + const authTestResult = await this.client.auth.test({ token: this.argToken }); + if (authTestResult.ok) { + authorization = { + botUserId: authTestResult.user_id as string, + botId: authTestResult.bot_id as string, + botToken: this.argToken, + }; + } } + this.authorize = singleAuthorization( + this.client, + authorization, + this.tokenVerificationEnabled, + ); + this.initialized = true; + } else { + this.logger.error('Something has gone wrong. Please report this issue to the maintainers. https://github.com/slackapi/bolt-js/issues'); + assertNever(); } - this.authorize = singleAuthorization( - this.client, - authorization, - this.tokenVerificationEnabled, - ); - } else { - this.logger.error('Something has gone wrong. Please report this issue to the maintainers. https://github.com/slackapi/bolt-js/issues'); - assertNever(); + } catch (e: unknown) { + // Revert the flag change as the initialization failed + this.initialized = false; + throw e; } } From fa695aeeb7b72d528fedd92a2bc58ff7894307c4 Mon Sep 17 00:00:00 2001 From: Kazuhiro Sera Date: Sat, 5 Feb 2022 08:14:46 +0900 Subject: [PATCH 5/7] Fix --- src/App.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/App.ts b/src/App.ts index 4c91e168d..13d8cb84c 100644 --- a/src/App.ts +++ b/src/App.ts @@ -476,7 +476,7 @@ export default class App { this.logger.error('Something has gone wrong. Please report this issue to the maintainers. https://github.com/slackapi/bolt-js/issues'); assertNever(); } - } catch (e: unknown) { + } catch (e) { // Revert the flag change as the initialization failed this.initialized = false; throw e; From 3b7b6aa3b210d961bcfbb56616a2796c0a9c1f64 Mon Sep 17 00:00:00 2001 From: Kazuhiro Sera Date: Sat, 5 Feb 2022 08:19:02 +0900 Subject: [PATCH 6/7] Fix examples --- examples/custom-properties/http.js | 11 +++++++++-- examples/custom-properties/package.json | 4 ++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/examples/custom-properties/http.js b/examples/custom-properties/http.js index cf62a31d1..0e6e9ba8b 100644 --- a/examples/custom-properties/http.js +++ b/examples/custom-properties/http.js @@ -32,6 +32,8 @@ const app = new App({ }, unhandledRequestTimeoutMillis: 2000, // the default is 3001 }), + // This option enables developer to call #init() in async/await style + deferInitialization: true, }); app.use(async ({ logger, context, next }) => { @@ -41,7 +43,12 @@ app.use(async ({ logger, context, next }) => { (async () => { // Start your app - await app.start(process.env.PORT || 3000); - + try { + await app.init(); + await app.start(process.env.PORT || 3000); + } catch (e) { + console.error(e); + process.exit(255); + } console.log('⚡️ Bolt app is running!'); })(); \ No newline at end of file diff --git a/examples/custom-properties/package.json b/examples/custom-properties/package.json index 23481d786..ec2af1efa 100644 --- a/examples/custom-properties/package.json +++ b/examples/custom-properties/package.json @@ -2,10 +2,10 @@ "name": "bolt-js-custom-properties-app", "version": "1.0.0", "description": "Having custom request properties in ⚡️ Bolt for JavaScript", - "main": "app.js", + "main": "http.js", "scripts": { "ngrok": "ngrok http 3000", - "start": "node app.js", + "start": "node http.js", "test": "echo \"Error: no test specified\" && exit 1" }, "license": "MIT", From f4d52c9cf7c3b229342251f99c65efd1d95ab5dc Mon Sep 17 00:00:00 2001 From: Kazuhiro Sera Date: Sat, 5 Feb 2022 08:28:08 +0900 Subject: [PATCH 7/7] Update src/App.spec.ts Co-authored-by: Sarah Jiang --- src/App.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/App.spec.ts b/src/App.spec.ts index 34b9f3477..133fbd0eb 100644 --- a/src/App.spec.ts +++ b/src/App.spec.ts @@ -448,7 +448,7 @@ describe('App', () => { try { // call #start() before #init() await app.start(); - assert.fail('The start() method should fail befofe init() call'); + assert.fail('The start() method should fail before init() call'); } catch (err: any) { assert.equal(err.message, 'This App instance is not yet initialized. Call `await App#init()` before starting the app.'); }