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
=== modified file 'Makefile'
--- Makefile 2013-09-13 15:24:49 +0000
+++ Makefile 2014-11-12 16:15:21 +0000
@@ -1,19 +1,14 @@
1ENV_PATH=.env1TOX_PATH=.tox
2PIP=pip
3PYTHON=python
4VIRTUALENV=virtualenv
52
6.PHONY: clean env test3.PHONY: clean env test
74
8all: clean env test5all: clean env test
96
10env:7env:
11 $(VIRTUALENV) $(ENV_PATH)8 tox --notest
129
13clean:10clean:
14 rm -rf $(ENV_PATH)11 rm -rf $(TOX_PATH)
15 rm -rf mock-*.egg oauthlib-*.egg requests-*.egg requests_oauthlib-*.egg
16 rm -rf ssoclient.egg-info
1712
18test: env13test:
19 . $(ENV_PATH)/bin/activate && $(PYTHON) setup.py test14 tox
2015
=== modified file 'ssoclient/__init__.py'
--- ssoclient/__init__.py 2013-09-13 15:13:08 +0000
+++ ssoclient/__init__.py 2014-11-12 16:15:21 +0000
@@ -1,4 +1,4 @@
1# Copyright 2013 Canonical Ltd. This software is licensed under1# Copyright 2013 Canonical Ltd. This software is licensed under
2# the GNU Affero General Public License version 3 (see the file2# the GNU Affero General Public License version 3 (see the file
3# LICENSE).3# LICENSE).
4__version__ = '2.0'4__version__ = '2.1'
55
=== modified file 'ssoclient/tests/__init__.py'
--- ssoclient/tests/__init__.py 2013-09-13 15:13:08 +0000
+++ ssoclient/tests/__init__.py 2014-11-12 16:15:21 +0000
@@ -1,4 +1,3 @@
1# Copyright 2013 Canonical Ltd. This software is licensed under1# Copyright 2013 Canonical Ltd. This software is licensed under
2# the GNU Affero General Public License version 3 (see the file2# the GNU Affero General Public License version 3 (see the file
3# LICENSE).3# LICENSE).
4
54
=== modified file 'ssoclient/tests/test_v2.py'
--- ssoclient/tests/test_v2.py 2013-11-07 18:27:41 +0000
+++ ssoclient/tests/test_v2.py 2014-11-12 16:15:21 +0000
@@ -3,12 +3,21 @@
3# Copyright 2013 Canonical Ltd. This software is licensed under3# Copyright 2013 Canonical Ltd. This software is licensed under
4# the GNU Affero General Public License version 3 (see the file4# the GNU Affero General Public License version 3 (see the file
5# LICENSE).5# LICENSE).
6
7from __future__ import unicode_literals
8
9try:
10 str = unicode
11except NameError:
12 pass # Forward compatibility with Py3k
13
6import json14import json
7import unittest15import unittest
816
9from datetime import datetime17from datetime import datetime
1018
11from mock import (19from mock import (
20 ANY,
12 MagicMock,21 MagicMock,
13 patch,22 patch,
14)23)
@@ -92,7 +101,7 @@
92 response = Response()101 response = Response()
93 response.status_code = status_code102 response.status_code = status_code
94 if content is not None and json_dump:103 if content is not None and json_dump:
95 content = json.dumps(content)104 content = str(json.dumps(content)).encode('utf-8')
96 response._content = content105 response._content = content
97 return response106 return response
98107
@@ -105,7 +114,7 @@
105 return response114 return response
106115
107 def test_error_code_raises_correct_exception(self):116 def test_error_code_raises_correct_exception(self):
108 for code, exc in ERRORS.iteritems():117 for code, exc in ERRORS.items():
109 response = mock_response(exc.status_code, dict(code=code))118 response = mock_response(exc.status_code, dict(code=code))
110 with self.assertRaises(exc):119 with self.assertRaises(exc):
111 self.do_test(response)120 self.do_test(response)
@@ -167,6 +176,16 @@
167 token_secret='token_secret',176 token_secret='token_secret',
168 )177 )
169178
179 def assert_request_called_with(self, *args, **kwargs):
180 # grab parameters independently to ensure json-encoded data is correct
181 mock_args, mock_kwargs = self.mock_request.call_args
182 self.assertEqual(mock_args, args)
183 for k, v in kwargs.items():
184 actual = mock_kwargs.get(k)
185 if k == 'data':
186 actual = json.loads(actual)
187 self.assertEqual(actual, v)
188
170 def unparsed_account_details(self, expand=False):189 def unparsed_account_details(self, expand=False):
171 """Unparsed account details as how parse_response returns them."""190 """Unparsed account details as how parse_response returns them."""
172 result = {191 result = {
@@ -241,7 +260,7 @@
241 credentials['consumer_key'], credentials['consumer_secret'],260 credentials['consumer_key'], credentials['consumer_secret'],
242 credentials['token_key'], credentials['token_secret'],261 credentials['token_key'], credentials['token_secret'],
243 )262 )
244 self.assertTrue(all(isinstance(val, unicode) for263 self.assertTrue(all(isinstance(val, str) for
245 val in self.mock_oauth.call_args[0]))264 val in self.mock_oauth.call_args[0]))
246265
247266
@@ -307,6 +326,63 @@
307 self.assert_invalid_response(500, errors.ServerError)326 self.assert_invalid_response(500, errors.ServerError)
308327
309328
329class GetOrCreateAccountTestCase(V2ClientApiTestCase):
330
331 def assert_invalid_response(self, status_code, ExceptionClass):
332 # Test the client can handle an error response that doesn't have
333 # a json body - ideally our server will never send these
334 response = mock_response(
335 status_code=status_code, content='some error message',
336 json_dump=False)
337
338 self.mock_request.return_value = response
339 with self.assertRaises(ExceptionClass) as ctx:
340 self.client.get_or_create_account(
341 token=self.credentials, email='blah')
342
343 if status_code >= 500:
344 self.assertIn('some error message', str(ctx.exception))
345
346 def test_register_invalid_data(self):
347 self.mock_request.return_value = mock_response(
348 400, dict(code="INVALID_DATA"))
349 with self.assertRaises(errors.InvalidData):
350 self.client.get_or_create_account(
351 token=self.credentials, email='blah')
352 self.assert_unicode_credentials(self.credentials)
353
354 def test_register_already_registered(self):
355 content = self.unparsed_account_details()
356 self.mock_request.return_value = mock_response(
357 200, content=content)
358 response, created = self.client.get_or_create_account(
359 token=self.credentials, email='blah')
360
361 self.assertFalse(created)
362 self.assertEqual(response, content)
363 self.assert_unicode_credentials(self.credentials)
364
365 def test_register_success(self):
366 content = self.unparsed_account_details()
367 self.mock_request.return_value = mock_response(
368 status_code=201, content=content)
369
370 response, created = self.client.get_or_create_account(
371 token=self.credentials, email='blah')
372
373 self.assertTrue(created)
374 self.assertEqual(response, content)
375 self.assert_unicode_credentials(self.credentials)
376
377 def test_invalid_response_400(self):
378 self.assert_invalid_response(400, errors.ClientError)
379 self.assert_unicode_credentials(self.credentials)
380
381 def test_invalid_response_500(self):
382 self.assert_invalid_response(500, errors.ServerError)
383 self.assert_unicode_credentials(self.credentials)
384
385
310class LoginV2ClientApiTestCase(V2ClientApiTestCase):386class LoginV2ClientApiTestCase(V2ClientApiTestCase):
311387
312 def test_login_invalid_data(self):388 def test_login_invalid_data(self):
@@ -385,11 +461,10 @@
385 response = self.client.request_password_reset(self.email)461 response = self.client.request_password_reset(self.email)
386462
387 self.assertEqual(response, content)463 self.assertEqual(response, content)
388 self.mock_request.assert_called_once_with(464 self.assert_request_called_with(
389 'POST', 'http://foo.com/tokens/password',465 'POST', 'http://foo.com/tokens/password',
390 headers={'Content-Type': 'application/json'},466 headers={'Content-Type': 'application/json'},
391 data='{"token": null, "email": "%s"}' % self.email,467 data={'token': None, 'email': self.email})
392 )
393468
394 def test_request_password_reset_without_email(self):469 def test_request_password_reset_without_email(self):
395 self.mock_request.return_value = mock_response(470 self.mock_request.return_value = mock_response(
@@ -411,11 +486,10 @@
411 self.email, 'token1234')486 self.email, 'token1234')
412487
413 self.assertEqual(response, content)488 self.assertEqual(response, content)
414 self.assertEqual(self.mock_request.call_args, [489 self.assert_request_called_with(
415 ('POST', 'http://foo.com/tokens/password'),490 'POST', 'http://foo.com/tokens/password',
416 {'headers': {'Content-Type': 'application/json'},491 headers={'Content-Type': 'application/json'},
417 'data': '{"token": "token1234", "email": "%s"}' % self.email},492 data={'token': 'token1234', 'email': self.email})
418 ])
419493
420 def test_request_password_reset_for_suspended_account(self):494 def test_request_password_reset_for_suspended_account(self):
421 self.mock_request.return_value = mock_response(495 self.mock_request.return_value = mock_response(
@@ -644,10 +718,9 @@
644 def assert_validate_request_called(self, **kwargs):718 def assert_validate_request_called(self, **kwargs):
645 data = dict(http_url=self.http_url, http_method='GET')719 data = dict(http_url=self.http_url, http_method='GET')
646 data.update(kwargs)720 data.update(kwargs)
647 self.mock_request.assert_called_once_with(721 self.assert_request_called_with(
648 'POST', TEST_ENDPOINT + 'requests/validate',722 'POST', TEST_ENDPOINT + 'requests/validate',
649 headers={'Content-Type': 'application/json'},723 headers={'Content-Type': 'application/json'}, data=data)
650 data=json.dumps(data))
651724
652 def test_valid_request(self):725 def test_valid_request(self):
653 self.mock_request.return_value = mock_response(726 self.mock_request.return_value = mock_response(
@@ -717,7 +790,7 @@
717 self.mock_request.return_value = mock_response(790 self.mock_request.return_value = mock_response(
718 200, dict(is_valid=True))791 200, dict(is_valid=True))
719792
720 http_url = u'http://localhost/~/test/doc/dåc-id'793 http_url = 'http://localhost/~/test/doc/dåc-id'
721 result = self.client.validate_request(794 result = self.client.validate_request(
722 http_url=http_url, http_method='GET', authorization='something')795 http_url=http_url, http_method='GET', authorization='something')
723796
@@ -729,16 +802,17 @@
729 self.mock_request.return_value = mock_response(802 self.mock_request.return_value = mock_response(
730 200, dict(is_valid=True))803 200, dict(is_valid=True))
731804
732 http_url = u'http://localhost/~/test/doc/dåc-id'.encode('utf-8')805 http_url = 'http://localhost/~/test/doc/dåc-id'
733 result = self.client.validate_request(806 result = self.client.validate_request(
734 http_url=http_url, http_method='GET', authorization='something')807 http_url=http_url.encode('utf-8'), http_method='GET',
808 authorization='something')
735809
736 self.assertEqual(result, {'is_valid': True})810 self.assertEqual(result, {'is_valid': True})
737 self.assert_validate_request_called(811 self.assert_validate_request_called(
738 http_url=http_url, authorization='something')812 http_url=http_url, authorization='something')
739813
740 def test_non_ascii_url_not_utf8_encoded(self):814 def test_non_ascii_url_not_utf8_encoded(self):
741 http_url = u'http://localhost/~/test/doc/dåc-id'.encode('latin-1')815 http_url = 'http://localhost/~/test/doc/dåc-id'.encode('latin-1')
742816
743 with self.assertRaises(errors.ClientError) as ctx:817 with self.assertRaises(errors.ClientError) as ctx:
744 self.client.validate_request(818 self.client.validate_request(
745819
=== modified file 'ssoclient/v2/client.py'
--- ssoclient/v2/client.py 2013-11-07 18:15:38 +0000
+++ ssoclient/v2/client.py 2014-11-12 16:15:21 +0000
@@ -1,6 +1,14 @@
1# Copyright 2013 Canonical Ltd. This software is licensed under1# Copyright 2013 Canonical Ltd. This software is licensed under
2# the GNU Affero General Public License version 3 (see the file2# the GNU Affero General Public License version 3 (see the file
3# LICENSE).3# LICENSE).
4
5from __future__ import unicode_literals
6
7try:
8 str = unicode
9except NameError:
10 pass # Forward compatibility with Py3k
11
4import logging12import logging
513
6from datetime import datetime14from datetime import datetime
@@ -27,7 +35,7 @@
27 """Recursively look for dates and try to parse them into datetimes."""35 """Recursively look for dates and try to parse them into datetimes."""
28 assert isinstance(value, dict)36 assert isinstance(value, dict)
29 result = value.copy()37 result = value.copy()
30 for k, v in value.iteritems():38 for k, v in value.items():
31 if isinstance(v, dict):39 if isinstance(v, dict):
32 result[k] = parse_datetimes(v)40 result[k] = parse_datetimes(v)
33 elif isinstance(v, list):41 elif isinstance(v, list):
@@ -49,10 +57,10 @@
49 # json library is in use.57 # json library is in use.
50 # oauthlib requires them to be unicode - so we coerce to be sure.58 # oauthlib requires them to be unicode - so we coerce to be sure.
51 if credentials is not None:59 if credentials is not None:
52 consumer_key = unicode(credentials.get('consumer_key', ''))60 consumer_key = str(credentials.get('consumer_key', ''))
53 consumer_secret = unicode(credentials.get('consumer_secret', ''))61 consumer_secret = str(credentials.get('consumer_secret', ''))
54 token_key = unicode(credentials.get('token_key', ''))62 token_key = str(credentials.get('token_key', ''))
55 token_secret = unicode(credentials.get('token_secret', ''))63 token_secret = str(credentials.get('token_secret', ''))
56 oauth = OAuth1(64 oauth = OAuth1(
57 consumer_key,65 consumer_key,
58 consumer_secret,66 consumer_secret,
@@ -82,8 +90,16 @@
82 result = parse_datetimes(response.content)90 result = parse_datetimes(response.content)
83 return result91 return result
8492
93 def get_or_create_account(self, token, **kwargs):
94 oauth = self._unicode_credentials(token)
95 response = self.session.post(
96 '/accounts', data=kwargs, auth=oauth)
97 result = parse_datetimes(response.content)
98 created = response.status_code == 201
99 return result, created
100
85 def account_details(self, openid, token=None, expand=False):101 def account_details(self, openid, token=None, expand=False):
86 openid = unicode(openid)102 openid = str(openid)
87 oauth = self._unicode_credentials(token)103 oauth = self._unicode_credentials(token)
88 url = '/accounts/%s?expand=%s' % (openid, str(expand).lower())104 url = '/accounts/%s?expand=%s' % (openid, str(expand).lower())
89105
@@ -143,7 +159,7 @@
143 raise errors.ClientError(msg=msg)159 raise errors.ClientError(msg=msg)
144160
145 http_url = data.get('http_url', '')161 http_url = data.get('http_url', '')
146 if not isinstance(http_url, unicode):162 if not isinstance(http_url, str):
147 try:163 try:
148 data['http_url'] = http_url.decode('utf-8')164 data['http_url'] = http_url.decode('utf-8')
149 except UnicodeError:165 except UnicodeError:
150166
=== modified file 'ssoclient/v2/errors.py'
--- ssoclient/v2/errors.py 2013-09-30 20:47:01 +0000
+++ ssoclient/v2/errors.py 2014-11-12 16:15:21 +0000
@@ -2,6 +2,8 @@
2# the GNU Affero General Public License version 3 (see the file2# the GNU Affero General Public License version 3 (see the file
3# LICENSE).3# LICENSE).
44
5from __future__ import unicode_literals
6
5class UnexpectedApiError(Exception):7class UnexpectedApiError(Exception):
6 """An unexpected client error."""8 """An unexpected client error."""
79
@@ -39,7 +41,7 @@
39 # of a subclass - so still fetch it from the payload body41 # of a subclass - so still fetch it from the payload body
40 code = body.get('code')42 code = body.get('code')
41 msg = "%s: %s" % (response.status_code, code)43 msg = "%s: %s" % (response.status_code, code)
42 extra = ', '.join('%s: %r' % i for i in self.extra.iteritems())44 extra = ', '.join('%s: %r' % i for i in self.extra.items())
43 if extra:45 if extra:
44 msg += ' (%s)' % extra46 msg += ' (%s)' % extra
45 super(ApiException, self).__init__(msg)47 super(ApiException, self).__init__(msg)
4648
=== modified file 'ssoclient/v2/http.py'
--- ssoclient/v2/http.py 2013-10-01 00:17:10 +0000
+++ ssoclient/v2/http.py 2014-11-12 16:15:21 +0000
@@ -1,6 +1,9 @@
1# Copyright 2013 Canonical Ltd. This software is licensed under1# Copyright 2013 Canonical Ltd. This software is licensed under
2# the GNU Affero General Public License version 3 (see the file2# the GNU Affero General Public License version 3 (see the file
3# LICENSE).3# LICENSE).
4
5from __future__ import unicode_literals
6
4import functools7import functools
5import json8import json
69
710
=== added file 'tox.ini'
--- tox.ini 1970-01-01 00:00:00 +0000
+++ tox.ini 2014-11-12 16:15:21 +0000
@@ -0,0 +1,9 @@
1# Copyright 2013 Canonical Ltd. This software is licensed under
2# the GNU Affero General Public License version 3 (see the file
3# LICENSE).
4
5[tox]
6envlist = py27, py34
7
8[testenv]
9commands = {envpython} setup.py test {posargs}

Subscribers

People subscribed via source and target branches