diff --git a/autopush/router/gcm.py b/autopush/router/gcm.py index e49cb013..f9241ae9 100644 --- a/autopush/router/gcm.py +++ b/autopush/router/gcm.py @@ -116,13 +116,15 @@ def _route(self, notification, uaid_data): gcm = self.gcm[router_data['creds']['senderID']] result = gcm.send(payload) except RouterException: - raise + raise # pragma nocover except KeyError: self.log.critical("Missing GCM bridge credentials") - raise RouterException("Server error", status_code=500) + raise RouterException("Server error", status_code=500, + errno=900) except gcmclient.GCMAuthenticationError as e: self.log.error("GCM Authentication Error: %s" % e) - raise RouterException("Server error", status_code=500) + raise RouterException("Server error", status_code=500, + errno=901) except ConnectionError as e: self.log.warn("GCM Unavailable: %s" % e) self.metrics.increment("notification.bridge.error", @@ -130,6 +132,7 @@ def _route(self, notification, uaid_data): self._base_tags, reason="connection_unavailable")) raise RouterException("Server error", status_code=502, + errno=902, log_exception=False) except Exception as e: self.log.error("Unhandled exception in GCM Routing: %s" % e) @@ -192,8 +195,8 @@ def _process_reply(self, reply, uaid_data, ttl, notification): raise RouterException("GCM failure to deliver, retry", status_code=503, headers={"Retry-After": reply.retry_after}, - response_body= - "Please try request in {} seconds.".format( + response_body="Please try request " + "in {} seconds.".format( reply.retry_after ), log_exception=False) diff --git a/autopush/router/gcmclient.py b/autopush/router/gcmclient.py index ca93a7c2..7c32fcfa 100644 --- a/autopush/router/gcmclient.py +++ b/autopush/router/gcmclient.py @@ -5,21 +5,10 @@ from autopush.exceptions import RouterException -GCM_MAX_CONNECTIONS = 20 - - class GCMAuthenticationError(Exception): pass -class GCMRetryError(Exception): - retry = 0 - - def __init__(self, msg, retry): - super(GCMRetryError, self).__init__(msg) - self.retry = retry - - class Result(object): def __init__(self, message, response): @@ -37,14 +26,14 @@ def __init__(self, message, response): if response.status_code != 200: self.retry_message = message else: - self._parse_response(response.content) + self._parse_response(message, response.content) - def _parse_response(self, content): + def _parse_response(self, message, content): data = json.loads(content) - if 'results' not in data or len(data.get('results')): + if not data.get('results'): raise RouterException("Recv'd invalid response from GCM") - - for reg_id, res in data['results']: + reg_id = message.payload['registration_ids'][0] + for res in data['results']: if 'message_id' in res: self.success[reg_id] = res['message_id'] if 'registration_id' in res: @@ -70,8 +59,9 @@ def __init__(self, raise RouterException("No Registration IDs specified") if not isinstance(registration_ids, list): registration_ids = [registration_ids] - self.regisration_ids = registration_ids + self.registration_ids = registration_ids self.payload = { + 'registration_ids': self.registration_ids, 'collapse_key': collapse_key, 'time_to_live': int(time_to_live), 'delay_while_idle': False, @@ -95,6 +85,7 @@ def __init__(self, self.metrics = metrics self.log = logger self._options = options + self._sender = requests.post def send(self, payload): headers = { @@ -102,10 +93,10 @@ def send(self, payload): 'Authorization': 'key={}'.format(self._api_key), } - response = requests.post( + response = self._sender( url=self._endpoint, headers=headers, - data=payload.payload, + data=json.dumps(payload.payload), **self._options ) diff --git a/autopush/tests/test_router.py b/autopush/tests/test_router.py index c3057007..34d822dc 100644 --- a/autopush/tests/test_router.py +++ b/autopush/tests/test_router.py @@ -17,6 +17,7 @@ import hyper.tls import pyfcm from hyper.http20.exceptions import HTTP20Error +import requests from autopush.config import AutopushConfig from autopush.db import ( @@ -295,6 +296,174 @@ def check_results(result): return d +class GCMClientTestCase(unittest.TestCase): + + def setUp(self): + self.gcm = gcmclient.GCM(api_key="FakeValue") + self.gcm._sender = self._m_request = Mock(spec=requests.post) + self._m_response = Mock(spec=requests.Response) + self._m_response.return_value = 200 + self._m_response.headers = dict() + self.m_payload = gcmclient.JSONMessage( + registration_ids="some_reg_id", + collapse_key="coll_key", + time_to_live=60, + dry_run=False, + data={"foo": "bar"} + ) + + def test_send(self): + self._m_response.status_code = 200 + self._m_response.content = json.dumps({ + "multicast_id": 5174939174563864884, + "success": 1, + "failure": 0, + "canonical_ids": 0, + "results": [ + { + "message_id": "0:1510011451922224%7a0e7efbaab8b7cc" + } + ] + }) + self._m_request.return_value = self._m_response + result = self.gcm.send(self.m_payload) + assert len(result.failed) == 0 + assert len(result.canonicals) == 0 + assert (len(result.success) == 1 + and self.m_payload.registration_ids[0] in result.success) + + def test_canonical(self): + self._m_response.status_code = 200 + self._m_response.content = json.dumps({ + "multicast_id": 5174939174563864884, + "success": 1, + "failure": 0, + "canonical_ids": 0, + "results": [ + { + "message_id": "0:1510011451922224%7a0e7efbaab8b7cc", + "registration_id": "otherId", + } + ] + }) + self._m_request.return_value = self._m_response + result = self.gcm.send(self.m_payload) + assert len(result.failed) == 0 + assert len(result.canonicals) == 1 + assert (len(result.success) == 1 + and self.m_payload.registration_ids[0] in result.success) + + def test_bad_jsonmessage(self): + with pytest.raises(RouterException): + self.m_payload = gcmclient.JSONMessage( + registration_ids=None, + collapse_key="coll_key", + time_to_live=60, + dry_run=False, + data={"foo": "bar"} + ) + + def test_fail_invalid(self): + self._m_response.status_code = 200 + self._m_response.content = json.dumps({ + "multicast_id": 5174939174563864884, + "success": 0, + "failure": 1, + "canonical_ids": 0, + "results": [ + { + "error": "InvalidRegistration" + } + ] + }) + self._m_request.return_value = self._m_response + result = self.gcm.send(self.m_payload) + assert len(result.failed) == 1 + assert len(result.success) == 0 + + def test_fail_unavailable(self): + self._m_response.status_code = 200 + self._m_response.content = json.dumps({ + "multicast_id": 5174939174563864884, + "success": 0, + "failure": 1, + "canonical_ids": 0, + "results": [ + { + "error": "Unavailable" + } + ] + }) + self._m_request.return_value = self._m_response + result = self.gcm.send(self.m_payload) + assert len(result.unavailable) == 1 + assert len(result.success) == 0 + + def test_fail_not_registered(self): + self._m_response.status_code = 200 + self._m_response.content = json.dumps({ + "multicast_id": 5174939174563864884, + "success": 0, + "failure": 1, + "canonical_ids": 0, + "results": [ + { + "error": "NotRegistered" + } + ] + }) + self._m_request.return_value = self._m_response + result = self.gcm.send(self.m_payload) + assert len(result.not_registered) == 1 + assert len(result.success) == 0 + + def test_fail_bad_response(self): + self._m_response.status_code = 200 + self._m_response.content = json.dumps({ + "multicast_id": 5174939174563864884, + "success": 0, + "failure": 1, + "canonical_ids": 0, + }) + self._m_request.return_value = self._m_response + with pytest.raises(RouterException): + self.gcm.send(self.m_payload) + + def test_fail_400(self): + self._m_response.status_code = 400 + self._m_response.content = msg = "Invalid JSON" + self._m_request.return_value = self._m_response + with pytest.raises(RouterException) as ex: + self.gcm.send(self.m_payload) + assert ex.value.status_code == 500 + assert ex.value.message == msg + + def test_fail_404(self): + self._m_response.status_code = 404 + self._m_response.content = msg = "Invalid URL" + self._m_request.return_value = self._m_response + with pytest.raises(RouterException) as ex: + self.gcm.send(self.m_payload) + assert ex.value.status_code == 500 + assert ex.value.message == msg + + def test_fail_401(self): + self._m_response.status_code = 401 + self._m_response.content = "Unauthorized" + self._m_request.return_value = self._m_response + with pytest.raises(gcmclient.GCMAuthenticationError): + self.gcm.send(self.m_payload) + + def test_fail_500(self): + self._m_response.status_code = 500 + self._m_response.content = "OMG" + self._m_response.headers['Retry-After'] = 123 + self._m_request.return_value = self._m_response + result = self.gcm.send(self.m_payload) + assert 'some_reg_id' in result.retry_message.registration_ids + assert result.retry_after == 123 + + class GCMRouterTestCase(unittest.TestCase): @patch("autopush.router.gcmclient.GCM", spec=gcmclient.GCM) @@ -334,9 +503,11 @@ def setUp(self, fgcm): self.mock_result = mock_result fgcm.send.return_value = mock_result - def _check_error_call(self, exc, code, response=None): + def _check_error_call(self, exc, code, response=None, errno=None): assert isinstance(exc, RouterException) assert exc.status_code == code + if errno is not None: + assert exc.errno == errno assert self.router.gcm['test123'].send.called if response: assert exc.response_body == response @@ -501,7 +672,7 @@ def throw_auth(arg): d = self.router.route_notification(self.notif, self.router_data) def check_results(fail): - self._check_error_call(fail.value, 500, "Server error") + self._check_error_call(fail.value, 500, "Server error", 901) d.addBoth(check_results) return d @@ -528,7 +699,7 @@ def throw_other(*args, **kwargs): d = self.router.route_notification(self.notif, self.router_data) def check_results(fail): - self._check_error_call(fail.value, 502, "Server error") + self._check_error_call(fail.value, 502, "Server error", 902) d.addBoth(check_results) return d @@ -609,7 +780,10 @@ def test_router_notification_gcm_no_auth(self): {"router_data": {"token": "abc"}}) def check_results(fail): - assert fail.value.status_code == 500, "Server error" + assert isinstance(fail.value, RouterException) + assert fail.value.message == "Server error" + assert fail.value.status_code == 500 + assert fail.value.errno == 900 d.addBoth(check_results) return d