Skip to content

Commit 90d5ffe

Browse files
committed
Some but not all unit tests refactored; webook validation and authentication needs sorting out.
1 parent df0402c commit 90d5ffe

File tree

11 files changed

+441
-398
lines changed

11 files changed

+441
-398
lines changed

pusher/aiohttp.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# -*- coding: utf-8 -*-
22

3+
from __future__ import (
4+
print_function,
5+
unicode_literals,
6+
absolute_import,
7+
division)
8+
39
import aiohttp
410
import asyncio
511

pusher/errors.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# -*- coding: utf-8 -*-
22

3+
from __future__ import (
4+
print_function,
5+
unicode_literals,
6+
absolute_import,
7+
division)
8+
9+
310
class PusherError(Exception):
411
pass
512

pusher/pusher.py

Lines changed: 8 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -136,73 +136,16 @@ def users_info(self, channel):
136136
return self._pusher_client.users_info(channel)
137137

138138

139-
@doc_string(NotificationClient.notify.__doc__)
140-
def notify(self, interest, notification):
141-
return self._notification_client.notify(interest, notification)
142-
143-
139+
@doc_string(PusherClient.authenticate.__doc__)
144140
def authenticate(self, channel, socket_id, custom_data=None):
145-
"""Used to generate delegated client subscription token.
146-
147-
:param channel: name of the channel to authorize subscription to
148-
:param socket_id: id of the socket that requires authorization
149-
:param custom_data: used on presence channels to provide user info
150-
"""
151-
channel = validate_channel(channel)
152-
153-
if not channel_name_re.match(channel):
154-
raise ValueError('Channel should be a valid channel, got: %s' % channel)
155-
156-
socket_id = validate_socket_id(socket_id)
157-
158-
if custom_data:
159-
custom_data = json.dumps(custom_data, cls=self._json_encoder)
160-
161-
string_to_sign = "%s:%s" % (socket_id, channel)
162-
163-
if custom_data:
164-
string_to_sign += ":%s" % custom_data
165-
166-
signature = sign(self.secret, string_to_sign)
141+
return self.pusher_client.authenticate(channel, socket_id, custom_data)
167142

168-
auth = "%s:%s" % (self.key, signature)
169-
result = {'auth': auth}
170143

171-
if custom_data:
172-
result['channel_data'] = custom_data
144+
@doc_string(PusherClient.validate_webhook.__doc__)
145+
def authenticate(self, key, signature, body):
146+
return self.pusher_client.validate_webhook(key, signature, body)
173147

174-
return result
175148

176-
177-
def validate_webhook(self, key, signature, body):
178-
"""Used to validate incoming webhook messages. When used it guarantees
179-
that the sender is Pusher and not someone else impersonating it.
180-
181-
:param key: key used to sign the body
182-
:param signature: signature that was given with the body
183-
:param body: content that needs to be verified
184-
"""
185-
key = ensure_text(key, "key")
186-
signature = ensure_text(signature, "signature")
187-
body = ensure_text(body, "body")
188-
189-
if key != self.key:
190-
return None
191-
192-
if not verify(self.secret, body, signature):
193-
return None
194-
195-
try:
196-
body_data = json.loads(body, cls=self._json_decoder)
197-
198-
except ValueError:
199-
return None
200-
201-
time_ms = body_data.get('time_ms')
202-
if not time_ms:
203-
return None
204-
205-
if abs(time.time()*1000 - time_ms) > 300000:
206-
return None
207-
208-
return body_data
149+
@doc_string(NotificationClient.notify.__doc__)
150+
def notify(self, interest, notification):
151+
return self._notification_client.notify(interest, notification)

pusher/pusher_client.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,3 +147,70 @@ def users_info(self, channel):
147147

148148
return Request(
149149
self, GET, "/apps/%s/channels/%s/users" % (self.app_id, channel))
150+
151+
152+
def authenticate(self, channel, socket_id, custom_data=None):
153+
"""Used to generate delegated client subscription token.
154+
155+
:param channel: name of the channel to authorize subscription to
156+
:param socket_id: id of the socket that requires authorization
157+
:param custom_data: used on presence channels to provide user info
158+
"""
159+
channel = validate_channel(channel)
160+
161+
if not channel_name_re.match(channel):
162+
raise ValueError('Channel should be a valid channel, got: %s' % channel)
163+
164+
socket_id = validate_socket_id(socket_id)
165+
166+
if custom_data:
167+
custom_data = json.dumps(custom_data, cls=self._json_encoder)
168+
169+
string_to_sign = "%s:%s" % (socket_id, channel)
170+
171+
if custom_data:
172+
string_to_sign += ":%s" % custom_data
173+
174+
signature = sign(self.secret, string_to_sign)
175+
176+
auth = "%s:%s" % (self.key, signature)
177+
result = {'auth': auth}
178+
179+
if custom_data:
180+
result['channel_data'] = custom_data
181+
182+
return result
183+
184+
185+
def validate_webhook(self, key, signature, body):
186+
"""Used to validate incoming webhook messages. When used it guarantees
187+
that the sender is Pusher and not someone else impersonating it.
188+
189+
:param key: key used to sign the body
190+
:param signature: signature that was given with the body
191+
:param body: content that needs to be verified
192+
"""
193+
key = ensure_text(key, "key")
194+
signature = ensure_text(signature, "signature")
195+
body = ensure_text(body, "body")
196+
197+
if key != self.key:
198+
return None
199+
200+
if not verify(self.secret, body, signature):
201+
return None
202+
203+
try:
204+
body_data = json.loads(body, cls=self._json_decoder)
205+
206+
except ValueError:
207+
return None
208+
209+
time_ms = body_data.get('time_ms')
210+
if not time_ms:
211+
return None
212+
213+
if abs(time.time()*1000 - time_ms) > 300000:
214+
return None
215+
216+
return body_data

pusher/signature.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# -*- coding: utf-8 -*-
22

3+
from __future__ import (
4+
print_function,
5+
unicode_literals,
6+
absolute_import,
7+
division)
8+
39
import hashlib
410
import hmac
511
import six

pusher_tests/test_client.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# -*- coding: utf-8 -*-
2+
3+
from __future__ import (
4+
print_function,
5+
unicode_literals,
6+
absolute_import,
7+
division)
8+
9+
import hashlib
10+
import hmac
11+
import os
12+
import six
13+
import unittest
14+
15+
from pusher.client import Client
16+
17+
try:
18+
import unittest.mock as mock
19+
except ImportError:
20+
import mock
21+
22+
23+
class TestClient(unittest.TestCase):
24+
def test_should_be_constructable(self):
25+
Client(app_id=u'4', key=u'key', secret=u'secret', ssl=False)
26+
27+
def test_app_id_should_be_text_if_present(self):
28+
self.assertRaises(TypeError, lambda: Client(app_id=4, key=u'key', secret=u'secret', ssl=False))
29+
30+
def test_key_should_be_text_if_present(self):
31+
self.assertRaises(TypeError, lambda: Client(app_id=u'4', key=4, secret=u'secret', ssl=False))
32+
33+
def test_secret_should_be_text_if_present(self):
34+
self.assertRaises(TypeError, lambda: Client(app_id=u'4', key=u'key', secret=4, ssl=False))
35+
36+
def test_ssl_should_be_boolean(self):
37+
Client(app_id=u'4', key=u'key', secret=u'secret', ssl=False)
38+
Client(app_id=u'4', key=u'key', secret=u'secret', ssl=True)
39+
40+
self.assertRaises(TypeError, lambda: Client(app_id=u'4', key=u'key', secret=u'secret', ssl=4))
41+
42+
def test_port_should_be_number(self):
43+
Client(app_id=u'4', key=u'key', secret=u'secret', ssl=True, port=400)
44+
45+
self.assertRaises(TypeError, lambda: Client(app_id=u'4', key=u'key', secret=u'secret', ssl=True, port=u'400'))
46+
47+
def test_port_behaviour(self):
48+
conf = Client(app_id=u'4', key=u'key', secret=u'secret', ssl=True)
49+
self.assertEqual(conf.port, 443, u'port should be 443 for ssl')
50+
51+
conf = Client(app_id=u'4', key=u'key', secret=u'secret', ssl=False)
52+
self.assertEqual(conf.port, 80, u'port should be 80 for non ssl')
53+
54+
conf = Client(app_id=u'4', key=u'key', secret=u'secret', ssl=False, port=4000)
55+
self.assertEqual(conf.port, 4000, u'the port setting override the default')
56+
57+
58+
if __name__ == '__main__':
59+
unittest.main()

pusher_tests/test_config.py

Lines changed: 0 additions & 94 deletions
This file was deleted.

pusher_tests/test_notification_client.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ def setUp(self):
2323
}
2424
}
2525

26+
def test_host_should_be_text(self):
27+
NotificationClient(app_id=u'4', key=u'key', secret=u'secret', ssl=True, host=u'foo')
28+
29+
self.assertRaises(TypeError, lambda: NotificationClient(app_id=u'4', key=u'key', secret=u'secret', ssl=True, host=4))
30+
2631
def test_notify_success_case(self):
2732
request = self.client.notify.make_request(['yolo'], self.success_fixture)
2833
self.assertEqual(request.method, u'POST')

0 commit comments

Comments
 (0)