Repository: Yoctol/bottender
Pull request #450 (2019-09-26 04:08:03)

fix: apply path in bottender.config.js in webhook setting process
When we use the following commands:

```
bottender messenger webhook set
bottender telegram webhook set
bottender viber webhook set
```

It can get your `path` configured in the `bottender.config.js` and match the behavior with `bottender start/dev`.


Patch:
diff --git a/packages/bottender/src/cli/providers/messenger/persona.ts b/packages/bottender/src/cli/providers/messenger/persona.ts
index 8c895416..68babf57 100644
--- a/packages/bottender/src/cli/providers/messenger/persona.ts
+++ b/packages/bottender/src/cli/providers/messenger/persona.ts
@@ -56,14 +56,22 @@ export async function createPersona(ctx: CliContext): Promise<void> {
   try {
     const config = getConfig('messenger');
 
-    invariant(config.accessToken, 'accessToken is not found in config file');
+    const { accessToken } = config;
 
-    const accessToken = config.accessToken;
+    invariant(accessToken, 'accessToken is not found in config file');
 
-    invariant(personaName, 'Name is not specified!!');
-    invariant(personaUrl, 'Profile picture url is not specified!!');
+    invariant(
+      personaName,
+      '`name` is required but not found. Use --name <name> to specify persona name'
+    );
+    invariant(
+      personaUrl,
+      '`pic` is required but not found. Use --pic <url> to specify persona profile picture url'
+    );
 
-    const client = MessengerClient.connect(accessToken);
+    const client = MessengerClient.connect({
+      accessToken,
+    });
 
     const persona = {
       name: personaName as string,
@@ -73,9 +81,9 @@ export async function createPersona(ctx: CliContext): Promise<void> {
     const personaID = await client.createPersona(persona);
 
     print(`Successfully create ${bold('persona')} ${bold(personaID.id)}`);
-    return;
   } catch (err) {
     error(`Failed to create ${bold('persona')}`);
+
     if (err.response) {
       error(`status: ${bold(err.response.status)}`);
       if (err.response.data) {
@@ -84,19 +92,22 @@ export async function createPersona(ctx: CliContext): Promise<void> {
     } else {
       error(err.message);
     }
+
     return process.exit(1);
   }
 }
 
-export async function listPersona(_: CliContext) {
+export async function listPersona(_: CliContext): Promise<void> {
   try {
     const config = getConfig('messenger');
 
-    invariant(config.accessToken, 'accessToken is not found in config file');
+    const { accessToken } = config;
 
-    const accessToken = config.accessToken;
+    invariant(accessToken, 'accessToken is not found in config file');
 
-    const client = MessengerClient.connect(accessToken);
+    const client = MessengerClient.connect({
+      accessToken,
+    });
 
     const personas = await client.getAllPersonas();
 
@@ -113,7 +124,6 @@ export async function listPersona(_: CliContext) {
     } else {
       print('No personas are found.');
     }
-    return;
   } catch (err) {
     error(`Failed to list ${bold('personas')}`);
     if (err.response) {
@@ -138,13 +148,17 @@ export async function getPersona(ctx: CliContext): Promise<void> {
   try {
     const config = getConfig('messenger');
 
-    invariant(config.accessToken, 'accessToken is not found in config file');
-
-    const accessToken = config.accessToken;
+    const { accessToken } = config;
 
-    invariant(personaId, 'Persona ID is not specified!!');
+    invariant(accessToken, 'accessToken is not found in config file');
+    invariant(
+      personaId,
+      '`id` is required but not found. Use --id <id> to specify persona id'
+    );
 
-    const client = MessengerClient.connect(accessToken);
+    const client = MessengerClient.connect({
+      accessToken,
+    });
 
     const persona = await client.getPersona(personaId as string);
 
@@ -155,11 +169,11 @@ export async function getPersona(ctx: CliContext): Promise<void> {
     } else {
       print(`Cannot get persona of ID ${bold(personaId as string)}`);
     }
-    return;
   } catch (err) {
     error(
       `Failed to get ${bold('persona')} of ID ${bold(personaId as string)}`
     );
+
     if (err.response) {
       error(`status: ${bold(err.response.status)}`);
       if (err.response.data) {
@@ -168,6 +182,7 @@ export async function getPersona(ctx: CliContext): Promise<void> {
     } else {
       error(err.message);
     }
+
     return process.exit(1);
   }
 }
@@ -182,13 +197,17 @@ export async function deletePersona(ctx: CliContext): Promise<void> {
   try {
     const config = getConfig('messenger');
 
-    invariant(config.accessToken, 'accessToken is not found in config file');
-
-    const accessToken = config.accessToken;
+    const { accessToken } = config;
 
-    invariant(personaId, 'Persona ID is not specified!!');
+    invariant(accessToken, 'accessToken is not found in config file');
+    invariant(
+      personaId,
+      '`id` is required but not found. Use --id <id> to specify persona id'
+    );
 
-    const client = MessengerClient.connect(accessToken);
+    const client = MessengerClient.connect({
+      accessToken,
+    });
 
     const res = await client.deletePersona(personaId as string);
 
@@ -197,11 +216,11 @@ export async function deletePersona(ctx: CliContext): Promise<void> {
     } else {
       print(`Cannot get persona of ID ${bold(personaId as string)}`);
     }
-    return;
   } catch (err) {
     error(
       `Failed to delete ${bold('persona')} of ID ${bold(personaId as string)}`
     );
+
     if (err.response) {
       error(`status: ${bold(err.response.status)}`);
       if (err.response.data) {
@@ -210,6 +229,7 @@ export async function deletePersona(ctx: CliContext): Promise<void> {
     } else {
       error(err.message);
     }
+
     return process.exit(1);
   }
 }
diff --git a/packages/bottender/src/cli/providers/messenger/profile.ts b/packages/bottender/src/cli/providers/messenger/profile.ts
index fd4e13ad..8582efcc 100644
--- a/packages/bottender/src/cli/providers/messenger/profile.ts
+++ b/packages/bottender/src/cli/providers/messenger/profile.ts
@@ -76,11 +76,13 @@ export async function getMessengerProfile(_: CliContext): Promise<void> {
   try {
     const config = getConfig('messenger');
 
-    invariant(config.accessToken, 'accessToken is not found in config file');
+    const { accessToken } = config;
 
-    const accessToken = config.accessToken;
+    invariant(accessToken, 'accessToken is not found in config file');
 
-    const client = MessengerClient.connect(accessToken);
+    const client = MessengerClient.connect({
+      accessToken,
+    });
 
     const profile = await client.getMessengerProfile(FIELDS);
 
@@ -91,9 +93,9 @@ export async function getMessengerProfile(_: CliContext): Promise<void> {
     } else {
       error(`Failed to find ${bold('messenger_profile')} setting`);
     }
-    return;
   } catch (err) {
     error(`Failed to get ${bold('messenger_profile')} settings`);
+
     if (err.response) {
       error(`status: ${bold(err.response.status)}`);
       if (err.response.data) {
@@ -102,6 +104,7 @@ export async function getMessengerProfile(_: CliContext): Promise<void> {
     } else {
       error(err.message);
     }
+
     return process.exit(1);
   }
 }
@@ -117,21 +120,25 @@ export async function setMessengerProfile(ctx: CliContext): Promise<void> {
   try {
     const config = getConfig('messenger');
 
-    invariant(config.accessToken, 'accessToken is not found in config file');
+    const { accessToken } = config;
 
-    const accessToken = config.accessToken;
+    invariant(accessToken, 'accessToken is not found in config file');
 
     const { profile: _profile } = getConfig('messenger');
 
-    const client = MessengerClient.connect(accessToken);
+    const client = MessengerClient.connect({
+      accessToken,
+    });
 
     if (force) {
       await client.deleteMessengerProfile(FIELDS);
+
       print(
         `Successfully delete all ${bold(
           'messenger_profile'
         )} settings due to ${bold('--force')} option`
       );
+
       if (_profile.whitelisted_domains) {
         await client.setMessengerProfile(pick(_profile, 'whitelisted_domains'));
         await client.setMessengerProfile(omit(_profile, 'whitelisted_domains'));
@@ -189,9 +196,9 @@ export async function setMessengerProfile(ctx: CliContext): Promise<void> {
         'bottender messenger profile get'
       )} to see the full profile setting.`
     );
-    return;
   } catch (err) {
     error(`Failed to set ${bold('messenger_profile')} settings`);
+
     if (err.response) {
       error(`status: ${bold(err.response.status)}`);
       if (err.response.data) {
@@ -200,6 +207,7 @@ export async function setMessengerProfile(ctx: CliContext): Promise<void> {
     } else {
       error(err.message);
     }
+
     return process.exit(1);
   }
 }
@@ -212,14 +220,16 @@ export async function deleteMessengerProfile(_: CliContext): Promise<void> {
 
     const accessToken = config.accessToken;
 
-    const client = MessengerClient.connect(accessToken);
+    const client = MessengerClient.connect({
+      accessToken,
+    });
 
     await client.deleteMessengerProfile(FIELDS);
 
     print(`Successfully delete ${bold('messenger_profile')} settings`);
-    return;
   } catch (err) {
     error(`Failed to delete ${bold('messenger_profile')} settings`);
+
     if (err.response) {
       error(`status: ${bold(err.response.status)}`);
       if (err.response.data) {
@@ -228,6 +238,7 @@ export async function deleteMessengerProfile(_: CliContext): Promise<void> {
     } else {
       error(err.message);
     }
+
     return process.exit(1);
   }
 }
diff --git a/packages/bottender/src/cli/providers/messenger/webhook.ts b/packages/bottender/src/cli/providers/messenger/webhook.ts
index fae867b8..b56919c2 100644
--- a/packages/bottender/src/cli/providers/messenger/webhook.ts
+++ b/packages/bottender/src/cli/providers/messenger/webhook.ts
@@ -21,7 +21,6 @@ const help = (): void => {
     ${chalk.dim('Options:')}
 
       -w, --webhook         Webhook callback URL
-      -v, --verify-token    Verify token
       --ngrok-port          Ngrok port(default: 4040)
 
     ${chalk.dim('Examples:')}
@@ -44,13 +43,23 @@ export async function setWebhook(ctx: CliContext): Promise<void> {
   });
 
   let webhook = argv['--webhook'];
-  const ngrokPort = argv['--ngrok-port'];
+  const ngrokPort = argv['--ngrok-port'] || '4040';
 
   try {
     const config = getConfig('messenger');
 
-    invariant(config.accessToken, 'accessToken is not found in config file');
-    const { accessToken, appId, appSecret } = config;
+    const {
+      accessToken,
+      appId,
+      appSecret,
+      verifyToken,
+      path = '/webhooks/messenger',
+    } = config;
+
+    invariant(accessToken, 'accessToken is not found in config file');
+    invariant(appId, 'appId is not found in config file');
+    invariant(appSecret, 'appSecret is not found in config file');
+    invariant(verifyToken, 'verifyToken is not found in config file');
 
     const client = MessengerClient.connect({
       accessToken,
@@ -58,15 +67,6 @@ export async function setWebhook(ctx: CliContext): Promise<void> {
       appSecret,
     });
 
-    const defaultFields = [
-      'messages',
-      'messaging_postbacks',
-      'messaging_optins',
-      'messaging_referrals',
-      'messaging_handovers',
-      'messaging_policy_enforcement',
-    ];
-
     if (!webhook) {
       warn('We can not find the webhook callback url you provided.');
       const prompt = new Confirm(
@@ -74,23 +74,24 @@ export async function setWebhook(ctx: CliContext): Promise<void> {
       );
       const result = await prompt.run();
       if (result) {
-        webhook = await getWebhookFromNgrok(ngrokPort);
+        webhook = `${await getWebhookFromNgrok(ngrokPort)}${path}`;
       }
     }
 
-    const verifyToken = config.verifyToken;
-
-    invariant(config.appId, '`appId` is not found in config file');
-    invariant(config.appSecret, '`appSecret` is not found in config file');
-    invariant(
-      verifyToken,
-      '`verifyToken` is required but not found. using -v <verifyToken> to setup or list `verifyToken` key it in config file.'
-    );
     invariant(
       webhook,
       '`webhook` is required but not found. Use -w <webhook> to setup or make sure you are running ngrok server.'
     );
 
+    const defaultFields = [
+      'messages',
+      'messaging_postbacks',
+      'messaging_optins',
+      'messaging_referrals',
+      'messaging_handovers',
+      'messaging_policy_enforcement',
+    ];
+
     if (!config.fields) {
       print(
         `\`fields\` is not found in config file, we will use ${bold(
@@ -144,20 +145,21 @@ export async function setWebhook(ctx: CliContext): Promise<void> {
       callback_url: webhook as string,
       verify_token: verifyToken as string,
       fields,
-      access_token: `${config.appId}|${config.appSecret}`,
+      access_token: `${appId}|${appSecret}`,
     });
+
     invariant(success, 'Setting for webhook is failed');
 
     print('Successfully set Messenger webhook callback URL');
     print(
-      `Check callback URL on: https://developers.facebook.com/apps/${config.appId}/webhooks/`
+      `Check callback URL on: https://developers.facebook.com/apps/${appId}/webhooks/`
     );
     print(
-      `Check selected events on: https://developers.facebook.com/apps/${config.appId}/messenger/`
+      `Check selected events on: https://developers.facebook.com/apps/${appId}/messenger/`
     );
-    return;
   } catch (err) {
     error('Failed to set Messenger webhook');
+
     if (err.response) {
       error(`status: ${bold(err.response.status)}`);
       if (err.response.data) {
@@ -166,6 +168,7 @@ export async function setWebhook(ctx: CliContext): Promise<void> {
     } else {
       warn(err.message);
     }
+
     return process.exit(1);
   }
 }
@@ -174,10 +177,12 @@ export default async function main(ctx: CliContext): Promise<void> {
   const subcommand = ctx.argv._[2];
 
   switch (subcommand) {
+    // TODO: implement get
     case 'set': {
       await setWebhook(ctx);
       break;
     }
+    // TODO: implement delete
     case 'help':
       help();
       break;
diff --git a/packages/bottender/src/cli/providers/telegram/webhook.ts b/packages/bottender/src/cli/providers/telegram/webhook.ts
index b8be5038..4b581d61 100644
--- a/packages/bottender/src/cli/providers/telegram/webhook.ts
+++ b/packages/bottender/src/cli/providers/telegram/webhook.ts
@@ -14,17 +14,20 @@ export async function getWebhook(_: CliContext): Promise<void> {
   try {
     const config = getConfig('telegram');
 
-    invariant(config.accessToken, 'accessToken is not found in config file');
+    const { accessToken } = config;
 
-    const accessToken = config.accessToken;
+    invariant(accessToken, 'accessToken is not found in config file');
+
+    const client = TelegramClient.connect({
+      accessToken,
+    });
 
-    const client = TelegramClient.connect(accessToken);
     const result = await client.getWebhookInfo();
 
     Object.entries(result).forEach(([key, value]) => print(`${key}: ${value}`));
-    return;
   } catch (err) {
     error('Failed to get Telegram webhook');
+
     if (err.response) {
       error(`status: ${bold(err.response.status)}`);
       if (err.response.data) {
@@ -33,6 +36,7 @@ export async function getWebhook(_: CliContext): Promise<void> {
     } else {
       error(err.message);
     }
+
     return process.exit(1);
   }
 }
@@ -44,17 +48,19 @@ export async function setWebhook(ctx: CliContext): Promise<void> {
     '--ngrok-port': String,
   });
 
-  const ngrokPort = argv['--ngrok-port'];
+  const ngrokPort = argv['--ngrok-port'] || '4040';
   let webhook = argv['--webhook'];
 
   try {
     const config = getConfig('telegram');
 
-    invariant(config.accessToken, 'accessToken is not found in config file');
+    const { accessToken, path = '/webhooks/telegram' } = config;
 
-    const accessToken = config.accessToken;
+    invariant(accessToken, 'accessToken is not found in config file');
 
-    const client = TelegramClient.connect(accessToken);
+    const client = TelegramClient.connect({
+      accessToken,
+    });
 
     if (!webhook) {
       warn('We can not find the webhook callback url you provided.');
@@ -63,7 +69,7 @@ export async function setWebhook(ctx: CliContext): Promise<void> {
       );
       const result = await prompt.run();
       if (result) {
-        webhook = await getWebhookFromNgrok(ngrokPort);
+        webhook = `${await getWebhookFromNgrok(ngrokPort)}${path}`;
       }
     }
 
@@ -75,9 +81,9 @@ export async function setWebhook(ctx: CliContext): Promise<void> {
     await client.setWebhook(webhook as string);
 
     print('Successfully set Telegram webhook callback URL');
-    return;
   } catch (err) {
     error('Failed to set Telegram webhook');
+
     if (err.response) {
       error(`status: ${bold(err.response.status)}`);
       if (err.response.data) {
@@ -86,6 +92,7 @@ export async function setWebhook(ctx: CliContext): Promise<void> {
     } else {
       error(err.message);
     }
+
     return process.exit(1);
   }
 }
@@ -94,18 +101,20 @@ export async function deleteWebhook(_: CliContext): Promise<void> {
   try {
     const config = getConfig('telegram');
 
-    invariant(config.accessToken, 'accessToken is not found in config file');
+    const { accessToken } = config;
 
-    const accessToken = config.accessToken;
+    invariant(accessToken, 'accessToken is not found in config file');
 
-    const client = TelegramClient.connect(accessToken);
+    const client = TelegramClient.connect({
+      accessToken,
+    });
 
     await client.deleteWebhook();
 
     print('Successfully delete Telegram webhook');
-    return;
   } catch (err) {
     error('Failed to delete Telegram webhook');
+
     if (err.response) {
       error(`status: ${bold(err.response.status)}`);
       if (err.response.data) {
@@ -114,6 +123,7 @@ export async function deleteWebhook(_: CliContext): Promise<void> {
     } else {
       error(err.message);
     }
+
     return process.exit(1);
   }
 }
@@ -125,10 +135,9 @@ export default async function main(ctx: CliContext): Promise<void> {
     case 'get':
       await getWebhook(ctx);
       break;
-    case 'set': {
+    case 'set':
       await setWebhook(ctx);
       break;
-    }
     case 'delete':
     case 'del':
       await deleteWebhook(ctx);
diff --git a/packages/bottender/src/cli/providers/viber/webhook.ts b/packages/bottender/src/cli/providers/viber/webhook.ts
index f3d8eccf..a407a19d 100644
--- a/packages/bottender/src/cli/providers/viber/webhook.ts
+++ b/packages/bottender/src/cli/providers/viber/webhook.ts
@@ -14,6 +14,7 @@ type ViberConfig = {
   accessToken: string;
   sender: ViberSender;
   eventTypes: ViberEventType[];
+  path?: string;
 };
 
 export async function setWebhook(ctx: CliContext): Promise<void> {
@@ -24,23 +25,35 @@ export async function setWebhook(ctx: CliContext): Promise<void> {
   });
 
   let webhook = argv['--webhook'];
-  const ngrokPort = argv['--ngrok-port'];
+  const ngrokPort = argv['--ngrok-port'] || '4040';
 
   try {
     const config: ViberConfig = getConfig('viber');
 
-    invariant(config.accessToken, 'accessToken is not found in config file');
+    const { accessToken, sender, path = '/webhooks/viber' } = config;
 
-    const client = ViberClient.connect(config.accessToken, config.sender);
+    invariant(accessToken, 'accessToken is not found in config file');
+    invariant(sender, 'sender is not found in config file');
+
+    const client = ViberClient.connect(
+      {
+        accessToken,
+        sender,
+      },
+      sender
+    );
 
     if (!webhook) {
       warn('We can not find the webhook callback url you provided.');
+
       const prompt = new Confirm(
         `Are you using ngrok (get url from ngrok server on http://127.0.0.1:${ngrokPort})?`
       );
+
       const result = await prompt.run();
+
       if (result) {
-        webhook = await getWebhookFromNgrok(ngrokPort);
+        webhook = `${await getWebhookFromNgrok(ngrokPort)}${path}`;
       }
     }
 
@@ -54,6 +67,7 @@ export async function setWebhook(ctx: CliContext): Promise<void> {
     print('Successfully set Viber webhook callback URL');
   } catch (err) {
     error('Failed to set Viber webhook');
+
     if (err.response) {
       error(`status: ${bold(err.response.status)}`);
       if (err.response.data) {
@@ -62,6 +76,7 @@ export async function setWebhook(ctx: CliContext): Promise<void> {
     } else {
       error(err.message);
     }
+
     return process.exit(1);
   }
 }
@@ -70,14 +85,22 @@ export async function deleteWebhook(_: CliContext): Promise<void> {
   try {
     const config: ViberConfig = getConfig('viber');
 
-    invariant(config.accessToken, 'accessToken is not found in config file');
+    const { accessToken, sender } = config;
+
+    invariant(accessToken, 'accessToken is not found in config file');
+    invariant(sender, 'sender is not found in config file');
 
-    const client = ViberClient.connect(config.accessToken, config.sender);
+    const client = ViberClient.connect(
+      {
+        accessToken,
+        sender,
+      },
+      sender
+    );
 
     await client.removeWebhook();
 
     print('Successfully delete Viber webhook');
-    return;
   } catch (err) {
     error('Failed to delete Viber webhook');
     if (err.response) {
@@ -88,6 +111,7 @@ export async function deleteWebhook(_: CliContext): Promise<void> {
     } else {
       error(err.message);
     }
+
     return process.exit(1);
   }
 }
@@ -96,10 +120,9 @@ export default async function main(ctx: CliContext): Promise<void> {
   const subcommand = ctx.argv._[2];
 
   switch (subcommand) {
-    case 'set': {
+    case 'set':
       await setWebhook(ctx);
       break;
-    }
     case 'delete':
     case 'del':
       await deleteWebhook(ctx);
diff --git a/packages/bottender/src/cli/shared/__fixtures__/tooManyMessengerFirstLayer.ts b/packages/bottender/src/cli/shared/__fixtures__/tooManyMessengerFirstLayer.ts
index fad591fb..24773575 100644
--- a/packages/bottender/src/cli/shared/__fixtures__/tooManyMessengerFirstLayer.ts
+++ b/packages/bottender/src/cli/shared/__fixtures__/tooManyMessengerFirstLayer.ts
@@ -14,38 +14,6 @@ export default {
             locale: 'default',
             composer_input_disabled: true,
             call_to_actions: [
-              {
-                title: 'My Account',
-                type: 'nested',
-                call_to_actions: [
-                  {
-                    title: 'My Account',
-                    type: 'nested',
-                    call_to_actions: [
-                      {
-                        title: 'Contact Info',
-                        type: 'postback',
-                        payload: 'CONTACT_INFO_PAYLOAD',
-                      },
-                    ],
-                  },
-                  {
-                    title: 'Pay Bill',
-                    type: 'postback',
-                    payload: 'PAYBILL_PAYLOAD',
-                  },
-                  {
-                    title: 'History',
-                    type: 'postback',
-                    payload: 'HISTORY_PAYLOAD',
-                  },
-                  {
-                    title: 'Contact Info',
-                    type: 'postback',
-                    payload: 'CONTACT_INFO_PAYLOAD',
-                  },
-                ],
-              },
               {
                 type: 'web_url',
                 title: 'Latest News',
@@ -59,10 +27,14 @@ export default {
                 webview_height_ratio: 'full',
               },
               {
-                type: 'web_url',
-                title: 'Latest News',
-                url: 'http://petershats.parseapp.com/hat-news',
-                webview_height_ratio: 'full',
+                title: 'History',
+                type: 'postback',
+                payload: 'HISTORY_PAYLOAD',
+              },
+              {
+                title: 'Contact Info',
+                type: 'postback',
+                payload: 'CONTACT_INFO_PAYLOAD',
               },
             ],
           },
diff --git a/packages/bottender/src/cli/shared/schema.ts b/packages/bottender/src/cli/shared/schema.ts
index c1a5d112..61724794 100644
--- a/packages/bottender/src/cli/shared/schema.ts
+++ b/packages/bottender/src/cli/shared/schema.ts
@@ -1,7 +1,20 @@
 import Joi from '@hapi/joi';
 
 const menuItemSchema = Joi.object().keys({
-  // TODO:
+  type: Joi.string(),
+  title: Joi.string(),
+  url: Joi.string().when('type', {
+    is: 'web_url',
+    then: Joi.string().required(),
+  }),
+  payload: Joi.string().when('type', {
+    is: 'postback',
+    then: Joi.string().required(),
+  }),
+  webview_height_ratio: Joi.string(),
+  messenger_extensions: Joi.boolean(),
+  fallback_url: Joi.string(),
+  webview_share_button: Joi.string(),
 });
 
 const schema = Joi.object().keys({


Test patch:
diff --git a/packages/bottender/src/cli/providers/messenger/__tests__/createPersona.spec.ts b/packages/bottender/src/cli/providers/messenger/__tests__/createPersona.spec.ts
index 26cdf8d3..0dff6561 100644
--- a/packages/bottender/src/cli/providers/messenger/__tests__/createPersona.spec.ts
+++ b/packages/bottender/src/cli/providers/messenger/__tests__/createPersona.spec.ts
@@ -48,7 +48,9 @@ describe('resolved', () => {
 
     await createPersona(ctx);
 
-    expect(MessengerClient.connect).toBeCalledWith('__FAKE_TOKEN__');
+    expect(MessengerClient.connect).toBeCalledWith({
+      accessToken: '__FAKE_TOKEN__',
+    });
     expect(_client.createPersona).toBeCalledWith({
       name: 'kpman',
       profile_picture_url: 'https://i.imgur.com/zV6uy4T.jpg',
diff --git a/packages/bottender/src/cli/providers/messenger/__tests__/deletePersona.spec.ts b/packages/bottender/src/cli/providers/messenger/__tests__/deletePersona.spec.ts
index 9aa41a00..366034ba 100644
--- a/packages/bottender/src/cli/providers/messenger/__tests__/deletePersona.spec.ts
+++ b/packages/bottender/src/cli/providers/messenger/__tests__/deletePersona.spec.ts
@@ -45,7 +45,9 @@ describe('resolved', () => {
 
     await deletePersona(ctx);
 
-    expect(MessengerClient.connect).toBeCalledWith('__FAKE_TOKEN__');
+    expect(MessengerClient.connect).toBeCalledWith({
+      accessToken: '__FAKE_TOKEN__',
+    });
     expect(_client.deletePersona).toBeCalledWith('54321');
   });
 
diff --git a/packages/bottender/src/cli/providers/messenger/__tests__/getPersona.spec.ts b/packages/bottender/src/cli/providers/messenger/__tests__/getPersona.spec.ts
index a8f77d52..49bdc2c0 100644
--- a/packages/bottender/src/cli/providers/messenger/__tests__/getPersona.spec.ts
+++ b/packages/bottender/src/cli/providers/messenger/__tests__/getPersona.spec.ts
@@ -45,7 +45,9 @@ describe('resolved', () => {
 
     await getPersona(ctx);
 
-    expect(MessengerClient.connect).toBeCalledWith('__FAKE_TOKEN__');
+    expect(MessengerClient.connect).toBeCalledWith({
+      accessToken: '__FAKE_TOKEN__',
+    });
     expect(_client.getPersona).toBeCalledWith('54321');
   });
 
diff --git a/packages/bottender/src/cli/providers/messenger/__tests__/listPersona.spec.ts b/packages/bottender/src/cli/providers/messenger/__tests__/listPersona.spec.ts
index af3c0f8e..2323663c 100644
--- a/packages/bottender/src/cli/providers/messenger/__tests__/listPersona.spec.ts
+++ b/packages/bottender/src/cli/providers/messenger/__tests__/listPersona.spec.ts
@@ -46,7 +46,9 @@ describe('resolved', () => {
 
     await listPersona(ctx);
 
-    expect(MessengerClient.connect).toBeCalledWith('__FAKE_TOKEN__');
+    expect(MessengerClient.connect).toBeCalledWith({
+      accessToken: '__FAKE_TOKEN__',
+    });
     expect(_client.getAllPersonas).toBeCalled();
   });
 
diff --git a/packages/bottender/src/cli/providers/messenger/__tests__/setWebhook.spec.ts b/packages/bottender/src/cli/providers/messenger/__tests__/setWebhook.spec.ts
index de79ddd2..8198ad9e 100644
--- a/packages/bottender/src/cli/providers/messenger/__tests__/setWebhook.spec.ts
+++ b/packages/bottender/src/cli/providers/messenger/__tests__/setWebhook.spec.ts
@@ -116,10 +116,10 @@ describe('resolve', () => {
 
     await setWebhook(ctx);
 
-    expect(getWebhookFromNgrok).toBeCalledWith(undefined);
+    expect(getWebhookFromNgrok).toBeCalledWith('4040');
     expect(client.createSubscription).toBeCalledWith({
       access_token: '__APP_ID__|__APP_SECRET__',
-      callback_url: 'https://fakeDomain.ngrok.io',
+      callback_url: 'https://fakeDomain.ngrok.io/webhooks/messenger',
       fields: [
         'messages',
         'messaging_postbacks',
diff --git a/packages/bottender/src/cli/providers/telegram/__tests__/setWebhook.spec.ts b/packages/bottender/src/cli/providers/telegram/__tests__/setWebhook.spec.ts
index 463edd8d..3537d253 100644
--- a/packages/bottender/src/cli/providers/telegram/__tests__/setWebhook.spec.ts
+++ b/packages/bottender/src/cli/providers/telegram/__tests__/setWebhook.spec.ts
@@ -69,7 +69,7 @@ describe('resolve', () => {
 
     await setWebhook(ctx);
 
-    expect(getWebhookFromNgrok).toBeCalledWith(undefined);
+    expect(getWebhookFromNgrok).toBeCalledWith('4040');
     expect(log.print).toHaveBeenCalledTimes(1);
     expect(log.print.mock.calls[0][0]).toMatch(/Successfully/);
   });
diff --git a/packages/bottender/src/cli/providers/viber/__tests__/deleteWebhook.spec.ts b/packages/bottender/src/cli/providers/viber/__tests__/deleteWebhook.spec.ts
index 674aec6a..a4ba2905 100644
--- a/packages/bottender/src/cli/providers/viber/__tests__/deleteWebhook.spec.ts
+++ b/packages/bottender/src/cli/providers/viber/__tests__/deleteWebhook.spec.ts
@@ -1,5 +1,6 @@
 import { ViberClient } from 'messaging-api-viber';
 
+import getConfig from '../../../shared/getConfig';
 import { deleteWebhook } from '../webhook';
 import * as log from '../../../shared/log';
 
@@ -7,19 +8,19 @@ jest.mock('messaging-api-viber');
 jest.mock('../../../shared/log');
 jest.mock('../../../shared/getConfig');
 
-const getConfig = require('../../../shared/getConfig').default;
+const ACCESS_TOKEN = '__ACCESS_TOKEN__';
 
-const MOCK_FILE_WITH_PLATFORM = {
-  channels: {
-    viber: {
-      accessToken: '__accessToken__',
-    },
-  },
-};
+function setup({ config }: { config?: Record<string, any> } = {}) {
+  getConfig.mockReturnValue(
+    config || {
+      accessToken: ACCESS_TOKEN,
+      sender: {
+        name: 'sender',
+      },
+    }
+  );
 
-beforeEach(() => {
   process.exit = jest.fn();
-  getConfig.mockReturnValue(MOCK_FILE_WITH_PLATFORM.channels.viber);
 
   ViberClient.connect.mockReturnValue({
     removeWebhook: jest.fn(() => ({
@@ -27,7 +28,7 @@ beforeEach(() => {
       status_message: 'ok',
     })),
   });
-});
+}
 
 it('be defined', () => {
   expect(deleteWebhook).toBeDefined();
@@ -35,19 +36,22 @@ it('be defined', () => {
 
 describe('resolve', () => {
   it('successfully delete webhook', async () => {
+    setup();
+
     const ctx = {
       argv: {},
     };
 
     await deleteWebhook(ctx);
 
-    expect(log.print).toHaveBeenCalledTimes(1);
-    expect(log.print.mock.calls[0][0]).toMatch(/Successfully/);
+    expect(log.print).toBeCalled();
   });
 });
 
 describe('reject', () => {
   it('reject when Viber return not success', () => {
+    setup();
+
     const ctx = {
       argv: {},
     };
@@ -60,12 +64,14 @@ describe('reject', () => {
   });
 
   it('reject when `accessToken` is not found in config file', () => {
+    setup();
+
+    getConfig.mockReturnValueOnce(null);
+
     const ctx = {
       argv: {},
     };
 
-    getConfig.mockReturnValueOnce(null);
-
     expect(deleteWebhook(ctx).then).toThrow();
   });
 });
diff --git a/packages/bottender/src/cli/providers/viber/__tests__/setWebhook.spec.ts b/packages/bottender/src/cli/providers/viber/__tests__/setWebhook.spec.ts
index 40310ee5..e2380e13 100644
--- a/packages/bottender/src/cli/providers/viber/__tests__/setWebhook.spec.ts
+++ b/packages/bottender/src/cli/providers/viber/__tests__/setWebhook.spec.ts
@@ -71,6 +71,9 @@ describe('resolve', () => {
     const { client } = setup({
       config: {
         accessToken: ACCESS_TOKEN,
+        sender: {
+          name: 'sender',
+        },
         eventTypes: ['delivered', 'seen'],
       },
     });
@@ -102,9 +105,9 @@ describe('resolve', () => {
 
     await setWebhook(ctx);
 
-    expect(getWebhookFromNgrok).toBeCalledWith(undefined);
+    expect(getWebhookFromNgrok).toBeCalledWith('4040');
     expect(client.setWebhook).toBeCalledWith(
-      'https://fakeDomain.ngrok.io',
+      'https://fakeDomain.ngrok.io/webhooks/viber',
       undefined
     );
     expect(log.print).toBeCalled();