Merge lp:~elachuni/piston-mini-client/oauthlib-fix into lp:piston-mini-client

Proposed by Anthony Lenton
Status: Merged
Approved by: Barry Warsaw
Approved revision: 71
Merged at revision: 70
Proposed branch: lp:~elachuni/piston-mini-client/oauthlib-fix
Merge into: lp:piston-mini-client
Diff against target: 257 lines (+82/-18)
7 files modified
.bzrignore (+1/-0)
piston_mini_client/__init__.py (+1/-1)
piston_mini_client/auth.py (+7/-2)
piston_mini_client/tests/test_auth.py (+59/-1)
piston_mini_client/tests/test_failhandlers.py (+1/-1)
piston_mini_client/tests/test_log_to_file.py (+1/-1)
piston_mini_client/tests/test_resource.py (+12/-12)
To merge this branch: bzr merge lp:~elachuni/piston-mini-client/oauthlib-fix
Reviewer Review Type Date Requested Status
Barry Warsaw Approve
Review via email: mp+149931@code.launchpad.net

Commit message

Fixed two issues related to OAuth authentication with oauthlib.

Description of the change

This branch fixes two issues related to OAuth authentication with oauthlib:
 - oauthlib refuses to sign requests with a urlencoded body if the Content-Type header isn't set to 'application/x-www-form-urlencoded'. This check is case-sensitive, so the header can't be 'Content-type' or 'content-type'. Submitted this issue as https://github.com/idan/oauthlib/issues/121

 - oauthlib also refuses to sign requests with a Content-Type header set to 'application/x-www-form-urlencoded' if the body is None. Submitted this issue as https://github.com/idan/oauthlib/issues/122 thinking that the issue was if the body was blank (''), but it turns out that it *only* works with '' iff Content-Type is 'application/x-www-form-urlencoded', otherwise it needs to be None.

To post a comment you must log in.
Revision history for this message
Barry Warsaw (barry) wrote :

<barry> one suggestion: use example.{com,net,org} urls instead of
        bathsandco.com since the formers are actually reserved for exactly
        that purpose (i.e. they will never resolve)
<achuni> true [16:31]
<achuni> will do, even though we're mocking out httplib and will never attempt
         to resolve
<achuni> httplib2
* barry nods

review: Approve
72. By Anthony Lenton

Use example.com for tests, and work with 0.3.0

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file '.bzrignore'
2--- .bzrignore 2012-06-13 13:25:54 +0000
3+++ .bzrignore 2013-02-21 21:37:22 +0000
4@@ -5,5 +5,6 @@
5 .tox
6 build/
7 dist/
8+piston_mini_client.egg-info/
9 piston_mini_client/__pycache__/
10 piston_mini_client/tests/__pycache__/
11
12=== modified file 'piston_mini_client/__init__.py'
13--- piston_mini_client/__init__.py 2013-02-01 14:25:10 +0000
14+++ piston_mini_client/__init__.py 2013-02-21 21:37:22 +0000
15@@ -371,7 +371,7 @@
16 """
17 headers = {}
18 if content_type:
19- headers['Content-type'] = content_type
20+ headers['Content-Type'] = content_type
21 if self._extra_headers is not None:
22 headers.update(self._extra_headers)
23 if extra_headers is not None:
24
25=== modified file 'piston_mini_client/auth.py'
26--- piston_mini_client/auth.py 2012-11-21 15:37:34 +0000
27+++ piston_mini_client/auth.py 2013-02-21 21:37:22 +0000
28@@ -43,10 +43,15 @@
29 # would treat the empty string as "no body", but python-oauthlib
30 # requires None.
31 if not body:
32- body = None
33+ content_type = headers.get('Content-Type')
34+ if content_type == 'application/x-www-form-urlencoded':
35+ body = ''
36+ else:
37+ body = None
38 # Import oauthlib here so that you don't need it if you're not going
39 # to use it. Plan B: move this out into a separate oauth module.
40- from oauthlib.oauth1 import Client, SIGNATURE_PLAINTEXT
41+ from oauthlib.oauth1 import Client
42+ from oauthlib.oauth1.rfc5849 import SIGNATURE_PLAINTEXT
43 oauth_client = Client(self.consumer_key, self.consumer_secret,
44 self.token_key, self.token_secret,
45 signature_method=SIGNATURE_PLAINTEXT,
46
47=== modified file 'piston_mini_client/tests/test_auth.py'
48--- piston_mini_client/tests/test_auth.py 2013-02-01 14:25:10 +0000
49+++ piston_mini_client/tests/test_auth.py 2013-02-21 21:37:22 +0000
50@@ -2,8 +2,11 @@
51 # Copyright 2010-2012 Canonical Ltd. This software is licensed under the
52 # GNU Lesser General Public License version 3 (see the file LICENSE).
53
54+from mock import patch
55+from unittest import TestCase
56+from piston_mini_client import PistonAPI, returns_json
57+from piston_mini_client.validators import oauth_protected
58 from piston_mini_client.auth import OAuthAuthorizer, BasicAuthorizer
59-from unittest import TestCase
60
61
62 class BasicAuthorizerTestCase(TestCase):
63@@ -33,3 +36,58 @@
64 auth.sign_request(url=url, method='GET', body='', headers=headers)
65 self.assertTrue('Authorization' in headers)
66 self.assertTrue(headers['Authorization'].startswith('OAuth '))
67+
68+ @patch('httplib2.Http.request')
69+ def test_body_is_signed_if_urlencoded(self, mock_request):
70+ formencoded = 'application/x-www-form-urlencoded'
71+
72+ class BathAPI(PistonAPI):
73+ default_content_type = formencoded
74+ default_service_root = 'http://example.com'
75+
76+ @returns_json
77+ @oauth_protected
78+ def soak(self):
79+ return self._post('/soak/', data={'time': 900})
80+
81+ mock_request.return_value = {'status': '200'}, '"done"'
82+
83+ auth = OAuthAuthorizer('tkey', 'tsecret', 'ckey', 'csecret')
84+ api = BathAPI(auth=auth)
85+
86+ response = api.soak()
87+
88+ self.assertEqual('done', response)
89+ self.assertEqual(1, mock_request.call_count)
90+ args, kwargs = mock_request.call_args
91+ self.assertEqual(formencoded, kwargs['headers']['Content-Type'])
92+ auth_header = kwargs['headers']['Authorization']
93+ self.assertTrue(auth_header.startswith('OAuth '))
94+
95+ @patch('httplib2.Http.request')
96+ def test_post_works_with_no_body(self, mock_request):
97+ cases = [
98+ 'application/x-www-form-urlencoded',
99+ 'application/json',
100+ ]
101+
102+ class ShowerAPI(PistonAPI):
103+ default_service_root = 'http://example.com'
104+
105+ @returns_json
106+ @oauth_protected
107+ def soak(self):
108+ return self._post('/noop/', data='')
109+ auth = OAuthAuthorizer('tkey', 'tsecret', 'ckey', 'csecret')
110+ for content_type in cases:
111+ mock_request.return_value = {'status': '200'}, '"done"'
112+ api = ShowerAPI(auth=auth)
113+ api.default_content_type = content_type
114+
115+ response = api.soak()
116+
117+ self.assertEqual('done', response)
118+ args, kwargs = mock_request.call_args
119+ self.assertEqual(content_type, kwargs['headers']['Content-Type'])
120+ auth_header = kwargs['headers']['Authorization']
121+ self.assertTrue(auth_header.startswith('OAuth '))
122
123=== modified file 'piston_mini_client/tests/test_failhandlers.py'
124--- piston_mini_client/tests/test_failhandlers.py 2012-09-20 17:48:10 +0000
125+++ piston_mini_client/tests/test_failhandlers.py 2013-02-21 21:37:22 +0000
126@@ -234,7 +234,7 @@
127 mock_request.return_value = self.response, self.body
128 self.expected['method'] = 'POST'
129 self.expected['request_body'] = '{"plants": "all"}'
130- self.expected['request_headers'] = {'Content-type': 'application/json'}
131+ self.expected['request_headers'] = {'Content-Type': 'application/json'}
132 self.expected['url'] = 'http://localhost:12345/grow'
133
134 self.assertEqual(self.expected, self.api.grow())
135
136=== modified file 'piston_mini_client/tests/test_log_to_file.py'
137--- piston_mini_client/tests/test_log_to_file.py 2012-11-27 16:39:05 +0000
138+++ piston_mini_client/tests/test_log_to_file.py 2013-02-21 21:37:22 +0000
139@@ -41,7 +41,7 @@
140 self.assertTrue(lines[0].endswith(
141 'Request: POST http://test.info/api/1.0/poke/'))
142 self.assertEqual(
143- set(['Content-type: application/json',
144+ set(['Content-Type: application/json',
145 'location: ribs', '', '[1, 2, 3]']),
146 set(lines[1:5]))
147 self.assertTrue(lines[5].endswith('Response: 201'))
148
149=== modified file 'piston_mini_client/tests/test_resource.py'
150--- piston_mini_client/tests/test_resource.py 2012-07-30 17:09:26 +0000
151+++ piston_mini_client/tests/test_resource.py 2013-02-21 21:37:22 +0000
152@@ -97,7 +97,7 @@
153 api = self.CoffeeAPI()
154 api._post('/serve', data={'foo': 'bar'})
155 kwargs = mock_request.call_args[1]
156- self.assertEqual(kwargs['headers']['Content-type'], 'application/json')
157+ self.assertEqual(kwargs['headers']['Content-Type'], 'application/json')
158 self.assertEqual(kwargs['method'], 'POST')
159
160 @patch('httplib2.Http.request')
161@@ -109,7 +109,7 @@
162 api = self.CoffeeAPI()
163 api._post('/serve', data=MyCoffeeRequest(strength='mild'))
164 kwargs = mock_request.call_args[1]
165- self.assertEqual(kwargs['headers']['Content-type'], 'application/json')
166+ self.assertEqual(kwargs['headers']['Content-Type'], 'application/json')
167 self.assertEqual(kwargs['method'], 'POST')
168
169 @patch('httplib2.Http.request')
170@@ -119,7 +119,7 @@
171 api._post('/serve', data={'foo': 'bar'},
172 content_type='application/x-www-form-urlencoded')
173 kwargs = mock_request.call_args[1]
174- self.assertEqual(kwargs['headers']['Content-type'],
175+ self.assertEqual(kwargs['headers']['Content-Type'],
176 'application/x-www-form-urlencoded')
177 self.assertEqual(kwargs['method'], 'POST')
178
179@@ -205,7 +205,7 @@
180 api._post('/foo', scheme='https')
181 mock_request.assert_called_with(
182 'https://localhost:12345/foo',
183- body='null', headers={'Content-type': 'application/json'},
184+ body='null', headers={'Content-Type': 'application/json'},
185 method='POST')
186
187 @patch('httplib2.Http.request')
188@@ -223,7 +223,7 @@
189 api = self.CoffeeAPI()
190 api._put('/serve', data={'foo': 'bar'})
191 kwargs = mock_request.call_args[1]
192- self.assertEqual(kwargs['headers']['Content-type'], 'application/json')
193+ self.assertEqual(kwargs['headers']['Content-Type'], 'application/json')
194 self.assertEqual(kwargs['method'], 'PUT')
195
196 @patch('httplib2.Http.request')
197@@ -235,7 +235,7 @@
198 api = self.CoffeeAPI()
199 api._put('/serve', data=MyCoffeeRequest(strength='mild'))
200 kwargs = mock_request.call_args[1]
201- self.assertEqual(kwargs['headers']['Content-type'], 'application/json')
202+ self.assertEqual(kwargs['headers']['Content-Type'], 'application/json')
203 self.assertEqual(kwargs['method'], 'PUT')
204
205 @patch('httplib2.Http.request')
206@@ -254,7 +254,7 @@
207 api._put('/foo', scheme='https')
208 mock_request.assert_called_with(
209 'https://localhost:12345/foo',
210- body='null', headers={'Content-type': 'application/json'},
211+ body='null', headers={'Content-Type': 'application/json'},
212 method='PUT')
213
214 @patch('httplib2.Http.request')
215@@ -265,7 +265,7 @@
216 content_type='application/x-www-form-urlencoded')
217 kwargs = mock_request.call_args[1]
218 self.assertEqual(kwargs['body'], 'foo=bar')
219- self.assertEqual(kwargs['headers']['Content-type'],
220+ self.assertEqual(kwargs['headers']['Content-Type'],
221 'application/x-www-form-urlencoded')
222 self.assertEqual(kwargs['method'], 'PUT')
223
224@@ -283,7 +283,7 @@
225 mock_request.assert_called_with(
226 'http://localhost:12345/foo',
227 body='', headers=expected_headers, method='DELETE')
228- expected_headers['Content-type'] = 'application/json'
229+ expected_headers['Content-Type'] = 'application/json'
230 api._post('/foo')
231 mock_request.assert_called_with(
232 'http://localhost:12345/foo',
233@@ -306,7 +306,7 @@
234 mock_request.assert_called_with(
235 'http://localhost:12345/foo',
236 body='', headers=expected_headers, method='DELETE')
237- expected_headers['Content-type'] = 'application/json'
238+ expected_headers['Content-Type'] = 'application/json'
239 api._post('/foo', extra_headers={'X-Foo': 'bar'})
240 mock_request.assert_called_with(
241 'http://localhost:12345/foo',
242@@ -330,13 +330,13 @@
243 api._post('/foo', data=[])
244 mock_request.assert_called_with(
245 'http://localhost:12345/foo',
246- body=expected, headers={'Content-type': 'application/json'},
247+ body=expected, headers={'Content-Type': 'application/json'},
248 method='POST')
249
250 api._put('/foo', data=None)
251 mock_request.assert_called_with(
252 'http://localhost:12345/foo',
253- body=expected, headers={'Content-type': 'application/json'},
254+ body=expected, headers={'Content-Type': 'application/json'},
255 method='PUT')
256
257 @patch('httplib2.Http.request')

Subscribers

People subscribed via source and target branches