Skip to content

Commit

Permalink
Bind this for rejection handling from say utility (slackapi#184)
Browse files Browse the repository at this point in the history
Bind this for rejection handling from say utility
  • Loading branch information
aoberoi authored May 6, 2019
2 parents 210ef06 + ab7da55 commit 7596366
Show file tree
Hide file tree
Showing 3 changed files with 248 additions and 12 deletions.
253 changes: 245 additions & 8 deletions src/App.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ describe('App', () => {
app.use(fakeFirstMiddleware);
app.use(fakeSecondMiddleware);
fakeReceiver.emit('message', dummyReceiverEvent);
await delay(10);
await delay();

// Assert
assert(fakeFirstMiddleware.calledOnce);
Expand All @@ -288,12 +288,248 @@ describe('App', () => {
});
describe('middleware and listener arguments', () => {
describe('say()', () => {
it.skip('should send a message to a channel where the incoming event originates', async () => {

function createChannelContextualReceiverEvents(channelId: string): ReceiverEvent[] {
return [
// IncomingEventType.Event with channel in payload
{
body: {
event: {
channel: channelId,
},
team_id: 'TEAM_ID',
},
respond: noop,
ack: noop,
},
// IncomingEventType.Event with channel in item
{
body: {
event: {
item: {
channel: channelId,
},
},
team_id: 'TEAM_ID',
},
respond: noop,
ack: noop,
},
// IncomingEventType.Command
{
body: {
command: '/COMMAND_NAME',
channel_id: channelId,
team_id: 'TEAM_ID',
},
respond: noop,
ack: noop,
},
// IncomingEventType.Action from block action, interactive message, or message action
{
body: {
actions: [{}],
channel: {
id: channelId,
},
user: {
id: 'USER_ID',
},
team: {
id: 'TEAM_ID',
},
},
respond: noop,
ack: noop,
},
// IncomingEventType.Action from dialog submission
{
body: {
type: 'dialog_submission',
channel: {
id: channelId,
},
user: {
id: 'USER_ID',
},
team: {
id: 'TEAM_ID',
},
},
respond: noop,
ack: noop,
},
];
}

it('should send a simple message to a channel where the incoming event originates', async () => {
// Arrange
const fakeReceiver = createFakeReceiver();
const fakePostMessage = sinon.fake();
const dummyReceiverEvent = createDummyReceiverEvent();
const fakePostMessage = sinon.fake.resolves({});
const fakeErrorHandler = sinon.fake();
const dummyAuthorizationResult = { botToken: '', botId: '' };
const dummyMessage = 'test';
const dummyChannelId = 'CHANNEL_ID';
const dummyReceiverEvents = createChannelContextualReceiverEvents(dummyChannelId);
const overrides = mergeOverrides(
withNoopAppMetadata(),
withPostMessage(fakePostMessage),
withMemoryStore(sinon.fake()),
withConversationContext(sinon.fake.returns(noopMiddleware)),
);
const App = await importApp(overrides); // tslint:disable-line:variable-name

// Act
const app = new App({ receiver: fakeReceiver, authorize: sinon.fake.resolves(dummyAuthorizationResult) });
app.use((args) => {
// By definition, these events should all produce a say function, so we cast args.say into a SayFn
const say = (args as any).say as SayFn;
say(dummyMessage);
});
app.error(fakeErrorHandler);
dummyReceiverEvents.forEach(dummyEvent => fakeReceiver.emit('message', dummyEvent));
await delay();

// Assert
assert.equal(fakePostMessage.callCount, dummyReceiverEvents.length);
// Assert that each call to fakePostMessage had the right arguments
fakePostMessage.getCalls().forEach((call) => {
const firstArg = call.args[0];
assert.propertyVal(firstArg, 'text', dummyMessage);
assert.propertyVal(firstArg, 'channel', dummyChannelId);
});
assert(fakeErrorHandler.notCalled);
});

// TODO: DRY up this case with the case above
it('should send a complex message to a channel where the incoming event originates', async () => {
// Arrange
const fakeReceiver = createFakeReceiver();
const fakePostMessage = sinon.fake.resolves({});
const fakeErrorHandler = sinon.fake();
const dummyAuthorizationResult = { botToken: '', botId: '' };
const dummyMessage = { text: 'test' };
const dummyChannelId = 'CHANNEL_ID';
const dummyReceiverEvents = createChannelContextualReceiverEvents(dummyChannelId);
const overrides = mergeOverrides(
withNoopAppMetadata(),
withPostMessage(fakePostMessage),
withMemoryStore(sinon.fake()),
withConversationContext(sinon.fake.returns(noopMiddleware)),
);
const App = await importApp(overrides); // tslint:disable-line:variable-name

// Act
const app = new App({ receiver: fakeReceiver, authorize: sinon.fake.resolves(dummyAuthorizationResult) });
app.use((args) => {
// By definition, these events should all produce a say function, so we cast args.say into a SayFn
const say = (args as any).say as SayFn;
say(dummyMessage);
});
app.error(fakeErrorHandler);
dummyReceiverEvents.forEach(dummyEvent => fakeReceiver.emit('message', dummyEvent));
await delay();

// Assert
assert.equal(fakePostMessage.callCount, dummyReceiverEvents.length);
// Assert that each call to fakePostMessage had the right arguments
fakePostMessage.getCalls().forEach((call) => {
const firstArg = call.args[0];
assert.propertyVal(firstArg, 'channel', dummyChannelId);
for (const prop in dummyMessage) {
assert.propertyVal(firstArg, prop, (dummyMessage as any)[prop]);
}
});
assert(fakeErrorHandler.notCalled);
});

function createReceiverEventsWithoutSay(channelId: string): ReceiverEvent[] {
return [
// IncomingEventType.Options from block action
{
body: {
type: 'block_suggestion',
channel: {
id: channelId,
},
user: {
id: 'USER_ID',
},
team: {
id: 'TEAM_ID',
},
},
respond: noop,
ack: noop,
},
// IncomingEventType.Options from interactive message or dialog
{
body: {
name: 'select_field_name',
channel: {
id: channelId,
},
user: {
id: 'USER_ID',
},
team: {
id: 'TEAM_ID',
},
},
respond: noop,
ack: noop,
},
// IncomingEventType.Event without a channel context
{
body: {
event: {
},
team_id: 'TEAM_ID',
},
respond: noop,
ack: noop,
},
];
}

it('should not exist in the arguments on incoming events that don\'t support say', async () => {
// Arrange
const fakeReceiver = createFakeReceiver();
const assertionAggregator = sinon.fake();
const dummyAuthorizationResult = { botToken: '', botId: '' };
const dummyReceiverEvents = createReceiverEventsWithoutSay('CHANNEL_ID');
const overrides = mergeOverrides(
withNoopAppMetadata(),
withNoopWebClient(),
withMemoryStore(sinon.fake()),
withConversationContext(sinon.fake.returns(noopMiddleware)),
);
const App = await importApp(overrides); // tslint:disable-line:variable-name

// Act
const app = new App({ receiver: fakeReceiver, authorize: sinon.fake.resolves(dummyAuthorizationResult) });
app.use((args) => {
assert.isUndefined((args as any).say);
// If the above assertion fails, then it would throw an AssertionError and the following line will not be
// called
assertionAggregator();
});
dummyReceiverEvents.forEach(dummyEvent => fakeReceiver.emit('message', dummyEvent));
await delay();

// Assert
assert.equal(assertionAggregator.callCount, dummyReceiverEvents.length);
});

it('should handle failures through the App\'s global error handler', async () => {
// Arrange
const fakeReceiver = createFakeReceiver();
const fakePostMessage = sinon.fake.rejects(new Error('fake error'));
const fakeErrorHandler = sinon.fake();
const dummyAuthorizationResult = { botToken: '', botId: '' };
const dummyMessage = { text: 'test' };
const dummyChannelId = 'CHANNEL_ID';
const dummyReceiverEvents = createChannelContextualReceiverEvents(dummyChannelId);
const overrides = mergeOverrides(
withNoopAppMetadata(),
withPostMessage(fakePostMessage),
Expand All @@ -303,17 +539,18 @@ describe('App', () => {
const App = await importApp(overrides); // tslint:disable-line:variable-name

// Act
const app = new App({ receiver: fakeReceiver, authorize: noopAuthorize });
const app = new App({ receiver: fakeReceiver, authorize: sinon.fake.resolves(dummyAuthorizationResult) });
app.use((args) => {
// By definition, the mockEvents should all produce a say function, so we cast args.say into a SayFn
// By definition, these events should all produce a say function, so we cast args.say into a SayFn
const say = (args as any).say as SayFn;
say(dummyMessage);
});
fakeReceiver.emit('message', dummyReceiverEvent);
app.error(fakeErrorHandler);
dummyReceiverEvents.forEach(dummyEvent => fakeReceiver.emit('message', dummyEvent));
await delay();

// Assert
// TODO
assert.equal(fakeErrorHandler.callCount, dummyReceiverEvents.length);
});
});
});
Expand Down
2 changes: 1 addition & 1 deletion src/App.ts
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,7 @@ export default class App {
const postMessageArguments: ChatPostMessageArguments = (typeof message === 'string') ?
{ token, text: message, channel: channelId } : { ...message, token, channel: channelId };
this.client.chat.postMessage(postMessageArguments)
.catch(this.onGlobalError);
.catch(error => this.onGlobalError(error));
};
};

Expand Down
5 changes: 2 additions & 3 deletions src/types/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@ export type AnyMiddlewareArgs =
SlackEventMiddlewareArgs | SlackActionMiddlewareArgs | SlackCommandMiddlewareArgs | SlackOptionsMiddlewareArgs;

export interface PostProcessFn {
// TODO: should the return value any be unknown type?
(error: Error | undefined, done: (error?: Error) => void): any;
(error: Error | undefined, done: (error?: Error) => void): unknown;
}

export interface Context extends StringIndexed {
Expand All @@ -20,7 +19,7 @@ export interface Context extends StringIndexed {
// constraint would mess up the interface of App#event(), App#message(), etc.
export interface Middleware<Args> {
// TODO: is there something nice we can do to get context's property types to flow from one middleware to the next?
(args: Args & { next: NextMiddleware, context: Context }): void;
(args: Args & { next: NextMiddleware, context: Context }): unknown;
}

export interface NextMiddleware {
Expand Down

0 comments on commit 7596366

Please sign in to comment.