Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix #248 by adding a new option deferInitialization #1303

Merged
merged 8 commits into from
Feb 14, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions src/App.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
309 changes: 218 additions & 91 deletions src/App.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ export interface AppOptions {
socketMode?: boolean;
developerMode?: boolean;
tokenVerificationEnabled?: boolean;
deferInitialization?: boolean;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Naming suggestions?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This naming makes the most sense to me.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is fine

extendedErrorHandler?: boolean;
}

Expand Down Expand Up @@ -241,6 +242,17 @@ export default class App {

private hasCustomErrorHandler: boolean;

// used for the deferred initialization
srajiang marked this conversation as resolved.
Show resolved Hide resolved
private argToken?: string;

// used for the deferred initialization
srajiang marked this conversation as resolved.
Show resolved Hide resolved
private argAuthorize?: Authorize;

// used for the deferred initialization
private argAuthorization?: Authorization;

private tokenVerificationEnabled: boolean;

public constructor({
signingSecret = undefined,
endpoints = undefined,
Expand Down Expand Up @@ -272,6 +284,7 @@ export default class App {
developerMode = false,
tokenVerificationEnabled = true,
extendedErrorHandler = false,
deferInitialization = false,
}: AppOptions = {}) {
// this.logLevel = logLevel;

Expand Down Expand Up @@ -369,101 +382,45 @@ export default class App {
}

/* --------------------- Initialize receiver ---------------------- */
srajiang marked this conversation as resolved.
Show resolved Hide resolved
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.',
);
} 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.receiver = this.initReceiver(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is so nice! 💯

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, I like splitting up the initialization into discrete functions!

receiver,
signingSecret,
endpoints,
port,
customRoutes,
processBeforeResponse,
signatureVerification,
clientId,
clientSecret,
stateSecret,
redirectUri,
installationStore,
scopes,
appToken,
logger,
);

/* ------------------------ Set authorize ----------------------------- */
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;
}
this.tokenVerificationEnabled = tokenVerificationEnabled;
let argAuthorization: Authorization | undefined;
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) {
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;
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.error('Something has gone wrong. Please report this issue to the maintainers. https://github.com/slackapi/bolt-js/issues');
assertNever();
this.initInConstructor(
token,
authorize,
argAuthorization,
);
}

// Conditionally use a global middleware that ignores events (including messages) that are sent from this app
Expand All @@ -483,6 +440,37 @@ export default class App {
this.receiver.init(this);
}

public async init(): Promise<void> {
this.initAuthorizeIfNoTokenIsGiven(
this.argToken,
this.argAuthorize,
);
if (this.authorize !== undefined) {
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,
);
} else {
this.logger.error('Something has gone wrong. Please report this issue to the maintainers. https://github.com/slackapi/bolt-js/issues');
assertNever();
}
}

/**
* Register a new middleware, processed in the order registered.
*
Expand Down Expand Up @@ -1040,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(
filmaj marked this conversation as resolved.
Show resolved Hide resolved
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<boolean>;
}
}

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 {
Expand Down