Merge lp:~canonical-isd-hackers/canonical-identity-provider/ssoclient into lp:~ubuntuone-pqm-team/canonical-identity-provider/ssoclient

Proposed by Natalia Bidart
Status: Merged
Approved by: Natalia Bidart
Approved revision: no longer in the source branch.
Merged at revision: 17
Proposed branch: lp:~canonical-isd-hackers/canonical-identity-provider/ssoclient
Merge into: lp:~ubuntuone-pqm-team/canonical-identity-provider/ssoclient
Diff against target: 374 lines (+136/-38)
8 files modified
Makefile (+5/-10)
ssoclient/__init__.py (+1/-1)
ssoclient/tests/__init__.py (+0/-1)
ssoclient/tests/test_v2.py (+92/-18)
ssoclient/v2/client.py (+23/-7)
ssoclient/v2/errors.py (+3/-1)
ssoclient/v2/http.py (+3/-0)
tox.ini (+9/-0)
To merge this branch: bzr merge lp:~canonical-isd-hackers/canonical-identity-provider/ssoclient
Reviewer Review Type Date Requested Status
Natalia Bidart (community) Approve
Review via email: mp+241580@code.launchpad.net

Commit message

Bump stable from trunk.

To post a comment you must log in.
Revision history for this message
Natalia Bidart (nataliabidart) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'Makefile'
2--- Makefile 2013-09-13 15:24:49 +0000
3+++ Makefile 2014-11-12 16:15:21 +0000
4@@ -1,19 +1,14 @@
5-ENV_PATH=.env
6-PIP=pip
7-PYTHON=python
8-VIRTUALENV=virtualenv
9+TOX_PATH=.tox
10
11 .PHONY: clean env test
12
13 all: clean env test
14
15 env:
16- $(VIRTUALENV) $(ENV_PATH)
17+ tox --notest
18
19 clean:
20- rm -rf $(ENV_PATH)
21- rm -rf mock-*.egg oauthlib-*.egg requests-*.egg requests_oauthlib-*.egg
22- rm -rf ssoclient.egg-info
23+ rm -rf $(TOX_PATH)
24
25-test: env
26- . $(ENV_PATH)/bin/activate && $(PYTHON) setup.py test
27+test:
28+ tox
29
30=== modified file 'ssoclient/__init__.py'
31--- ssoclient/__init__.py 2013-09-13 15:13:08 +0000
32+++ ssoclient/__init__.py 2014-11-12 16:15:21 +0000
33@@ -1,4 +1,4 @@
34 # Copyright 2013 Canonical Ltd. This software is licensed under
35 # the GNU Affero General Public License version 3 (see the file
36 # LICENSE).
37-__version__ = '2.0'
38+__version__ = '2.1'
39
40=== modified file 'ssoclient/tests/__init__.py'
41--- ssoclient/tests/__init__.py 2013-09-13 15:13:08 +0000
42+++ ssoclient/tests/__init__.py 2014-11-12 16:15:21 +0000
43@@ -1,4 +1,3 @@
44 # Copyright 2013 Canonical Ltd. This software is licensed under
45 # the GNU Affero General Public License version 3 (see the file
46 # LICENSE).
47-
48
49=== modified file 'ssoclient/tests/test_v2.py'
50--- ssoclient/tests/test_v2.py 2013-11-07 18:27:41 +0000
51+++ ssoclient/tests/test_v2.py 2014-11-12 16:15:21 +0000
52@@ -3,12 +3,21 @@
53 # Copyright 2013 Canonical Ltd. This software is licensed under
54 # the GNU Affero General Public License version 3 (see the file
55 # LICENSE).
56+
57+from __future__ import unicode_literals
58+
59+try:
60+ str = unicode
61+except NameError:
62+ pass # Forward compatibility with Py3k
63+
64 import json
65 import unittest
66
67 from datetime import datetime
68
69 from mock import (
70+ ANY,
71 MagicMock,
72 patch,
73 )
74@@ -92,7 +101,7 @@
75 response = Response()
76 response.status_code = status_code
77 if content is not None and json_dump:
78- content = json.dumps(content)
79+ content = str(json.dumps(content)).encode('utf-8')
80 response._content = content
81 return response
82
83@@ -105,7 +114,7 @@
84 return response
85
86 def test_error_code_raises_correct_exception(self):
87- for code, exc in ERRORS.iteritems():
88+ for code, exc in ERRORS.items():
89 response = mock_response(exc.status_code, dict(code=code))
90 with self.assertRaises(exc):
91 self.do_test(response)
92@@ -167,6 +176,16 @@
93 token_secret='token_secret',
94 )
95
96+ def assert_request_called_with(self, *args, **kwargs):
97+ # grab parameters independently to ensure json-encoded data is correct
98+ mock_args, mock_kwargs = self.mock_request.call_args
99+ self.assertEqual(mock_args, args)
100+ for k, v in kwargs.items():
101+ actual = mock_kwargs.get(k)
102+ if k == 'data':
103+ actual = json.loads(actual)
104+ self.assertEqual(actual, v)
105+
106 def unparsed_account_details(self, expand=False):
107 """Unparsed account details as how parse_response returns them."""
108 result = {
109@@ -241,7 +260,7 @@
110 credentials['consumer_key'], credentials['consumer_secret'],
111 credentials['token_key'], credentials['token_secret'],
112 )
113- self.assertTrue(all(isinstance(val, unicode) for
114+ self.assertTrue(all(isinstance(val, str) for
115 val in self.mock_oauth.call_args[0]))
116
117
118@@ -307,6 +326,63 @@
119 self.assert_invalid_response(500, errors.ServerError)
120
121
122+class GetOrCreateAccountTestCase(V2ClientApiTestCase):
123+
124+ def assert_invalid_response(self, status_code, ExceptionClass):
125+ # Test the client can handle an error response that doesn't have
126+ # a json body - ideally our server will never send these
127+ response = mock_response(
128+ status_code=status_code, content='some error message',
129+ json_dump=False)
130+
131+ self.mock_request.return_value = response
132+ with self.assertRaises(ExceptionClass) as ctx:
133+ self.client.get_or_create_account(
134+ token=self.credentials, email='blah')
135+
136+ if status_code >= 500:
137+ self.assertIn('some error message', str(ctx.exception))
138+
139+ def test_register_invalid_data(self):
140+ self.mock_request.return_value = mock_response(
141+ 400, dict(code="INVALID_DATA"))
142+ with self.assertRaises(errors.InvalidData):
143+ self.client.get_or_create_account(
144+ token=self.credentials, email='blah')
145+ self.assert_unicode_credentials(self.credentials)
146+
147+ def test_register_already_registered(self):
148+ content = self.unparsed_account_details()
149+ self.mock_request.return_value = mock_response(
150+ 200, content=content)
151+ response, created = self.client.get_or_create_account(
152+ token=self.credentials, email='blah')
153+
154+ self.assertFalse(created)
155+ self.assertEqual(response, content)
156+ self.assert_unicode_credentials(self.credentials)
157+
158+ def test_register_success(self):
159+ content = self.unparsed_account_details()
160+ self.mock_request.return_value = mock_response(
161+ status_code=201, content=content)
162+
163+ response, created = self.client.get_or_create_account(
164+ token=self.credentials, email='blah')
165+
166+ self.assertTrue(created)
167+ self.assertEqual(response, content)
168+ self.assert_unicode_credentials(self.credentials)
169+
170+ def test_invalid_response_400(self):
171+ self.assert_invalid_response(400, errors.ClientError)
172+ self.assert_unicode_credentials(self.credentials)
173+
174+ def test_invalid_response_500(self):
175+ self.assert_invalid_response(500, errors.ServerError)
176+ self.assert_unicode_credentials(self.credentials)
177+
178+
179 class LoginV2ClientApiTestCase(V2ClientApiTestCase):
180
181 def test_login_invalid_data(self):
182@@ -385,11 +461,10 @@
183 response = self.client.request_password_reset(self.email)
184
185 self.assertEqual(response, content)
186- self.mock_request.assert_called_once_with(
187+ self.assert_request_called_with(
188 'POST', 'http://foo.com/tokens/password',
189 headers={'Content-Type': 'application/json'},
190- data='{"token": null, "email": "%s"}' % self.email,
191- )
192+ data={'token': None, 'email': self.email})
193
194 def test_request_password_reset_without_email(self):
195 self.mock_request.return_value = mock_response(
196@@ -411,11 +486,10 @@
197 self.email, 'token1234')
198
199 self.assertEqual(response, content)
200- self.assertEqual(self.mock_request.call_args, [
201- ('POST', 'http://foo.com/tokens/password'),
202- {'headers': {'Content-Type': 'application/json'},
203- 'data': '{"token": "token1234", "email": "%s"}' % self.email},
204- ])
205+ self.assert_request_called_with(
206+ 'POST', 'http://foo.com/tokens/password',
207+ headers={'Content-Type': 'application/json'},
208+ data={'token': 'token1234', 'email': self.email})
209
210 def test_request_password_reset_for_suspended_account(self):
211 self.mock_request.return_value = mock_response(
212@@ -644,10 +718,9 @@
213 def assert_validate_request_called(self, **kwargs):
214 data = dict(http_url=self.http_url, http_method='GET')
215 data.update(kwargs)
216- self.mock_request.assert_called_once_with(
217+ self.assert_request_called_with(
218 'POST', TEST_ENDPOINT + 'requests/validate',
219- headers={'Content-Type': 'application/json'},
220- data=json.dumps(data))
221+ headers={'Content-Type': 'application/json'}, data=data)
222
223 def test_valid_request(self):
224 self.mock_request.return_value = mock_response(
225@@ -717,7 +790,7 @@
226 self.mock_request.return_value = mock_response(
227 200, dict(is_valid=True))
228
229- http_url = u'http://localhost/~/test/doc/dåc-id'
230+ http_url = 'http://localhost/~/test/doc/dåc-id'
231 result = self.client.validate_request(
232 http_url=http_url, http_method='GET', authorization='something')
233
234@@ -729,16 +802,17 @@
235 self.mock_request.return_value = mock_response(
236 200, dict(is_valid=True))
237
238- http_url = u'http://localhost/~/test/doc/dåc-id'.encode('utf-8')
239+ http_url = 'http://localhost/~/test/doc/dåc-id'
240 result = self.client.validate_request(
241- http_url=http_url, http_method='GET', authorization='something')
242+ http_url=http_url.encode('utf-8'), http_method='GET',
243+ authorization='something')
244
245 self.assertEqual(result, {'is_valid': True})
246 self.assert_validate_request_called(
247 http_url=http_url, authorization='something')
248
249 def test_non_ascii_url_not_utf8_encoded(self):
250- http_url = u'http://localhost/~/test/doc/dåc-id'.encode('latin-1')
251+ http_url = 'http://localhost/~/test/doc/dåc-id'.encode('latin-1')
252
253 with self.assertRaises(errors.ClientError) as ctx:
254 self.client.validate_request(
255
256=== modified file 'ssoclient/v2/client.py'
257--- ssoclient/v2/client.py 2013-11-07 18:15:38 +0000
258+++ ssoclient/v2/client.py 2014-11-12 16:15:21 +0000
259@@ -1,6 +1,14 @@
260 # Copyright 2013 Canonical Ltd. This software is licensed under
261 # the GNU Affero General Public License version 3 (see the file
262 # LICENSE).
263+
264+from __future__ import unicode_literals
265+
266+try:
267+ str = unicode
268+except NameError:
269+ pass # Forward compatibility with Py3k
270+
271 import logging
272
273 from datetime import datetime
274@@ -27,7 +35,7 @@
275 """Recursively look for dates and try to parse them into datetimes."""
276 assert isinstance(value, dict)
277 result = value.copy()
278- for k, v in value.iteritems():
279+ for k, v in value.items():
280 if isinstance(v, dict):
281 result[k] = parse_datetimes(v)
282 elif isinstance(v, list):
283@@ -49,10 +57,10 @@
284 # json library is in use.
285 # oauthlib requires them to be unicode - so we coerce to be sure.
286 if credentials is not None:
287- consumer_key = unicode(credentials.get('consumer_key', ''))
288- consumer_secret = unicode(credentials.get('consumer_secret', ''))
289- token_key = unicode(credentials.get('token_key', ''))
290- token_secret = unicode(credentials.get('token_secret', ''))
291+ consumer_key = str(credentials.get('consumer_key', ''))
292+ consumer_secret = str(credentials.get('consumer_secret', ''))
293+ token_key = str(credentials.get('token_key', ''))
294+ token_secret = str(credentials.get('token_secret', ''))
295 oauth = OAuth1(
296 consumer_key,
297 consumer_secret,
298@@ -82,8 +90,16 @@
299 result = parse_datetimes(response.content)
300 return result
301
302+ def get_or_create_account(self, token, **kwargs):
303+ oauth = self._unicode_credentials(token)
304+ response = self.session.post(
305+ '/accounts', data=kwargs, auth=oauth)
306+ result = parse_datetimes(response.content)
307+ created = response.status_code == 201
308+ return result, created
309+
310 def account_details(self, openid, token=None, expand=False):
311- openid = unicode(openid)
312+ openid = str(openid)
313 oauth = self._unicode_credentials(token)
314 url = '/accounts/%s?expand=%s' % (openid, str(expand).lower())
315
316@@ -143,7 +159,7 @@
317 raise errors.ClientError(msg=msg)
318
319 http_url = data.get('http_url', '')
320- if not isinstance(http_url, unicode):
321+ if not isinstance(http_url, str):
322 try:
323 data['http_url'] = http_url.decode('utf-8')
324 except UnicodeError:
325
326=== modified file 'ssoclient/v2/errors.py'
327--- ssoclient/v2/errors.py 2013-09-30 20:47:01 +0000
328+++ ssoclient/v2/errors.py 2014-11-12 16:15:21 +0000
329@@ -2,6 +2,8 @@
330 # the GNU Affero General Public License version 3 (see the file
331 # LICENSE).
332
333+from __future__ import unicode_literals
334+
335 class UnexpectedApiError(Exception):
336 """An unexpected client error."""
337
338@@ -39,7 +41,7 @@
339 # of a subclass - so still fetch it from the payload body
340 code = body.get('code')
341 msg = "%s: %s" % (response.status_code, code)
342- extra = ', '.join('%s: %r' % i for i in self.extra.iteritems())
343+ extra = ', '.join('%s: %r' % i for i in self.extra.items())
344 if extra:
345 msg += ' (%s)' % extra
346 super(ApiException, self).__init__(msg)
347
348=== modified file 'ssoclient/v2/http.py'
349--- ssoclient/v2/http.py 2013-10-01 00:17:10 +0000
350+++ ssoclient/v2/http.py 2014-11-12 16:15:21 +0000
351@@ -1,6 +1,9 @@
352 # Copyright 2013 Canonical Ltd. This software is licensed under
353 # the GNU Affero General Public License version 3 (see the file
354 # LICENSE).
355+
356+from __future__ import unicode_literals
357+
358 import functools
359 import json
360
361
362=== added file 'tox.ini'
363--- tox.ini 1970-01-01 00:00:00 +0000
364+++ tox.ini 2014-11-12 16:15:21 +0000
365@@ -0,0 +1,9 @@
366+# Copyright 2013 Canonical Ltd. This software is licensed under
367+# the GNU Affero General Public License version 3 (see the file
368+# LICENSE).
369+
370+[tox]
371+envlist = py27, py34
372+
373+[testenv]
374+commands = {envpython} setup.py test {posargs}

Subscribers

People subscribed via source and target branches