Repository: Yoctol/bottender
Pull request #605 (2019-12-26 10:33:55)

feat(router): add viber routes
Add viber routes:

```js
const { router, viber } = require('bottender/router');

function Action() {
  // ...
}

function App() {
  return router([
    viber.message(Action),
    viber.subscribed(Action),
    viber.unsubscribed(Action),
    viber.conversationStarted(Action),
    viber.delivered(Action),
    viber.seen(Action),
    viber.failed(Action),
    viber(Action),
  ]);
}
```

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

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

```diff
@@               Coverage Diff               @@
##           release/1.1     #605      +/-   ##
===============================================
+ Coverage        80.61%   80.77%   +0.15%     
===============================================
  Files               93       94       +1     
  Lines             4205     4239      +34     
  Branches          1034     1041       +7     
===============================================
+ Hits              3390     3424      +34     
  Misses             775      775              
  Partials            40       40
```


| [Impacted Files](https://codecov.io/gh/Yoctol/bottender/pull/605?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [packages/bottender/src/router/index.ts](https://codecov.io/gh/Yoctol/bottender/pull/605/diff?src=pr&el=tree#diff-cGFja2FnZXMvYm90dGVuZGVyL3NyYy9yb3V0ZXIvaW5kZXgudHM=) | `90.27% <100%> (+1.38%)` | :arrow_up: |
| [packages/bottender/src/viber/routes.ts](https://codecov.io/gh/Yoctol/bottender/pull/605/diff?src=pr&el=tree#diff-cGFja2FnZXMvYm90dGVuZGVyL3NyYy92aWJlci9yb3V0ZXMudHM=) | `100% <100%> (ø)` | |

------

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


Patch:
diff --git a/packages/bottender/src/router/index.ts b/packages/bottender/src/router/index.ts
index 391484d3..4088f659 100644
--- a/packages/bottender/src/router/index.ts
+++ b/packages/bottender/src/router/index.ts
@@ -2,6 +2,7 @@ import Context from '../context/Context';
 import line from '../line/routes';
 import messenger from '../messenger/routes';
 import telegram from '../telegram/routes';
+import viber from '../viber/routes';
 import { Action, Client, Event, Props } from '../types';
 
 type MatchPattern = string | Array<string> | RegExp;
@@ -194,4 +195,14 @@ function platform<C extends Client = any, E extends Event = any>(
 
 export default router;
 
-export { router, route, text, payload, platform, line, messenger, telegram };
+export {
+  router,
+  route,
+  text,
+  payload,
+  platform,
+  line,
+  messenger,
+  telegram,
+  viber,
+};
diff --git a/packages/bottender/src/viber/routes.ts b/packages/bottender/src/viber/routes.ts
new file mode 100644
index 00000000..6049ed20
--- /dev/null
+++ b/packages/bottender/src/viber/routes.ts
@@ -0,0 +1,112 @@
+import Context from '../context/Context';
+import { Action, Client, Event } from '../types';
+import { RoutePredicate, route } from '../router';
+
+type Route = <C extends Client = any, E extends Event = any>(
+  action: Action<C, E>
+) => {
+  predicate: RoutePredicate<C, E>;
+  action: Action<C, E>;
+};
+
+type Viber = Route & {
+  message: Route;
+  subscribed: Route;
+  unsubscribed: Route;
+  conversationStarted: Route;
+  delivered: Route;
+  seen: Route;
+  failed: Route;
+};
+
+const viber: Viber = <C extends Client = any, E extends Event = any>(
+  action: Action<C, E>
+) => {
+  return route(context => context.platform === 'viber', action);
+};
+
+function message<C extends Client = any, E extends Event = any>(
+  action: Action<C, E>
+) {
+  return route(
+    (context: Context<C, E>) =>
+      context.platform === 'viber' && context.event.isMessage,
+    action
+  );
+}
+
+viber.message = message;
+
+function subscribed<C extends Client = any, E extends Event = any>(
+  action: Action<C, E>
+) {
+  return route(
+    (context: Context<C, E>) =>
+      context.platform === 'viber' && context.event.isSubscribed,
+    action
+  );
+}
+
+viber.subscribed = subscribed;
+
+function unsubscribed<C extends Client = any, E extends Event = any>(
+  action: Action<C, E>
+) {
+  return route(
+    (context: Context<C, E>) =>
+      context.platform === 'viber' && context.event.isUnsubscribed,
+    action
+  );
+}
+
+viber.unsubscribed = unsubscribed;
+
+function conversationStarted<C extends Client = any, E extends Event = any>(
+  action: Action<C, E>
+) {
+  return route(
+    (context: Context<C, E>) =>
+      context.platform === 'viber' && context.event.isConversationStarted,
+    action
+  );
+}
+
+viber.conversationStarted = conversationStarted;
+
+function delivered<C extends Client = any, E extends Event = any>(
+  action: Action<C, E>
+) {
+  return route(
+    (context: Context<C, E>) =>
+      context.platform === 'viber' && context.event.delivered,
+    action
+  );
+}
+
+viber.delivered = delivered;
+
+function seen<C extends Client = any, E extends Event = any>(
+  action: Action<C, E>
+) {
+  return route(
+    (context: Context<C, E>) =>
+      context.platform === 'viber' && context.event.seen,
+    action
+  );
+}
+
+viber.seen = seen;
+
+function failed<C extends Client = any, E extends Event = any>(
+  action: Action<C, E>
+) {
+  return route(
+    (context: Context<C, E>) =>
+      context.platform === 'viber' && context.event.failed,
+    action
+  );
+}
+
+viber.failed = failed;
+
+export default viber;


Test patch:
diff --git a/packages/bottender/src/router/__tests__/router.spec.ts b/packages/bottender/src/router/__tests__/router.spec.ts
index d0c50a75..4c8a1af9 100644
--- a/packages/bottender/src/router/__tests__/router.spec.ts
+++ b/packages/bottender/src/router/__tests__/router.spec.ts
@@ -8,6 +8,7 @@ import router, {
   route,
   telegram,
   text,
+  viber,
 } from '..';
 
 function textContext(message = '') {
@@ -361,3 +362,7 @@ it('#messenger should be exported', () => {
 it('#telegram should be exported', () => {
   expect(telegram).toBeDefined();
 });
+
+it('#viber should be exported', () => {
+  expect(viber).toBeDefined();
+});
diff --git a/packages/bottender/src/viber/__tests__/routes.spec.ts b/packages/bottender/src/viber/__tests__/routes.spec.ts
new file mode 100644
index 00000000..cce094a1
--- /dev/null
+++ b/packages/bottender/src/viber/__tests__/routes.spec.ts
@@ -0,0 +1,277 @@
+import Context from '../../context/Context';
+import ViberContext from '../ViberContext';
+import ViberEvent from '../ViberEvent';
+import router from '../../router';
+import viber from '../routes';
+import { run } from '../../bot/Bot';
+
+const viberEventTextMessage = new ViberEvent({
+  event: 'message',
+  timestamp: 1457764197627,
+  messageToken: 4912661846655238145,
+  sender: {
+    id: '01234567890A=',
+    name: 'John McClane',
+    avatar: 'http://avatar.example.com',
+    country: 'UK',
+    language: 'en',
+    apiVersion: 1,
+  },
+  message: {
+    type: 'text',
+    text: 'a message to the service',
+    trackingData: 'tracking data',
+  },
+});
+
+const viberEventSubscribed = new ViberEvent({
+  event: 'subscribed',
+  timestamp: 1457764197627,
+  user: {
+    id: '01234567890A=',
+    name: 'John McClane',
+    avatar: 'http://avatar.example.com',
+    country: 'UK',
+    language: 'en',
+    apiVersion: 1,
+  },
+  messageToken: 4912661846655238145,
+});
+
+const viberEventUnsubscribed = new ViberEvent({
+  event: 'unsubscribed',
+  timestamp: 1457764197627,
+  userId: '01234567890A=',
+  messageToken: 4912661846655238145,
+});
+
+const viberEventConversationStarted = new ViberEvent({
+  event: 'conversation_started',
+  timestamp: 1457764197627,
+  messageToken: 4912661846655238145,
+  type: 'open',
+  context: 'context information',
+  user: {
+    id: '01234567890A=',
+    name: 'John McClane',
+    avatar: 'http://avatar.example.com',
+    country: 'UK',
+    language: 'en',
+    apiVersion: 1,
+  },
+  subscribed: false,
+});
+
+const viberEventDelivered = new ViberEvent({
+  event: 'delivered',
+  timestamp: 1457764197627,
+  messageToken: 4912661846655238145,
+  userId: '01234567890A=',
+});
+
+const viberEventSeen = new ViberEvent({
+  event: 'seen',
+  timestamp: 1457764197627,
+  messageToken: 4912661846655238145,
+  userId: '01234567890A=',
+});
+
+const viberEventFailed = new ViberEvent({
+  event: 'failed',
+  timestamp: 1457764197627,
+  messageToken: 4912661846655238145,
+  userId: '01234567890A=',
+  desc: 'failure description',
+});
+
+async function Action(context) {
+  await context.sendText('hello');
+}
+
+async function expectRouteMatchContext({ route, context }) {
+  const Router = router([route]);
+
+  const app = run(Router);
+
+  context.sendText = jest.fn();
+
+  await app(context);
+
+  expect(context.sendText).toBeCalledWith('hello');
+}
+
+async function expectRouteMatchViberEvent({ route, event }) {
+  const context = new ViberContext({
+    client: {} as any,
+    event,
+  });
+
+  await expectRouteMatchContext({
+    route,
+    context,
+  });
+}
+
+async function expectRouteNotMatchContext({ route, context }) {
+  const Router = router([route]);
+
+  const app = run(Router);
+
+  context.sendText = jest.fn();
+
+  await app(context);
+
+  expect(context.sendText).not.toBeCalledWith('hello');
+}
+
+async function expectRouteNotMatchViberEvent({ route, event }) {
+  const context = new ViberContext({
+    client: {} as any,
+    event,
+  });
+
+  await expectRouteNotMatchContext({
+    route,
+    context,
+  });
+}
+
+class TestContext extends Context<any, any> {
+  get platform() {
+    return 'test';
+  }
+
+  // eslint-disable-next-line @typescript-eslint/no-empty-function
+  sendText() {}
+}
+
+describe('#viber', () => {
+  it('should call action when it receives a viber event', async () => {
+    await expectRouteMatchViberEvent({
+      route: viber(Action),
+      event: viberEventTextMessage,
+    });
+  });
+
+  it('should not call action when it receives a non-viber event', async () => {
+    await expectRouteNotMatchContext({
+      route: viber(Action),
+      context: new TestContext({
+        client: {} as any,
+        event: {},
+      }),
+    });
+  });
+
+  describe('#viber.message', () => {
+    it('should call action when it receives a viber message event', async () => {
+      await expectRouteMatchViberEvent({
+        route: viber.message(Action),
+        event: viberEventTextMessage,
+      });
+    });
+
+    it('should not call action when it receives a non-message event', async () => {
+      await expectRouteNotMatchViberEvent({
+        route: viber.message(Action),
+        event: viberEventSubscribed,
+      });
+    });
+  });
+
+  describe('#viber.subscribed', () => {
+    it('should call action when it receives a viber subscribed event', async () => {
+      await expectRouteMatchViberEvent({
+        route: viber.subscribed(Action),
+        event: viberEventSubscribed,
+      });
+    });
+
+    it('should not call action when it receives a non-subscribed event', async () => {
+      await expectRouteNotMatchViberEvent({
+        route: viber.subscribed(Action),
+        event: viberEventTextMessage,
+      });
+    });
+  });
+
+  describe('#viber.unsubscribed', () => {
+    it('should call action when it receives a viber unsubscribed event', async () => {
+      await expectRouteMatchViberEvent({
+        route: viber.unsubscribed(Action),
+        event: viberEventUnsubscribed,
+      });
+    });
+
+    it('should not call action when it receives a non-unsubscribed event', async () => {
+      await expectRouteNotMatchViberEvent({
+        route: viber.unsubscribed(Action),
+        event: viberEventTextMessage,
+      });
+    });
+  });
+
+  describe('#viber.conversationStarted', () => {
+    it('should call action when it receives a viber conversationStarted event', async () => {
+      await expectRouteMatchViberEvent({
+        route: viber.conversationStarted(Action),
+        event: viberEventConversationStarted,
+      });
+    });
+
+    it('should not call action when it receives a non-conversationStarted event', async () => {
+      await expectRouteNotMatchViberEvent({
+        route: viber.conversationStarted(Action),
+        event: viberEventTextMessage,
+      });
+    });
+  });
+
+  describe('#viber.delivered', () => {
+    it('should call action when it receives a viber delivered event', async () => {
+      await expectRouteMatchViberEvent({
+        route: viber.delivered(Action),
+        event: viberEventDelivered,
+      });
+    });
+
+    it('should not call action when it receives a non-delivered event', async () => {
+      await expectRouteNotMatchViberEvent({
+        route: viber.delivered(Action),
+        event: viberEventTextMessage,
+      });
+    });
+  });
+
+  describe('#viber.seen', () => {
+    it('should call action when it receives a viber seen event', async () => {
+      await expectRouteMatchViberEvent({
+        route: viber.seen(Action),
+        event: viberEventSeen,
+      });
+    });
+
+    it('should not call action when it receives a non-seen event', async () => {
+      await expectRouteNotMatchViberEvent({
+        route: viber.seen(Action),
+        event: viberEventTextMessage,
+      });
+    });
+  });
+
+  describe('#viber.failed', () => {
+    it('should call action when it receives a viber failed event', async () => {
+      await expectRouteMatchViberEvent({
+        route: viber.failed(Action),
+        event: viberEventFailed,
+      });
+    });
+
+    it('should not call action when it receives a non-failed event', async () => {
+      await expectRouteNotMatchViberEvent({
+        route: viber.failed(Action),
+        event: viberEventTextMessage,
+      });
+    });
+  });
+});