-
Notifications
You must be signed in to change notification settings - Fork 428
Testing #157
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
Merged
Merged
Testing #157
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
345b4d6
basic unit testing rig
aoberoi 44cbf43
implementing some basic tests
aoberoi 78b32ce
Merge branch 'v4' into add-testing
aoberoi 45bdc34
complete app initialization tests
aoberoi 3a280c6
add more app constructor tests, add vscode launch config for debugging
aoberoi d8cda57
adds App constructor tests for conversation store options
aoberoi 80de02a
simplify receiver interface, add more tests for App
aoberoi 3fb0cc0
extend test case timeout
aoberoi File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,4 @@ | ||
| { | ||
| "require": ["ts-node/register", "source-map-support/register"] | ||
| "require": ["ts-node/register", "source-map-support/register"], | ||
| "timeout": 3000 | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,7 +3,7 @@ | |
| "src/**/*.ts" | ||
| ], | ||
| "exclude": [ | ||
| "**/*.spec.js" | ||
| "**/*.spec.ts" | ||
| ], | ||
| "reporter": ["lcov"], | ||
| "extension": [ | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| { | ||
| // Use IntelliSense to learn about possible attributes. | ||
| // Hover to view descriptions of existing attributes. | ||
| // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 | ||
| "version": "0.2.0", | ||
| "configurations": [ | ||
| { | ||
| "type": "node", | ||
| "request": "launch", | ||
| "name": "Spec tests", | ||
| "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", | ||
| "stopOnEntry": false, | ||
| "args": [ | ||
| "--config", | ||
| ".mocharc.json", | ||
| "--no-timeouts", | ||
| "src/*.spec.ts", | ||
| "src/**/*.spec.ts" | ||
| ], | ||
| "cwd": "${workspaceFolder}", | ||
| "runtimeExecutable": null, | ||
| "env": { | ||
| "NODE_ENV": "testing" | ||
| } | ||
| } | ||
| ] | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,291 @@ | ||
| // tslint:disable:ter-prefer-arrow-callback typedef no-implicit-dependencies no-this-assignment | ||
| import 'mocha'; | ||
| import { EventEmitter } from 'events'; | ||
| import sinon, { SinonSpy } from 'sinon'; | ||
| import { assert } from 'chai'; | ||
| import rewiremock from 'rewiremock'; | ||
| import { ErrorCode } from './errors'; | ||
| import { Receiver, ReceiverEvent, Middleware, AnyMiddlewareArgs } from './types'; | ||
| import { ConversationStore } from './conversation-store'; | ||
| import { Logger } from '@slack/logger'; | ||
|
|
||
| describe('App', () => { | ||
| describe('constructor', () => { | ||
| // TODO: test when the single team authorization results fail. that should still succeed but warn. it also means | ||
| // that the `ignoreSelf` middleware will fail (or maybe just warn) a bunch. | ||
| describe('with successful single team authorization results', () => { | ||
| it('should succeed with a token for single team authorization', async () => { | ||
| const { App } = await importAppWhichFetchesOwnBotIds(); | ||
| new App({ token: '', signingSecret: '' }); // tslint:disable-line:no-unused-expression | ||
| }); | ||
| }); | ||
| it('should succeed with an authorize callback', async () => { | ||
| const { App } = await importApp(); | ||
| const authorizeCallback = sinon.spy(); | ||
| new App({ authorize: authorizeCallback, signingSecret: '' }); // tslint:disable-line:no-unused-expression | ||
| assert(authorizeCallback.notCalled); | ||
| }); | ||
| it('should fail without a token for single team authorization or authorize callback', async () => { | ||
| const { App } = await importApp(); | ||
| try { | ||
| new App({ signingSecret: '' }); // tslint:disable-line:no-unused-expression | ||
| assert.fail(); | ||
| } catch (error) { | ||
| assert.instanceOf(error, Error); | ||
| assert.propertyVal(error, 'code', ErrorCode.AppInitializationError); | ||
| } | ||
| }); | ||
| it('should fail when both a token and authorize callback are specified', async () => { | ||
| const { App } = await importApp(); | ||
| const authorizeCallback = sinon.spy(); | ||
| try { | ||
| // tslint:disable-next-line:no-unused-expression | ||
| new App({ token: '', authorize: authorizeCallback, signingSecret: '' }); | ||
| assert.fail(); | ||
| } catch (error) { | ||
| assert.instanceOf(error, Error); | ||
| assert.propertyVal(error, 'code', ErrorCode.AppInitializationError); | ||
| assert(authorizeCallback.notCalled); | ||
| } | ||
| }); | ||
| describe('with a custom receiver', () => { | ||
| it('should succeed with no signing secret for the default receiver', async () => { | ||
| const { App } = await importApp(); | ||
| const mockReceiver = createMockReceiver(); | ||
| new App({ receiver: mockReceiver, authorize: sinon.spy() }); // tslint:disable-line:no-unused-expression | ||
| }); | ||
| }); | ||
| it('should fail when no signing secret for the default receiver is specified', async () => { | ||
| const { App } = await importApp(); | ||
| try { | ||
| new App({ authorize: sinon.spy() }); // tslint:disable-line:no-unused-expression | ||
| assert.fail(); | ||
| } catch (error) { | ||
| assert.instanceOf(error, Error); | ||
| assert.propertyVal(error, 'code', ErrorCode.AppInitializationError); | ||
| } | ||
| }); | ||
| it('should initialize MemoryStore conversation store by default', async () => { | ||
| const { App, memoryStoreStub, conversationContextStub } = await importApp(); | ||
| new App({ authorize: sinon.spy(), signingSecret: '' }); // tslint:disable-line:no-unused-expression | ||
| assert(memoryStoreStub.calledWithNew); | ||
| assert((conversationContextStub as SinonSpy).called); | ||
| }); | ||
| it('should initialize without a conversation store when option is false', async () => { | ||
| const { App, conversationContextStub } = await importApp(); | ||
| // tslint:disable-next-line:no-unused-expression | ||
| new App({ convoStore: false, authorize: sinon.spy(), signingSecret: '' }); | ||
| assert((conversationContextStub as SinonSpy).notCalled); | ||
| }); | ||
| describe('with a custom conversation store', () => { | ||
| it('should initialize the conversation store', async () => { | ||
| const { App, conversationContextStub } = await importApp(); | ||
| const mockConvoStore = createMockConvoStore(); | ||
| // tslint:disable-next-line:no-unused-expression | ||
| new App({ convoStore: mockConvoStore, authorize: sinon.spy(), signingSecret: '' }); | ||
| assert((conversationContextStub as SinonSpy).firstCall.calledWith(mockConvoStore)); | ||
| }); | ||
| }); | ||
| // TODO: tests for ignoreSelf option | ||
| // TODO: tests for logger and logLevel option | ||
| // TODO: tests for providing botId and botUserId options | ||
| // TODO: tests for providing endpoints option | ||
| }); | ||
|
|
||
| describe('#start', () => { | ||
| it('should pass calls through to receiver', async () => { | ||
| const { App } = await importApp(); | ||
| const mockReceiver = createMockReceiver(); | ||
| const mockReturns = Symbol(); | ||
| const mockParameterList = [Symbol(), Symbol()]; | ||
| mockReceiver.start = sinon.fake.resolves(mockReturns); | ||
| const app = new App({ receiver: mockReceiver, authorize: sinon.spy() }); | ||
| const actualMockReturns = await app.start(...mockParameterList); | ||
| assert.equal(actualMockReturns, mockReturns); | ||
| assert.deepEqual(mockParameterList, (mockReceiver.start as SinonSpy).firstCall.args); | ||
| }); | ||
| }); | ||
|
|
||
| describe('#stop', () => { | ||
| it('should pass calls through to receiver', async () => { | ||
| const { App } = await importApp(); | ||
| const mockReceiver = createMockReceiver(); | ||
| const mockReturns = Symbol(); | ||
| const mockParameterList = [Symbol(), Symbol()]; | ||
| mockReceiver.stop = sinon.fake.resolves(mockReturns); | ||
| const app = new App({ receiver: mockReceiver, authorize: sinon.spy() }); | ||
| const actualMockReturns = await app.stop(...mockParameterList); | ||
| assert.equal(actualMockReturns, mockReturns); | ||
| assert.deepEqual(mockParameterList, (mockReceiver.stop as SinonSpy).firstCall.args); | ||
| }); | ||
| }); | ||
|
|
||
| describe('event processing', () => { | ||
| it('should warn and skip when processing a receiver event with unknown type (never crash)', async () => { | ||
| const { App } = await importApp(); | ||
| const mockReceiver = createMockReceiver(); | ||
| const invalidReceiverEvents = createInvalidReceiverEvents(); | ||
| const spyLogger = createSpyLogger(); | ||
| const spyMiddleware = createSpyMiddleware(); | ||
| const app = new App({ receiver: mockReceiver, logger: spyLogger, authorize: sinon.spy() }); | ||
| app.use(spyMiddleware); | ||
| for (const event of invalidReceiverEvents) { | ||
| (mockReceiver as unknown as EventEmitter).emit('message', event); | ||
| } | ||
| assert((spyMiddleware as SinonSpy).notCalled); | ||
| assert.isAtLeast((spyLogger.warn as SinonSpy).callCount, invalidReceiverEvents.length); | ||
| }); | ||
| it('should warn, send to global error handler, and skip when a receiver event fails authorization', async () => { | ||
| const { App } = await importApp(); | ||
| const mockReceiver = createMockReceiver(); | ||
| const mockReceiverEvent = createMockReceiverEvent(); | ||
| const spyLogger = createSpyLogger(); | ||
| const spyMiddleware = createSpyMiddleware(); | ||
| const spyErrorHandler = sinon.spy(); | ||
| const rejection = new Error(); | ||
| const app = new App({ receiver: mockReceiver, logger: spyLogger, authorize: sinon.fake.rejects(rejection) }); | ||
| app.use(spyMiddleware); | ||
| app.error(spyErrorHandler); | ||
| (mockReceiver as unknown as EventEmitter).emit('message', mockReceiverEvent); | ||
| await delay(); | ||
| assert((spyMiddleware as SinonSpy).notCalled); | ||
| assert((spyLogger.warn as SinonSpy).called); | ||
| assert.instanceOf(spyErrorHandler.firstCall.args[0], Error); | ||
| assert.propertyVal(spyErrorHandler.firstCall.args[0], 'code', ErrorCode.AuthorizationError); | ||
| assert.propertyVal(spyErrorHandler.firstCall.args[0], 'original', rejection); | ||
| }); | ||
| describe('global middleware', () => { | ||
| it('should process receiver events in order or #use', async () => { | ||
| const { App } = await importApp(); | ||
| const mockReceiver = createMockReceiver(); | ||
| const mockReceiverEvent = createMockReceiverEvent(); | ||
| const spyFirstMiddleware = createSpyMiddleware(); | ||
| const spySecondMiddleware = createSpyMiddleware(); | ||
| const app = new App({ receiver: mockReceiver, authorize: sinon.fake.resolves({ botToken: '', botId: '' }) }); | ||
| app.use(spyFirstMiddleware); | ||
| app.use(spySecondMiddleware); | ||
| (mockReceiver as unknown as EventEmitter).emit('message', mockReceiverEvent); | ||
| await delay(); | ||
| assert((spyFirstMiddleware as SinonSpy).calledOnce); | ||
| assert((spyFirstMiddleware as SinonSpy).calledBefore(spySecondMiddleware as SinonSpy)); | ||
| assert((spySecondMiddleware as SinonSpy).calledOnce); | ||
| }); | ||
| }); | ||
| }); | ||
| }); | ||
|
|
||
| /* Test Helpers */ | ||
|
|
||
| async function importAppWhichFetchesOwnBotIds() { | ||
| const fakeBotUserId = 'fake_bot_user_id'; | ||
| const fakeBotId = 'fake_bot_id'; | ||
| const App = (await rewiremock.module(() => import('./App'), { // tslint:disable-line:variable-name | ||
| '@slack/web-api': { | ||
| WebClient: class { | ||
| public readonly auth = { | ||
| test: sinon.fake.resolves({ user_id: fakeBotUserId }), | ||
| }; | ||
| public readonly users = { | ||
| info: sinon.fake.resolves({ | ||
| user: { | ||
| profile: { | ||
| bot_id: fakeBotId, | ||
| }, | ||
| }, | ||
| }), | ||
| }; | ||
| public readonly chat = { | ||
| postMessage: sinon.fake.resolves({}), | ||
| }; | ||
| }, | ||
| addAppMetadata: sinon.fake(), | ||
| }, | ||
| })).default; | ||
|
|
||
| return { | ||
| fakeBotId, | ||
| fakeBotUserId, | ||
| App, | ||
| }; | ||
| } | ||
|
|
||
| async function importApp() { | ||
| const memoryStoreStub = sinon.stub(); | ||
| const conversationContextStub: typeof import('./conversation-store').conversationContext = | ||
| sinon.spy(() => createSpyMiddleware()); | ||
| const App = (await rewiremock.module(() => import('./App'), { // tslint:disable-line:variable-name | ||
| '@slack/web-api': { | ||
| WebClient: class { | ||
| public readonly chat = { | ||
| postMessage: sinon.fake.resolves({}), | ||
| }; | ||
| }, | ||
| addAppMetadata: sinon.fake(), | ||
| }, | ||
| './conversation-store': { | ||
| conversationContext: conversationContextStub, | ||
| MemoryStore: memoryStoreStub, | ||
| }, | ||
| })).default; | ||
|
|
||
| return { | ||
| App, | ||
| memoryStoreStub, | ||
| conversationContextStub, | ||
| }; | ||
| } | ||
|
|
||
| function createSpyMiddleware(): Middleware<AnyMiddlewareArgs> { | ||
| return sinon.spy(({ next }) => { next(); }); | ||
| } | ||
|
|
||
| function createMockReceiver(): Receiver { | ||
| const mock = new EventEmitter(); | ||
| (mock as unknown as Receiver).start = sinon.fake.resolves(undefined); | ||
| (mock as unknown as Receiver).stop = sinon.fake.resolves(undefined); | ||
| return mock as unknown as Receiver; | ||
| } | ||
|
|
||
| function createMockConvoStore(): ConversationStore { | ||
| return { | ||
| set: sinon.fake.resolves(undefined), | ||
| get: sinon.fake.resolves(undefined), | ||
| }; | ||
| } | ||
|
|
||
| function createSpyLogger(): Logger { | ||
| return { | ||
| setLevel: sinon.fake(), | ||
| setName: sinon.fake(), | ||
| debug: sinon.fake(), | ||
| info: sinon.fake(), | ||
| warn: sinon.fake(), | ||
| error: sinon.fake(), | ||
| }; | ||
| } | ||
|
|
||
| function createInvalidReceiverEvents(): ReceiverEvent[] { | ||
| // TODO: create many more invalid receiver events (fuzzing) | ||
| return [{ | ||
| body: {}, | ||
| respond: sinon.fake(), | ||
| ack: sinon.fake(), | ||
| }]; | ||
| } | ||
|
|
||
| function createMockReceiverEvent(): ReceiverEvent { | ||
| return { | ||
| body: { | ||
| event: { | ||
| }, | ||
| }, | ||
| respond: sinon.fake(), | ||
| ack: sinon.fake(), | ||
| }; | ||
| } | ||
|
|
||
| function delay(ms: number = 0) { | ||
| return new Promise((resolve) => { | ||
| setTimeout(resolve, ms); | ||
| }); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should this be "should succeed with no signing secret for custom receiver"?