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

Fixed the issue with member joined\left channel event not firing for own bot #236

Merged
merged 8 commits into from
Oct 4, 2019
169 changes: 168 additions & 1 deletion src/middleware/builtin.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import sinon from 'sinon';
import { ErrorCode } from '../errors';
import { Override, delay, wrapToResolveOnFirstCall } from '../test-helpers';
import rewiremock from 'rewiremock';
import { SlackEventMiddlewareArgs, NextMiddleware, Context, MessageEvent } from '../types';
import { SlackEventMiddlewareArgs, NextMiddleware, Context, MessageEvent, ContextMissingPropertyError, SlackCommandMiddlewareArgs } from '../types';

describe('matchMessage()', () => {
function initializeTestCase(pattern: string | RegExp): Mocha.AsyncFunc {
Expand Down Expand Up @@ -261,13 +261,180 @@ describe('directMention()', () => {
});
});

describe('ignoreSelf()', () => {
it('should handle context missing error', async () => {
// Arrange
const fakeNext = sinon.fake();
const fakeBotUserId = undefined;
const fakeArgs = {
next: fakeNext,
context: { botUserId: fakeBotUserId },
} as unknown as MemberJoinedOrLeftChannelMiddlewareArgs;

const { ignoreSelf: getIgnoreSelfMiddleware, contextMissingPropertyError } = await importBuiltin();

// Act
const middleware = getIgnoreSelfMiddleware();
middleware(fakeArgs);
await delay();

// Assert
const expectedError = contextMissingPropertyError(
'botId',
'Cannot ignore events from the app without a bot ID. Ensure authorize callback returns a botId.',
);

const contextMissingError: ContextMissingPropertyError = fakeNext.getCall(0).lastArg;

assert.equal(contextMissingError.code, expectedError.code);
assert.equal(contextMissingError.missingProperty, expectedError.missingProperty);
});

it('should immediately call next(), because incoming middleware args don\'t contain event', async () => {
// Arrange
const fakeNext = sinon.fake();
const fakeBotUserId = 'BUSER1';
const fakeArgs = {
next: fakeNext,
context: { botUserId: fakeBotUserId },
command: {
command: '/fakeCommand',
},
} as unknown as CommandMiddlewareArgs;

const { ignoreSelf: getIgnoreSelfMiddleware } = await importBuiltin();

// Act
const middleware = getIgnoreSelfMiddleware();
middleware(fakeArgs);
await delay();

// Assert
assert(fakeNext.calledOnce);
});

it('should look for an event identified as a bot message from the same bot ID as this app and skip it', async () => {
// Arrange
const fakeNext = sinon.fake();
const fakeBotUserId = 'BUSER1';
// TODO: Fix typings here
const fakeArgs = {
next: fakeNext,
context: { botUserId: fakeBotUserId, botId: fakeBotUserId },
event: {
type: 'message',
subtype: 'bot_message',
bot_id: fakeBotUserId,
},
message: {
type: 'message',
subtype: 'bot_message',
bot_id: fakeBotUserId,
},
} as any;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Unfortunately, I wasn't able to provide the correct type here :(
If someone could help, it would be great!


const { ignoreSelf: getIgnoreSelfMiddleware } = await importBuiltin();

// Act
const middleware = getIgnoreSelfMiddleware();
middleware(fakeArgs);
await delay();

// Assert
assert(fakeNext.notCalled);
});

it('should filter an event out, because it matches our own app and shouldn\'t be retained', async () => {
// Arrange
const fakeNext = sinon.fake();
const fakeBotUserId = 'BUSER1';
const fakeArgs = {
next: fakeNext,
context: { botUserId: fakeBotUserId, botId: fakeBotUserId },
event: {
type: 'tokens_revoked',
user: fakeBotUserId,
},
} as unknown as TokensRevokedMiddlewareArgs;

const { ignoreSelf: getIgnoreSelfMiddleware } = await importBuiltin();

// Act
const middleware = getIgnoreSelfMiddleware();
middleware(fakeArgs);
await delay();

// Assert
assert(fakeNext.notCalled);
});

it('should filter an event out, because it matches our own app and shouldn\'t be retained', async () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

is this the same test as above?

// Arrange
const fakeNext = sinon.fake();
const fakeBotUserId = 'BUSER1';
const fakeArgs = {
next: fakeNext,
context: { botUserId: fakeBotUserId, botId: fakeBotUserId },
event: {
type: 'tokens_revoked',
user: fakeBotUserId,
},
} as unknown as TokensRevokedMiddlewareArgs;

const { ignoreSelf: getIgnoreSelfMiddleware } = await importBuiltin();

// Act
const middleware = getIgnoreSelfMiddleware();
middleware(fakeArgs);
await delay();

// Assert
assert(fakeNext.notCalled);
});

it('shouldn\'t filter an event out, because it should be retained', async () => {
// Arrange
const fakeNext = sinon.fake();
const fakeBotUserId = 'BUSER1';
const eventsWhichShouldNotBeFilteredOut = ['member_joined_channel', 'member_left_channel'];

const listOfFakeArgs = eventsWhichShouldNotBeFilteredOut.map((eventType) => {
return {
next: fakeNext,
context: { botUserId: fakeBotUserId, botId: fakeBotUserId },
event: {
type: eventType,
user: fakeBotUserId,
},
} as unknown as MemberJoinedOrLeftChannelMiddlewareArgs;
});

const { ignoreSelf: getIgnoreSelfMiddleware } = await importBuiltin();

// Act
const middleware = getIgnoreSelfMiddleware();
listOfFakeArgs.forEach(middleware);

await delay();

// Assert
assert.equal(fakeNext.callCount, listOfFakeArgs.length);
});
});

/* Testing Harness */

interface DummyContext {
matches?: RegExpExecArray;
}

type MessageMiddlewareArgs = SlackEventMiddlewareArgs<'message'> & { next: NextMiddleware, context: Context };
type TokensRevokedMiddlewareArgs = SlackEventMiddlewareArgs<'tokens_revoked'> & { next: NextMiddleware, context: Context };

type MemberJoinedOrLeftChannelMiddlewareArgs = SlackEventMiddlewareArgs<'member_joined_channel' | 'member_left_channel'>
& { next: NextMiddleware, context: Context };

type CommandMiddlewareArgs = SlackCommandMiddlewareArgs & { next: NextMiddleware; context: Context };

async function importBuiltin(
overrides: Override = {},
Expand Down
10 changes: 7 additions & 3 deletions src/middleware/builtin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,8 @@ export const onlyEvents: Middleware<AnyMiddlewareArgs & { event?: SlackEvent }>
* Middleware that checks for matches given constraints
*/
export function matchConstraints(
constraints: ActionConstraints,
): Middleware<SlackActionMiddlewareArgs | SlackOptionsMiddlewareArgs> {
constraints: ActionConstraints,
): Middleware<SlackActionMiddlewareArgs | SlackOptionsMiddlewareArgs> {
return ({ payload, body, next, context }) => {
// TODO: is putting matches in an array actually helpful? there's no way to know which of the regexps contributed
// which matches (and in which order)
Expand Down Expand Up @@ -231,7 +231,11 @@ export function ignoreSelf(): Middleware<AnyMiddlewareArgs> {
}

// Its an Events API event that isn't of type message, but the user ID might match our own app. Filter these out.
if (botUserId !== undefined && args.event.user === botUserId) {
// However, some events still must be fired, because they can make sense.
const eventsWhichShouldNotBeFilteredOut = ['member_joined_channel', 'member_left_channel'];
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
const eventsWhichShouldNotBeFilteredOut = ['member_joined_channel', 'member_left_channel'];
const eventsWhichShouldBeKept = [
'member_joined_channel',
'member_left_channel',
];

const isEventShouldBeSkipped = !eventsWhichShouldNotBeFilteredOut.includes(args.event.type);
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
const isEventShouldBeSkipped = !eventsWhichShouldNotBeFilteredOut.includes(args.event.type);
const isEventShouldBeSkipped = !eventsWhichShouldBeKept.includes(args.event.type);

I didn't like the double negative 🤔 this might be more readable...


if (botUserId !== undefined && args.event.user === botUserId && isEventShouldBeSkipped) {
return;
}
}
Expand Down