Skip to content

Commit 4f9fcd8

Browse files
committed
Add publish error handling
1 parent a4f0e0e commit 4f9fcd8

File tree

2 files changed

+288
-18
lines changed

2 files changed

+288
-18
lines changed

pusher_push_notifications/__init__.py

Lines changed: 82 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,48 @@
11
"""Pusher Push Notifications Python server SDK"""
22

3+
import json
4+
import re
5+
36
import requests
47
import six
58

69
SDK_VERSION = '0.9.0'
10+
INTEREST_MAX_LENGTH = 164
11+
INTEREST_REGEX = re.compile('^(_|=|@|,|\\.|:|[A-Z]|[a-z]|[0-9])*$')
12+
13+
14+
class PusherValidationError(ValueError):
15+
"""Error thrown when the Push Notifications publish body is invalid"""
16+
pass
17+
18+
class PusherAuthError(ValueError):
19+
"""Error thrown when the Push Notifications secret key is incorrect"""
20+
pass
21+
22+
class PusherMissingInstanceError(KeyError):
23+
"""Error thrown when the instance id used does not exist"""
24+
pass
25+
26+
class PusherServerError(Exception):
27+
"""Error thrown when the Push Notifications service has an internal server
28+
error
29+
"""
30+
pass
31+
32+
def handle_http_error(response_body, status_code):
33+
"""Handle different http error codes from the Push Notifications service"""
34+
error_string = '{}: {}'.format(
35+
response_body.get('error', 'Unknown error'),
36+
response_body.get('description', 'no description'),
37+
)
38+
if status_code == 401:
39+
raise PusherAuthError(error_string)
40+
elif status_code == 404:
41+
raise PusherMissingInstanceError(error_string)
42+
elif 400 <= status_code < 500:
43+
raise PusherValidationError(error_string)
44+
elif 500 <= status_code < 600:
45+
raise PusherServerError(error_string)
746

847

948
class PushNotifications(object):
@@ -39,8 +78,8 @@ def publish(self, interests, publish_body):
3978
interests (list): List of interests that the publish body should
4079
be sent to.
4180
publish_body (dict): Dict containing the body of the push
42-
notification publish request.
43-
(see https://docs.pusher.com/push-notifications)
81+
notification publish request.
82+
(see https://docs.pusher.com/push-notifications)
4483
4584
Returns:
4685
A dict containing the publish response from the Pusher Push
@@ -55,9 +94,38 @@ def publish(self, interests, publish_body):
5594
raise TypeError('interests must be a list')
5695
if not isinstance(publish_body, dict):
5796
raise TypeError('publish_body must be a dictionary')
97+
if not interests:
98+
raise ValueError('Publishes must target at least one interest')
5899

59-
publish_body['interests'] = interests
100+
for interest in interests:
101+
if not isinstance(interest, six.string_types):
102+
raise TypeError(
103+
'Interest {} is not a string'.format(interest)
104+
)
105+
if len(interest) > INTEREST_MAX_LENGTH:
106+
raise ValueError(
107+
'Interest "{}" is longer than the maximum of {} chars'.format(
108+
interest,
109+
INTEREST_MAX_LENGTH,
110+
)
111+
)
112+
if '-' in interest:
113+
raise ValueError(
114+
'Interest "{}" contains a "-" which is forbidden. '.format(
115+
interest,
116+
)
117+
+ 'have you considered using a "_" instead?'
118+
)
119+
if not INTEREST_REGEX.match(interest):
120+
raise ValueError(
121+
'Interest "{}" contains a forbidden character. '.format(
122+
interest,
123+
)
124+
+ 'Allowed characters are: ASCII upper/lower-case letters, '
125+
+ 'numbers or one of _=@,.:'
126+
)
60127

128+
publish_body['interests'] = interests
61129
session = requests.Session()
62130
request = requests.Request(
63131
'POST',
@@ -74,4 +142,14 @@ def publish(self, interests, publish_body):
74142
)
75143
},
76144
)
77-
session.send(request.prepare())
145+
146+
response = session.send(request.prepare())
147+
try:
148+
response_body = response.json()
149+
except json.decoder.JSONDecodeError:
150+
response_body = {}
151+
152+
if response.status_code != 200:
153+
handle_http_error(response_body, response.status_code)
154+
155+
return response_body

tests/test_push_notifications.py

Lines changed: 206 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,13 @@
44

55
import requests_mock
66

7-
from pusher_push_notifications import PushNotifications
7+
from pusher_push_notifications import (
8+
PushNotifications,
9+
PusherAuthError,
10+
PusherMissingInstanceError,
11+
PusherServerError,
12+
PusherValidationError,
13+
)
814

915

1016
class TestPushNotifications(unittest.TestCase):
@@ -66,8 +72,12 @@ def test_publish_should_make_correct_http_request(self):
6672
http_mock.register_uri(
6773
requests_mock.ANY,
6874
requests_mock.ANY,
75+
status_code=200,
76+
json={
77+
'publishId': '1234',
78+
},
6979
)
70-
pn_client.publish(
80+
response = pn_client.publish(
7181
interests=['donuts'],
7282
publish_body={
7383
'apns': {
@@ -113,15 +123,184 @@ def test_publish_should_make_correct_http_request(self):
113123
},
114124
},
115125
)
126+
self.assertDictEqual(
127+
response,
128+
{
129+
'publishId': '1234',
130+
},
131+
)
132+
133+
134+
def test_publish_should_fail_if_interests_not_list(self):
135+
pn_client = PushNotifications(
136+
'INSTANCE_ID',
137+
'SECRET_KEY'
138+
)
139+
with self.assertRaises(TypeError):
140+
pn_client.publish(
141+
interests=False,
142+
publish_body={
143+
'apns': {
144+
'aps': {
145+
'alert': 'Hello World!',
146+
},
147+
},
148+
},
149+
)
150+
151+
def test_publish_should_fail_if_body_not_dict(self):
152+
pn_client = PushNotifications(
153+
'INSTANCE_ID',
154+
'SECRET_KEY'
155+
)
156+
with self.assertRaises(TypeError):
157+
pn_client.publish(
158+
interests=['donuts'],
159+
publish_body=False,
160+
)
161+
162+
def test_publish_should_fail_if_no_interests_passed(self):
163+
pn_client = PushNotifications(
164+
'INSTANCE_ID',
165+
'SECRET_KEY'
166+
)
167+
with self.assertRaises(ValueError):
168+
pn_client.publish(
169+
interests=[],
170+
publish_body={
171+
'apns': {
172+
'aps': {
173+
'alert': 'Hello World!',
174+
},
175+
},
176+
},
177+
)
178+
179+
def test_publish_should_fail_if_interest_not_a_string(self):
180+
pn_client = PushNotifications(
181+
'INSTANCE_ID',
182+
'SECRET_KEY'
183+
)
184+
with self.assertRaises(TypeError):
185+
pn_client.publish(
186+
interests=[False],
187+
publish_body={
188+
'apns': {
189+
'aps': {
190+
'alert': 'Hello World!',
191+
},
192+
},
193+
},
194+
)
195+
196+
def test_publish_should_fail_if_interest_too_long(self):
197+
pn_client = PushNotifications(
198+
'INSTANCE_ID',
199+
'SECRET_KEY'
200+
)
201+
with self.assertRaises(ValueError):
202+
pn_client.publish(
203+
interests=['A'*200],
204+
publish_body={
205+
'apns': {
206+
'aps': {
207+
'alert': 'Hello World!',
208+
},
209+
},
210+
},
211+
)
212+
213+
def test_publish_should_fail_if_interest_contains_invalid_chars(self):
214+
pn_client = PushNotifications(
215+
'INSTANCE_ID',
216+
'SECRET_KEY'
217+
)
218+
with self.assertRaises(ValueError):
219+
pn_client.publish(
220+
interests=['bad-interest'],
221+
publish_body={
222+
'apns': {
223+
'aps': {
224+
'alert': 'Hello World!',
225+
},
226+
},
227+
},
228+
)
229+
with self.assertRaises(ValueError):
230+
pn_client.publish(
231+
interests=['bad(interest)'],
232+
publish_body={
233+
'apns': {
234+
'aps': {
235+
'alert': 'Hello World!',
236+
},
237+
},
238+
},
239+
)
240+
241+
def test_publish_should_raise_on_http_4xx_error(self):
242+
pn_client = PushNotifications(
243+
'INSTANCE_ID',
244+
'SECRET_KEY'
245+
)
246+
with requests_mock.Mocker() as http_mock:
247+
http_mock.register_uri(
248+
requests_mock.ANY,
249+
requests_mock.ANY,
250+
status_code=400,
251+
json={'error': 'Invalid request', 'description': 'blah'},
252+
)
253+
with self.assertRaises(PusherValidationError):
254+
pn_client.publish(
255+
interests=['donuts'],
256+
publish_body={
257+
'apns': {
258+
'aps': {
259+
'alert': 'Hello World!',
260+
},
261+
},
262+
},
263+
)
264+
265+
def test_publish_should_raise_on_http_5xx_error(self):
266+
pn_client = PushNotifications(
267+
'INSTANCE_ID',
268+
'SECRET_KEY'
269+
)
270+
with requests_mock.Mocker() as http_mock:
271+
http_mock.register_uri(
272+
requests_mock.ANY,
273+
requests_mock.ANY,
274+
status_code=500,
275+
json={'error': 'Server error', 'description': 'blah'},
276+
)
277+
with self.assertRaises(PusherServerError):
278+
pn_client.publish(
279+
interests=['donuts'],
280+
publish_body={
281+
'apns': {
282+
'aps': {
283+
'alert': 'Hello World!',
284+
},
285+
},
286+
},
287+
)
116288

117-
def test_publish_should_fail_if_interests_not_list(self):
118-
pn_client = PushNotifications(
119-
'INSTANCE_ID',
120-
'SECRET_KEY'
289+
def test_publish_should_raise_on_http_401_error(self):
290+
pn_client = PushNotifications(
291+
'INSTANCE_ID',
292+
'SECRET_KEY'
293+
)
294+
with requests_mock.Mocker() as http_mock:
295+
http_mock.register_uri(
296+
requests_mock.ANY,
297+
requests_mock.ANY,
298+
status_code=401,
299+
json={'error': 'Auth error', 'description': 'blah'},
121300
)
122-
with self.assertRaises(TypeError):
301+
with self.assertRaises(PusherAuthError):
123302
pn_client.publish(
124-
interests=False,
303+
interests=['donuts'],
125304
publish_body={
126305
'apns': {
127306
'aps': {
@@ -131,13 +310,26 @@ def test_publish_should_fail_if_interests_not_list(self):
131310
},
132311
)
133312

134-
def test_publish_should_fail_if_body_not_dict(self):
135-
pn_client = PushNotifications(
136-
'INSTANCE_ID',
137-
'SECRET_KEY'
313+
def test_publish_should_raise_on_http_404_error(self):
314+
pn_client = PushNotifications(
315+
'INSTANCE_ID',
316+
'SECRET_KEY'
317+
)
318+
with requests_mock.Mocker() as http_mock:
319+
http_mock.register_uri(
320+
requests_mock.ANY,
321+
requests_mock.ANY,
322+
status_code=404,
323+
json={'error': 'Instance not found', 'description': 'blah'},
138324
)
139-
with self.assertRaises(TypeError):
325+
with self.assertRaises(PusherMissingInstanceError):
140326
pn_client.publish(
141327
interests=['donuts'],
142-
publish_body=False,
328+
publish_body={
329+
'apns': {
330+
'aps': {
331+
'alert': 'Hello World!',
332+
},
333+
},
334+
},
143335
)

0 commit comments

Comments
 (0)