From c7ab24d2edd4eba3ba1f5f95cb1e65e7b43ae39e Mon Sep 17 00:00:00 2001 From: "Yuyang Zhang(helloqiu)" Date: Wed, 9 May 2018 11:39:28 +0800 Subject: [PATCH] fix(client): urlencode filename before uploading file, fix #292 (#294) * fix(client): urlencode file name #292 * refactor(client): move encoding filename to post method * test(client): split client.post test into unit test and integration test * test(windows): set PYTHONIOENCODING to utf-8 * fix(client): check name attribute before urlencoding filename * test(client): create a fake picture file for related tests * style(client): correct coding style * fix(client): urlencode filenames for all files in "files" before upload * refactor(client): refactor urlencode to reduce block nest and make it more pythonic * style(client): fix client style problem --- appveyor.yml | 3 +- tests/test_client.py | 205 +++++++++++++++++++++++++++++++------------ tox-requirements.txt | 2 + werobot/client.py | 17 ++++ 4 files changed, 171 insertions(+), 56 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 957bdeb3..1265efeb 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -15,6 +15,7 @@ install: - redis-server --service-start - set DATABASE_MYSQL_USERNAME=root - set DATABASE_MYSQL_PASSWORD=Password12! + - set PYTHONIOENCODING=utf-8 before_test: - ps: $env:MYSQL_PWD="Password12!" - ps: $cmd = '"C:\Program Files\MySQL\MySQL Server 5.7\bin\mysql" -e "create database werobot;" --user=root' @@ -31,4 +32,4 @@ on_failure: notifications: - provider: Slack incoming_webhook: - secure: OUWL/ky2abFxup5CTKyGrmQRdqAE/TU5To/6bxxvS6GRaV6/JdyKyfxZHwnB1HOIg805obLeC4ngnSNEUd87to9JLSerBUb5b3VjYieuJVY= \ No newline at end of file + secure: OUWL/ky2abFxup5CTKyGrmQRdqAE/TU5To/6bxxvS6GRaV6/JdyKyfxZHwnB1HOIg805obLeC4ngnSNEUd87to9JLSerBUb5b3VjYieuJVY= diff --git a/tests/test_client.py b/tests/test_client.py index 8ac7c0b7..a72ded52 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -4,6 +4,9 @@ import json import pytest import requests +import multipart +from six.moves import urllib +from six import BytesIO from werobot import WeRoBot from werobot.config import Config @@ -15,14 +18,14 @@ except ImportError: import urlparse -basedir = os.path.dirname(os.path.abspath(__file__)) - +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +GOD_PIC = os.path.join(os.path.dirname(__file__), '照桥心美.png') TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token" -json_header = {'content-type': 'application/json'} +JSON_HEADER = {'content-type': 'application/json'} def token_callback(request): - return 200, json_header, json.dumps({"access_token": "ACCESS_TOKEN", "expires_in": 7200}) + return 200, JSON_HEADER, json.dumps({"access_token": "ACCESS_TOKEN", "expires_in": 7200}) def add_token_response(method): @@ -33,19 +36,32 @@ def wrapped_func(self, *args, **kwargs): return wrapped_func +def create_pic_file(func): + def wrapped_func(self, *args, **kwargs): + with open(GOD_PIC, 'a') as f: + f.write("just a test") + try: + func(self, *args, **kwargs) + finally: + os.remove(GOD_PIC) + + return wrapped_func + + class BaseTestClass: @cached_property def client(self): config = Config() - config.from_pyfile(os.path.join(basedir, "client_config.py")) + config.from_pyfile(os.path.join(BASE_DIR, "client_config.py")) return Client(config) @staticmethod def callback_without_check(request): - return 200, json_header, json.dumps({"errcode": 0, "errmsg": "ok"}) + return 200, JSON_HEADER, json.dumps({"errcode": 0, "errmsg": "ok"}) class TestClientBaseClass(BaseTestClass): + def test_id_and_secret(self): assert self.client.appid == "123" assert self.client.appsecret == "321" @@ -90,11 +106,11 @@ def test_client_request(self): def empty_params_callback(request): params = urlparse.parse_qs(urlparse.urlparse(request.url).query) assert params["access_token"][0] == self.client.token - return 200, json_header, json.dumps({"test": "test"}) + return 200, JSON_HEADER, json.dumps({"test": "test"}) def data_exists_url(request): assert json.loads(request.body.decode('utf-8')) == {"test": "test"} - return 200, json_header, json.dumps({"test": "test"}) + return 200, JSON_HEADER, json.dumps({"test": "test"}) responses.add_callback(responses.POST, DATA_EXISTS_URL, callback=data_exists_url) responses.add_callback(responses.GET, EMPTY_PARAMS_URL, callback=empty_params_callback) @@ -107,6 +123,75 @@ def data_exists_url(request): assert r == {"test": "test"} +class TestClientBaseClassPost(TestClientBaseClass): + @pytest.fixture(autouse=True) + def mock_request(self, mocker): + self.mocked_request = mocker.spy(self.client, 'request') + + @responses.activate + @add_token_response + @create_pic_file + def test_post_with_unittest(self): + POST_FILE_URL = "http://post_file.werobot.com/" + + def empty_post_file_callback(request): + return 200, JSON_HEADER, json.dumps({"test": "test"}) + + responses.add_callback(responses.POST, POST_FILE_URL, callback=empty_post_file_callback) + + with open(GOD_PIC, 'rb') as f: + self.client.post(url=POST_FILE_URL, files={"media": f}) + self.mocked_request.assert_any_call( + method='post', + url='http://post_file.werobot.com/', + files=dict(media=(urllib.parse.quote(GOD_PIC), f)) + ) + + # Test another attribute + with open(GOD_PIC, 'rb') as f: + self.client.post(url=POST_FILE_URL, files={"gugugu": f}) + self.mocked_request.assert_any_call( + method='post', + url='http://post_file.werobot.com/', + files=dict(gugugu=(urllib.parse.quote(GOD_PIC), f)) + ) + + @responses.activate + @add_token_response + def test_post_with_file_object_without_name(self): + POST_FILE_URL = "http://post_file.werobot.com/" + + def empty_post_file_callback(request): + return 200, JSON_HEADER, json.dumps({"test": "test"}) + + responses.add_callback(responses.POST, POST_FILE_URL, callback=empty_post_file_callback) + + f = BytesIO(b'gugugu') + self.client.post(url=POST_FILE_URL, files={"media": f}) + self.mocked_request.assert_any_call( + method='post', + url='http://post_file.werobot.com/', + files=dict(media=f) + ) + + @responses.activate + @add_token_response + @create_pic_file + def test_post_with_integration_test(self): + POST_FILE_URL = "http://post_file.werobot.com/" + + def post_file_callback(request): + s = request.body.split(b"\r")[0][2:] + p = list(multipart.MultipartParser(BytesIO(multipart.tob(request.body)), s))[0] + assert "filename" in p.options + return 200, JSON_HEADER, json.dumps({"test": "test"}) + + responses.add_callback(responses.POST, POST_FILE_URL, callback=post_file_callback) + + with open(GOD_PIC, 'rb') as f: + self.client.post(url=POST_FILE_URL, files={"media": f}) + + class TestClientMenuClass(BaseTestClass): CREATE_URL = "https://api.weixin.qq.com/cgi-bin/menu/create" GET_URL = "https://api.weixin.qq.com/cgi-bin/menu/get" @@ -166,13 +251,13 @@ def check_menu_data(item): try: body = json.loads(request.body.decode("utf-8"))["button"] except KeyError: - return 200, json_header, json.dumps({"errcode": 1, "errmsg": "error"}) + return 200, JSON_HEADER, json.dumps({"errcode": 1, "errmsg": "error"}) try: for item in body: check_menu_data(item) except AssertionError: - return 200, json_header, json.dumps({"errcode": 1, "errmsg": "error"}) - return 200, json_header, json.dumps({"errcode": 0, "errmsg": "ok"}) + return 200, JSON_HEADER, json.dumps({"errcode": 1, "errmsg": "error"}) + return 200, JSON_HEADER, json.dumps({"errcode": 0, "errmsg": "ok"}) @responses.activate @add_token_response @@ -213,13 +298,13 @@ def create_group_callback(request): body = json.loads(request.body.decode("utf-8")) assert "group" in body.keys() assert "name" in body["group"].keys() - return 200, json_header, json.dumps({"errcode": 0, "errmsg": "ok"}) + return 200, JSON_HEADER, json.dumps({"errcode": 0, "errmsg": "ok"}) @staticmethod def get_groups_with_id_callback(request): body = json.loads(request.body.decode("utf-8")) assert "openid" in body.keys() - return 200, json_header, json.dumps({"errcode": 0, "errmsg": "ok"}) + return 200, JSON_HEADER, json.dumps({"errcode": 0, "errmsg": "ok"}) @staticmethod def update_group_callback(request): @@ -227,28 +312,28 @@ def update_group_callback(request): assert "group" in body.keys() assert "id" in body["group"].keys() assert "name" in body["group"].keys() - return 200, json_header, json.dumps({"errcode": 0, "errmsg": "ok"}) + return 200, JSON_HEADER, json.dumps({"errcode": 0, "errmsg": "ok"}) @staticmethod def move_user_callback(request): body = json.loads(request.body.decode("utf-8")) assert "openid" in body.keys() assert "to_groupid" in body.keys() - return 200, json_header, json.dumps({"errcode": 0, "errmsg": "ok"}) + return 200, JSON_HEADER, json.dumps({"errcode": 0, "errmsg": "ok"}) @staticmethod def move_users_callback(request): body = json.loads(request.body.decode("utf-8")) assert "openid_list" in body.keys() assert "to_groupid" in body.keys() - return 200, json_header, json.dumps({"errcode": 0, "errmsg": "ok"}) + return 200, JSON_HEADER, json.dumps({"errcode": 0, "errmsg": "ok"}) @staticmethod def delete_group_callback(request): body = json.loads(request.body.decode("utf-8")) assert "group" in body.keys() assert "id" in body["group"].keys() - return 200, json_header, json.dumps({"errcode": 0, "errmsg": "ok"}) + return 200, JSON_HEADER, json.dumps({"errcode": 0, "errmsg": "ok"}) @responses.activate @add_token_response @@ -316,7 +401,7 @@ def remark_callback(request): body = json.loads(request.body.decode("utf-8")) assert "openid" in body.keys() assert "remark" in body.keys() - return 200, json_header, json.dumps({"errcode": 0, "errmsg": "ok"}) + return 200, JSON_HEADER, json.dumps({"errcode": 0, "errmsg": "ok"}) @responses.activate @add_token_response @@ -336,7 +421,7 @@ def single_user_callback(request): assert "access_token" in params.keys() assert "openid" in params.keys() assert "lang" in params.keys() - return 200, json_header, json.dumps({"errcode": 0, "errmsg": "ok"}) + return 200, JSON_HEADER, json.dumps({"errcode": 0, "errmsg": "ok"}) @staticmethod def multi_user_callback(request): @@ -345,7 +430,7 @@ def multi_user_callback(request): for user in body["user_list"]: assert "openid" in user.keys() assert "lang" in user.keys() - return 200, json_header, json.dumps({"errcode": 0, "errmsg": "ok"}) + return 200, JSON_HEADER, json.dumps({"errcode": 0, "errmsg": "ok"}) @responses.activate @add_token_response @@ -378,7 +463,7 @@ def get_followers_callback(request): params = urlparse.parse_qs(urlparse.urlparse(request.url).query) assert "access_token" in params.keys() assert "next_openid" in params.keys() - return 200, json_header, json.dumps({"errcode": 0, "errmsg": "ok"}) + return 200, JSON_HEADER, json.dumps({"errcode": 0, "errmsg": "ok"}) @responses.activate @add_token_response @@ -437,26 +522,26 @@ class TestClientCustomMenuClass(BaseTestClass): @staticmethod def get_custom_menu_callback(request): - return 200, json_header, json.dumps({"errcode": 0, "errmsg": "ok"}) + return 200, JSON_HEADER, json.dumps({"errcode": 0, "errmsg": "ok"}) @staticmethod def create_custom_menu_callback(request): body = json.loads(request.body.decode("utf-8")) assert "button" in body.keys() assert "matchrule" in body.keys() - return 200, json_header, json.dumps({"errcode": 0, "errmsg": "ok"}) + return 200, JSON_HEADER, json.dumps({"errcode": 0, "errmsg": "ok"}) @staticmethod def delete_custom_menu_callback(request): body = json.loads(request.body.decode("utf-8")) assert "menuid" in body.keys() - return 200, json_header, json.dumps({"errcode": 0, "errmsg": "ok"}) + return 200, JSON_HEADER, json.dumps({"errcode": 0, "errmsg": "ok"}) @staticmethod def match_custom_menu(request): body = json.loads(request.body.decode("utf-8")) assert "user_id" in body.keys() - return 200, json_header, json.dumps({"errcode": 0, "errmsg": "ok"}) + return 200, JSON_HEADER, json.dumps({"errcode": 0, "errmsg": "ok"}) @responses.activate @add_token_response @@ -535,13 +620,13 @@ class TestClientResourceClass(BaseTestClass): def upload_callback(request): params = urlparse.parse_qs(urlparse.urlparse(request.url).query) assert "type" in params.keys() - return 200, json_header, json.dumps({"errcode": 0, "errmsg": "ok"}) + return 200, JSON_HEADER, json.dumps({"errcode": 0, "errmsg": "ok"}) @staticmethod def download_callback(request): params = urlparse.parse_qs(urlparse.urlparse(request.url).query) assert "media_id" in params.keys() - return 200, json_header, json.dumps({"errcode": 0, "errmsg": "ok"}) + return 200, JSON_HEADER, json.dumps({"errcode": 0, "errmsg": "ok"}) @staticmethod def add_news_callback(request): @@ -555,20 +640,20 @@ def add_news_callback(request): assert "show_cover_pic" in article.keys() assert "content" in article.keys() assert "content_source_url" in article.keys() - return 200, json_header, json.dumps({"errcode": 0, "errmsg": "ok"}) + return 200, JSON_HEADER, json.dumps({"errcode": 0, "errmsg": "ok"}) @staticmethod def upload_picture_callback(request): params = urlparse.parse_qs(urlparse.urlparse(request.url).query) assert "access_token" in params.keys() - return 200, json_header, json.dumps({"errcode": 0, "errmsg": "ok"}) + return 200, JSON_HEADER, json.dumps({"errcode": 0, "errmsg": "ok"}) @staticmethod def upload_p_media_callback(request): params = urlparse.parse_qs(urlparse.urlparse(request.url).query) assert "access_token" in params.keys() assert "type" in params.keys() - return 200, json_header, json.dumps({"errcode": 0, "errmsg": "ok"}) + return 200, JSON_HEADER, json.dumps({"errcode": 0, "errmsg": "ok"}) @staticmethod def download_p_media_callback(request): @@ -576,13 +661,13 @@ def download_p_media_callback(request): assert "access_token" in params.keys() body = json.loads(request.body.decode("utf-8")) assert "media_id" in body.keys() - return 200, json_header, json.dumps({"errcode": 0, "errmsg": "ok"}) + return 200, JSON_HEADER, json.dumps({"errcode": 0, "errmsg": "ok"}) @staticmethod def delete_p_media_callback(request): body = json.loads(request.body.decode("utf-8")) assert "media_id" in body.keys() - return 200, json_header, json.dumps({"errcode": 0, "errmsg": "ok"}) + return 200, JSON_HEADER, json.dumps({"errcode": 0, "errmsg": "ok"}) @staticmethod def update_news_callback(request): @@ -598,13 +683,15 @@ def update_news_callback(request): assert "show_cover_pic" in articles.keys() assert "content" in articles.keys() assert "content_source_url" in articles.keys() - return 200, json_header, json.dumps({"errcode": 0, "errmsg": "ok"}) + return 200, JSON_HEADER, json.dumps({"errcode": 0, "errmsg": "ok"}) @responses.activate @add_token_response + @create_pic_file def test_upload_media(self): responses.add_callback(responses.POST, self.UPLOAD_URL, callback=self.upload_callback) - r = self.client.upload_media("test", "test") + with open(GOD_PIC, 'rb') as f: + r = self.client.upload_media('image', f) assert r == {"errcode": 0, "errmsg": "ok"} @responses.activate @@ -623,23 +710,27 @@ def test_add_news(self): @responses.activate @add_token_response + @create_pic_file def test_upload_news_picture(self): responses.add_callback( responses.POST, self.UPLOAD_PICTURE_URL, callback=self.upload_picture_callback ) - r = self.client.upload_news_picture("test") + with open(GOD_PIC, 'rb') as f: + r = self.client.upload_news_picture(f) assert r == {"errcode": 0, "errmsg": "ok"} @responses.activate @add_token_response + @create_pic_file def test_upload_permanent_media(self): responses.add_callback( responses.POST, self.UPLOAD_P_URL, callback=self.upload_p_media_callback) - r = self.client.upload_permanent_media("test", "test") + with open(GOD_PIC, 'rb') as f: + r = self.client.upload_permanent_media('image', f) assert r == {"errcode": 0, "errmsg": "ok"} @responses.activate @@ -684,17 +775,19 @@ def upload_video_callback(request): params = urlparse.parse_qs(urlparse.urlparse(request.url).query) assert "type" in params.keys() assert params["type"][0] == "video" - return 200, json_header, json.dumps({"errcode": 0, "errmsg": "ok"}) + return 200, JSON_HEADER, json.dumps({"errcode": 0, "errmsg": "ok"}) @responses.activate @add_token_response + @create_pic_file def test_upload_video(self): responses.add_callback( responses.POST, self.UPLOAD_VIDEO_URL, callback=self.upload_video_callback ) - r = self.client.upload_permanent_video("test", "test", "test") + with open(GOD_PIC, 'rb') as f: + r = self.client.upload_permanent_video("test", "test", f) assert isinstance(r, requests.Response) @@ -706,7 +799,7 @@ class TestMediaClass(BaseTestClass): def get_media_callback(request): params = urlparse.parse_qs(urlparse.urlparse(request.url).query) assert "access_token" in params.keys() - return 200, json_header, json.dumps({"errcode": 0, "errmsg": "ok"}) + return 200, JSON_HEADER, json.dumps({"errcode": 0, "errmsg": "ok"}) @staticmethod def get_media_list_callback(request): @@ -714,7 +807,7 @@ def get_media_list_callback(request): assert "type" in body.keys() assert "offset" in body.keys() assert "count" in body.keys() - return 200, json_header, json.dumps({"errcode": 0, "errmsg": "ok"}) + return 200, JSON_HEADER, json.dumps({"errcode": 0, "errmsg": "ok"}) @responses.activate @add_token_response @@ -742,7 +835,7 @@ class TestGetIpListClass(BaseTestClass): def get_ip_list_callback(request): params = urlparse.parse_qs(urlparse.urlparse(request.url).query) assert "access_token" in params.keys() - return 200, json_header, json.dumps({"errcode": 0, "errmsg": "ok"}) + return 200, JSON_HEADER, json.dumps({"errcode": 0, "errmsg": "ok"}) @responses.activate @add_token_response @@ -765,19 +858,19 @@ def add_update_delete_callback(request): assert "kf_account" in body.keys() assert "nickname" in body.keys() assert "password" in body.keys() - return 200, json_header, json.dumps({"errcode": 0, "errmsg": "ok"}) + return 200, JSON_HEADER, json.dumps({"errcode": 0, "errmsg": "ok"}) @staticmethod def upload_callback(request): params = urlparse.parse_qs(urlparse.urlparse(request.url).query) assert "access_token" in params.keys() - return 200, json_header, json.dumps({"errcode": 0, "errmsg": "ok"}) + return 200, JSON_HEADER, json.dumps({"errcode": 0, "errmsg": "ok"}) @staticmethod def get_callback(request): params = urlparse.parse_qs(urlparse.urlparse(request.url).query) assert "access_token" in params.keys() - return 200, json_header, json.dumps({"errcode": 0, "errmsg": "ok"}) + return 200, JSON_HEADER, json.dumps({"errcode": 0, "errmsg": "ok"}) @responses.activate @add_token_response @@ -814,9 +907,11 @@ def test_delete_custom_service_account(self): @responses.activate @add_token_response + @create_pic_file def test_upload_custom_service_account_avatar(self): responses.add_callback(responses.POST, self.UPLOAD_URL, callback=self.upload_callback) - r = self.client.upload_custom_service_account_avatar("test", "test") + with open(GOD_PIC, 'rb') as f: + r = self.client.upload_custom_service_account_avatar("image", f) assert r == {"errcode": 0, "errmsg": "ok"} @responses.activate @@ -835,13 +930,13 @@ class TestQrcodeClass(BaseTestClass): def create_callback(request): params = urlparse.parse_qs(urlparse.urlparse(request.url).query) assert "access_token" in params.keys() - return 200, json_header, json.dumps({"errcode": 0, "errmsg": "ok"}) + return 200, JSON_HEADER, json.dumps({"errcode": 0, "errmsg": "ok"}) @staticmethod def show_callback(request): params = urlparse.parse_qs(urlparse.urlparse(request.url).query) assert "ticket" in params.keys() - return 200, json_header, json.dumps({"errcode": 0, "errmsg": "ok"}) + return 200, JSON_HEADER, json.dumps({"errcode": 0, "errmsg": "ok"}) @responses.activate @add_token_response @@ -873,7 +968,7 @@ def article_callback(request): assert "description" in article.keys() assert "url" in article.keys() assert "picurl" in article.keys() - return 200, json_header, json.dumps({"errcode": 0, "errmsg": "ok"}) + return 200, JSON_HEADER, json.dumps({"errcode": 0, "errmsg": "ok"}) @responses.activate @add_token_response @@ -913,7 +1008,7 @@ def text_callback(request): assert "text" in body.keys() assert "content" in body["text"].keys() - return 200, json_header, json.dumps({"errcode": 0, "errmsg": "ok"}) + return 200, JSON_HEADER, json.dumps({"errcode": 0, "errmsg": "ok"}) @responses.activate @add_token_response @@ -936,7 +1031,7 @@ def image_callback(request): assert "image" in body.keys() assert "media_id" in body["image"].keys() - return 200, json_header, json.dumps({"errcode": 0, "errmsg": "ok"}) + return 200, JSON_HEADER, json.dumps({"errcode": 0, "errmsg": "ok"}) @responses.activate @add_token_response @@ -959,7 +1054,7 @@ def voice_callback(request): assert "voice" in body.keys() assert "media_id" in body["voice"].keys() - return 200, json_header, json.dumps({"errcode": 0, "errmsg": "ok"}) + return 200, JSON_HEADER, json.dumps({"errcode": 0, "errmsg": "ok"}) @responses.activate @add_token_response @@ -984,7 +1079,7 @@ def music_callback(request): assert "hqmusicurl" in body["music"].keys() assert "thumb_media_id" in body["music"].keys() - return 200, json_header, json.dumps({"errcode": 0, "errmsg": "ok"}) + return 200, JSON_HEADER, json.dumps({"errcode": 0, "errmsg": "ok"}) @responses.activate @add_token_response @@ -1014,7 +1109,7 @@ def video_callback(request): assert "video" in body.keys() assert "media_id" in body["video"].keys() - return 200, json_header, json.dumps({"errcode": 0, "errmsg": "ok"}) + return 200, JSON_HEADER, json.dumps({"errcode": 0, "errmsg": "ok"}) @responses.activate @add_token_response @@ -1041,7 +1136,7 @@ def news_callback(request): assert "mpnews" in body.keys() assert "media_id" in body["mpnews"].keys() - return 200, json_header, json.dumps({"errcode": 0, "errmsg": "ok"}) + return 200, JSON_HEADER, json.dumps({"errcode": 0, "errmsg": "ok"}) @responses.activate @add_token_response @@ -1063,7 +1158,7 @@ def template_callback(request): assert "url" in body.keys() assert "data" in body.keys() - return 200, json_header, json.dumps({"errcode": 0, "errmsg": "ok"}) + return 200, JSON_HEADER, json.dumps({"errcode": 0, "errmsg": "ok"}) @responses.activate @add_token_response diff --git a/tox-requirements.txt b/tox-requirements.txt index bd3a190f..34bedb4d 100644 --- a/tox-requirements.txt +++ b/tox-requirements.txt @@ -10,3 +10,5 @@ flask tornado responses PyMySQL +multipart +pytest-mock diff --git a/werobot/client.py b/werobot/client.py index acb46333..aff2ea5f 100644 --- a/werobot/client.py +++ b/werobot/client.py @@ -2,6 +2,7 @@ import time import requests +from six.moves import urllib from requests.compat import json as _json from werobot.utils import to_text @@ -41,6 +42,12 @@ def appid(self): def appsecret(self): return self.config.get("APP_SECRET", None) + @staticmethod + def _url_encode_files(file): + if hasattr(file, "name"): + file = (urllib.parse.quote(file.name), file) + return file + def request(self, method, url, **kwargs): if "params" not in kwargs: kwargs["params"] = {"access_token": self.token} @@ -60,6 +67,16 @@ def get(self, url, **kwargs): return self.request(method="get", url=url, **kwargs) def post(self, url, **kwargs): + if "files" in kwargs: + # Although there is only one key "media" possible in "files" now, + # we decide to check every key to support possible keys in the future + # Fix chinese file name error #292 + kwargs["files"] = dict( + zip( + kwargs["files"], + map(self._url_encode_files, kwargs["files"].values()) + ) + ) return self.request(method="post", url=url, **kwargs) def grant_token(self):