From 2c5dd2df593956d7f795b37c7686ca44728a34a5 Mon Sep 17 00:00:00 2001 From: Emile Pels Date: Wed, 15 Aug 2018 15:47:17 +0200 Subject: [PATCH 1/9] Refactor Client for easier mocking --- messagebird/client.py | 42 ++++++++++++-------------------------- messagebird/http_client.py | 41 +++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 29 deletions(-) create mode 100644 messagebird/http_client.py diff --git a/messagebird/client.py b/messagebird/client.py index 6ffceeb..64c25bf 100644 --- a/messagebird/client.py +++ b/messagebird/client.py @@ -1,16 +1,11 @@ import sys import json -import requests - -try: - from urllib.parse import urljoin -except ImportError: - from urlparse import urljoin from messagebird.base import Base from messagebird.balance import Balance from messagebird.error import Error from messagebird.hlr import HLR +from messagebird.http_client import HttpClient from messagebird.message import Message from messagebird.voicemessage import VoiceMessage from messagebird.lookup import Lookup @@ -19,6 +14,7 @@ ENDPOINT = 'https://rest.messagebird.com' CLIENT_VERSION = '1.2.1' PYTHON_VERSION = '%d.%d.%d' % (sys.version_info[0], sys.version_info[1], sys.version_info[2]) +USER_AGENT = 'MessageBird/ApiClient/%s Python/%s' % (CLIENT_VERSION, PYTHON_VERSION) class ErrorException(Exception): @@ -29,35 +25,23 @@ def __init__(self, errors): class Client(object): - def __init__(self, access_key): + def __init__(self, access_key, http_client=None): self.access_key = access_key - self._supported_status_codes = [200, 201, 204, 401, 404, 405, 422] - - def request(self, path, method='GET', params=None): - if params is None: params = {} - url = urljoin(ENDPOINT, path) - headers = { - 'Accept' : 'application/json', - 'Authorization' : 'AccessKey ' + self.access_key, - 'User-Agent' : 'MessageBird/ApiClient/%s Python/%s' % (CLIENT_VERSION, PYTHON_VERSION), - 'Content-Type' : 'application/json' - } - - if method == 'GET': - response = requests.get(url, verify=True, headers=headers, params=params) + if http_client is None: + self.http_client = HttpClient(ENDPOINT, access_key, USER_AGENT) else: - response = requests.post(url, verify=True, headers=headers, data=json.dumps(params)) + self.http_client = http_client - if response.status_code in self._supported_status_codes: - json_response = response.json() - else: - response.raise_for_status() + def request(self, path, method='GET', params=None): + """Builds a request, gets a response and decodes it.""" + response_text = self.http_client.request(path, method, params) + response_json = json.loads(response_text) - if 'errors' in json_response: - raise(ErrorException([Error().load(e) for e in json_response['errors']])) + if 'errors' in response_json: + raise(ErrorException([Error().load(e) for e in response_json['errors']])) - return json_response + return response_json def balance(self): """Retrieve your balance.""" diff --git a/messagebird/http_client.py b/messagebird/http_client.py new file mode 100644 index 0000000..e07a10c --- /dev/null +++ b/messagebird/http_client.py @@ -0,0 +1,41 @@ +import requests + +try: + from urllib.parse import urljoin +except ImportError: + from urlparse import urljoin + + +class HttpClient(object): + """Used for sending simple HTTP requests.""" + + def __init__(self, endpoint, access_key, user_agent): + self.__supported_status_codes = [200, 201, 204, 401, 404, 405, 422] + + self.endpoint = endpoint + self.access_key = access_key + self.user_agent = user_agent + + def request(self, path, method='GET', params=None): + """Builds a request and gets a response.""" + if params is None: params = {} + url = urljoin(self.endpoint, path) + + headers = { + 'Accept': 'application/json', + 'Authorization': 'AccessKey ' + self.access_key, + 'User-Agent': self.user_agent, + 'Content-Type': 'application/json' + } + + if method == 'GET': + response = requests.get(url, verify=True, headers=headers, params=params) + else: + response = requests.post(url, verify=True, headers=headers, data=json.dumps(params)) + + if response.status_code in self.__supported_status_codes: + response_text = response.text + else: + response.raise_for_status() + + return response_text \ No newline at end of file From ed694bac6a3a9cbd13ae632cede3aa1348038d7e Mon Sep 17 00:00:00 2001 From: Emile Pels Date: Wed, 15 Aug 2018 15:48:30 +0200 Subject: [PATCH 2/9] Add simple Balance test for demonstration --- tests/__init__.py | 0 tests/test_balance.py | 23 +++++++++++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 tests/__init__.py create mode 100644 tests/test_balance.py diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_balance.py b/tests/test_balance.py new file mode 100644 index 0000000..ca025ee --- /dev/null +++ b/tests/test_balance.py @@ -0,0 +1,23 @@ +import unittest +from messagebird import Client + +try: + from unittest.mock import Mock +except ImportError: + # mock was added to unittest in Python 3.3, but was an external library + # before. + from mock import Mock + + +class TestBalance(unittest.TestCase): + + def test_balance(self): + http_client = Mock() + http_client.request.return_value = '{"payment": "prepaid","type": "credits","amount": 9.2}' + + balance = Client('', http_client).balance() + + http_client.request.assert_called_once_with('balance', 'GET', None) + + self.assertEqual('prepaid', balance.payment) + self.assertEqual('credits', balance.type) From 7872e196fe02c1945e262b8adb6303a1ea57fdc8 Mon Sep 17 00:00:00 2001 From: Emile Pels Date: Wed, 15 Aug 2018 15:55:56 +0200 Subject: [PATCH 3/9] Add Travis integration --- .travis.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..520f16b --- /dev/null +++ b/.travis.yml @@ -0,0 +1,13 @@ +language: python +python: + - 'pypy2.7' + - 'pypy3.5' + - '2.7' + - '3.3' + - '3.7' + - 'nightly' +install: + - pip install mock==2.0 +matrix: + allow_failures: + - python: 'nightly' From 4ae7d794f2ebfb0fb086159e69fbc2a808d5dccb Mon Sep 17 00:00:00 2001 From: Emile Pels Date: Wed, 15 Aug 2018 15:58:00 +0200 Subject: [PATCH 4/9] Add test script to .travis.yml --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 520f16b..e6e855c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,6 +8,8 @@ python: - 'nightly' install: - pip install mock==2.0 +script: + - python -m unittest discover -s tests/ -p test_*.py -v matrix: allow_failures: - python: 'nightly' From cab74893315c256d4b5a1934c71b2329a98abd60 Mon Sep 17 00:00:00 2001 From: Emile Pels Date: Wed, 15 Aug 2018 16:04:00 +0200 Subject: [PATCH 5/9] Install requests before running tests --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index e6e855c..6169c3b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,6 +8,7 @@ python: - 'nightly' install: - pip install mock==2.0 + - pip install requests script: - python -m unittest discover -s tests/ -p test_*.py -v matrix: From 2a5a6311b5c5568260dafe7f34f521f0ac829150 Mon Sep 17 00:00:00 2001 From: Emile Pels Date: Wed, 15 Aug 2018 16:07:50 +0200 Subject: [PATCH 6/9] Use Python 3.6 instead of 3.7: latter doesn't seem to be supported by Travis --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 6169c3b..7e32f0d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,7 @@ python: - 'pypy3.5' - '2.7' - '3.3' - - '3.7' + - '3.6' - 'nightly' install: - pip install mock==2.0 From b844c64556aed84e10c673eaa92884039d97670b Mon Sep 17 00:00:00 2001 From: Emile Pels Date: Wed, 15 Aug 2018 16:14:15 +0200 Subject: [PATCH 7/9] Allow pypy2.7 to fail: Travis can not find virtualenv script --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 7e32f0d..2aebb87 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,3 +14,4 @@ script: matrix: allow_failures: - python: 'nightly' + - python: 'pypy2.7' From b6cf05bff2dd20cf6d4c2a6dd88ec98ad7fd4215 Mon Sep 17 00:00:00 2001 From: Emile Pels Date: Wed, 15 Aug 2018 16:17:57 +0200 Subject: [PATCH 8/9] Add Travis badge to README --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 4a51510..5adac28 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ MessageBird's REST API for Python ================================= This repository contains the open source Python client for MessageBird's REST API. Documentation can be found at: https://developers.messagebird.com/. +[![Build Status](https://travis-ci.org/messagebird/python-rest-api.svg?branch=master)](https://travis-ci.org/messagebird/python-rest-api) + Requirements ------------ - [Sign up](https://www.messagebird.com/en/signup) for a free MessageBird account From e93a241cc204cd2c3ea9a9db6057846e077a6f35 Mon Sep 17 00:00:00 2001 From: Emile Pels Date: Fri, 17 Aug 2018 10:25:41 +0200 Subject: [PATCH 9/9] Add tests for other endpoints --- tests/test_hlr.py | 30 ++++++++++++++++++++++++++++ tests/test_lookup.py | 40 ++++++++++++++++++++++++++++++++++++++ tests/test_message.py | 30 ++++++++++++++++++++++++++++ tests/test_verify.py | 40 ++++++++++++++++++++++++++++++++++++++ tests/test_voicemessage.py | 30 ++++++++++++++++++++++++++++ 5 files changed, 170 insertions(+) create mode 100644 tests/test_hlr.py create mode 100644 tests/test_lookup.py create mode 100644 tests/test_message.py create mode 100644 tests/test_verify.py create mode 100644 tests/test_voicemessage.py diff --git a/tests/test_hlr.py b/tests/test_hlr.py new file mode 100644 index 0000000..5f2fa2b --- /dev/null +++ b/tests/test_hlr.py @@ -0,0 +1,30 @@ +import unittest +from messagebird import Client + +try: + from unittest.mock import Mock +except ImportError: + # mock was added to unittest in Python 3.3, but was an external library + # before. + from mock import Mock + + +class TestHLR(unittest.TestCase): + + def test_hlr(self): + http_client = Mock() + http_client.request.return_value = '{"id":"hlr-id","href":"https://rest.messagebird.com/hlr/hlr-id","msisdn":31612345678,"network":20406,"reference":"MyReference","status": "sent","createdDatetime": "2015-01-04T13:14:08+00:00","statusDatetime": "2015-01-04T13:14:09+00:00"}' + + hlr = Client('', http_client).hlr('hlr-id') + + http_client.request.assert_called_once_with('hlr/hlr-id', 'GET', None) + + self.assertEqual('hlr-id', hlr.id) + + def test_hlr_create(self): + http_client = Mock() + http_client.request.return_value = '{}' + + Client('', http_client).hlr_create(31612345678, 'MyReference') + + http_client.request.assert_called_once_with('hlr', 'POST', {'msisdn': 31612345678, 'reference': 'MyReference'}) diff --git a/tests/test_lookup.py b/tests/test_lookup.py new file mode 100644 index 0000000..2d2d534 --- /dev/null +++ b/tests/test_lookup.py @@ -0,0 +1,40 @@ +import unittest +from messagebird import Client + +try: + from unittest.mock import Mock +except ImportError: + # mock was added to unittest in Python 3.3, but was an external library + # before. + from mock import Mock + + +class TestLookup(unittest.TestCase): + + def test_lookup(self): + http_client = Mock() + http_client.request.return_value = '{"href": "https://rest.messagebird.com/lookup/31612345678","countryCode": "NL","countryPrefix": 31,"phoneNumber": 31612345678,"type": "mobile","formats": {"e164": "+31612345678","international": "+31 6 12345678","national": "06 12345678","rfc3966": "tel:+31-6-12345678"},"hlr": {"id": "hlr-id","network": 20416,"reference": "reference2000","status": "active","createdDatetime": "2015-12-15T08:19:24+00:00","statusDatetime": "2015-12-15T08:19:25+00:00"}}' + + lookup = Client('', http_client).lookup('0612345678', {'countryCode': 'NL'}) + + http_client.request.assert_called_once_with('lookup/0612345678', 'GET', {'countryCode': 'NL'}) + + self.assertEqual('mobile', lookup.type) + + def test_lookup_hlr(self): + http_client = Mock() + http_client.request.return_value = '{"id": "hlr-id","network": 20416,"reference": "reference2000","status": "active","createdDatetime": "2015-12-15T08:19:24+00:00","statusDatetime": "2015-12-15T08:19:25+00:00"}' + + lookup_hlr = Client('', http_client).lookup_hlr(31612345678, {'reference': 'reference2000'}) + + http_client.request.assert_called_once_with('lookup/31612345678/hlr', 'GET', {'reference': 'reference2000'}) + + self.assertEqual(lookup_hlr.status, 'active') + + def test_lookup_hlr_create(self): + http_client = Mock() + http_client.request.return_value = '{}' + + Client('', http_client).lookup_hlr_create(31612345678, {'reference': 'MyReference'}) + + http_client.request.assert_called_once_with('lookup/31612345678/hlr', 'POST', {'reference': 'MyReference'}) diff --git a/tests/test_message.py b/tests/test_message.py new file mode 100644 index 0000000..a81003e --- /dev/null +++ b/tests/test_message.py @@ -0,0 +1,30 @@ +import unittest +from messagebird import Client + +try: + from unittest.mock import Mock +except ImportError: + # mock was added to unittest in Python 3.3, but was an external library + # before. + from mock import Mock + + +class TestMessage(unittest.TestCase): + + def test_message(self): + http_client = Mock() + http_client.request.return_value = '{"body": "Hello World","createdDatetime": "2015-01-05T10:02:59+00:00","datacoding": "plain","direction": "mt","gateway": 239,"href": "https://rest.messagebird.com/messages/message-id","id": "message-id","mclass": 1,"originator": "TestName","recipients": {"items": [{"recipient": 31612345678,"status": "sent","statusDatetime": "2015-01-05T10:02:59+00:00"}],"totalCount": 1,"totalDeliveredCount": 0,"totalDeliveryFailedCount": 0,"totalSentCount": 1},"reference": null,"scheduledDatetime": null,"type": "sms","typeDetails": {},"validity": null}' + + message = Client('', http_client).message('message-id') + + http_client.request.assert_called_once_with('messages/message-id', 'GET', None) + + self.assertEqual('mt', message.direction) + + def test_message_create(self): + http_client = Mock() + http_client.request.return_value = '{}' + + Client('', http_client).message_create('MessageBird', ['31612345678', '31687654321'], 'Hello World', {'datacoding': 'unicode'}) + + http_client.request.assert_called_once_with('messages', 'POST', {'datacoding': 'unicode', 'originator': 'MessageBird', 'body': 'Hello World', 'recipients': '31612345678,31687654321' }) diff --git a/tests/test_verify.py b/tests/test_verify.py new file mode 100644 index 0000000..eee9760 --- /dev/null +++ b/tests/test_verify.py @@ -0,0 +1,40 @@ +import unittest +from messagebird import Client + +try: + from unittest.mock import Mock +except ImportError: + # mock was added to unittest in Python 3.3, but was an external library + # before. + from mock import Mock + + +class TestVerify(unittest.TestCase): + + def test_verify(self): + http_client = Mock() + http_client.request.return_value = '{"id": "verify-id","href": "https://rest.messagebird.com/verify/verify-id","recipient": 31612345678,"reference": "MyReference","messages": {"href": "https://rest.messagebird.com/messages/message-id"},"status": "verified","createdDatetime": "2017-05-30T12:39:50+00:00","validUntilDatetime": "2017-05-30T12:40:20+00:00"}' + + verify = Client('', http_client).verify('verify-id') + + http_client.request.assert_called_once_with('verify/verify-id', 'GET', None) + + self.assertEqual('verify-id', verify.id) + + def test_verify_create(self): + http_client = Mock() + http_client.request.return_value = '{}' + + Client('', http_client).verify_create('31612345678', {}) + + http_client.request.assert_called_once_with('verify', 'POST', {'recipient': '31612345678'}) + + def test_verify_verify(self): + http_client = Mock() + http_client.request.return_value = '{"id": "verify-id","href": "https://rest.messagebird.com/verify/verify-id","recipient": 31612345678,"reference": "MyReference","messages": {"href": "https://rest.messagebird.com/messages/63b168423592d681641eb07b76226648"},"status": "verified","createdDatetime": "2017-05-30T12:39:50+00:00","validUntilDatetime": "2017-05-30T12:40:20+00:00"}' + + verify = Client('', http_client).verify_verify('verify-id', 'secret') + + http_client.request.assert_called_once_with('verify/verify-id', 'GET', {'token': 'secret'}) + + self.assertEqual('verified', verify.status) diff --git a/tests/test_voicemessage.py b/tests/test_voicemessage.py new file mode 100644 index 0000000..c7f2a14 --- /dev/null +++ b/tests/test_voicemessage.py @@ -0,0 +1,30 @@ +import unittest +from messagebird import Client + +try: + from unittest.mock import Mock +except ImportError: + # mock was added to unittest in Python 3.3, but was an external library + # before. + from mock import Mock + + +class TestVoicemessage(unittest.TestCase): + + def test_voicemessage(self): + http_client = Mock() + http_client.request.return_value = '{"body": "Hello World","createdDatetime": "2015-01-05T16:11:24+00:00","href": "https://rest.messagebird.com/voicemessages/voicemessage-id","id": "voicemessage-id","ifMachine": "continue","language": "en-gb","originator": "MessageBird","recipients": {"items": [{"recipient": 31612345678,"status": "calling","statusDatetime": "2015-01-05T16:11:24+00:00"}],"totalCount": 1,"totalDeliveredCount": 0,"totalDeliveryFailedCount": 0,"totalSentCount": 1},"reference": null,"repeat": 1,"scheduledDatetime": null,"voice": "female"}' + + voice_message = Client('', http_client).voice_message('voicemessage-id') + + http_client.request.assert_called_once_with('voicemessages/voicemessage-id', 'GET', None) + + self.assertEqual('voicemessage-id', voice_message.id) + + def test_voicemessage_create(self): + http_client = Mock() + http_client.request.return_value = '{}' + + Client('', http_client).voice_message_create(['31612345678', '31687654321'], 'Hello World', { 'reference': 'MyReference' }) + + http_client.request.assert_called_once_with('voicemessages', 'POST', {'body': 'Hello World', 'recipients': '31612345678,31687654321', 'reference': 'MyReference'})