Repository: Yoctol/bottender
Pull request #406 (2019-02-21 07:56:51)

[LINE] move verify webhook check out of session creation
- [x] express
- [x] koa
- [x] micro
- [x] restify

codecov-io: # [Codecov](https://codecov.io/gh/Yoctol/bottender/pull/406?src=pr&el=h1) Report
> Merging [#406](https://codecov.io/gh/Yoctol/bottender/pull/406?src=pr&el=desc) into [master](https://codecov.io/gh/Yoctol/bottender/commit/d403e4e95808995d98405a566318558665f58dfc?src=pr&el=desc) will **increase** coverage by `0.02%`.
> The diff coverage is `100%`.

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

```diff
@@            Coverage Diff            @@
##           master    #406      +/-   ##
=========================================
+ Coverage   90.28%   90.3%   +0.02%     
=========================================
  Files         117     121       +4     
  Lines        3511    3529      +18     
  Branches      797     800       +3     
=========================================
+ Hits         3170    3187      +17     
- Misses        299     300       +1     
  Partials       42      42
```


| [Impacted Files](https://codecov.io/gh/Yoctol/bottender/pull/406?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [src/restify/registerRoutes.js](https://codecov.io/gh/Yoctol/bottender/pull/406/diff?src=pr&el=tree#diff-c3JjL3Jlc3RpZnkvcmVnaXN0ZXJSb3V0ZXMuanM=) | `83.33% <100%> (+0.72%)` | :arrow_up: |
| [src/restify/verifyLineWebhook.js](https://codecov.io/gh/Yoctol/bottender/pull/406/diff?src=pr&el=tree#diff-c3JjL3Jlc3RpZnkvdmVyaWZ5TGluZVdlYmhvb2suanM=) | `100% <100%> (ø)` | |
| [src/koa/registerRoutes.js](https://codecov.io/gh/Yoctol/bottender/pull/406/diff?src=pr&el=tree#diff-c3JjL2tvYS9yZWdpc3RlclJvdXRlcy5qcw==) | `80.76% <100%> (+0.76%)` | :arrow_up: |
| [src/express/registerRoutes.js](https://codecov.io/gh/Yoctol/bottender/pull/406/diff?src=pr&el=tree#diff-c3JjL2V4cHJlc3MvcmVnaXN0ZXJSb3V0ZXMuanM=) | `79.16% <100%> (+0.9%)` | :arrow_up: |
| [src/bot/LineConnector.js](https://codecov.io/gh/Yoctol/bottender/pull/406/diff?src=pr&el=tree#diff-c3JjL2JvdC9MaW5lQ29ubmVjdG9yLmpz) | `92.85% <100%> (-0.39%)` | :arrow_down: |
| [src/micro/createRequestHandler.js](https://codecov.io/gh/Yoctol/bottender/pull/406/diff?src=pr&el=tree#diff-c3JjL21pY3JvL2NyZWF0ZVJlcXVlc3RIYW5kbGVyLmpz) | `88.57% <100%> (-2.61%)` | :arrow_down: |
| [src/koa/verifyLineWebhook.js](https://codecov.io/gh/Yoctol/bottender/pull/406/diff?src=pr&el=tree#diff-c3JjL2tvYS92ZXJpZnlMaW5lV2ViaG9vay5qcw==) | `100% <100%> (ø)` | |
| [src/express/verifyLineWebhook.js](https://codecov.io/gh/Yoctol/bottender/pull/406/diff?src=pr&el=tree#diff-c3JjL2V4cHJlc3MvdmVyaWZ5TGluZVdlYmhvb2suanM=) | `100% <100%> (ø)` | |
| [src/micro/verifyLineWebhook.js](https://codecov.io/gh/Yoctol/bottender/pull/406/diff?src=pr&el=tree#diff-c3JjL21pY3JvL3ZlcmlmeUxpbmVXZWJob29rLmpz) | `100% <100%> (ø)` | |
| ... and [3 more](https://codecov.io/gh/Yoctol/bottender/pull/406/diff?src=pr&el=tree-more) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/Yoctol/bottender/pull/406?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/406?src=pr&el=footer). Last update [d403e4e...b58f0ff](https://codecov.io/gh/Yoctol/bottender/pull/406?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/src/bot/LineConnector.js b/src/bot/LineConnector.js
index fd5e9ba1..f7045bed 100644
--- a/src/bot/LineConnector.js
+++ b/src/bot/LineConnector.js
@@ -79,8 +79,13 @@ export default class LineConnector implements Connector<LineRequestBody> {
     );
   }
 
-  _isWebhookVerifyRequest(body: LineRequestBody): boolean {
-    return body.events.every(this._isWebhookVerifyEvent);
+  isWebhookVerifyRequest(body: LineRequestBody): boolean {
+    return (
+      body &&
+      Array.isArray(body.events) &&
+      body.events.length > 0 &&
+      body.events.every(this._isWebhookVerifyEvent)
+    );
   }
 
   get platform(): string {
@@ -92,9 +97,6 @@ export default class LineConnector implements Connector<LineRequestBody> {
   }
 
   getUniqueSessionKey(body: LineRequestBody): string {
-    if (this._isWebhookVerifyRequest(body)) {
-      return '';
-    }
     const { source } = body.events[0];
     if (source.type === 'user') {
       return source.userId;
@@ -111,9 +113,6 @@ export default class LineConnector implements Connector<LineRequestBody> {
   }
 
   async updateSession(session: Session, body: LineRequestBody): Promise<void> {
-    if (this._isWebhookVerifyRequest(body)) {
-      return;
-    }
     const { source } = body.events[0];
 
     if (!session.type) {
diff --git a/src/express/index.js b/src/express/index.js
index be3f8b2a..e11a9ed7 100644
--- a/src/express/index.js
+++ b/src/express/index.js
@@ -4,6 +4,7 @@ export { default as createServer } from './createServer';
 export { default as createMiddleware } from './createMiddleware';
 export { default as registerRoutes } from './registerRoutes';
 export { default as verifyLineSignature } from './verifyLineSignature';
+export { default as verifyLineWebhook } from './verifyLineWebhook';
 export {
   default as verifyMessengerSignature,
 } from './verifyMessengerSignature';
diff --git a/src/express/registerRoutes.js b/src/express/registerRoutes.js
index 1f0f6dd2..11222cf1 100644
--- a/src/express/registerRoutes.js
+++ b/src/express/registerRoutes.js
@@ -4,6 +4,7 @@ import connectNgrok from '../connectNgrok';
 
 import createMiddleware from './createMiddleware';
 import verifyLineSignature from './verifyLineSignature';
+import verifyLineWebhook from './verifyLineWebhook';
 import verifyMessengerSignature from './verifyMessengerSignature';
 import verifyMessengerWebhook from './verifyMessengerWebhook';
 import verifySlackSignature from './verifySlackSignature';
@@ -25,6 +26,7 @@ function registerRoutes(server, bot, config = {}) {
     middleware.unshift(verifySlackWebhook(bot));
     middleware.unshift(verifySlackSignature(bot));
   } else if (bot.connector.platform === 'line') {
+    middleware.unshift(verifyLineWebhook(bot));
     middleware.unshift(verifyLineSignature(bot));
   } else if (bot.connector.platform === 'viber') {
     middleware.unshift(verifyViberSignature(bot));
diff --git a/src/express/verifyLineWebhook.js b/src/express/verifyLineWebhook.js
new file mode 100644
index 00000000..667295ac
--- /dev/null
+++ b/src/express/verifyLineWebhook.js
@@ -0,0 +1,9 @@
+const verifyLineWebhook = bot => (req, res, next) => {
+  if (bot.connector.isWebhookVerifyRequest(req.body)) {
+    res.sendStatus(200);
+  } else {
+    return next();
+  }
+};
+
+export default verifyLineWebhook;
diff --git a/src/koa/index.js b/src/koa/index.js
index d2c45756..3b33fa2f 100644
--- a/src/koa/index.js
+++ b/src/koa/index.js
@@ -4,6 +4,7 @@ export { default as createServer } from './createServer';
 export { default as createMiddleware } from './createMiddleware';
 export { default as registerRoutes } from './registerRoutes';
 export { default as verifyLineSignature } from './verifyLineSignature';
+export { default as verifyLineWebhook } from './verifyLineWebhook';
 export {
   default as verifyMessengerSignature,
 } from './verifyMessengerSignature';
diff --git a/src/koa/registerRoutes.js b/src/koa/registerRoutes.js
index 4573eb3c..c92ce378 100644
--- a/src/koa/registerRoutes.js
+++ b/src/koa/registerRoutes.js
@@ -5,6 +5,7 @@ import connectNgrok from '../connectNgrok';
 
 import createMiddleware from './createMiddleware';
 import verifyLineSignature from './verifyLineSignature';
+import verifyLineWebhook from './verifyLineWebhook';
 import verifyMessengerSignature from './verifyMessengerSignature';
 import verifyMessengerWebhook from './verifyMessengerWebhook';
 import verifySlackSignature from './verifySlackSignature';
@@ -28,6 +29,7 @@ function registerRoutes(server, bot, config = {}) {
     middleware.unshift(verifySlackWebhook(bot));
     middleware.unshift(verifySlackSignature(bot));
   } else if (bot.connector.platform === 'line') {
+    middleware.unshift(verifyLineWebhook(bot));
     middleware.unshift(verifyLineSignature(bot));
   } else if (bot.connector.platform === 'viber') {
     middleware.unshift(verifyViberSignature(bot));
diff --git a/src/koa/verifyLineWebhook.js b/src/koa/verifyLineWebhook.js
new file mode 100644
index 00000000..d36b9114
--- /dev/null
+++ b/src/koa/verifyLineWebhook.js
@@ -0,0 +1,9 @@
+const verifyLineWebhook = bot => ({ request, response }, next) => {
+  if (bot.connector.isWebhookVerifyRequest(request.body)) {
+    response.status = 200;
+  } else {
+    return next();
+  }
+};
+
+export default verifyLineWebhook;
diff --git a/src/micro/createRequestHandler.js b/src/micro/createRequestHandler.js
index e73a0767..283fc2ec 100644
--- a/src/micro/createRequestHandler.js
+++ b/src/micro/createRequestHandler.js
@@ -5,6 +5,7 @@ import shortid from 'shortid';
 import { json, send } from 'micro';
 
 import verifyLineSignature from './verifyLineSignature';
+import verifyLineWebhook from './verifyLineWebhook';
 import verifyMessengerSignature from './verifyMessengerSignature';
 import verifyMessengerWebhook from './verifyMessengerWebhook';
 import verifySlackSignature from './verifySlackSignature';
@@ -39,8 +40,10 @@ function createRequestHandler(bot, config = {}) {
             return;
           }
         } else if (bot.connector.platform === 'line') {
-          const valid = await verifyLineSignature(bot)(req, res);
-          if (!valid) {
+          if (!(await verifyLineSignature(bot)(req, res))) {
+            return;
+          }
+          if (await verifyLineWebhook(bot)(req, res)) {
             return;
           }
         } else if (bot.connector.platform === 'slack') {
diff --git a/src/micro/index.js b/src/micro/index.js
index d64ac953..e28bdfe1 100644
--- a/src/micro/index.js
+++ b/src/micro/index.js
@@ -3,6 +3,7 @@
 export { default as createServer } from './createServer';
 export { default as createRequestHandler } from './createRequestHandler';
 export { default as verifyLineSignature } from './verifyLineSignature';
+export { default as verifyLineWebhook } from './verifyLineWebhook';
 export {
   default as verifyMessengerSignature,
 } from './verifyMessengerSignature';
diff --git a/src/micro/verifyLineWebhook.js b/src/micro/verifyLineWebhook.js
new file mode 100644
index 00000000..cb2eb754
--- /dev/null
+++ b/src/micro/verifyLineWebhook.js
@@ -0,0 +1,14 @@
+import { json, send } from 'micro';
+
+const verifyLineWebhook = bot => async (req, res) => {
+  const body = await json(req);
+
+  if (bot.connector.isWebhookVerifyRequest(body)) {
+    send(res, 200);
+    return true;
+  }
+
+  return false;
+};
+
+export default verifyLineWebhook;
diff --git a/src/restify/index.js b/src/restify/index.js
index d2c45756..3b33fa2f 100644
--- a/src/restify/index.js
+++ b/src/restify/index.js
@@ -4,6 +4,7 @@ export { default as createServer } from './createServer';
 export { default as createMiddleware } from './createMiddleware';
 export { default as registerRoutes } from './registerRoutes';
 export { default as verifyLineSignature } from './verifyLineSignature';
+export { default as verifyLineWebhook } from './verifyLineWebhook';
 export {
   default as verifyMessengerSignature,
 } from './verifyMessengerSignature';
diff --git a/src/restify/registerRoutes.js b/src/restify/registerRoutes.js
index 5aeff10c..4cb947f0 100644
--- a/src/restify/registerRoutes.js
+++ b/src/restify/registerRoutes.js
@@ -4,6 +4,7 @@ import connectNgrok from '../connectNgrok';
 
 import createMiddleware from './createMiddleware';
 import verifyLineSignature from './verifyLineSignature';
+import verifyLineWebhook from './verifyLineWebhook';
 import verifyMessengerSignature from './verifyMessengerSignature';
 import verifyMessengerWebhook from './verifyMessengerWebhook';
 import verifySlackSignature from './verifySlackSignature';
@@ -25,6 +26,7 @@ function registerRoutes(server, bot, config = {}) {
     middleware.unshift(verifySlackWebhook(bot));
     middleware.unshift(verifySlackSignature(bot));
   } else if (bot.connector.platform === 'line') {
+    middleware.unshift(verifyLineWebhook(bot));
     middleware.unshift(verifyLineSignature(bot));
   } else if (bot.connector.platform === 'viber') {
     middleware.unshift(verifyViberSignature(bot));
diff --git a/src/restify/verifyLineWebhook.js b/src/restify/verifyLineWebhook.js
new file mode 100644
index 00000000..dbf9ebe4
--- /dev/null
+++ b/src/restify/verifyLineWebhook.js
@@ -0,0 +1,8 @@
+const verifyLineWebhook = bot => (req, res, next) => {
+  if (bot.connector.isWebhookVerifyRequest(req.rawBody)) {
+    return res.send(200);
+  }
+  return next();
+};
+
+export default verifyLineWebhook;


Test patch:
diff --git a/src/bot/__tests__/LineConnector.spec.js b/src/bot/__tests__/LineConnector.spec.js
index 6fd83aaa..eaac7e7c 100644
--- a/src/bot/__tests__/LineConnector.spec.js
+++ b/src/bot/__tests__/LineConnector.spec.js
@@ -177,12 +177,6 @@ describe('#getUniqueSessionKey', () => {
     expect(senderId).toBe('U206d25c2ea6bd87c17655609a1c37cb8');
   });
 
-  it('extract empty string from webhook verify request', () => {
-    const { connector } = setup();
-    const senderId = connector.getUniqueSessionKey(webhookVerifyRequest.body);
-    expect(senderId).toBe('');
-  });
-
   it('should throw error if source.type is not user, group or room', () => {
     const { connector } = setup();
     let error;
@@ -588,16 +582,6 @@ describe('#updateSession', () => {
       value: session.user,
     });
   });
-
-  it('do nothing with webhook verify request', async () => {
-    const { connector } = setup();
-
-    const session = {};
-
-    await connector.updateSession(session, webhookVerifyRequest.body);
-
-    expect(session).toEqual({});
-  });
 });
 
 describe('#mapRequestToEvents', () => {
@@ -611,13 +595,6 @@ describe('#mapRequestToEvents', () => {
     expect(events[0].destination).toBe('Uea8667adaf43586706170ff25ff47ae6');
     expect(events[1].destination).toBe('Uea8667adaf43586706170ff25ff47ae6');
   });
-
-  it('should map webhook verify request to empty array', () => {
-    const { connector } = setup();
-    const events = connector.mapRequestToEvents(webhookVerifyRequest.body);
-
-    expect(events).toEqual([]);
-  });
 });
 
 describe('#createContext', () => {
@@ -654,3 +631,16 @@ it('should warning if sendMethod is not one of `reply`, `push`', () => {
 
   expect(warning).toBeCalledWith(false, expect.any(String));
 });
+
+describe('#isWebhookVerifyRequest', () => {
+  it('check if request is for webhook verification', async () => {
+    const { connector } = setup();
+
+    await connector.isWebhookVerifyRequest(webhookVerifyRequest.body);
+
+    expect(connector.isWebhookVerifyRequest(webhookVerifyRequest.body)).toEqual(
+      true
+    );
+    expect(connector.isWebhookVerifyRequest({ events: [] })).toEqual(false);
+  });
+});
diff --git a/src/express/__tests__/index.spec.js b/src/express/__tests__/index.spec.js
index 2b27521a..4989e05f 100644
--- a/src/express/__tests__/index.spec.js
+++ b/src/express/__tests__/index.spec.js
@@ -6,6 +6,7 @@ describe('express', () => {
     expect(express.createMiddleware).toBeDefined();
     expect(express.registerRoutes).toBeDefined();
     expect(express.verifyLineSignature).toBeDefined();
+    expect(express.verifyLineWebhook).toBeDefined();
     expect(express.verifyMessengerSignature).toBeDefined();
     expect(express.verifyMessengerWebhook).toBeDefined();
     expect(express.verifySlackSignature).toBeDefined();
diff --git a/src/express/__tests__/verifyLineWebhook.spec.js b/src/express/__tests__/verifyLineWebhook.spec.js
new file mode 100644
index 00000000..93a2b713
--- /dev/null
+++ b/src/express/__tests__/verifyLineWebhook.spec.js
@@ -0,0 +1,87 @@
+import verifyLineWebhook from '../verifyLineWebhook';
+
+function setup() {
+  const bot = {
+    connector: {
+      isWebhookVerifyRequest: jest.fn(),
+    },
+  };
+
+  const middleware = verifyLineWebhook(bot);
+
+  const req = {
+    query: {},
+    body: {
+      events: [
+        {
+          replyToken: '00000000000000000000000000000000',
+          type: 'message',
+          timestamp: 1513065174862,
+          source: {
+            type: 'user',
+            userId: 'Udeadbeefdeadbeefdeadbeefdeadbeef',
+          },
+          message: {
+            id: '100001',
+            type: 'text',
+            text: 'Hello, world',
+          },
+        },
+        {
+          replyToken: 'ffffffffffffffffffffffffffffffff',
+          type: 'message',
+          timestamp: 1513065174862,
+          source: {
+            type: 'user',
+            userId: 'Udeadbeefdeadbeefdeadbeefdeadbeef',
+          },
+          message: {
+            id: '100002',
+            type: 'sticker',
+            packageId: '1',
+            stickerId: '1',
+          },
+        },
+      ],
+    },
+  };
+
+  const res = {
+    sendStatus: jest.fn(),
+  };
+
+  const next = jest.fn();
+
+  return {
+    bot,
+    middleware,
+    req,
+    res,
+    next,
+  };
+}
+
+beforeEach(() => {
+  console.error = jest.fn();
+});
+
+it('should correctly response 200 when is webhook verify request', () => {
+  const { bot, middleware, req, res, next } = setup();
+
+  bot.connector.isWebhookVerifyRequest.mockReturnValueOnce(true);
+
+  middleware(req, res, next);
+
+  expect(res.sendStatus).toBeCalledWith(200);
+  expect(next).not.toBeCalled();
+});
+
+it('should call next when is not webhook verify request', () => {
+  const { bot, middleware, req, res, next } = setup();
+
+  bot.connector.isWebhookVerifyRequest.mockReturnValueOnce(false);
+
+  middleware(req, res, next);
+
+  expect(next).toBeCalled();
+});
diff --git a/src/koa/__tests__/index.spec.js b/src/koa/__tests__/index.spec.js
index a7666451..9fde1daa 100644
--- a/src/koa/__tests__/index.spec.js
+++ b/src/koa/__tests__/index.spec.js
@@ -6,6 +6,7 @@ describe('koa', () => {
     expect(koa.createMiddleware).toBeDefined();
     expect(koa.registerRoutes).toBeDefined();
     expect(koa.verifyLineSignature).toBeDefined();
+    expect(koa.verifyLineWebhook).toBeDefined();
     expect(koa.verifyMessengerSignature).toBeDefined();
     expect(koa.verifyMessengerWebhook).toBeDefined();
     expect(koa.verifySlackSignature).toBeDefined();
diff --git a/src/koa/__tests__/verifyLineWebhook.spec.js b/src/koa/__tests__/verifyLineWebhook.spec.js
new file mode 100644
index 00000000..ddbee697
--- /dev/null
+++ b/src/koa/__tests__/verifyLineWebhook.spec.js
@@ -0,0 +1,85 @@
+import verifyLineWebhook from '../verifyLineWebhook';
+
+function setup() {
+  const bot = {
+    connector: {
+      isWebhookVerifyRequest: jest.fn(),
+    },
+  };
+
+  const middleware = verifyLineWebhook(bot);
+
+  const ctx = {
+    request: {
+      query: {},
+      body: {
+        events: [
+          {
+            replyToken: '00000000000000000000000000000000',
+            type: 'message',
+            timestamp: 1513065174862,
+            source: {
+              type: 'user',
+              userId: 'Udeadbeefdeadbeefdeadbeefdeadbeef',
+            },
+            message: {
+              id: '100001',
+              type: 'text',
+              text: 'Hello, world',
+            },
+          },
+          {
+            replyToken: 'ffffffffffffffffffffffffffffffff',
+            type: 'message',
+            timestamp: 1513065174862,
+            source: {
+              type: 'user',
+              userId: 'Udeadbeefdeadbeefdeadbeefdeadbeef',
+            },
+            message: {
+              id: '100002',
+              type: 'sticker',
+              packageId: '1',
+              stickerId: '1',
+            },
+          },
+        ],
+      },
+    },
+    response: {},
+  };
+
+  const next = jest.fn();
+
+  return {
+    bot,
+    middleware,
+    ctx,
+    next,
+  };
+}
+
+beforeEach(() => {
+  console.error = jest.fn();
+});
+
+it('should correctly response 200 when is webhook verify request', () => {
+  const { bot, middleware, ctx, next } = setup();
+
+  bot.connector.isWebhookVerifyRequest.mockReturnValueOnce(true);
+
+  middleware(ctx, next);
+
+  expect(ctx.response.status).toBe(200);
+  expect(next).not.toBeCalled();
+});
+
+it('should call next when is not webhook verify request', () => {
+  const { bot, middleware, ctx, next } = setup();
+
+  bot.connector.isWebhookVerifyRequest.mockReturnValueOnce(false);
+
+  middleware(ctx, next);
+
+  expect(next).toBeCalled();
+});
diff --git a/src/micro/__tests__/createRequestHandler.spec.js b/src/micro/__tests__/createRequestHandler.spec.js
index 95ac4393..e1ce5eaa 100644
--- a/src/micro/__tests__/createRequestHandler.spec.js
+++ b/src/micro/__tests__/createRequestHandler.spec.js
@@ -21,6 +21,7 @@ function setup({ platform }) {
     connector: {
       platform,
       verifySignature: jest.fn(),
+      isWebhookVerifyRequest: jest.fn(),
     },
   };
   const res = {
diff --git a/src/micro/__tests__/index.spec.js b/src/micro/__tests__/index.spec.js
index d85de5e4..1f721e27 100644
--- a/src/micro/__tests__/index.spec.js
+++ b/src/micro/__tests__/index.spec.js
@@ -5,6 +5,7 @@ describe('micro', () => {
     expect(micro.createServer).toBeDefined();
     expect(micro.createRequestHandler).toBeDefined();
     expect(micro.verifyLineSignature).toBeDefined();
+    expect(micro.verifyLineWebhook).toBeDefined();
     expect(micro.verifyMessengerSignature).toBeDefined();
     expect(micro.verifyMessengerWebhook).toBeDefined();
     expect(micro.verifySlackSignature).toBeDefined();
diff --git a/src/micro/__tests__/verifyLineWebhook.spec.js b/src/micro/__tests__/verifyLineWebhook.spec.js
new file mode 100644
index 00000000..35423f86
--- /dev/null
+++ b/src/micro/__tests__/verifyLineWebhook.spec.js
@@ -0,0 +1,87 @@
+import micro from 'micro';
+
+import verifyLineWebhook from '../verifyLineWebhook';
+
+jest.mock('micro');
+
+function setup() {
+  const bot = {
+    connector: {
+      isWebhookVerifyRequest: jest.fn(),
+    },
+  };
+
+  const middleware = verifyLineWebhook(bot);
+
+  const req = {
+    headers: {
+      'x-line-signature': 'signature from line',
+    },
+  };
+
+  const res = {
+    send: jest.fn(),
+    status: jest.fn(),
+  };
+
+  return {
+    bot,
+    middleware,
+    req,
+    res,
+  };
+}
+
+it('should response 200 when verification pass', async () => {
+  const { bot, middleware, req, res } = setup();
+
+  micro.json.mockResolvedValueOnce({
+    events: [
+      {
+        replyToken: '00000000000000000000000000000000',
+        type: 'message',
+        timestamp: 1513065174862,
+        source: {
+          type: 'user',
+          userId: 'Udeadbeefdeadbeefdeadbeefdeadbeef',
+        },
+        message: {
+          id: '100001',
+          type: 'text',
+          text: 'Hello, world',
+        },
+      },
+      {
+        replyToken: 'ffffffffffffffffffffffffffffffff',
+        type: 'message',
+        timestamp: 1513065174862,
+        source: {
+          type: 'user',
+          userId: 'Udeadbeefdeadbeefdeadbeefdeadbeef',
+        },
+        message: {
+          id: '100002',
+          type: 'sticker',
+          packageId: '1',
+          stickerId: '1',
+        },
+      },
+    ],
+  });
+  bot.connector.isWebhookVerifyRequest.mockReturnValueOnce(true);
+
+  await middleware(req, res);
+
+  expect(micro.send).toBeCalledWith(res, 200);
+});
+
+it('should send 400 when verification fail', async () => {
+  const { bot, middleware, req, res } = setup();
+
+  micro.json.mockReturnValueOnce({});
+  bot.connector.isWebhookVerifyRequest.mockReturnValueOnce(false);
+
+  await middleware(req, res);
+
+  expect(micro.send).not.toBeCalled();
+});
diff --git a/src/restify/__tests__/index.spec.js b/src/restify/__tests__/index.spec.js
index 0206b70f..7e981bd1 100644
--- a/src/restify/__tests__/index.spec.js
+++ b/src/restify/__tests__/index.spec.js
@@ -14,6 +14,7 @@ describe('restify', () => {
     expect(restify.createMiddleware).toBeDefined();
     expect(restify.registerRoutes).toBeDefined();
     expect(restify.verifyLineSignature).toBeDefined();
+    expect(restify.verifyLineWebhook).toBeDefined();
     expect(restify.verifyMessengerSignature).toBeDefined();
     expect(restify.verifyMessengerWebhook).toBeDefined();
     expect(restify.verifySlackSignature).toBeDefined();
diff --git a/src/restify/__tests__/verifyLineWebhook.spec.js b/src/restify/__tests__/verifyLineWebhook.spec.js
new file mode 100644
index 00000000..80c642ae
--- /dev/null
+++ b/src/restify/__tests__/verifyLineWebhook.spec.js
@@ -0,0 +1,87 @@
+import verifyLineWebhook from '../verifyLineWebhook';
+
+function setup() {
+  const bot = {
+    connector: {
+      isWebhookVerifyRequest: jest.fn(),
+    },
+  };
+
+  const middleware = verifyLineWebhook(bot);
+
+  const req = {
+    query: {},
+    body: {
+      events: [
+        {
+          replyToken: '00000000000000000000000000000000',
+          type: 'message',
+          timestamp: 1513065174862,
+          source: {
+            type: 'user',
+            userId: 'Udeadbeefdeadbeefdeadbeefdeadbeef',
+          },
+          message: {
+            id: '100001',
+            type: 'text',
+            text: 'Hello, world',
+          },
+        },
+        {
+          replyToken: 'ffffffffffffffffffffffffffffffff',
+          type: 'message',
+          timestamp: 1513065174862,
+          source: {
+            type: 'user',
+            userId: 'Udeadbeefdeadbeefdeadbeefdeadbeef',
+          },
+          message: {
+            id: '100002',
+            type: 'sticker',
+            packageId: '1',
+            stickerId: '1',
+          },
+        },
+      ],
+    },
+  };
+
+  const res = {
+    send: jest.fn(),
+  };
+
+  const next = jest.fn();
+
+  return {
+    bot,
+    middleware,
+    req,
+    res,
+    next,
+  };
+}
+
+beforeEach(() => {
+  console.error = jest.fn();
+});
+
+it('should correctly response 200 when is webhook verify request', () => {
+  const { bot, middleware, req, res, next } = setup();
+
+  bot.connector.isWebhookVerifyRequest.mockReturnValueOnce(true);
+
+  middleware(req, res, next);
+
+  expect(res.send).toBeCalledWith(200);
+  expect(next).not.toBeCalled();
+});
+
+it('should call next when is not webhook verify request', () => {
+  const { bot, middleware, req, res, next } = setup();
+
+  bot.connector.isWebhookVerifyRequest.mockReturnValueOnce(false);
+
+  middleware(req, res, next);
+
+  expect(next).toBeCalled();
+});