8000 Fixes after manual testing, add tests to validation client · Muix2015/twilio-python@fed7324 · GitHub
[go: up one dir, main page]

Skip to content

Commit fed7324

Browse files
committed
Fixes after manual testing, add tests to validation client
1 parent 77cb421 commit fed7324

File tree

5 files changed

+161
-28
lines changed

5 files changed

+161
-28
lines changed

tests/unit/http/__init__.py

Whitespace-only changes.
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import unittest
2+
3+
import mock
4+
from mock import patch, Mock
5+
from requests import Request
6+
from requests import Session
7+
8+
from twilio.http.validation_client import ValidationClient
9+
10+
11+
class TestValidationClientHelpers(unittest.TestCase):
12+
def setUp(self):
13+
self.request = Request(
14+
'GET',
15+
'https://api.twilio.com/2010-04-01/Accounts/AC123/Messages',
16+
auth=('Username', 'Password'),
17+
)
18+
self.request = self.request.prepare()
19+
self.client = ValidationClient('AC123', 'SK123', 'CR123', 'private_key')
20+
21+
def test_build_validation_payload_basic(self):
22+
validation_payload = self.client._build_validation_payload(self.request)
23+
self.assertEqual('GET', validation_payload.method)
24+
self.assertEqual('/2010-04-01/Accounts/AC123/Messages', validation_payload.path)
25+
self.assertEqual('', validation_payload.query_string)
26+
self.assertEqual(['authorization', 'host'], validation_payload.signed_headers)
27+
self.assertEqual('', validation_payload.body)
28+
29+
def test_build_validation_payload_query_string_parsed(self):
30+
self.request.url = self.request.url + '?QueryParam=1&Other=true'
31+
32+
validation_payload = self.client._build_validation_payload(self.request)
33+
34+
self.assertEqual('GET', validation_payload.method)
35+
self.assertEqual('/2010-04-01/Accounts/AC123/Messages', validation_payload.path)
36+
self.assertEqual('QueryParam=1& 6D40 ;Other=true', validation_payload.query_string)
37+
self.assertEqual(['authorization', 'host'], validation_payload.signed_headers)
38+
self.assertEqual('', validation_payload.body)
39+
40+
def test_build_validation_payload_body_parsed(self):
41+
self.request.body = 'foobar'
42+
43+
validation_payload = self.client._build_validation_payload(self.request)
44+
45+
self.assertEqual('GET', validation_payload.method)
46+
self.assertEqual('/2010-04-01/Accounts/AC123/Messages', validation_payload.path)
47+
self.assertEqual('', validation_payload.query_string)
48+
self.assertEqual(['authorization', 'host'], validation_payload.signed_headers)
49+
self.assertEqual('foobar', validation_payload.body)
50+
51+
def test_build_validation_payload_complex(self):
52+
self.request.body = 'foobar'
53+
self.request.url = self.request.url + '?WestCoast=BestCoast&EastCoast=LeastCoast'
54+
55+
validation_payload = self.client._build_validation_payload(self.request)
56+
57+
self.assertEqual('GET', validation_payload.method)
58+
self.assertEqual('/2010-04-01/Accounts/AC123/Messages', validation_payload.path)
59+
self.assertEqual(['authorization', 'host'], validation_payload.signed_headers)
60+
self.assertEqual('foobar', validation_payload.body)
61+
self.assertEqual('WestCoast=BestCoast&EastCoast=LeastCoast',
62+
validation_payload.query_string)
63+
64+
def test_get_host(self):
65+
self.assertEqual('api.twilio.com', self.client._get_host(self.request))
66+
67+
68+
class TestValidationClientRequest(unittest.TestCase):
69+
70+
def setUp(self):
71+
self.session_patcher = patch('twilio.http.validation_client.Session')
72+
self.jwt_patcher = patch('twilio.http.validation_client.ClientValidationJwt')
73+
74+
self.session_mock = Mock(wraps=Session())
75+
self.validation_token = self.jwt_patcher.start()
76+
self.request_mock = Mock()
77+
78+
self.session_mock.prepare_request.return_value = self.request_mock
79+
self.session_mock.send.return_value = Mock(status_code=200, content='test')
80+
self.validation_token.return_value.to_jwt.return_value = 'test-token'
81+
self.request_mock.headers = {}
82+
83+
session_constructor_mock = self.session_patcher.start()
84+
session_constructor_mock.return_value = self.session_mock
85+
86+
self.client = ValidationClient('AC123', 'SK123', 'CR123', 'private_key')
87+
88+
def tearDown(self):
89+
self.session_patcher.stop()
90+
self.jwt_patcher.stop()
91+
92+
def test_request_does_not_overwrite_host_header(self):
93+
self.request_mock.url = 'https://api.twilio.com/'
94+
95+
self.client.request('doesnt matter', 'doesnt matter')
96+
97+
self.assertEqual('api.twilio.com', self.request_mock.headers['Host'])
98+
self.assertEqual('test-token', self.request_mock.headers['Twilio-Client-Validation'])
99+
100+
def test_request_sets_host_header_if_missing(self):
101+
self.request_mock.url = 'https://api.twilio.com/'
102+
self.request_mock.headers = {'Host': 'other.twilio.com'}
103+
104+
self.client.request('doesnt matter', 'doesnt matter')
105+
106+
self.assertEqual('other.twilio.com', self.request_mock.headers['Host'])
107+
self.assertEqual('test-token', self.request_mock.headers['Twilio-Client-Validation'])

tests/unit/jwt/test_client_validation.py

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ class ClientValidationJwtTest(unittest.TestCase):
1919
def test_generate_payload_basic(self):
2020
vp = ValidationPayload(
2121
method='GET',
22-
url='https://api.twilio.com/',
22+
path='https://api.twilio.com/',
2323
query_string='q1=v1',
2424
signed_headers=['headerb', 'headera'],
2525
all_headers={'head': 'toe', 'headera': 'vala', 'headerb': 'valb'},
@@ -47,7 +47,7 @@ def test_generate_payload_basic(self):
4747
def test_generate_payload_complex(self):
4848
vp = ValidationPayload(
4949
method='GET',
50-
url='https://api.twilio.com/',
50+
path='https://api.twilio.com/',
5151
query_string='q1=v1&q2=v2&a=b',
5252
signed_headers=['headerb', 'headera'],
5353
all_headers={'head': 'toe', 'Headerb': 'valb', 'yeezy': 'weezy'},
@@ -74,7 +74,7 @@ def test_generate_payload_complex(self):
7474
def test_generate_payload_no_query_string(self):
7575
vp = ValidationPayload(
7676
method='GET',
77-
url='https://api.twilio.com/',
77+
path='https://api.twilio.com/',
7878
query_string='',
7979
signed_headers=['headerb', 'headera'],
8080
all_headers={'head': 'toe', 'Headerb': 'valb', 'yeezy': 'weezy'},
@@ -101,11 +101,11 @@ def test_generate_payload_no_query_string(self):
101101
def test_generate_payload_no_req_body(self):
102102
vp = ValidationPayload(
103103
method='GET',
104-
url='https://api.twilio.com/',
104+
path='https://api.twilio.com/',
105105
query_string='q1=v1',
106106
signed_headers=['headerb', 'headera'],
107107
all_headers={'head': 'toe', 'headera': 'vala', 'headerb': 'valb'},
108-
body=None
108+
body=''
109109
)
110110

111111
expected_payload = '\n'.join([
@@ -116,6 +116,7 @@ def test_generate_payload_no_req_body(self):
116116
'headerb:valb',
117117
'',
118118
'headera;headerb',
119+
''
119120
])
120121
expected_payload = ClientValidationJwt._hash(expected_payload)
121122

@@ -128,7 +129,7 @@ def test_generate_payload_no_req_body(self):
128129
def test_generate_payload_header_keys_lowercased(self):
129130
vp = ValidationPayload(
130131
method='GET',
131-
url='https://api.twilio.com/',
132+
path='https://api.twilio.com/',
132133
query_string='q1=v1',
133134
signed_headers=['headerb', 'headera'],
134135
all_headers={'head': 'toe', 'Headera': 'vala', 'Headerb': 'valb'},
@@ -156,7 +157,7 @@ def test_generate_payload_header_keys_lowercased(self):
156157
def test_generate_payload_no_headers(self):
157158
vp = ValidationPayload(
158159
method='GET',
159-
url='https://api.twilio.com/',
160+
path='https://api.twilio.com/',
160161
query_string='q1=v1',
161162
signed_headers=['headerb', 'headera'],
162163
all_headers={},
@@ -179,11 +180,11 @@ def test_generate_payload_no_headers(self):
179180
self.assertEqual('headera;headerb', actual_payload['hrh'])
180181
self.assertEqual(expected_payload, actual_payload['rqh'])
181182

182-
def test_generate_payload_schema_correct(self):
183+
def test_generate_payload_schema_correct_1(self):
183184
"""Test against a known good rqh payload hash"""
184185
vp = ValidationPayload(
185186
method='GET',
186-
url='/Messages',
187+
path='/Messages',
187188
query_string='PageSize=5&Limit=10',
188189
signed_headers=['authorization', 'host'],
189190
all_headers={'authorization': 'foobar', 'host': 'api.twilio.com'},
@@ -198,10 +199,29 @@ def test_generate_payload_schema_correct(self):
198199
self.assertEqual('authorization;host', actual_payload['hrh'])
199200
self.assertEqual(expected_hash, actual_payload['rqh'])
200201

202+
def test_generate_payload_schema_correct_2(self):
203+
"""Test against a known good rqh payload hash"""
204+
vp = ValidationPayload(
205+
method='POST',
206+
path='/Messages',
207+
query_string='',
208+
signed_headers=['authorization', 'host'],
209+
all_headers={'authorization': 'foobar', 'host': 'api.twilio.com'},
210+
body='testbody'
211+
)
212+
213+
expected_hash = 'bd792c967c20d546c738b94068f5f72758a10d26c12979677501e1eefe58c65a'
214+
215+
jwt = ClientValidationJwt('AC123', 'SK123', 'CR123', 'secret', vp)
216+
217+
actual_payload = jwt._generate_payload()
218+
self.assertEqual('authorization;host', actual_payload['hrh'])
219+
self.assertEqual(expected_hash, actual_payload['rqh'])
220+
201221
def test_jwt_payload(self):
202222
vp = ValidationPayload(
203223
method='GET',
204-
url='/Messages',
224+
path='/Messages',
205225
query_string='PageSize=5&Limit=10',
206226
signed_headers=['authorization', 'host'],
207227
all_headers={'authorization': 'foobar', 'host': 'api.twilio.com'},
@@ -229,7 +249,7 @@ def test_jwt_payload(self):
229249
def test_jwt_signing(self):
230250
vp = ValidationPayload(
231251
method='GET',
232-
url='/Messages',
252+
path='/Messages',
233253
query_string='PageSize=5&Limit=10',
234254
signed_headers=['authorization', 'host'],
235255
all_headers={'authorization': 'foobar', 'host': 'api.twilio.com'},

twilio/http/validation_client.py

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from urlparse import urlparse
2+
13
from collections import namedtuple
24

35
from requests import Request, Session
@@ -7,7 +9,7 @@
79
from twilio.jwt.validation import ClientValidationJwt
810

911

10-
ValidationPayload = namedtuple('ValidationPayload', ['method', 'url', 'query_string', 'all_headers',
12+
ValidationPayload = namedtuple('ValidationPayload', ['method', 'path', 'query_string', 'all_headers',
1113
'signed_headers', 'body'])
1214

1315

@@ -54,7 +56,10 @@ def request(self, method, url, params=None, data=None, headers=None, auth=None,
5456
request = Request(method.upper(), url, params=params, data=data, headers=headers, auth=auth)
5557
prepared_request = session.prepare_request(request)
5658

57-
validation_payload = self.__build_validation_payload(prepared_request)
59+
if 'Host' not in prepared_request.headers and 'host' not in prepared_request.headers:
60+
prepared_request.headers['Host'] = self._get_host(prepared_request)
61+
62+
validation_payload = self._build_validation_payload(prepared_request)
5863
jwt = ClientValidationJwt(self.account_sid, self.api_key_sid, self.credential_sid,
5964
self.private_key, validation_payload)
6065
prepared_request.headers['Twilio-Client-Validation'] = jwt.to_jwt()
@@ -67,23 +72,26 @@ def request(self, method, url, params=None, data=None, headers=None, auth=None,
6772

6873
return Response(int(response.status_code), response.content.decode('utf-8'))
6974

70-
def __build_validation_payload(self, request):
75+
def _build_validation_payload(self, request):
7176
"""
7277
Extract relevant information from request to build a ClientValidationJWT
7378
:param PreparedRequest request: request we will extract information from.
7479
:return: ValidationPayload
7580
"""
76-
try:
77-
url, query_string = request.url.split('?', 1)
78-
except ValueError:
79-
url = request.url
80-
query_string = ''
81+
parsed = urlparse(request.url)
82+
path = parsed.path
83+
query_string = parsed.query or ''
8184

8285
return ValidationPayload(
8386
method=request.method,
84-
url=url,
87+
path=path,
8588
query_string=query_string,
8689
all_headers=request.headers,
8790
signed_headers=ValidationClient.__SIGNED_HEADERS,
88-
body=request.body
91+
body=request.body or ''
8992
)
93+
94+
def _get_host(self, request):
95+
"""Pull the Host out of the request"""
96+
parsed = urlparse(request.url)
97+
return parsed.netloc

twilio/jwt/validation/__init__.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -48,23 +48,21 @@ def _generate_payload(self):
4848
query_string = self.validation_payload.query_string.split('&')
4949
query_string = self._sort_and_join(query_string, '&')
5050

51-
req_body_hash = self._hash(self.validation_payload.body)
51+
req_body_hash = self._hash(self.validation_payload.body) or ''
5252

5353
signed_headers_str = ';'.join(signed_headers)
5454

5555
signed_payload = [
5656
self.validation_payload.method,
57-
self.validation_payload.url,
57+
self.validation_payload.path,
5858
query_string,
5959
]
6060

6161
if headers_str:
6262
signed_payload.append(headers_str)
6363
signed_payload.append('')
6464
signed_payload.append(signed_headers_str)
65-
66-
if req_body_hash:
67-
signed_payload.append(req_body_hash)
65+
signed_payload.append(req_body_hash)
6866

6967
signed_payload = '\n'.join(signed_payload)
7068

@@ -81,8 +79,8 @@ def _sort_and_join(self, values, joiner):
8179

8280
@classmethod
8381
def _hash(self, input_str):
84-
if input_str is None:
85-
return None
82+
if not input_str:
83+
return input_str
8684

8785
if not isinstance(input_str, bytes):
8886
input_str = input_str.encode('utf-8')

0 commit comments

Comments
 (0)
0