Repository: line/line-bot-sdk-python
Pull request #165 (2019-05-21 01:26:29)

remove sender_id in test_webhook.py

be-hase: @clsung 
Ah. There is a conflict.

Patch:
diff --git a/README.rst b/README.rst
index 2d8772c..e9f1d0d 100644
--- a/README.rst
+++ b/README.rst
@@ -320,6 +320,17 @@ https://developers.line.me/en/docs/messaging-api/reference/#link-rich-menu-to-us
 
     line_bot_api.link_rich_menu_to_user(user_id, rich_menu_id)
 
+link\_rich\_menu\_to\_users(self, user\_ids, rich\_menu\_id, timeout=None)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Links a rich menu to multiple users.
+
+https://developers.line.biz/en/reference/messaging-api/#link-rich-menu-to-users
+
+.. code:: python
+
+    line_bot_api.link_rich_menu_to_users(<user_ids>, <rich_menu_id>)
+
 unlink\_rich\_menu\_from\_user(self, user\_id, timeout=None)
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 
@@ -331,6 +342,17 @@ https://developers.line.me/en/docs/messaging-api/reference/#unlink-rich-menu-fro
 
     line_bot_api.unlink_rich_menu_from_user(user_id)
 
+unlink\_rich\_menu\_from\_users(self, user\_ids, timeout=None)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Unlinks rich menus from multiple users.
+
+https://developers.line.biz/en/reference/messaging-api/#unlink-rich-menu-from-users
+
+.. code:: python
+
+    line_bot_api.unlink_rich_menu_from_users(<user_ids>)
+
 get\_rich\_menu\_image(self, rich\_menu\_id, timeout=None)
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 
@@ -370,6 +392,74 @@ https://developers.line.me/en/docs/messaging-api/reference/#get-rich-menu-list
     for rich_menu in rich_menu_list:
         print(rich_menu.rich_menu_id)
 
+set\_default\_rich\_menu(self, rich\_menu\_id, timeout=None)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Sets the default rich menu.
+
+https://developers.line.biz/en/reference/messaging-api/#set-default-rich-menu
+
+.. code:: python
+
+    line_bot_api.set_default_rich_menu(<rich_menu_id>)
+
+get\_default\_rich\_menu(self, timeout=None)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Gets the ID of the default rich menu set with the Messaging API.
+
+https://developers.line.biz/en/reference/messaging-api/#get-default-rich-menu-id
+
+.. code:: python
+
+    line_bot_api.get_default_rich_menu()
+
+cancel\_default\_rich\_menu(self, timeout=None)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Cancels the default rich menu set with the Messaging API.
+
+https://developers.line.biz/en/reference/messaging-api/#cancel-default-rich-menu
+
+.. code:: python
+
+    line_bot_api.cancel_default_rich_menu()
+
+issue\_link\_token(self, user\_id, timeout=None)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Issues a link token used for the account link feature.
+
+https://developers.line.biz/en/reference/messaging-api/#issue-link-token
+
+.. code:: python
+
+    link_token_response = line_bot_api.issue_link_token(<user_id>)
+    print(link_token_response)
+
+issue\_channel\_token(self, client_id, client_secret, grant_type='client_credentials', timeout=None)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Issues a short-lived channel access token.
+
+https://developers.line.biz/en/reference/messaging-api/#issue-channel-access-token
+
+.. code:: python
+
+    channel_token_response = line_bot_api.issue_channel_token(<client_id>, <client_secret>)
+    print(access_token_response)
+
+revoke\_channel\_token(self, access_token, timeout=None)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Revokes a channel access token.
+
+https://developers.line.biz/en/reference/messaging-api/#revoke-channel-access-token
+
+.. code:: python
+
+    line_bot_api.revoke_channel_token(<access_token>)
+
 ※ Error handling
 ^^^^^^^^^^^^^^^^
 
@@ -502,7 +592,7 @@ TemplateSendMessage - ButtonsTemplate
             actions=[
                 PostbackAction(
                     label='postback',
-                    text='postback text',
+                    display_text='postback text',
                     data='action=buy&itemid=1'
                 ),
                 MessageAction(
@@ -529,7 +619,7 @@ TemplateSendMessage - ConfirmTemplate
             actions=[
                 PostbackAction(
                     label='postback',
-                    text='postback text',
+                    display_text='postback text',
                     data='action=buy&itemid=1'
                 ),
                 MessageAction(
@@ -556,7 +646,7 @@ TemplateSendMessage - CarouselTemplate
                     actions=[
                         PostbackAction(
                             label='postback1',
-                            text='postback text1',
+                            display_text='postback text1',
                             data='action=buy&itemid=1'
                         ),
                         MessageAction(
@@ -576,7 +666,7 @@ TemplateSendMessage - CarouselTemplate
                     actions=[
                         PostbackAction(
                             label='postback2',
-                            text='postback text2',
+                            display_text='postback text2',
                             data='action=buy&itemid=2'
                         ),
                         MessageAction(
@@ -606,7 +696,7 @@ TemplateSendMessage - ImageCarouselTemplate
                     image_url='https://example.com/item1.jpg',
                     action=PostbackAction(
                         label='postback1',
-                        text='postback text1',
+                        display_text='postback text1',
                         data='action=buy&itemid=1'
                     )
                 ),
@@ -614,7 +704,7 @@ TemplateSendMessage - ImageCarouselTemplate
                     image_url='https://example.com/item2.jpg',
                     action=PostbackAction(
                         label='postback2',
-                        text='postback text2',
+                        display_text='postback text2',
                         data='action=buy&itemid=2'
                     )
                 )
@@ -867,7 +957,7 @@ See http://semver.org/
 
 Contributing
 ------------
-Please check `CONTRIBUTING <contributing.md>`__ before making a contribution.
+Please check `CONTRIBUTING <CONTRIBUTING.md>`__ before making a contribution.
 
 For SDK developers
 ------------------
@@ -914,7 +1004,6 @@ And more... TBD
    :target: https://badge.fury.io/py/line-bot-sdk
 .. |Documentation Status| image:: https://readthedocs.org/projects/line-bot-sdk-python/badge/?version=latest
    :target: http://line-bot-sdk-python.readthedocs.io/en/latest/?badge=latest
-   
 
 License
 --------
diff --git a/examples/flask-kitchensink/app.py b/examples/flask-kitchensink/app.py
index bce1099..48395d5 100644
--- a/examples/flask-kitchensink/app.py
+++ b/examples/flask-kitchensink/app.py
@@ -109,7 +109,7 @@ def handle_text_message(event):
             line_bot_api.reply_message(
                 event.reply_token, [
                     TextSendMessage(text='Display name: ' + profile.display_name),
-                    TextSendMessage(text='Status message: ' + profile.status_message)
+                    TextSendMessage(text='Status message: ' + str(profile.status_message))
                 ]
             )
         else:
@@ -362,6 +362,13 @@ def handle_text_message(event):
                             action=LocationAction(label="label6")
                         ),
                     ])))
+    elif text == 'link_token' and isinstance(event.source, SourceUser):
+        link_token_response = line_bot_api.issue_link_token(event.source.user_id)
+        line_bot_api.reply_message(
+            event.reply_token, [
+                TextSendMessage(text='link_token: ' + link_token_response.link_token)
+            ]
+        )
     else:
         line_bot_api.reply_message(
             event.reply_token, TextSendMessage(text=event.message.text))
diff --git a/linebot/__about__.py b/linebot/__about__.py
index d37134a..7854322 100644
--- a/linebot/__about__.py
+++ b/linebot/__about__.py
@@ -16,7 +16,7 @@
 
 from __future__ import unicode_literals
 
-__version__ = '1.9.0'
+__version__ = '1.11.0'
 __author__ = 'LINE Corporation'
 __copyright__ = 'Copyright 2016, LINE Corporation'
 __license__ = 'Apache 2.0'
diff --git a/linebot/api.py b/linebot/api.py
index ce2e11f..ea6569e 100644
--- a/linebot/api.py
+++ b/linebot/api.py
@@ -23,7 +23,8 @@ from .exceptions import LineBotApiError
 from .http_client import HttpClient, RequestsHttpClient
 from .models import (
     Error, Profile, MemberIds, Content, RichMenuResponse, MessageQuotaResponse,
-    MessageQuotaConsumptionResponse, MessageDeliveryBroadcastResponse
+    MessageQuotaConsumptionResponse, MessageDeliveryBroadcastResponse, IssueLinkTokenResponse,
+    IssueChannelTokenResponse,
 )
 
 
@@ -498,8 +499,12 @@ class LineBotApi(object):
 
         https://developers.line.me/en/docs/messaging-api/reference/#link-rich-menu-to-user
 
-        :param str user_id: ID of an uploaded rich menu
-        :param str rich_menu_id: ID of the user
+        :param str user_id: ID of the user
+        :param str rich_menu_id: ID of an uploaded rich menu
+        :param timeout: (optional) How long to wait for the server
+            to send data before giving up, as a float,
+            or a (connect timeout, read timeout) float tuple.
+            Default is self.http_client.timeout
         :type timeout: float | tuple(float, float)
         """
         self._post(
@@ -510,6 +515,30 @@ class LineBotApi(object):
             timeout=timeout
         )
 
+    def link_rich_menu_to_users(self, user_ids, rich_menu_id, timeout=None):
+        """Links a rich menu to multiple users.
+
+        https://developers.line.biz/en/reference/messaging-api/#link-rich-menu-to-users
+
+        :param user_ids: user IDs
+            Max: 150 users
+        :type user_ids: list[str]
+        :param str rich_menu_id: ID of an uploaded rich menu
+        :param timeout: (optional) How long to wait for the server
+            to send data before giving up, as a float,
+            or a (connect timeout, read timeout) float tuple.
+            Default is self.http_client.timeout
+        :type timeout: float | tuple(float, float)
+        """
+        self._post(
+            '/v2/bot/richmenu/bulk/link',
+            data=json.dumps({
+                'userIds': user_ids,
+                'richMenuId': rich_menu_id,
+            }),
+            timeout=timeout
+        )
+
     def unlink_rich_menu_from_user(self, user_id, timeout=None):
         """Call unlink rich menu from user API.
 
@@ -527,6 +556,28 @@ class LineBotApi(object):
             timeout=timeout
         )
 
+    def unlink_rich_menu_from_users(self, user_ids, timeout=None):
+        """Unlinks rich menus from multiple users.
+
+        https://developers.line.biz/en/reference/messaging-api/#unlink-rich-menu-from-users
+
+        :param user_ids: user IDs
+            Max: 150 users
+        :type user_ids: list[str]
+        :param timeout: (optional) How long to wait for the server
+            to send data before giving up, as a float,
+            or a (connect timeout, read timeout) float tuple.
+            Default is self.http_client.timeout
+        :type timeout: float | tuple(float, float)
+        """
+        self._post(
+            '/v2/bot/richmenu/bulk/unlink',
+            data=json.dumps({
+                'userIds': user_ids,
+            }),
+            timeout=timeout
+        )
+
     def get_rich_menu_image(self, rich_menu_id, timeout=None):
         """Call download rich menu image API.
 
@@ -595,6 +646,59 @@ class LineBotApi(object):
 
         return result
 
+    def set_default_rich_menu(self, rich_menu_id, timeout=None):
+        """Set the default rich menu.
+
+        https://developers.line.biz/en/reference/messaging-api/#set-default-rich-menu
+
+        :param str rich_menu_id: ID of an uploaded rich menu
+        :param timeout: (optional) How long to wait for the server
+            to send data before giving up, as a float,
+            or a (connect timeout, read timeout) float tuple.
+            Default is self.http_client.timeout
+        :type timeout: float | tuple(float, float)
+        """
+        self._post(
+            '/v2/bot/user/all/richmenu/{rich_menu_id}'.format(
+                rich_menu_id=rich_menu_id,
+            ),
+            timeout=timeout
+        )
+
+    def get_default_rich_menu(self, timeout=None):
+        """Get the ID of the default rich menu set with the Messaging API.
+
+        https://developers.line.biz/en/reference/messaging-api/#get-default-rich-menu-id
+
+        :param timeout: (optional) How long to wait for the server
+            to send data before giving up, as a float,
+            or a (connect timeout, read timeout) float tuple.
+            Default is self.http_client.timeout
+        :type timeout: float | tuple(float, float)
+        """
+        response = self._get(
+            '/v2/bot/user/all/richmenu',
+            timeout=timeout
+        )
+
+        return response.json.get('richMenuId')
+
+    def cancel_default_rich_menu(self, timeout=None):
+        """Cancel the default rich menu set with the Messaging API.
+
+        https://developers.line.biz/en/reference/messaging-api/#cancel-default-rich-menu
+
+        :param timeout: (optional) How long to wait for the server
+            to send data before giving up, as a float,
+            or a (connect timeout, read timeout) float tuple.
+            Default is self.http_client.timeout
+        :type timeout: float | tuple(float, float)
+        """
+        self._delete(
+            '/v2/bot/user/all/richmenu',
+            timeout=timeout
+        )
+
     def get_message_quota(self, timeout=None):
         """Call Get the target limit for additional messages.
 
@@ -635,6 +739,78 @@ class LineBotApi(object):
 
         return MessageQuotaConsumptionResponse.new_from_json_dict(response.json)
 
+    def issue_link_token(self, user_id, timeout=None):
+        """Issues a link token used for the account link feature.
+
+        https://developers.line.biz/en/reference/messaging-api/#issue-link-token
+
+        :param str user_id: User ID for the LINE account to be linked
+        :param timeout: (optional) How long to wait for the server
+            to send data before giving up, as a float,
+            or a (connect timeout, read timeout) float tuple.
+            Default is self.http_client.timeout
+        :type timeout: float | tuple(float, float)
+        :rtype: :py:class:`linebot.models.responses.IssueLinkTokenResponse`
+        :return: IssueLinkTokenResponse instance
+        """
+        response = self._post(
+            '/v2/bot/user/{user_id}/linkToken'.format(
+                user_id=user_id
+            ),
+            timeout=timeout
+        )
+
+        return IssueLinkTokenResponse.new_from_json_dict(response.json)
+
+    def issue_channel_token(self, client_id, client_secret,
+                            grant_type='client_credentials', timeout=None):
+        """Issues a short-lived channel access token.
+
+        https://developers.line.biz/en/reference/messaging-api/#issue-channel-access-token
+
+        :param str client_id: Channel ID.
+        :param str client_secret: Channel secret.
+        :param str grant_type: `client_credentials`
+        :param timeout: (optional) How long to wait for the server
+            to send data before giving up, as a float,
+            or a (connect timeout, read timeout) float tuple.
+            Default is self.http_client.timeout
+        :type timeout: float | tuple(float, float)
+        :rtype: :py:class:`linebot.models.responses.IssueChannelTokenResponse`
+        :return: IssueChannelTokenResponse instance
+        """
+        response = self._post(
+            '/v2/oauth/accessToken',
+            data={
+                'client_id': client_id,
+                'client_secret': client_secret,
+                'grant_type': grant_type,
+            },
+            headers={'Content-Type': 'application/x-www-form-urlencoded'},
+            timeout=timeout
+        )
+
+        return IssueChannelTokenResponse.new_from_json_dict(response.json)
+
+    def revoke_channel_token(self, access_token, timeout=None):
+        """Revokes a channel access token.
+
+        https://developers.line.biz/en/reference/messaging-api/#revoke-channel-access-token
+
+        :param str access_token: Channel access token.
+        :param timeout: (optional) How long to wait for the server
+            to send data before giving up, as a float,
+            or a (connect timeout, read timeout) float tuple.
+            Default is self.http_client.timeout
+        :type timeout: float | tuple(float, float)
+        """
+        self._post(
+            '/v2/oauth/revoke',
+            data={'access_token': access_token},
+            headers={'Content-Type': 'application/x-www-form-urlencoded'},
+            timeout=timeout
+        )
+
     def _get(self, path, params=None, headers=None, stream=False, timeout=None):
         url = self.endpoint + path
 
diff --git a/linebot/models/__init__.py b/linebot/models/__init__.py
index e4f87c8..cb52705 100644
--- a/linebot/models/__init__.py
+++ b/linebot/models/__init__.py
@@ -99,7 +99,9 @@ from .responses import (  # noqa
     MessageQuotaResponse,
     MessageQuotaConsumptionResponse,
     MessageDeliveryBroadcastResponse,
-    Content as MessageContent,  # backward compatibility
+    Content as MessageContent,  # backward compatibility,
+    IssueLinkTokenResponse,
+    IssueChannelTokenResponse,
 )
 from .rich_menu import (  # noqa
     RichMenu,
diff --git a/linebot/models/responses.py b/linebot/models/responses.py
index ab09af4..d1415e4 100644
--- a/linebot/models/responses.py
+++ b/linebot/models/responses.py
@@ -207,3 +207,42 @@ class MessageDeliveryBroadcastResponse(Base):
 
         self.status = status
         self.success = success
+
+
+class IssueLinkTokenResponse(Base):
+    """IssueLinkTokenResponse.
+
+    https://developers.line.biz/en/reference/messaging-api/#issue-link-token
+    """
+
+    def __init__(self, link_token=None, **kwargs):
+        """__init__ method.
+
+        :param str link_token: Link token.
+        :param kwargs:
+        """
+        super(IssueLinkTokenResponse, self).__init__(**kwargs)
+
+        self.link_token = link_token
+
+
+class IssueChannelTokenResponse(Base):
+    """IssueAccessTokenResponse.
+
+    https://developers.line.biz/en/reference/messaging-api/#issue-channel-access-token
+    """
+
+    def __init__(self, access_token=None, expires_in=None, token_type=None, **kwargs):
+        """__init__ method.
+
+        :param str access_token: Short-lived channel access token.
+        :param int expires_in: Time until channel access token expires in seconds
+            from time the token is issued.
+        :param str token_type: Bearer.
+        :param kwargs:
+        """
+        super(IssueChannelTokenResponse, self).__init__(**kwargs)
+
+        self.access_token = access_token
+        self.expires_in = expires_in
+        self.token_type = token_type


Test patch:
diff --git a/tests/api/test_issue_channel_token.py b/tests/api/test_issue_channel_token.py
new file mode 100644
index 0000000..b0d5dbc
--- /dev/null
+++ b/tests/api/test_issue_channel_token.py
@@ -0,0 +1,76 @@
+# -*- coding: utf-8 -*-
+
+#  Licensed under the Apache License, Version 2.0 (the "License"); you may
+#  not use this file except in compliance with the License. You may obtain
+#  a copy of the License at
+#
+#       https://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#  WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#  License for the specific language governing permissions and limitations
+#  under the License.
+
+from __future__ import unicode_literals, absolute_import
+
+import sys
+import unittest
+
+import responses
+
+from linebot import (
+    LineBotApi
+)
+
+PY3 = sys.version_info[0] == 3
+if PY3:
+    from urllib import parse
+else:
+    import urlparse as parse
+
+
+class TestLineBotApi(unittest.TestCase):
+    def setUp(self):
+        self.tested = LineBotApi('channel_secret')
+        self.endpoint = LineBotApi.DEFAULT_API_ENDPOINT + '/v2/oauth/accessToken'
+        self.access_token = "W1TeHCgfH2Liwa....."
+        self.expires_in = 2592000
+        self.token_type = "Bearer"
+        self.client_id = 'client_id'
+        self.client_secret = 'client_secret'
+
+    @responses.activate
+    def test_issue_line_token(self):
+        responses.add(
+            responses.POST,
+            self.endpoint,
+            json={
+                "access_token": self.access_token,
+                "expires_in": self.expires_in,
+                "token_type": self.token_type
+            },
+            status=200
+        )
+
+        issue_access_token_response = self.tested.issue_channel_token(
+            self.client_id,
+            self.client_secret
+        )
+
+        request = responses.calls[0].request
+        self.assertEqual('POST', request.method)
+        self.assertEqual(self.endpoint, request.url)
+        self.assertEqual('application/x-www-form-urlencoded', request.headers['content-type'])
+        self.assertEqual(self.access_token, issue_access_token_response.access_token)
+        self.assertEqual(self.expires_in, issue_access_token_response.expires_in)
+        self.assertEqual(self.token_type, issue_access_token_response.token_type)
+
+        encoded_body = parse.parse_qs(request.body)
+        self.assertEqual('client_credentials', encoded_body['grant_type'][0])
+        self.assertEqual(self.client_id, encoded_body['client_id'][0])
+        self.assertEqual(self.client_secret, encoded_body['client_secret'][0])
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/tests/api/test_issue_link_token.py b/tests/api/test_issue_link_token.py
new file mode 100644
index 0000000..90517e0
--- /dev/null
+++ b/tests/api/test_issue_link_token.py
@@ -0,0 +1,52 @@
+# -*- coding: utf-8 -*-
+
+#  Licensed under the Apache License, Version 2.0 (the "License"); you may
+#  not use this file except in compliance with the License. You may obtain
+#  a copy of the License at
+#
+#       https://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#  WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#  License for the specific language governing permissions and limitations
+#  under the License.
+
+from __future__ import unicode_literals, absolute_import
+
+import unittest
+
+import responses
+
+from linebot import (
+    LineBotApi
+)
+
+
+class TestLineBotApi(unittest.TestCase):
+    def setUp(self):
+        self.tested = LineBotApi('channel_secret')
+
+    @responses.activate
+    def test_issue_line_token(self):
+        responses.add(
+            responses.POST,
+            LineBotApi.DEFAULT_API_ENDPOINT + '/v2/bot/user/user_id/linkToken',
+            json={
+                "linkToken": "xxxx",
+            },
+            status=200
+        )
+
+        issue_link_token_response = self.tested.issue_link_token('user_id')
+
+        request = responses.calls[0].request
+        self.assertEqual(request.method, 'POST')
+        self.assertEqual(
+            request.url,
+            LineBotApi.DEFAULT_API_ENDPOINT + '/v2/bot/user/user_id/linkToken')
+        self.assertEqual(issue_link_token_response.link_token, 'xxxx')
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/tests/api/test_revoke_channel_token.py b/tests/api/test_revoke_channel_token.py
new file mode 100644
index 0000000..64543ce
--- /dev/null
+++ b/tests/api/test_revoke_channel_token.py
@@ -0,0 +1,50 @@
+# -*- coding: utf-8 -*-
+
+#  Licensed under the Apache License, Version 2.0 (the "License"); you may
+#  not use this file except in compliance with the License. You may obtain
+#  a copy of the License at
+#
+#       https://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#  WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#  License for the specific language governing permissions and limitations
+#  under the License.
+
+from __future__ import unicode_literals, absolute_import
+
+import unittest
+
+import responses
+
+from linebot import (
+    LineBotApi
+)
+
+
+class TestLineBotApi(unittest.TestCase):
+    def setUp(self):
+        self.tested = LineBotApi('channel_secret')
+        self.endpoint = LineBotApi.DEFAULT_API_ENDPOINT + '/v2/oauth/revoke'
+        self.access_token = "W1TeHCgfH2Liwa....."
+
+    @responses.activate
+    def test_issue_line_token(self):
+        responses.add(
+            responses.POST,
+            self.endpoint,
+            status=200
+        )
+
+        self.tested.revoke_channel_token(self.access_token)
+
+        request = responses.calls[0].request
+        self.assertEqual('POST', request.method)
+        self.assertEqual(self.endpoint, request.url)
+        self.assertEqual('application/x-www-form-urlencoded', request.headers['content-type'])
+        self.assertEqual('access_token={}'.format(self.access_token), request.body)
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/tests/api/test_rich_menu.py b/tests/api/test_rich_menu.py
index 3654a24..383758f 100644
--- a/tests/api/test_rich_menu.py
+++ b/tests/api/test_rich_menu.py
@@ -14,6 +14,7 @@
 
 from __future__ import unicode_literals, absolute_import
 
+import json
 import unittest
 
 import responses
@@ -283,6 +284,110 @@ class TestLineBotApi(unittest.TestCase):
         self.assertEqual(rich_menus[0].areas[0].action.type, 'postback')
         self.assertEqual(rich_menus[0].areas[0].action.data, 'action=buy&itemid=123')
 
+    @responses.activate
+    def test_link_rich_menu_to_users(self):
+        responses.add(
+            responses.POST,
+            LineBotApi.DEFAULT_API_ENDPOINT +
+            '/v2/bot/richmenu/bulk/link',
+            json={}, status=202
+        )
+
+        self.tested.link_rich_menu_to_users(['user_id1', 'user_id2'], 'rich_menu_id')
+
+        request = responses.calls[0].request
+        self.assertEqual(request.method, 'POST')
+        self.assertEqual(
+            request.url,
+            LineBotApi.DEFAULT_API_ENDPOINT + '/v2/bot/richmenu/bulk/link'
+        )
+        self.assertEqual(
+            json.loads(request.body),
+            {
+                "richMenuId": "rich_menu_id",
+                "userIds": ["user_id1", "user_id2"],
+            }
+        )
+
+    @responses.activate
+    def test_unlink_rich_menu_to_users(self):
+        responses.add(
+            responses.POST,
+            LineBotApi.DEFAULT_API_ENDPOINT +
+            '/v2/bot/richmenu/bulk/unlink',
+            json={}, status=202
+        )
+
+        self.tested.unlink_rich_menu_from_users(['user_id1', 'user_id2'], 'rich_menu_id')
+
+        request = responses.calls[0].request
+        self.assertEqual(request.method, 'POST')
+        self.assertEqual(
+            request.url,
+            LineBotApi.DEFAULT_API_ENDPOINT + '/v2/bot/richmenu/bulk/unlink'
+        )
+        self.assertEqual(
+            json.loads(request.body),
+            {
+                "userIds": ["user_id1", "user_id2"],
+            }
+        )
+
+    @responses.activate
+    def test_set_default_rich_menu(self):
+        responses.add(
+            responses.POST,
+            LineBotApi.DEFAULT_API_ENDPOINT +
+            '/v2/bot/user/all/richmenu/rich_menu_id',
+            json={}, status=200
+        )
+
+        self.tested.set_default_rich_menu('rich_menu_id')
+
+        request = responses.calls[0].request
+        self.assertEqual(request.method, 'POST')
+        self.assertEqual(
+            request.url,
+            LineBotApi.DEFAULT_API_ENDPOINT + '/v2/bot/user/all/richmenu/rich_menu_id'
+        )
+
+    @responses.activate
+    def test_get_default_rich_menu(self):
+        responses.add(
+            responses.GET,
+            LineBotApi.DEFAULT_API_ENDPOINT +
+            '/v2/bot/user/all/richmenu',
+            json={"richMenuId": "richMenuId"}, status=200
+        )
+
+        result = self.tested.get_default_rich_menu()
+
+        request = responses.calls[0].request
+        self.assertEqual(request.method, 'GET')
+        self.assertEqual(
+            request.url,
+            LineBotApi.DEFAULT_API_ENDPOINT + '/v2/bot/user/all/richmenu'
+        )
+        self.assertEqual(result, "richMenuId")
+
+    @responses.activate
+    def test_cancel_default_rich_menu(self):
+        responses.add(
+            responses.DELETE,
+            LineBotApi.DEFAULT_API_ENDPOINT +
+            '/v2/bot/user/all/richmenu',
+            json={}, status=200
+        )
+
+        self.tested.cancel_default_rich_menu()
+
+        request = responses.calls[0].request
+        self.assertEqual(request.method, 'DELETE')
+        self.assertEqual(
+            request.url,
+            LineBotApi.DEFAULT_API_ENDPOINT + '/v2/bot/user/all/richmenu'
+        )
+
 
 if __name__ == '__main__':
     unittest.main()
diff --git a/tests/test_webhook.py b/tests/test_webhook.py
index a6e3b5f..203881d 100644
--- a/tests/test_webhook.py
+++ b/tests/test_webhook.py
@@ -68,7 +68,6 @@ class TestWebhookParser(unittest.TestCase):
         self.assertIsInstance(events[0].source, SourceUser)
         self.assertEqual(events[0].source.type, 'user')
         self.assertEqual(events[0].source.user_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
-        self.assertEqual(events[0].source.sender_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
         self.assertIsInstance(events[0].message, TextMessage)
         self.assertEqual(events[0].message.id, '325708')
         self.assertEqual(events[0].message.type, 'text')
@@ -83,7 +82,6 @@ class TestWebhookParser(unittest.TestCase):
         self.assertEqual(events[1].source.type, 'room')
         self.assertEqual(events[1].source.room_id, 'Ra8dbf4673c4c812cd491258042226c99')
         self.assertEqual(events[1].source.user_id, None)
-        self.assertEqual(events[1].source.sender_id, 'Ra8dbf4673c4c812cd491258042226c99')
         self.assertIsInstance(events[1].message, ImageMessage)
         self.assertEqual(events[1].message.id, '325708')
         self.assertEqual(events[1].message.type, 'image')
@@ -96,7 +94,6 @@ class TestWebhookParser(unittest.TestCase):
         self.assertIsInstance(events[2].source, SourceUser)
         self.assertEqual(events[2].source.type, 'user')
         self.assertEqual(events[2].source.user_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
-        self.assertEqual(events[2].source.sender_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
         self.assertIsInstance(events[2].message, VideoMessage)
         self.assertEqual(events[2].message.id, '325708')
         self.assertEqual(events[2].message.type, 'video')
@@ -109,7 +106,6 @@ class TestWebhookParser(unittest.TestCase):
         self.assertIsInstance(events[3].source, SourceUser)
         self.assertEqual(events[3].source.type, 'user')
         self.assertEqual(events[3].source.user_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
-        self.assertEqual(events[3].source.sender_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
         self.assertIsInstance(events[3].message, AudioMessage)
         self.assertEqual(events[3].message.id, '325708')
         self.assertEqual(events[3].message.type, 'audio')
@@ -122,7 +118,6 @@ class TestWebhookParser(unittest.TestCase):
         self.assertIsInstance(events[4].source, SourceUser)
         self.assertEqual(events[4].source.type, 'user')
         self.assertEqual(events[4].source.user_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
-        self.assertEqual(events[4].source.sender_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
         self.assertIsInstance(events[4].message, LocationMessage)
         self.assertEqual(events[4].message.id, '325708')
         self.assertEqual(events[4].message.type, 'location')
@@ -139,7 +134,6 @@ class TestWebhookParser(unittest.TestCase):
         self.assertIsInstance(events[5].source, SourceUser)
         self.assertEqual(events[5].source.type, 'user')
         self.assertEqual(events[5].source.user_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
-        self.assertEqual(events[5].source.sender_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
         self.assertIsInstance(events[5].message, StickerMessage)
         self.assertEqual(events[5].message.id, '325708')
         self.assertEqual(events[5].message.type, 'sticker')
@@ -154,7 +148,6 @@ class TestWebhookParser(unittest.TestCase):
         self.assertIsInstance(events[6].source, SourceUser)
         self.assertEqual(events[6].source.type, 'user')
         self.assertEqual(events[6].source.user_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
-        self.assertEqual(events[6].source.sender_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
 
         # UnfollowEvent, SourceUser
         self.assertIsInstance(events[7], UnfollowEvent)
@@ -163,7 +156,6 @@ class TestWebhookParser(unittest.TestCase):
         self.assertIsInstance(events[7].source, SourceUser)
         self.assertEqual(events[7].source.type, 'user')
         self.assertEqual(events[7].source.user_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
-        self.assertEqual(events[7].source.sender_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
 
         # JoinEvent, SourceGroup
         self.assertIsInstance(events[8], JoinEvent)
@@ -174,7 +166,6 @@ class TestWebhookParser(unittest.TestCase):
         self.assertEqual(events[8].source.type, 'group')
         self.assertEqual(events[8].source.group_id, 'Ca56f94637cc4347f90a25382909b24b9')
         self.assertEqual(events[8].source.user_id, None)
-        self.assertEqual(events[8].source.sender_id, 'Ca56f94637cc4347f90a25382909b24b9')
 
         # LeaveEvent, SourceGroup
         self.assertIsInstance(events[9], LeaveEvent)
@@ -184,7 +175,6 @@ class TestWebhookParser(unittest.TestCase):
         self.assertEqual(events[9].source.type, 'group')
         self.assertEqual(events[9].source.group_id, 'Ca56f94637cc4347f90a25382909b24b9')
         self.assertEqual(events[9].source.user_id, None)
-        self.assertEqual(events[9].source.sender_id, 'Ca56f94637cc4347f90a25382909b24b9')
 
         # PostbackEvent, SourceUser
         self.assertIsInstance(events[10], PostbackEvent)
@@ -194,7 +184,6 @@ class TestWebhookParser(unittest.TestCase):
         self.assertIsInstance(events[10].source, SourceUser)
         self.assertEqual(events[10].source.type, 'user')
         self.assertEqual(events[10].source.user_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
-        self.assertEqual(events[10].source.sender_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
         self.assertEqual(events[10].postback.data, 'action=buyItem&itemId=123123&color=red')
         self.assertEqual(events[10].postback.params, None)
 
@@ -206,7 +195,6 @@ class TestWebhookParser(unittest.TestCase):
         self.assertIsInstance(events[11].source, SourceUser)
         self.assertEqual(events[11].source.type, 'user')
         self.assertEqual(events[11].source.user_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
-        self.assertEqual(events[11].source.sender_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
         self.assertEqual(events[11].beacon.hwid, 'd41d8cd98f')
         self.assertEqual(events[11].beacon.type, 'enter')
         self.assertEqual(events[11].beacon.dm, None)
@@ -220,7 +208,6 @@ class TestWebhookParser(unittest.TestCase):
         self.assertIsInstance(events[12].source, SourceUser)
         self.assertEqual(events[12].source.type, 'user')
         self.assertEqual(events[12].source.user_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
-        self.assertEqual(events[12].source.sender_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
         self.assertEqual(events[12].beacon.hwid, 'd41d8cd98f')
         self.assertEqual(events[12].beacon.type, 'enter')
         self.assertEqual(events[12].beacon.dm, '1234567890abcdef')
@@ -234,7 +221,6 @@ class TestWebhookParser(unittest.TestCase):
         self.assertIsInstance(events[13].source, SourceUser)
         self.assertEqual(events[13].source.type, 'user')
         self.assertEqual(events[13].source.user_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
-        self.assertEqual(events[13].source.sender_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
         self.assertEqual(events[13].link.result, 'ok')
         self.assertEqual(events[13].link.nonce, 'Vb771wDYtXuammLszK6h9A')
 
@@ -247,7 +233,6 @@ class TestWebhookParser(unittest.TestCase):
         self.assertEqual(events[14].source.type, 'group')
         self.assertEqual(events[14].source.group_id, 'Ca56f94637cc4347f90a25382909b24b9')
         self.assertEqual(events[14].source.user_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
-        self.assertEqual(events[14].source.sender_id, 'Ca56f94637cc4347f90a25382909b24b9')
         self.assertIsInstance(events[14].message, TextMessage)
         self.assertEqual(events[14].message.id, '325708')
         self.assertEqual(events[14].message.type, 'text')
@@ -262,7 +247,6 @@ class TestWebhookParser(unittest.TestCase):
         self.assertEqual(events[15].source.type, 'room')
         self.assertEqual(events[15].source.room_id, 'Ra8dbf4673c4c812cd491258042226c99')
         self.assertEqual(events[15].source.user_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
-        self.assertEqual(events[15].source.sender_id, 'Ra8dbf4673c4c812cd491258042226c99')
         self.assertIsInstance(events[15].message, TextMessage)
         self.assertEqual(events[15].message.id, '325708')
         self.assertEqual(events[15].message.type, 'text')
@@ -276,7 +260,6 @@ class TestWebhookParser(unittest.TestCase):
         self.assertIsInstance(events[16].source, SourceUser)
         self.assertEqual(events[16].source.type, 'user')
         self.assertEqual(events[16].source.user_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
-        self.assertEqual(events[16].source.sender_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
         self.assertEqual(events[16].postback.data, 'action=buyItem&itemId=123123&color=red')
         self.assertEqual(events[16].postback.params['date'], '2013-04-01')
 
@@ -288,7 +271,6 @@ class TestWebhookParser(unittest.TestCase):
         self.assertIsInstance(events[17].source, SourceUser)
         self.assertEqual(events[17].source.type, 'user')
         self.assertEqual(events[17].source.user_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
-        self.assertEqual(events[17].source.sender_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
         self.assertEqual(events[17].postback.data, 'action=buyItem&itemId=123123&color=red')
         self.assertEqual(events[17].postback.params['time'], '10:00')
 
@@ -300,7 +282,6 @@ class TestWebhookParser(unittest.TestCase):
         self.assertIsInstance(events[18].source, SourceUser)
         self.assertEqual(events[18].source.type, 'user')
         self.assertEqual(events[18].source.user_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
-        self.assertEqual(events[18].source.sender_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
         self.assertEqual(events[18].postback.data, 'action=buyItem&itemId=123123&color=red')
         self.assertEqual(events[18].postback.params['datetime'], '2013-04-01T10:00')