Repository: Yoctol/bottender
Pull request #773 (2020-05-20 05:08:19)

feat(Bot): implement the onRequest option
Add the `onRequest` option to the Bot.  

codecov-commenter: # [Codecov](https://codecov.io/gh/Yoctol/bottender/pull/773?src=pr&el=h1) Report
> :exclamation: No coverage uploaded for pull request base (`release/1.5@b5d0e17`). [Click here to learn what that means](https://docs.codecov.io/docs/error-reference#section-missing-base-commit).
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/Yoctol/bottender/pull/773/graphs/tree.svg?width=650&height=150&src=pr&token=JX3EGtxYH3)](https://codecov.io/gh/Yoctol/bottender/pull/773?src=pr&el=tree)

```diff
@@              Coverage Diff               @@
##             release/1.5     #773   +/-   ##
==============================================
  Coverage               ?   81.07%           
==============================================
  Files                  ?      108           
  Lines                  ?     4741           
  Branches               ?     1195           
==============================================
  Hits                   ?     3844           
  Misses                 ?      862           
  Partials               ?       35           
```


| [Impacted Files](https://codecov.io/gh/Yoctol/bottender/pull/773?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [packages/bottender/src/types.ts](https://codecov.io/gh/Yoctol/bottender/pull/773/diff?src=pr&el=tree#diff-cGFja2FnZXMvYm90dGVuZGVyL3NyYy90eXBlcy50cw==) | `100.00% <ø> (ø)` | |
| [packages/bottender/src/bot/Bot.ts](https://codecov.io/gh/Yoctol/bottender/pull/773/diff?src=pr&el=tree#diff-cGFja2FnZXMvYm90dGVuZGVyL3NyYy9ib3QvQm90LnRz) | `98.50% <100.00%> (ø)` | |
| [packages/bottender/src/console/ConsoleBot.ts](https://codecov.io/gh/Yoctol/bottender/pull/773/diff?src=pr&el=tree#diff-cGFja2FnZXMvYm90dGVuZGVyL3NyYy9jb25zb2xlL0NvbnNvbGVCb3QudHM=) | `100.00% <100.00%> (ø)` | |
| [packages/bottender/src/line/LineBot.ts](https://codecov.io/gh/Yoctol/bottender/pull/773/diff?src=pr&el=tree#diff-cGFja2FnZXMvYm90dGVuZGVyL3NyYy9saW5lL0xpbmVCb3QudHM=) | `100.00% <100.00%> (ø)` | |
| [packages/bottender/src/messenger/MessengerBot.ts](https://codecov.io/gh/Yoctol/bottender/pull/773/diff?src=pr&el=tree#diff-cGFja2FnZXMvYm90dGVuZGVyL3NyYy9tZXNzZW5nZXIvTWVzc2VuZ2VyQm90LnRz) | `100.00% <100.00%> (ø)` | |
| [packages/bottender/src/slack/SlackBot.ts](https://codecov.io/gh/Yoctol/bottender/pull/773/diff?src=pr&el=tree#diff-cGFja2FnZXMvYm90dGVuZGVyL3NyYy9zbGFjay9TbGFja0JvdC50cw==) | `100.00% <100.00%> (ø)` | |
| [packages/bottender/src/telegram/TelegramBot.ts](https://codecov.io/gh/Yoctol/bottender/pull/773/diff?src=pr&el=tree#diff-cGFja2FnZXMvYm90dGVuZGVyL3NyYy90ZWxlZ3JhbS9UZWxlZ3JhbUJvdC50cw==) | `100.00% <100.00%> (ø)` | |
| [packages/bottender/src/viber/ViberBot.ts](https://codecov.io/gh/Yoctol/bottender/pull/773/diff?src=pr&el=tree#diff-cGFja2FnZXMvYm90dGVuZGVyL3NyYy92aWJlci9WaWJlckJvdC50cw==) | `100.00% <100.00%> (ø)` | |
| [packages/bottender/src/whatsapp/WhatsappBot.ts](https://codecov.io/gh/Yoctol/bottender/pull/773/diff?src=pr&el=tree#diff-cGFja2FnZXMvYm90dGVuZGVyL3NyYy93aGF0c2FwcC9XaGF0c2FwcEJvdC50cw==) | `100.00% <100.00%> (ø)` | |
| [packages/bottender/src/messenger/routes.ts](https://codecov.io/gh/Yoctol/bottender/pull/773/diff?src=pr&el=tree#diff-cGFja2FnZXMvYm90dGVuZGVyL3NyYy9tZXNzZW5nZXIvcm91dGVzLnRz) | `100.00% <0.00%> (ø)` | |
| ... and [107 more](https://codecov.io/gh/Yoctol/bottender/pull/773/diff?src=pr&el=tree-more) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/Yoctol/bottender/pull/773?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/Yoctol/bottender/pull/773?src=pr&el=footer). Last update [b5d0e17...b843281](https://codecov.io/gh/Yoctol/bottender/pull/773?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/packages/bottender/src/bot/Bot.ts b/packages/bottender/src/bot/Bot.ts
index 1aa1ef00..f62873b0 100644
--- a/packages/bottender/src/bot/Bot.ts
+++ b/packages/bottender/src/bot/Bot.ts
@@ -3,6 +3,7 @@ import { EventEmitter } from 'events';
 import debug from 'debug';
 import invariant from 'invariant';
 import pMap from 'p-map';
+import { JsonObject } from 'type-fest';
 import { camelcaseKeysDeep } from 'messaging-api-common';
 
 import CacheBasedSessionStore from '../session/CacheBasedSessionStore';
@@ -72,6 +73,11 @@ type RequestHandler<B> = (
   requestContext?: RequestContext
 ) => void | Promise<void>;
 
+export type OnRequest = (
+  body: JsonObject,
+  requestContext?: RequestContext
+) => void;
+
 export default class Bot<
   B extends Body,
   C extends Client,
@@ -96,14 +102,18 @@ export default class Bot<
 
   _emitter: EventEmitter;
 
+  _onRequest: OnRequest | undefined;
+
   constructor({
     connector,
     sessionStore = createMemorySessionStore(),
     sync = false,
+    onRequest,
   }: {
     connector: Connector<B, C>;
     sessionStore?: SessionStore;
     sync?: boolean;
+    onRequest?: OnRequest;
   }) {
     this._sessions = sessionStore;
     this._initialized = false;
@@ -112,6 +122,7 @@ export default class Bot<
     this._errorHandler = null;
     this._sync = sync;
     this._emitter = new EventEmitter();
+    this._onRequest = onRequest;
   }
 
   get connector(): Connector<B, C> {
@@ -191,6 +202,10 @@ export default class Bot<
 
       const body = camelcaseKeysDeep(inputBody) as B;
 
+      if (this._onRequest) {
+        this._onRequest(body, requestContext);
+      }
+
       const events = this._connector.mapRequestToEvents(body);
 
       const contexts = await pMap(
diff --git a/packages/bottender/src/console/ConsoleBot.ts b/packages/bottender/src/console/ConsoleBot.ts
index 89896a18..68e1f0fd 100644
--- a/packages/bottender/src/console/ConsoleBot.ts
+++ b/packages/bottender/src/console/ConsoleBot.ts
@@ -1,6 +1,6 @@
 import readline from 'readline';
 
-import Bot from '../bot/Bot';
+import Bot, { OnRequest } from '../bot/Bot';
 import Session from '../session/Session';
 import SessionStore from '../session/SessionStore';
 
@@ -20,13 +20,15 @@ export default class ConsoleBot extends Bot<
     sessionStore,
     fallbackMethods,
     mockPlatform,
+    onRequest,
   }: {
     sessionStore?: SessionStore;
     fallbackMethods?: boolean;
     mockPlatform?: string;
+    onRequest?: OnRequest;
   } = {}) {
     const connector = new ConsoleConnector({ fallbackMethods, mockPlatform });
-    super({ connector, sessionStore, sync: true });
+    super({ connector, sessionStore, sync: true, onRequest });
   }
 
   async getSession(): Promise<Session> {
diff --git a/packages/bottender/src/line/LineBot.ts b/packages/bottender/src/line/LineBot.ts
index f2251cf9..aae39275 100644
--- a/packages/bottender/src/line/LineBot.ts
+++ b/packages/bottender/src/line/LineBot.ts
@@ -1,6 +1,6 @@
 import { LineClient } from 'messaging-api-line';
 
-import Bot from '../bot/Bot';
+import Bot, { OnRequest } from '../bot/Bot';
 import SessionStore from '../session/SessionStore';
 
 import LineConnector, {
@@ -19,12 +19,14 @@ export default class LineBot extends Bot<
   constructor({
     sessionStore,
     sync,
+    onRequest,
     ...connectorOptions
   }: LineConnectorOptions & {
     sessionStore?: SessionStore;
     sync?: boolean;
+    onRequest?: OnRequest;
   }) {
     const connector = new LineConnector(connectorOptions);
-    super({ connector, sessionStore, sync });
+    super({ connector, sessionStore, sync, onRequest });
   }
 }
diff --git a/packages/bottender/src/messenger/MessengerBot.ts b/packages/bottender/src/messenger/MessengerBot.ts
index b27ccd3d..88258cc7 100644
--- a/packages/bottender/src/messenger/MessengerBot.ts
+++ b/packages/bottender/src/messenger/MessengerBot.ts
@@ -1,6 +1,6 @@
 import { MessengerClient } from 'messaging-api-messenger';
 
-import Bot from '../bot/Bot';
+import Bot, { OnRequest } from '../bot/Bot';
 import SessionStore from '../session/SessionStore';
 
 import MessengerConnector, {
@@ -19,12 +19,14 @@ export default class MessengerBot extends Bot<
   constructor({
     sessionStore,
     sync,
+    onRequest,
     ...connectorOptions
   }: MessengerConnectorOptions & {
     sessionStore?: SessionStore;
     sync?: boolean;
+    onRequest?: OnRequest;
   }) {
     const connector = new MessengerConnector(connectorOptions);
-    super({ connector, sessionStore, sync });
+    super({ connector, sessionStore, sync, onRequest });
   }
 }
diff --git a/packages/bottender/src/slack/SlackBot.ts b/packages/bottender/src/slack/SlackBot.ts
index ff308639..47af23bd 100644
--- a/packages/bottender/src/slack/SlackBot.ts
+++ b/packages/bottender/src/slack/SlackBot.ts
@@ -1,7 +1,7 @@
 import { RTMClient } from '@slack/rtm-api';
 import { SlackOAuthClient } from 'messaging-api-slack';
 
-import Bot from '../bot/Bot';
+import Bot, { OnRequest } from '../bot/Bot';
 import SessionStore from '../session/SessionStore';
 
 import SlackConnector, { SlackRequestBody } from './SlackConnector';
@@ -25,6 +25,7 @@ export default class SlackBot extends Bot<
     origin,
     skipLegacyProfile,
     includeBotMessages,
+    onRequest,
   }: {
     accessToken: string;
     sessionStore?: SessionStore;
@@ -34,6 +35,7 @@ export default class SlackBot extends Bot<
     origin?: string;
     skipLegacyProfile?: boolean;
     includeBotMessages?: boolean;
+    onRequest?: OnRequest;
   }) {
     const connector = new SlackConnector({
       accessToken,
@@ -43,7 +45,7 @@ export default class SlackBot extends Bot<
       skipLegacyProfile,
       includeBotMessages,
     });
-    super({ connector, sessionStore, sync });
+    super({ connector, sessionStore, sync, onRequest });
     this._accessToken = accessToken;
   }
 
diff --git a/packages/bottender/src/telegram/TelegramBot.ts b/packages/bottender/src/telegram/TelegramBot.ts
index d752e8cb..34611bf9 100644
--- a/packages/bottender/src/telegram/TelegramBot.ts
+++ b/packages/bottender/src/telegram/TelegramBot.ts
@@ -1,6 +1,6 @@
 import { TelegramClient } from 'messaging-api-telegram';
 
-import Bot from '../bot/Bot';
+import Bot, { OnRequest } from '../bot/Bot';
 import SessionStore from '../session/SessionStore';
 
 import TelegramConnector, {
@@ -30,13 +30,15 @@ export default class TelegramBot extends Bot<
   constructor({
     sessionStore,
     sync,
+    onRequest,
     ...connectorOptions
   }: TelegramConnectorOptions & {
     sessionStore?: SessionStore;
     sync?: boolean;
+    onRequest?: OnRequest;
   }) {
     const connector = new TelegramConnector(connectorOptions);
-    super({ connector, sessionStore, sync });
+    super({ connector, sessionStore, sync, onRequest });
 
     this._offset = null;
     this._shouldGetUpdates = false;
diff --git a/packages/bottender/src/types.ts b/packages/bottender/src/types.ts
index f9e6ccd9..1147a0a6 100644
--- a/packages/bottender/src/types.ts
+++ b/packages/bottender/src/types.ts
@@ -23,6 +23,7 @@ import {
   MessengerConnectorOptions,
   MessengerRequestBody,
 } from './messenger/MessengerConnector';
+import { OnRequest } from './bot/Bot';
 import {
   SlackConnectorOptions,
   SlackRequestBody,
@@ -132,6 +133,7 @@ type ChannelCommonConfig = {
   enabled: boolean;
   path?: string;
   sync?: boolean;
+  OnRequest?: OnRequest;
 };
 
 export type BottenderConfig = {
diff --git a/packages/bottender/src/viber/ViberBot.ts b/packages/bottender/src/viber/ViberBot.ts
index b615ddbd..249dfcce 100644
--- a/packages/bottender/src/viber/ViberBot.ts
+++ b/packages/bottender/src/viber/ViberBot.ts
@@ -1,6 +1,6 @@
 import { ViberClient } from 'messaging-api-viber';
 
-import Bot from '../bot/Bot';
+import Bot, { OnRequest } from '../bot/Bot';
 import SessionStore from '../session/SessionStore';
 
 import ViberConnector, {
@@ -19,12 +19,14 @@ export default class ViberBot extends Bot<
   constructor({
     sessionStore,
     sync,
+    onRequest,
     ...connectorOptions
   }: ViberConnectorOptions & {
     sessionStore?: SessionStore;
     sync?: boolean;
+    onRequest?: OnRequest;
   }) {
     const connector = new ViberConnector(connectorOptions);
-    super({ connector, sessionStore, sync });
+    super({ connector, sessionStore, sync, onRequest });
   }
 }
diff --git a/packages/bottender/src/whatsapp/WhatsappBot.ts b/packages/bottender/src/whatsapp/WhatsappBot.ts
index c4f219e7..a0f98dba 100644
--- a/packages/bottender/src/whatsapp/WhatsappBot.ts
+++ b/packages/bottender/src/whatsapp/WhatsappBot.ts
@@ -1,4 +1,4 @@
-import Bot from '../bot/Bot';
+import Bot, { OnRequest } from '../bot/Bot';
 import SessionStore from '../session/SessionStore';
 
 import TwilioClient from './TwilioClient';
@@ -18,12 +18,14 @@ export default class WhatsappBot extends Bot<
   constructor({
     sessionStore,
     sync,
+    onRequest,
     ...connectorOptions
   }: WhatsappConnectorOptions & {
     sessionStore?: SessionStore;
     sync?: boolean;
+    onRequest?: OnRequest;
   }) {
     const connector = new WhatsappConnector(connectorOptions);
-    super({ connector, sessionStore, sync });
+    super({ connector, sessionStore, sync, onRequest });
   }
 }


Test patch:
diff --git a/packages/bottender/src/bot/__tests__/Bot.spec.ts b/packages/bottender/src/bot/__tests__/Bot.spec.ts
index 460d4daa..567ae01d 100644
--- a/packages/bottender/src/bot/__tests__/Bot.spec.ts
+++ b/packages/bottender/src/bot/__tests__/Bot.spec.ts
@@ -1,30 +1,76 @@
+import { mocked } from 'ts-jest/utils';
+
 import Bot from '../Bot';
-import Context from '../../context/Context';
+import MemorySessionStore from '../../session/MemorySessionStore';
+import { Connector } from '../Connector';
+
+jest.mock('../../session/MemorySessionStore');
+
+const event = {
+  rawEvent: {
+    message: {
+      text: 'hi',
+    },
+  },
+  isMessage: true,
+  isText: true,
+  message: {
+    text: 'hi',
+  },
+};
+
+const session = {
+  user: {
+    id: '__id__',
+  },
+};
+
+const body = {};
+
+const requestContext = {
+  method: 'post',
+  path: '/webhooks/messengr',
+  query: {},
+  headers: {},
+  rawBody: '{}',
+  body: {},
+  params: {},
+  url: 'https://www.example.com/webhooks/messengr',
+};
+
+function App() {}
 
 function setup({
   connector = {
     platform: 'any',
-    getUniqueSessionKey: jest.fn(),
+    getUniqueSessionKey: jest.fn(() => '__id__'),
     updateSession: jest.fn(),
-    mapRequestToEvents: jest.fn(() => [{}]),
-    createContext: jest.fn(() => ({})),
-    customAccessToken: 'anyToken',
+    mapRequestToEvents: jest.fn(() => [event]),
+    createContext: jest.fn(params => ({ ...params, session })),
   },
-  sessionStore = {
-    init: jest.fn(),
-    read: jest.fn(),
-    write: jest.fn(),
-  },
-  sync = true,
-  mapPageToAccessToken,
-}) {
-  const bot = new Bot({ connector, sessionStore, sync, mapPageToAccessToken });
+  sessionStore = new MemorySessionStore(),
+}: {
+  connector?: Connector<any, any>;
+  sessionStore?: MemorySessionStore;
+} = {}): {
+  bot: Bot<any, any, any, any>;
+  connector: Connector<any, any>;
+  sessionStore: MemorySessionStore;
+  onRequest: Function;
+} {
+  const onRequest = jest.fn();
+  const bot = new Bot({
+    connector,
+    sessionStore,
+    sync: true,
+    onRequest,
+  });
 
   return {
     bot,
     connector,
     sessionStore,
-    mapPageToAccessToken,
+    onRequest,
   };
 }
 
@@ -33,113 +79,119 @@ beforeEach(() => {
 });
 
 describe('#connector', () => {
-  it('can be access', () => {
+  it('should be the connector provided to the constructor', () => {
     const { bot, connector } = setup({});
+
     expect(bot.connector).toBe(connector);
   });
 });
 
 describe('#sessions', () => {
-  it('can be access', () => {
-    const { bot, sessionStore } = setup({});
+  it('should be the session store provided to the constructor', () => {
+    const { bot, sessionStore } = setup();
+
     expect(bot.sessions).toBe(sessionStore);
   });
 });
 
 describe('#handler', () => {
-  it('can be access', () => {
-    const { bot } = setup({});
+  it('should be the action registered by onEvent', () => {
+    const { bot } = setup();
 
     expect(bot.handler).toBeNull();
 
-    const handler = () => {};
-    bot.onEvent(handler);
-    expect(bot.handler).toBe(handler);
+    bot.onEvent(App);
+
+    expect(bot.handler).toBe(App);
   });
 });
 
 describe('#createRequestHandler', () => {
-  it('throw when no handler', () => {
-    const { bot } = setup({});
+  it('should throw when no registered action', () => {
+    const { bot } = setup();
+
     expect(() => bot.createRequestHandler()).toThrow();
   });
 
-  it('throw when call without request body', async () => {
-    const { bot } = setup({});
+  it('requestHandler should throw when be called without a request body', async () => {
+    const { bot } = setup();
 
-    const handler = () => {};
-    bot.onEvent(handler);
+    bot.onEvent(App);
 
     const requestHandler = bot.createRequestHandler();
     let error;
+
     try {
+      // eslint-disable-next-line @typescript-eslint/ban-ts-ignore
+      // @ts-ignore
       await requestHandler();
     } catch (err) {
       error = err;
     }
-    expect(error).toBeDefined();
+
+    expect(error).toBeInstanceOf(Error);
   });
 
-  it('should call updateSession with session and body', async () => {
-    const { bot, connector, sessionStore } = setup({});
+  it('should call updateSession with the session and the body', async () => {
+    const { bot, connector, sessionStore } = setup();
 
-    connector.getUniqueSessionKey.mockReturnValue('__id__');
-    sessionStore.read.mockResolvedValue(null);
+    mocked(sessionStore).read.mockResolvedValue(null);
 
-    const handler = () => {};
-    bot.onEvent(handler);
+    bot.onEvent(App);
 
     const requestHandler = bot.createRequestHandler();
 
-    const body = {};
     await requestHandler(body);
 
     expect(connector.updateSession).toBeCalledWith(expect.any(Object), body);
   });
 
-  it('should call handler', async () => {
-    const event = {};
-    const session = { user: {} };
-    const connector = {
-      platform: 'any',
-      getUniqueSessionKey: jest.fn(),
-      updateSession: jest.fn(),
-      mapRequestToEvents: jest.fn(() => [event]),
-      createContext: jest.fn(() => ({ event, session })),
-    };
+  it('should call the registered action with the context', async () => {
+    const { bot, sessionStore } = setup();
 
-    const { bot, sessionStore } = setup({ connector });
-
-    connector.getUniqueSessionKey.mockReturnValue('__id__');
-    sessionStore.read.mockResolvedValue(session);
+    mocked(sessionStore).read.mockResolvedValue(session);
 
     let receivedContext;
-    const Action = context => {
+    bot.onEvent(function MyAction(context) {
       receivedContext = context;
-    };
-    bot.onEvent(Action);
+    });
 
     const requestHandler = bot.createRequestHandler();
 
-    const body = {};
     await requestHandler(body);
 
-    expect(receivedContext).toEqual({
-      event: {},
-      isSessionWritten: true,
-      session: expect.objectContaining({
-        id: 'any:__id__',
-        platform: 'any',
-        user: {},
-        lastActivity: expect.any(Number),
-      }),
-    });
+    expect(receivedContext).toEqual(
+      expect.objectContaining({
+        event,
+        isSessionWritten: true,
+        session: expect.objectContaining({
+          id: 'any:__id__',
+          platform: 'any',
+          user: session.user,
+          lastActivity: expect.any(Number),
+        }),
+      })
+    );
+  });
+
+  it('should call the onRequest function with the body and the request context', async () => {
+    const { bot, sessionStore, onRequest } = setup();
+
+    mocked(sessionStore).read.mockResolvedValue(session);
+
+    bot.onEvent(App);
+
+    const requestHandler = bot.createRequestHandler();
+
+    await requestHandler(body, requestContext);
+
+    expect(onRequest).toBeCalledWith(body, requestContext);
   });
 
-  it('should return response in sync mode', async () => {
-    const event = {};
-    const session = { user: {} };
-    const context = {
+  it('should return the response in sync mode', async () => {
+    const { bot, connector, sessionStore } = setup();
+
+    const ctx = {
       event,
       session,
       response: {
@@ -148,33 +200,22 @@ describe('#createRequestHandler', () => {
         body: null,
       },
     };
-    const connector = {
-      platform: 'any',
-      getUniqueSessionKey: jest.fn(),
-      updateSession: jest.fn(),
-      mapRequestToEvents: jest.fn(() => [event]),
-      createContext: jest.fn(() => context),
-    };
-
-    const { bot, sessionStore } = setup({ connector, sync: true });
 
-    connector.getUniqueSessionKey.mockReturnValue('__id__');
-    sessionStore.read.mockResolvedValue(session);
+    mocked(connector).createContext.mockReturnValue(ctx);
+    mocked(sessionStore).read.mockResolvedValue(session);
 
-    const handler = ({ response }) => {
-      response.status = 200;
-      response.headers = {
+    bot.onEvent(function MyAction(context) {
+      context.response.status = 200;
+      context.response.headers = {
         'X-Header': 'x',
       };
-      response.body = {
+      context.response.body = {
         name: 'x',
       };
-    };
-    bot.onEvent(handler);
+    });
 
     const requestHandler = bot.createRequestHandler();
 
-    const body = {};
     const response = await requestHandler(body);
 
     expect(response).toEqual({
@@ -189,21 +230,12 @@ describe('#createRequestHandler', () => {
   });
 
   it('should throw error if no handler after createRequestHandler', async () => {
-    const event = {};
-    const connector = {
-      platform: 'any',
-      getUniqueSessionKey: jest.fn(),
-      updateSession: jest.fn(),
-      mapRequestToEvents: jest.fn(() => [event]),
-      createContext: jest.fn(() => ({ event, session: undefined })),
-    };
-
-    const { bot } = setup({ connector, sync: true });
+    const { bot } = setup();
 
-    connector.getUniqueSessionKey.mockReturnValue(null);
+    bot.onEvent(App);
 
-    bot.onEvent(() => {});
     const requestHandler = bot.createRequestHandler();
+
     bot._handler = null;
 
     let error;
@@ -212,32 +244,26 @@ describe('#createRequestHandler', () => {
     } catch (err) {
       error = err;
     }
+
     expect(error).toBeDefined();
   });
 
   it('should call handler without session if unique session id is null', async () => {
-    const event = {};
-    const connector = {
-      platform: 'any',
-      getUniqueSessionKey: jest.fn(),
-      updateSession: jest.fn(),
-      mapRequestToEvents: jest.fn(() => [event]),
-      createContext: jest.fn(() => ({ event, session: undefined })),
-    };
+    const { bot, connector } = setup();
 
-    const { bot } = setup({ connector });
-
-    connector.getUniqueSessionKey.mockReturnValue(null);
+    mocked(connector).getUniqueSessionKey.mockReturnValue(null);
+    mocked(connector).createContext.mockReturnValue({
+      event,
+      session: undefined,
+    });
 
     let receivedContext;
-    const Action = context => {
+    bot.onEvent(function MyAction(context) {
       receivedContext = context;
-    };
-    bot.onEvent(Action);
+    });
 
     const requestHandler = bot.createRequestHandler();
 
-    const body = {};
     await requestHandler(body);
 
     expect(receivedContext).toEqual(
@@ -248,27 +274,15 @@ describe('#createRequestHandler', () => {
   });
 
   it('should handle error thrown by async handler', async () => {
-    const event = {};
-    const connector = {
-      platform: 'any',
-      getUniqueSessionKey: jest.fn(),
-      updateSession: jest.fn(),
-      mapRequestToEvents: jest.fn(() => [event]),
-      createContext: jest.fn(() => ({ event, session: 123456 })),
-    };
-
-    const { bot } = setup({ connector, sync: true });
-
-    connector.getUniqueSessionKey.mockReturnValue(null);
+    const { bot } = setup();
 
-    const handler = jest.fn(async () => {
+    const handler = async () => {
       throw new Error('async handler error');
-    });
+    };
     bot.onEvent(handler);
 
     const requestHandler = bot.createRequestHandler();
 
-    const body = {};
     let error;
     try {
       await requestHandler(body);
@@ -276,32 +290,19 @@ describe('#createRequestHandler', () => {
       error = err;
     }
 
-    expect(handler).toBeCalled();
     expect(error).toBeUndefined();
   });
 
   it('should handle error thrown by sync handler', async () => {
-    const event = {};
-    const connector = {
-      platform: 'any',
-      getUniqueSessionKey: jest.fn(),
-      updateSession: jest.fn(),
-      mapRequestToEvents: jest.fn(() => [event]),
-      createContext: jest.fn(() => ({ event, session: 123456 })),
-    };
+    const { bot } = setup();
 
-    const { bot } = setup({ connector, sync: true });
-
-    connector.getUniqueSessionKey.mockReturnValue(null);
-
-    const handler = jest.fn(() => {
+    const handler = () => {
       throw new Error('sync handler error');
-    });
+    };
     bot.onEvent(handler);
 
     const requestHandler = bot.createRequestHandler();
 
-    const body = {};
     let error;
     try {
       await requestHandler(body);
@@ -309,30 +310,18 @@ describe('#createRequestHandler', () => {
       error = err;
     }
 
-    expect(handler).toBeCalled();
     expect(error).toBeUndefined();
   });
 
   it('should call write on sessionStore', async () => {
     const _now = Date.now;
     Date.now = jest.fn(() => 1504514594622);
-    const event = {};
-    const session = { user: {} };
-    const connector = {
-      platform: 'any',
-      getUniqueSessionKey: jest.fn(),
-      updateSession: jest.fn(),
-      mapRequestToEvents: jest.fn(() => [event]),
-      createContext: jest.fn(() => ({ event, session })),
-    };
 
-    const { bot, sessionStore } = setup({ connector, sync: true });
+    const { bot, sessionStore } = setup();
 
-    connector.getUniqueSessionKey.mockReturnValue('__id__');
-    sessionStore.read.mockResolvedValue(session);
+    mocked(sessionStore).read.mockResolvedValue(session);
 
-    const handler = () => {};
-    bot.onEvent(handler);
+    bot.onEvent(App);
 
     const requestHandler = bot.createRequestHandler();
 
@@ -341,35 +330,55 @@ describe('#createRequestHandler', () => {
     expect(sessionStore.write).toBeCalledWith('any:__id__', {
       id: 'any:__id__',
       platform: 'any',
-      user: {},
+      user: session.user,
       lastActivity: Date.now(),
     });
     Date.now = _now;
   });
 
   it('should work with multiple events in one request', async () => {
-    const event1 = {};
-    const event2 = {};
-    const connector = {
-      platform: 'any',
-      getUniqueSessionKey: jest.fn(),
-      updateSession: jest.fn(),
-      mapRequestToEvents: jest.fn(() => [event1, event2]),
-      createContext: jest.fn(({ event, session }) => ({ event, session })),
+    const { bot, connector, sessionStore } = setup();
+
+    const event1 = {
+      rawEvent: {
+        message: {
+          text: 'hi',
+        },
+      },
+      isMessage: true,
+      isText: true,
+      message: {
+        text: 'hi',
+      },
+    };
+    const event2 = {
+      rawEvent: {
+        message: {
+          text: 'hi',
+        },
+      },
+      isMessage: true,
+      isText: true,
+      message: {
+        text: 'hi',
+      },
     };
-    const { bot, sessionStore } = setup({ connector });
 
-    connector.getUniqueSessionKey
-      .mockReturnValueOnce('1')
+    mocked(connector)
+      .getUniqueSessionKey.mockReturnValueOnce('1')
       .mockReturnValueOnce('2');
-    sessionStore.read.mockResolvedValue(null);
+    mocked(connector).mapRequestToEvents.mockReturnValue([event1, event2]);
+    mocked(connector).createContext.mockImplementation(params => ({
+      event: params.event,
+      session: params.session,
+    }));
 
-    const handler = () => {};
-    bot.onEvent(handler);
+    mocked(sessionStore).read.mockResolvedValue(null);
+
+    bot.onEvent(App);
 
     const requestHandler = bot.createRequestHandler();
 
-    const body = {};
     await requestHandler(body);
 
     expect(sessionStore.read).toBeCalledWith('any:1');
@@ -391,8 +400,8 @@ describe('#createRequestHandler', () => {
       })
     );
 
-    expect(connector.updateSession).toBeCalledWith(expect.any(Object), body);
-    expect(connector.updateSession).toBeCalledWith(expect.any(Object), body);
+    expect(connector.updateSession).toBeCalledWith(expect.any(Object), event1);
+    expect(connector.updateSession).toBeCalledWith(expect.any(Object), event2);
 
     expect(sessionStore.write).toBeCalledWith('any:1', {
       id: 'any:1',
@@ -409,46 +418,26 @@ describe('#createRequestHandler', () => {
 
 describe('#use', () => {
   it('should return this', () => {
-    const event = {};
-    const connector = {
-      platform: 'any',
-      getUniqueSessionKey: jest.fn(),
-      updateSession: jest.fn(),
-      mapRequestToEvents: jest.fn(() => [event]),
-      createContext: jest.fn(() => ({ event, session: undefined })),
-    };
+    const { bot } = setup();
 
-    const { bot } = setup({ connector });
-
-    expect(bot.use(() => {})).toBe(bot);
+    expect(bot.use(function plugin() {})).toBe(bot);
   });
 
   it('can extend function to context', async () => {
-    const event = {};
-    const connector = {
-      platform: 'any',
-      getUniqueSessionKey: jest.fn(),
-      updateSession: jest.fn(),
-      mapRequestToEvents: jest.fn(() => [event]),
-      createContext: jest.fn(() => ({ event, session: undefined })),
-    };
-
-    const { bot } = setup({ connector });
-
-    connector.getUniqueSessionKey.mockReturnValue('__id__');
+    const { bot } = setup();
 
     const monkeyPatchFn = jest.fn();
 
-    bot.use(context => {
+    bot.use(function plugin(context) {
       context.monkeyPatchFn = monkeyPatchFn;
     });
 
-    const handler = context => context.monkeyPatchFn();
-    bot.onEvent(handler);
+    bot.onEvent(function MyAction(context) {
+      context.monkeyPatchFn();
+    });
 
     const requestHandler = bot.createRequestHandler();
 
-    const body = {};
     await requestHandler(body);
 
     expect(monkeyPatchFn).toBeCalled();
@@ -457,34 +446,15 @@ describe('#use', () => {
 
 describe('#onEvent', () => {
   it('should return this', () => {
-    const event = {};
-    const connector = {
-      platform: 'any',
-      getUniqueSessionKey: jest.fn(),
-      updateSession: jest.fn(),
-      mapRequestToEvents: jest.fn(() => [event]),
-      createContext: jest.fn(() => ({ event, session: undefined })),
-    };
-
-    const { bot } = setup({ connector });
+    const { bot } = setup();
 
-    expect(bot.onEvent(() => {})).toBe(bot);
+    expect(bot.onEvent(App)).toBe(bot);
   });
 });
 
 describe('#onError', () => {
   it('should catch thrown handler error', async () => {
-    const event = {};
-    const connector = {
-      platform: 'any',
-      getUniqueSessionKey: jest.fn(),
-      updateSession: jest.fn(),
-      mapRequestToEvents: jest.fn(() => [event]),
-      createContext: ({ emitter }) =>
-        new Context({ event, session: undefined, emitter }),
-    };
-
-    const { bot } = setup({ connector });
+    const { bot } = setup();
 
     let receivedError;
     let receivedContext;
@@ -508,54 +478,25 @@ describe('#onError', () => {
     await promise;
 
     expect(receivedError).toBeInstanceOf(Error);
-    expect(receivedContext).toBeInstanceOf(Context);
+    expect(receivedContext).toBeDefined();
   });
 
   it('should return this', () => {
-    const event = {};
-    const connector = {
-      platform: 'any',
-      getUniqueSessionKey: jest.fn(),
-      updateSession: jest.fn(),
-      mapRequestToEvents: jest.fn(() => [event]),
-      createContext: jest.fn(() => ({ event, session: undefined })),
-    };
-
-    const { bot } = setup({ connector });
+    const { bot } = setup();
 
-    expect(bot.onError(() => {})).toBe(bot);
+    expect(bot.onError(function HandleError() {})).toBe(bot);
   });
 });
 
 describe('#setInitialState', () => {
   it('should return this', () => {
-    const event = {};
-    const connector = {
-      platform: 'any',
-      getUniqueSessionKey: jest.fn(),
-      updateSession: jest.fn(),
-      mapRequestToEvents: jest.fn(() => [event]),
-      createContext: jest.fn(() => ({ event, session: undefined })),
-    };
-
-    const { bot } = setup({ connector });
+    const { bot } = setup();
 
     expect(bot.setInitialState({})).toBe(bot);
   });
 
   it('initialState should be passed to createContext', async () => {
-    const event = {};
-    const connector = {
-      platform: 'any',
-      getUniqueSessionKey: jest.fn(),
-      updateSession: jest.fn(),
-      mapRequestToEvents: jest.fn(() => [event]),
-      createContext: jest.fn(() => ({ event, session: undefined })),
-    };
-
-    const { bot } = setup({ connector });
-
-    connector.getUniqueSessionKey.mockReturnValue('__id__');
+    const { bot, connector } = setup();
 
     bot.setInitialState({
       a: 1,
@@ -564,11 +505,10 @@ describe('#setInitialState', () => {
       },
     });
 
-    bot.onEvent(() => {});
+    bot.onEvent(App);
 
     const requestHandler = bot.createRequestHandler();
 
-    const body = {};
     await requestHandler(body);
 
     expect(connector.createContext).toBeCalledWith(
@@ -586,25 +526,12 @@ describe('#setInitialState', () => {
 
 describe('request context', () => {
   it('requestContext should be passed to createContext', async () => {
-    const event = {};
-    const connector = {
-      platform: 'any',
-      getUniqueSessionKey: jest.fn(),
-      updateSession: jest.fn(),
-      mapRequestToEvents: jest.fn(() => [event]),
-      createContext: jest.fn(() => ({ event, session: undefined })),
-    };
-
-    const { bot } = setup({ connector });
+    const { bot, connector } = setup();
 
-    connector.getUniqueSessionKey.mockReturnValue('__id__');
-
-    bot.onEvent(() => {});
+    bot.onEvent(App);
 
     const requestHandler = bot.createRequestHandler();
 
-    const body = {};
-    const requestContext = { req: {}, res: {} };
     await requestHandler(body, requestContext);
 
     expect(connector.createContext).toBeCalledWith(
@@ -615,31 +542,22 @@ describe('request context', () => {
   });
 });
 
-describe('context lifecycle', () => {
+describe('context life cycle', () => {
   it('should call context.handlerDidEnd when it exists', async () => {
-    const event = {};
     const handlerDidEnd = jest.fn();
-    const connector = {
-      platform: 'any',
-      getUniqueSessionKey: jest.fn(),
-      updateSession: jest.fn(),
-      mapRequestToEvents: jest.fn(() => [event]),
-      createContext: jest.fn(() => ({
-        event,
-        session: undefined,
-        handlerDidEnd,
-      })),
-    };
 
-    const { bot } = setup({ connector, sync: true });
+    const { bot, connector } = setup();
 
-    connector.getUniqueSessionKey.mockReturnValue('__id__');
+    mocked(connector).createContext.mockReturnValue({
+      event,
+      session: undefined,
+      handlerDidEnd,
+    });
 
-    bot.onEvent(() => {});
+    bot.onEvent(App);
 
     const requestHandler = bot.createRequestHandler();
 
-    const body = {};
     await requestHandler(body);
 
     expect(handlerDidEnd).toBeCalled();