From e8bd38e4b9f65ab950b78e456055abc6f40433f4 Mon Sep 17 00:00:00 2001 From: Seth Holloway Date: Thu, 12 Sep 2013 11:01:39 -0500 Subject: [PATCH 01/73] Add version constant to library, bump version, and test --- setup.py | 2 +- tests.py | 15 ++++++++++++--- toopher/__init__.py | 1 + 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/setup.py b/setup.py index 7460b75..12bfd5e 100755 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setup( name='toopher', - version='1.0.5', + version='1.0.6', author='Toopher, Inc.', author_email='support@toopher.com', url='https://dev.toopher.com', diff --git a/tests.py b/tests.py index eaf471b..3a0f6cc 100644 --- a/tests.py +++ b/tests.py @@ -26,6 +26,17 @@ def test_constructor(self): api = toopher.ToopherApi('key', 'secret', api_url='http://testonly') + def test_version_number_in_library(self): + self.assertGreaterEqual(toopher.VERSION, "1.0.6") + + def test_version_number_in_setup(self): + ''' Ensure that the setup.py file has the same version number as the toopher/__init__.py file ''' + for line in open('setup.py'): + if "version" in line: + # in setup.py the version is written as "version='1.0.6'," so we need to remove version=' and ', + version_number = line.strip().replace("version='", "").replace("',", "") + self.assertEqual(version_number, toopher.VERSION) + def test_create_pairing(self): api = toopher.ToopherApi('key', 'secret', api_url='http://testonly') api.client = HttpClientMock({ @@ -40,7 +51,7 @@ def test_create_pairing(self): self.assertEqual(api.client.last_called_data['pairing_phrase'], ['awkward turtle']) with self.assertRaises(KeyError): self.assertEqual(api.client.last_called_data['test_param'], ['42']) - + def test_pairing_status(self): api = toopher.ToopherApi('key', 'secret', api_url='http://testonly') api.client = HttpClientMock({ @@ -75,7 +86,6 @@ def test_create_authentication_request(self): with self.assertRaises(KeyError): self.assertEqual(api.client.last_called_data['test_param'], ['42']) - def test_authentication_status(self): api = toopher.ToopherApi('key', 'secret', api_url='http://testonly') api.client = HttpClientMock({ @@ -143,7 +153,6 @@ def test_access_arbitrary_keys_in_pairing_status(self): self.assertEqual(pairing.random_key, "84") - def test_access_arbitrary_keys_in_authentication_status(self): api = toopher.ToopherApi('key', 'secret', api_url='http://testonly') api.client = HttpClientMock({ diff --git a/toopher/__init__.py b/toopher/__init__.py index 8a1ef6c..036b7b5 100644 --- a/toopher/__init__.py +++ b/toopher/__init__.py @@ -3,6 +3,7 @@ import oauth2 import os DEFAULT_BASE_URL = "https://api.toopher.com/v1" +VERSION = "1.0.6" class ToopherApi(object): From 29acbcba7159d32e017980773450e5a8eb367ca4 Mon Sep 17 00:00:00 2001 From: Seth Holloway Date: Fri, 13 Sep 2013 11:23:18 -0500 Subject: [PATCH 02/73] Create a Toopher User-Agent string --- toopher/__init__.py | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/toopher/__init__.py b/toopher/__init__.py index 036b7b5..4a0965d 100644 --- a/toopher/__init__.py +++ b/toopher/__init__.py @@ -19,7 +19,7 @@ def pair(self, pairing_phrase, user_name, **kwargs): 'user_name': user_name} params.update(kwargs) - + result = self._request(uri, "POST", params) return PairingStatus(result) @@ -27,16 +27,16 @@ def pair_sms(self, phone_number, user_name, phone_country=None): uri = BASE_URL + "/pairings/create/sms" params = {'phone_number': phone_number, 'user_name': user_name} - + if phone_country: params['phone_country'] = phone_country result = self._request(uri, "POST", params) return PairingStatus(result) - + def get_pairing_status(self, pairing_id): uri = self.base_url + "/pairings/" + pairing_id - + result = self._request(uri, "GET") return PairingStatus(result) @@ -48,13 +48,13 @@ def authenticate(self, pairing_id, terminal_name, action_name=None, **kwargs): params['action_name'] = action_name params.update(kwargs) - + result = self._request(uri, "POST", params) return AuthenticationStatus(result) def get_authentication_status(self, authentication_request_id): uri = self.base_url + "/authentication_requests/" + authentication_request_id - + result = self._request(uri, "GET") return AuthenticationStatus(result) @@ -63,23 +63,24 @@ def authenticate_with_otp(self, authentication_request_id, otp): params = {'otp' : otp} result = self._request(uri, "POST", params) return AuthenticationStatus(result) - + def _request(self, uri, method, params=None): data = urllib.urlencode(params or {}) - - resp, content = self.client.request(uri, method, data) + header_data = {'User-Agent':'Toopher-Python/{}'.format(VERSION)} + + resp, content = self.client.request(uri, method, data, headers=header_data) if resp['status'] != '200': try: error_message = json.loads(content)['error_message'] except Exception: error_message = content raise ToopherApiError(error_message) - + try: result = json.loads(content) except Exception, e: raise ToopherApiError("Response from server could not be decoded as JSON: %s" % e) - + return result @@ -88,7 +89,7 @@ def __init__(self, json_response): try: self.id = json_response['id'] self.enabled = json_response['enabled'] - + user = json_response['user'] self.user_id = user['id'] self.user_name = user['name'] @@ -96,7 +97,7 @@ def __init__(self, json_response): raise ToopherApiError("Could not parse pairing status from response" + e.message) self._raw_data = json_response - + def __nonzero__(self): return self.enabled @@ -112,7 +113,7 @@ def __init__(self, json_response): self.granted = json_response['granted'] self.automated = json_response['automated'] self.reason = json_response['reason'] - + terminal = json_response['terminal'] self.terminal_id = terminal['id'] self.terminal_name = terminal['name'] @@ -129,3 +130,4 @@ def __getattr__(self, name): class ToopherApiError(Exception): pass + From 90d48ddda402d6e5e8f45993af68f33c5730c40b Mon Sep 17 00:00:00 2001 From: Seth Holloway Date: Fri, 13 Sep 2013 11:43:45 -0500 Subject: [PATCH 03/73] Add headers to the mock request method to fix broken tests --- tests.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests.py b/tests.py index 3a0f6cc..a50eef2 100644 --- a/tests.py +++ b/tests.py @@ -8,10 +8,11 @@ class HttpClientMock(object): def __init__(self, paths): self.paths = paths - def request(self, uri, method, data): + def request(self, uri, method, data, headers): self.last_called_uri = uri self.last_called_method = method self.last_called_data = urlparse.parse_qs(data) + self.last_called_headers = headers if uri in self.paths: return self.paths[uri] From a9139dc9684602bbaff4874bd14309e35f640069 Mon Sep 17 00:00:00 2001 From: Seth Holloway Date: Mon, 16 Sep 2013 14:48:34 -0500 Subject: [PATCH 04/73] Add Python version to User-Agent string --- toopher/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/toopher/__init__.py b/toopher/__init__.py index 4a0965d..4b5e075 100644 --- a/toopher/__init__.py +++ b/toopher/__init__.py @@ -2,6 +2,7 @@ import json import oauth2 import os +import sys DEFAULT_BASE_URL = "https://api.toopher.com/v1" VERSION = "1.0.6" @@ -66,7 +67,7 @@ def authenticate_with_otp(self, authentication_request_id, otp): def _request(self, uri, method, params=None): data = urllib.urlencode(params or {}) - header_data = {'User-Agent':'Toopher-Python/{}'.format(VERSION)} + header_data = {'User-Agent':'Toopher-Python/{} (Python {})'.format(VERSION, sys.version.split()[0])} resp, content = self.client.request(uri, method, data, headers=header_data) if resp['status'] != '200': From 1e63ba2d2f911ab9f134d1c23b717d19837f0396 Mon Sep 17 00:00:00 2001 From: Seth Holloway Date: Mon, 16 Sep 2013 14:53:39 -0500 Subject: [PATCH 05/73] Update version number test to be more robust --- tests.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests.py b/tests.py index a50eef2..f5dafae 100644 --- a/tests.py +++ b/tests.py @@ -28,7 +28,10 @@ def test_constructor(self): api = toopher.ToopherApi('key', 'secret', api_url='http://testonly') def test_version_number_in_library(self): - self.assertGreaterEqual(toopher.VERSION, "1.0.6") + major, minor, patch = toopher.VERSION.split('.') + self.assertGreaterEqual(int(major), 1) + self.assertGreaterEqual(int(minor), 0) + self.assertGreaterEqual(int(patch), 0) def test_version_number_in_setup(self): ''' Ensure that the setup.py file has the same version number as the toopher/__init__.py file ''' From b636bd630322e03dec41a543a5fbfbe59e9f42ae Mon Sep 17 00:00:00 2001 From: Seth Holloway Date: Mon, 16 Sep 2013 15:08:29 -0500 Subject: [PATCH 06/73] Add Travis CI build status indicator --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index bb91b9f..314b2aa 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ #ToopherPython +[![Build Status](https://travis-ci.org/toopher/toopher-python.png?branch=master)](https://travis-ci.org/toopher/toopher-python) + #### Introduction ToopherPython is a Toopher API library that simplifies the task of interfacing with the Toopher API from Python code. This project wrangles all the required OAuth and JSON functionality so you can focus on just using the API. From 973aa0c1d701dc650503ecd31ba52eddc93f8907 Mon Sep 17 00:00:00 2001 From: Thomas Darr Date: Mon, 18 Nov 2013 10:45:01 -0600 Subject: [PATCH 07/73] Add zero-storage --- toopher/__init__.py | 63 ++++++++++++++++++++++++++++++++------------- 1 file changed, 45 insertions(+), 18 deletions(-) diff --git a/toopher/__init__.py b/toopher/__init__.py index 4b5e075..617d9e7 100644 --- a/toopher/__init__.py +++ b/toopher/__init__.py @@ -6,6 +6,14 @@ DEFAULT_BASE_URL = "https://api.toopher.com/v1" VERSION = "1.0.6" +class ToopherApiError(Exception): pass +class UserDisabledError(ToopherApiError): pass +class UnknownUserError(ToopherApiError): pass +class UnknownTerminalError(ToopherApiError): pass +class PairingDeactivatedError(ToopherApiError): pass +error_codes_to_errors = {704: UserDisabledError, + 705: UnknownUserError, + 706: UnknownTerminalError} class ToopherApi(object): def __init__(self, key, secret, api_url=None): @@ -25,7 +33,7 @@ def pair(self, pairing_phrase, user_name, **kwargs): return PairingStatus(result) def pair_sms(self, phone_number, user_name, phone_country=None): - uri = BASE_URL + "/pairings/create/sms" + uri = self.base_url + "/pairings/create/sms" params = {'phone_number': phone_number, 'user_name': user_name} @@ -60,30 +68,53 @@ def get_authentication_status(self, authentication_request_id): return AuthenticationStatus(result) def authenticate_with_otp(self, authentication_request_id, otp): - uri = BASE_URL + "/authentication_requests/" + authentication_request_id + '/otp_auth' + uri = self.base_url + "/authentication_requests/" + authentication_request_id + '/otp_auth' params = {'otp' : otp} result = self._request(uri, "POST", params) return AuthenticationStatus(result) + def authenticate_by_user_name(self, user_name, terminal_name_extra, action_name, **kwargs): + kwargs.update(user_name=user_name, terminal_name_extra=terminal_name_extra) + return self.authenticate('', '', action_name, **kwargs) + + def assign_friendly_name_to_terminal(self, user_name, terminal_name, terminal_name_extra): + uri = self.base_url + '/user_terminals/create' + params = {'user_name': user_name, + 'name': terminal_name, + 'name_extra': terminal_name_extra} + result = self._request(uri, 'POST', params) + return + def _request(self, uri, method, params=None): data = urllib.urlencode(params or {}) header_data = {'User-Agent':'Toopher-Python/{} (Python {})'.format(VERSION, sys.version.split()[0])} - resp, content = self.client.request(uri, method, data, headers=header_data) - if resp['status'] != '200': - try: - error_message = json.loads(content)['error_message'] - except Exception: - error_message = content - raise ToopherApiError(error_message) - + response, content = self.client.request(uri, method, data, headers=header_data) try: - result = json.loads(content) - except Exception, e: - raise ToopherApiError("Response from server could not be decoded as JSON: %s" % e) + content = json.loads(content) + import pprint + pprint.pprint(content) + except ValueError: + raise ToopherApiError('Response from server could not be decoded as JSON.') + + if int(response['status']) > 300: + self._parse_request_error(content) + + return content - return result + def _parse_request_error(self, content): + error_code = content['error_code'] + error_message = content['error_message'] + if error_code in error_codes_to_errors: + error = error_codes_to_errors[error_code] + raise error(error_message) + # TODO: Add an error code for PairingDeactivatedError. + if ('pairing has been deactivated' in error_message + or 'pairing has not been authorized' in error_message): + raise PairingDeactivatedError(error_message) + + raise ToopherApiError(error_message) class PairingStatus(object): def __init__(self, json_response): @@ -128,7 +159,3 @@ def __nonzero__(self): def __getattr__(self, name): return self._raw_data[name] - - -class ToopherApiError(Exception): pass - From 5abb39e05a4c22056ff575d635b520b4d0c0d3e0 Mon Sep 17 00:00:00 2001 From: Thomas Darr Date: Mon, 18 Nov 2013 10:46:57 -0600 Subject: [PATCH 08/73] Remove response pprinting --- toopher/__init__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/toopher/__init__.py b/toopher/__init__.py index 617d9e7..5ca487b 100644 --- a/toopher/__init__.py +++ b/toopher/__init__.py @@ -92,8 +92,6 @@ def _request(self, uri, method, params=None): response, content = self.client.request(uri, method, data, headers=header_data) try: content = json.loads(content) - import pprint - pprint.pprint(content) except ValueError: raise ToopherApiError('Response from server could not be decoded as JSON.') From ad0de360284ac2f3f350b7b749d75ce8ba26ec49 Mon Sep 17 00:00:00 2001 From: Thomas Darr Date: Mon, 18 Nov 2013 12:19:30 -0600 Subject: [PATCH 09/73] Add tests --- tests.py | 56 ++++++++++++++++++++++++++++++++++++++++++--- toopher/__init__.py | 15 ++++++++++-- 2 files changed, 66 insertions(+), 5 deletions(-) diff --git a/tests.py b/tests.py index f5dafae..b14ef22 100644 --- a/tests.py +++ b/tests.py @@ -1,8 +1,8 @@ -import unittest +import json import os -import urlparse - import toopher +import unittest +import urlparse class HttpClientMock(object): def __init__(self, paths): @@ -177,6 +177,56 @@ def test_access_arbitrary_keys_in_authentication_status(self): self.assertEqual(auth_request.random_key, "84") + def test_disabled_user_raises_correct_error(self): + api = toopher.ToopherApi('key', 'secret', api_url='https://toopher.test/v1') + api.client = HttpClientMock({ + 'https://toopher.test/v1/authentication_requests/initiate': + ({'status': 409}, json.dumps( + {'error_code': 704, + 'error_message': 'disabled user'}))}) + with self.assertRaises(toopher.UserDisabledError): + auth_request = api.authenticate_by_user_name('disabled user', 'terminal name') + + def test_unknown_user_raises_correct_error(self): + api = toopher.ToopherApi('key', 'secret', api_url='https://toopher.test/v1') + api.client = HttpClientMock({ + 'https://toopher.test/v1/authentication_requests/initiate': + ({'status': 409}, json.dumps( + {'error_code': 705, + 'error_message': 'unknown user'}))}) + with self.assertRaises(toopher.UnknownUserError): + auth_request = api.authenticate_by_user_name('unknown user', 'terminal name') + + def test_unknown_terminal_raises_correct_error(self): + api = toopher.ToopherApi('key', 'secret', api_url='https://toopher.test/v1') + api.client = HttpClientMock({ + 'https://toopher.test/v1/authentication_requests/initiate': + ({'status': 409}, json.dumps( + {'error_code': 706, + 'error_message': 'unknown terminal'}))}) + with self.assertRaises(toopher.UnknownTerminalError): + auth_request = api.authenticate_by_user_name('user', 'unknown terminal name') + + def test_disabled_pairing_raises_correct_error(self): + api = toopher.ToopherApi('key', 'secret', api_url='https://toopher.test/v1') + api.client = HttpClientMock({ + 'https://toopher.test/v1/authentication_requests/initiate': + ({'status': 409}, json.dumps( + {'error_code': 601, + 'error_message': 'pairing has been deactivated'}))}) + with self.assertRaises(toopher.PairingDeactivatedError): + auth_request = api.authenticate_by_user_name('user', 'terminal name') + + def test_disabled_pairing_raises_correct_error(self): + api = toopher.ToopherApi('key', 'secret', api_url='https://toopher.test/v1') + api.client = HttpClientMock({ + 'https://toopher.test/v1/authentication_requests/initiate': + ({'status': 409}, json.dumps( + {'error_code': 601, + 'error_message': 'pairing has not been authorized'}))}) + with self.assertRaises(toopher.PairingDeactivatedError): + auth_request = api.authenticate_by_user_name('user', 'terminal name') + def main(): unittest.main() diff --git a/toopher/__init__.py b/toopher/__init__.py index 5ca487b..a89c411 100644 --- a/toopher/__init__.py +++ b/toopher/__init__.py @@ -73,7 +73,7 @@ def authenticate_with_otp(self, authentication_request_id, otp): result = self._request(uri, "POST", params) return AuthenticationStatus(result) - def authenticate_by_user_name(self, user_name, terminal_name_extra, action_name, **kwargs): + def authenticate_by_user_name(self, user_name, terminal_name_extra, action_name=None, **kwargs): kwargs.update(user_name=user_name, terminal_name_extra=terminal_name_extra) return self.authenticate('', '', action_name, **kwargs) @@ -83,7 +83,18 @@ def assign_friendly_name_to_terminal(self, user_name, terminal_name, terminal_na 'name': terminal_name, 'name_extra': terminal_name_extra} result = self._request(uri, 'POST', params) - return + + def set_enable_toopher_for_user(self, user_name, enabled): + uri = self.base_url + '/users' + users = self._request(uri, 'GET') + if len(users) > 1: + raise ToopherApiException('Multiple users with name = {}'.format(user_name)) + elif not len(users): + raise ToopherApiException('No users with name = {}'.format(user_name)) + + uri = self.base_url + '/users/' + users[0]['id'] + params = {'disable_toopher_auth': bool(enabled)} + result = self._request(uri, 'POST', params) def _request(self, uri, method, params=None): data = urllib.urlencode(params or {}) From 2c980f5769b2a59856acf3ecf63bbaf5ba1e6039 Mon Sep 17 00:00:00 2001 From: Thomas Darr Date: Mon, 18 Nov 2013 12:28:31 -0600 Subject: [PATCH 10/73] Update README --- README.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/README.md b/README.md index 314b2aa..8ce1f8c 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,37 @@ if (auth_status.pending == false and auth_status.granted == true): #### Handling Errors If any request runs into an error a `ToopherApiError` will be thrown with more details on what went wrong. +#### Zero-Storage usage option +Requesters can choose to integrate the Toopher API in a way does not require storing any per-user data such as Pairing ID and Terminal ID - all of the storage +is handled by the Toopher API Web Service, allowing your local database to remain unchanged. If the Toopher API needs more data, it will `die()` with a specific +error string that allows your code to respond appropriately. + +```python +try: + # optimistically try to authenticate against Toopher API with username and a Terminal Identifier + # Terminal Identifer is typically a randomly generated secure browser cookie. It does not + # need to be human-readable + auth = api.authenticate_by_user_name("username@yourservice.com", "") + + # if you got here, everything is good! poll the auth request status as described above + # there are four distinct errors ToopherAPI can return if it needs more data +except UserDisabledError: + # you have marked this user as disabled in the Toopher API. +except UnknownUserError: + # This user has not yet paired a mobile device with their account. Pair them + # using api.pair() as described above, then re-try authentication +except UnknownTerminalError: + # This user has not assigned a "Friendly Name" to this terminal identifier. + # Prompt them to enter a terminal name, then submit that "friendly name" to + # the Toopher API: + # api.assign_user_friendly_name_to_terminal(user_name, terminal_friendly_name, terminal_identifier) + # Afterwards, re-try authentication +except PairingDeactivatedError: + # this user does not have an active pairing, + # typically because they deleted the pairing. You can prompt + # the user to re-pair with a new mobile device. +``` + #### Dependencies This library uses the python-oauth2 library to handle OAuth signing and httplib2 to make the web requests. If you install using pip (or easy_install) they'll be installed automatically for you. From 38c6a87d7fb7315f32a31513c20e854282f6555d Mon Sep 17 00:00:00 2001 From: Thomas Darr Date: Mon, 25 Nov 2013 17:11:13 -0600 Subject: [PATCH 11/73] Comply with discussed nomenclature --- tests.py | 4 ++-- toopher/__init__.py | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/tests.py b/tests.py index b14ef22..d6f0dd7 100644 --- a/tests.py +++ b/tests.py @@ -194,7 +194,7 @@ def test_unknown_user_raises_correct_error(self): ({'status': 409}, json.dumps( {'error_code': 705, 'error_message': 'unknown user'}))}) - with self.assertRaises(toopher.UnknownUserError): + with self.assertRaises(toopher.UserUnknownError): auth_request = api.authenticate_by_user_name('unknown user', 'terminal name') def test_unknown_terminal_raises_correct_error(self): @@ -204,7 +204,7 @@ def test_unknown_terminal_raises_correct_error(self): ({'status': 409}, json.dumps( {'error_code': 706, 'error_message': 'unknown terminal'}))}) - with self.assertRaises(toopher.UnknownTerminalError): + with self.assertRaises(toopher.TerminalUnknownError): auth_request = api.authenticate_by_user_name('user', 'unknown terminal name') def test_disabled_pairing_raises_correct_error(self): diff --git a/toopher/__init__.py b/toopher/__init__.py index a89c411..71b3ea6 100644 --- a/toopher/__init__.py +++ b/toopher/__init__.py @@ -8,12 +8,12 @@ class ToopherApiError(Exception): pass class UserDisabledError(ToopherApiError): pass -class UnknownUserError(ToopherApiError): pass -class UnknownTerminalError(ToopherApiError): pass +class UserUnknownError(ToopherApiError): pass +class TerminalUnknownError(ToopherApiError): pass class PairingDeactivatedError(ToopherApiError): pass error_codes_to_errors = {704: UserDisabledError, - 705: UnknownUserError, - 706: UnknownTerminalError} + 705: UserUnknownError, + 706: TerminalUnknownError} class ToopherApi(object): def __init__(self, key, secret, api_url=None): @@ -77,11 +77,11 @@ def authenticate_by_user_name(self, user_name, terminal_name_extra, action_name= kwargs.update(user_name=user_name, terminal_name_extra=terminal_name_extra) return self.authenticate('', '', action_name, **kwargs) - def assign_friendly_name_to_terminal(self, user_name, terminal_name, terminal_name_extra): + def create_user_terminal(self, user_name, terminal_name, requester_terminal_id): uri = self.base_url + '/user_terminals/create' params = {'user_name': user_name, 'name': terminal_name, - 'name_extra': terminal_name_extra} + 'name_extra': requester_terminal_id} result = self._request(uri, 'POST', params) def set_enable_toopher_for_user(self, user_name, enabled): From 3c397f1bfacc401f9ad7f7aa57d8c9f68c074852 Mon Sep 17 00:00:00 2001 From: Thomas Darr Date: Mon, 2 Dec 2013 17:09:41 -0600 Subject: [PATCH 12/73] Feedback from @smholloway on PR 16 --- README.md | 2 +- toopher/__init__.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 8ce1f8c..957601c 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ except UnknownTerminalError: # This user has not assigned a "Friendly Name" to this terminal identifier. # Prompt them to enter a terminal name, then submit that "friendly name" to # the Toopher API: - # api.assign_user_friendly_name_to_terminal(user_name, terminal_friendly_name, terminal_identifier) + # api.create_user_terminal(user_name, terminal_name, requester_terminal_id) # Afterwards, re-try authentication except PairingDeactivatedError: # this user does not have an active pairing, diff --git a/toopher/__init__.py b/toopher/__init__.py index 71b3ea6..f18510b 100644 --- a/toopher/__init__.py +++ b/toopher/__init__.py @@ -4,7 +4,7 @@ import os import sys DEFAULT_BASE_URL = "https://api.toopher.com/v1" -VERSION = "1.0.6" +VERSION = '1.1.0' class ToopherApiError(Exception): pass class UserDisabledError(ToopherApiError): pass @@ -84,7 +84,7 @@ def create_user_terminal(self, user_name, terminal_name, requester_terminal_id): 'name_extra': requester_terminal_id} result = self._request(uri, 'POST', params) - def set_enable_toopher_for_user(self, user_name, enabled): + def set_toopher_enabled_for_user(self, user_name, enabled): uri = self.base_url + '/users' users = self._request(uri, 'GET') if len(users) > 1: @@ -106,7 +106,7 @@ def _request(self, uri, method, params=None): except ValueError: raise ToopherApiError('Response from server could not be decoded as JSON.') - if int(response['status']) > 300: + if int(response['status']) >= 400: self._parse_request_error(content) return content From 4007f8aa73efd2659b9451c5a5c5f9d5e1ddc499 Mon Sep 17 00:00:00 2001 From: Drew Shafer Date: Mon, 2 Dec 2013 18:11:41 -0600 Subject: [PATCH 13/73] Fix some perl-specific readme verbiage --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8ce1f8c..92af9fa 100644 --- a/README.md +++ b/README.md @@ -46,8 +46,8 @@ If any request runs into an error a `ToopherApiError` will be thrown with more d #### Zero-Storage usage option Requesters can choose to integrate the Toopher API in a way does not require storing any per-user data such as Pairing ID and Terminal ID - all of the storage -is handled by the Toopher API Web Service, allowing your local database to remain unchanged. If the Toopher API needs more data, it will `die()` with a specific -error string that allows your code to respond appropriately. +is handled by the Toopher API Web Service, allowing your local database to remain unchanged. If the Toopher API needs more data, it will `raise()` a specific +error that allows your code to respond appropriately. ```python try: From 4d3260edf44fc9819b96c277c33390e6f88ca2bd Mon Sep 17 00:00:00 2001 From: Thomas Darr Date: Mon, 2 Dec 2013 17:36:39 -0600 Subject: [PATCH 14/73] Update version number --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 12bfd5e..be32c7a 100755 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setup( name='toopher', - version='1.0.6', + version='1.1.0', author='Toopher, Inc.', author_email='support@toopher.com', url='https://dev.toopher.com', From e19c35a66275b7649fc6462a253fc40115253a8b Mon Sep 17 00:00:00 2001 From: Thomas Darr Date: Tue, 3 Dec 2013 13:03:28 -0600 Subject: [PATCH 15/73] Replace python-oauth2 with Requests --- toopher/__init__.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/toopher/__init__.py b/toopher/__init__.py index f18510b..2734e02 100644 --- a/toopher/__init__.py +++ b/toopher/__init__.py @@ -1,8 +1,8 @@ -import urllib import json -import oauth2 import os +import requests_oauthlib import sys + DEFAULT_BASE_URL = "https://api.toopher.com/v1" VERSION = '1.1.0' @@ -17,8 +17,10 @@ class PairingDeactivatedError(ToopherApiError): pass class ToopherApi(object): def __init__(self, key, secret, api_url=None): - self.client = oauth2.Client(oauth2.Consumer(key, secret)) - self.client.ca_certs = os.path.join(os.path.dirname(os.path.abspath(__file__)), "toopher.pem") + self.client = requests_oauthlib.OAuth1Session(key, client_secret=secret) + self.client.cert = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'toopher.pem') + self.client.verify = True + base_url = api_url if api_url else DEFAULT_BASE_URL self.base_url = base_url.rstrip('/') @@ -86,7 +88,9 @@ def create_user_terminal(self, user_name, terminal_name, requester_terminal_id): def set_toopher_enabled_for_user(self, user_name, enabled): uri = self.base_url + '/users' - users = self._request(uri, 'GET') + params = {'name': user_name} + users = self._request(uri, 'GET', params) + if len(users) > 1: raise ToopherApiException('Multiple users with name = {}'.format(user_name)) elif not len(users): @@ -97,16 +101,16 @@ def set_toopher_enabled_for_user(self, user_name, enabled): result = self._request(uri, 'POST', params) def _request(self, uri, method, params=None): - data = urllib.urlencode(params or {}) + data = {'params' if method == 'GET' else 'data': params} header_data = {'User-Agent':'Toopher-Python/{} (Python {})'.format(VERSION, sys.version.split()[0])} - response, content = self.client.request(uri, method, data, headers=header_data) + response = self.client.request(method, uri, headers=header_data, **data) try: - content = json.loads(content) + content = response.json() except ValueError: raise ToopherApiError('Response from server could not be decoded as JSON.') - if int(response['status']) >= 400: + if response.status_code >= 400: self._parse_request_error(content) return content From d3f098773dc8d05dac67102371d12113ab215ac3 Mon Sep 17 00:00:00 2001 From: Thomas Darr Date: Tue, 3 Dec 2013 13:04:40 -0600 Subject: [PATCH 16/73] Add requirements.txt --- requirements.txt | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 requirements.txt diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..dc0bcb7 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +oauthlib==0.6.0 +requests==2.0.1 +requests-oauthlib==0.4.0 From b5bf1db294833dd0825a96904481b30639155c4d Mon Sep 17 00:00:00 2001 From: Thomas Darr Date: Wed, 4 Dec 2013 11:48:16 -0600 Subject: [PATCH 17/73] Pyflakes --- toopher/__init__.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/toopher/__init__.py b/toopher/__init__.py index 2734e02..d5c299d 100644 --- a/toopher/__init__.py +++ b/toopher/__init__.py @@ -1,4 +1,3 @@ -import json import os import requests_oauthlib import sys @@ -84,7 +83,7 @@ def create_user_terminal(self, user_name, terminal_name, requester_terminal_id): params = {'user_name': user_name, 'name': terminal_name, 'name_extra': requester_terminal_id} - result = self._request(uri, 'POST', params) + self._request(uri, 'POST', params) def set_toopher_enabled_for_user(self, user_name, enabled): uri = self.base_url + '/users' @@ -92,13 +91,13 @@ def set_toopher_enabled_for_user(self, user_name, enabled): users = self._request(uri, 'GET', params) if len(users) > 1: - raise ToopherApiException('Multiple users with name = {}'.format(user_name)) + raise ToopherApiError('Multiple users with name = {}'.format(user_name)) elif not len(users): - raise ToopherApiException('No users with name = {}'.format(user_name)) + raise ToopherApiError('No users with name = {}'.format(user_name)) uri = self.base_url + '/users/' + users[0]['id'] params = {'disable_toopher_auth': bool(enabled)} - result = self._request(uri, 'POST', params) + self._request(uri, 'POST', params) def _request(self, uri, method, params=None): data = {'params' if method == 'GET' else 'data': params} From b90bb414340540b1273e98917c5333bbabd4330f Mon Sep 17 00:00:00 2001 From: Thomas Darr Date: Wed, 4 Dec 2013 11:49:48 -0600 Subject: [PATCH 18/73] Update tests to use Requests --- tests.py | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/tests.py b/tests.py index d6f0dd7..1049a6f 100644 --- a/tests.py +++ b/tests.py @@ -1,24 +1,28 @@ import json -import os import toopher +import requests import unittest -import urlparse class HttpClientMock(object): def __init__(self, paths): self.paths = paths - def request(self, uri, method, data, headers): + def request(self, method, uri, data=None, headers=None, params=None): self.last_called_uri = uri self.last_called_method = method - self.last_called_data = urlparse.parse_qs(data) + self.last_called_data = data if data else params self.last_called_headers = headers if uri in self.paths: - return self.paths[uri] + return ResponseMock(self.paths[uri]) else: - return {'status':400}, None + return {'status': 400}, '' +class ResponseMock(requests.Response): + def __init__(self, response): + self.encoding = 'utf-8' + self.status_code = int(response[0]['status']) + self._content = response[1] class ToopherTests(unittest.TestCase): def test_constructor(self): @@ -52,7 +56,7 @@ def test_create_pairing(self): pairing = api.pair('awkward turtle', 'some user') self.assertEqual(api.client.last_called_method, 'POST') - self.assertEqual(api.client.last_called_data['pairing_phrase'], ['awkward turtle']) + self.assertEqual(api.client.last_called_data['pairing_phrase'], 'awkward turtle') with self.assertRaises(KeyError): self.assertEqual(api.client.last_called_data['test_param'], ['42']) @@ -85,10 +89,10 @@ def test_create_authentication_request(self): }) auth_request = api.authenticate('1', 'test terminal') self.assertEqual(api.client.last_called_method, 'POST') - self.assertEqual(api.client.last_called_data['pairing_id'], ['1']) - self.assertEqual(api.client.last_called_data['terminal_name'], ['test terminal']) + self.assertEqual(api.client.last_called_data['pairing_id'], '1') + self.assertEqual(api.client.last_called_data['terminal_name'], 'test terminal') with self.assertRaises(KeyError): - self.assertEqual(api.client.last_called_data['test_param'], ['42']) + self.assertEqual(api.client.last_called_data['test_param'], '42') def test_authentication_status(self): api = toopher.ToopherApi('key', 'secret', api_url='http://testonly') @@ -122,8 +126,8 @@ def test_pass_arbitrary_parameters_on_pair(self): pairing = api.pair('awkward turtle', 'some user', test_param='42') self.assertEqual(api.client.last_called_method, 'POST') - self.assertEqual(api.client.last_called_data['pairing_phrase'], ['awkward turtle']) - self.assertEqual(api.client.last_called_data['test_param'], ['42']) + self.assertEqual(api.client.last_called_data['pairing_phrase'], 'awkward turtle') + self.assertEqual(api.client.last_called_data['test_param'], '42') def test_pass_arbitrary_parameters_on_authenticate(self): api = toopher.ToopherApi('key', 'secret', api_url='http://testonly') @@ -135,9 +139,9 @@ def test_pass_arbitrary_parameters_on_authenticate(self): }) auth_request = api.authenticate('1', 'test terminal', test_param='42') self.assertEqual(api.client.last_called_method, 'POST') - self.assertEqual(api.client.last_called_data['pairing_id'], ['1']) - self.assertEqual(api.client.last_called_data['terminal_name'], ['test terminal']) - self.assertEqual(api.client.last_called_data['test_param'], ['42']) + self.assertEqual(api.client.last_called_data['pairing_id'], '1') + self.assertEqual(api.client.last_called_data['terminal_name'], 'test terminal') + self.assertEqual(api.client.last_called_data['test_param'], '42') def test_access_arbitrary_keys_in_pairing_status(self): api = toopher.ToopherApi('key', 'secret', api_url='http://testonly') From cad4ccc21b8a21b04a5ccbc124ed730a3677141d Mon Sep 17 00:00:00 2001 From: Thomas Darr Date: Wed, 4 Dec 2013 11:53:41 -0600 Subject: [PATCH 19/73] Rename duplicate test --- tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests.py b/tests.py index 1049a6f..92d4c52 100644 --- a/tests.py +++ b/tests.py @@ -221,7 +221,7 @@ def test_disabled_pairing_raises_correct_error(self): with self.assertRaises(toopher.PairingDeactivatedError): auth_request = api.authenticate_by_user_name('user', 'terminal name') - def test_disabled_pairing_raises_correct_error(self): + def test_unauthorized_pairing_raises_correct_error(self): api = toopher.ToopherApi('key', 'secret', api_url='https://toopher.test/v1') api.client = HttpClientMock({ 'https://toopher.test/v1/authentication_requests/initiate': From c72aeca03fd19d5367cba9e10af9092ba90f0ffa Mon Sep 17 00:00:00 2001 From: Thomas Darr Date: Wed, 4 Dec 2013 12:01:12 -0600 Subject: [PATCH 20/73] Add nose and coverage --- requirements.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/requirements.txt b/requirements.txt index dc0bcb7..6f2c3df 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,5 @@ +coverage==3.7 +nose==1.3.0 oauthlib==0.6.0 requests==2.0.1 requests-oauthlib==0.4.0 From efafbe404e00670fbf679216f17e10a12cd51f11 Mon Sep 17 00:00:00 2001 From: Thomas Darr Date: Wed, 4 Dec 2013 12:54:51 -0600 Subject: [PATCH 21/73] Move api_url to module --- tests.py | 59 +++++++++++++++++++++++++++++--------------------------- 1 file changed, 31 insertions(+), 28 deletions(-) diff --git a/tests.py b/tests.py index 92d4c52..faf7601 100644 --- a/tests.py +++ b/tests.py @@ -13,6 +13,7 @@ def request(self, method, uri, data=None, headers=None, params=None): self.last_called_data = data if data else params self.last_called_headers = headers + uri = uri.split(toopher.DEFAULT_BASE_URL)[1][1:] if uri in self.paths: return ResponseMock(self.paths[uri]) else: @@ -25,11 +26,13 @@ def __init__(self, response): self._content = response[1] class ToopherTests(unittest.TestCase): + toopher.DEFAULT_BASE_URL = 'https://api.toopher.test/v1' + def test_constructor(self): with self.assertRaises(TypeError): api = toopher.ToopherApi() - api = toopher.ToopherApi('key', 'secret', api_url='http://testonly') + api = toopher.ToopherApi('key', 'secret') def test_version_number_in_library(self): major, minor, patch = toopher.VERSION.split('.') @@ -46,9 +49,9 @@ def test_version_number_in_setup(self): self.assertEqual(version_number, toopher.VERSION) def test_create_pairing(self): - api = toopher.ToopherApi('key', 'secret', api_url='http://testonly') + api = toopher.ToopherApi('key', 'secret') api.client = HttpClientMock({ - 'http://testonly/pairings/create':( + 'pairings/create':( {'status':'200'}, '{"id":"1", "enabled":true, "user":{"id":"1","name":"some user"}}' ) @@ -61,9 +64,9 @@ def test_create_pairing(self): self.assertEqual(api.client.last_called_data['test_param'], ['42']) def test_pairing_status(self): - api = toopher.ToopherApi('key', 'secret', api_url='http://testonly') + api = toopher.ToopherApi('key', 'secret') api.client = HttpClientMock({ - 'http://testonly/pairings/1':( + 'pairings/1':( {'status':'200'}, '{"id":"1", "enabled":true, "user":{"id":"1","name":"some user"}}' ) @@ -80,9 +83,9 @@ def test_pairing_status(self): foo = pairing.random_key def test_create_authentication_request(self): - api = toopher.ToopherApi('key', 'secret', api_url='http://testonly') + api = toopher.ToopherApi('key', 'secret') api.client = HttpClientMock({ - 'http://testonly/authentication_requests/initiate':( + 'authentication_requests/initiate':( {'status':'200'}, '{"id":"1", "pending":false, "granted":true, "automated":false, "reason":"its a test", "terminal":{"id":"1", "name":"test terminal"}}' ) @@ -95,9 +98,9 @@ def test_create_authentication_request(self): self.assertEqual(api.client.last_called_data['test_param'], '42') def test_authentication_status(self): - api = toopher.ToopherApi('key', 'secret', api_url='http://testonly') + api = toopher.ToopherApi('key', 'secret') api.client = HttpClientMock({ - 'http://testonly/authentication_requests/1':( + 'authentication_requests/1':( {'status':'200'}, '{"id":"1", "pending":false, "granted":true, "automated":false, "reason":"its a test", "terminal":{"id":"1", "name":"test terminal"}}' ) @@ -116,9 +119,9 @@ def test_authentication_status(self): foo = auth_request.random_key def test_pass_arbitrary_parameters_on_pair(self): - api = toopher.ToopherApi('key', 'secret', api_url='http://testonly') + api = toopher.ToopherApi('key', 'secret') api.client = HttpClientMock({ - 'http://testonly/pairings/create':( + 'pairings/create':( {'status':'200'}, '{"id":"1", "enabled":true, "user":{"id":"1","name":"some user"}}' ) @@ -130,9 +133,9 @@ def test_pass_arbitrary_parameters_on_pair(self): self.assertEqual(api.client.last_called_data['test_param'], '42') def test_pass_arbitrary_parameters_on_authenticate(self): - api = toopher.ToopherApi('key', 'secret', api_url='http://testonly') + api = toopher.ToopherApi('key', 'secret') api.client = HttpClientMock({ - 'http://testonly/authentication_requests/initiate':( + 'authentication_requests/initiate':( {'status':'200'}, '{"id":"1", "pending":false, "granted":true, "automated":false, "reason":"its a test", "terminal":{"id":"1", "name":"test terminal"}}' ) @@ -144,9 +147,9 @@ def test_pass_arbitrary_parameters_on_authenticate(self): self.assertEqual(api.client.last_called_data['test_param'], '42') def test_access_arbitrary_keys_in_pairing_status(self): - api = toopher.ToopherApi('key', 'secret', api_url='http://testonly') + api = toopher.ToopherApi('key', 'secret') api.client = HttpClientMock({ - 'http://testonly/pairings/1':( + 'pairings/1':( {'status':'200'}, '{"id":"1", "enabled":true, "user":{"id":"1","name":"some user"}, "random_key":"84"}' ) @@ -162,9 +165,9 @@ def test_access_arbitrary_keys_in_pairing_status(self): self.assertEqual(pairing.random_key, "84") def test_access_arbitrary_keys_in_authentication_status(self): - api = toopher.ToopherApi('key', 'secret', api_url='http://testonly') + api = toopher.ToopherApi('key', 'secret') api.client = HttpClientMock({ - 'http://testonly/authentication_requests/1':( + 'authentication_requests/1':( {'status':'200'}, '{"id":"1", "pending":false, "granted":true, "automated":false, "reason":"its a test", "terminal":{"id":"1", "name":"test terminal"}, "random_key":"84"}' ) @@ -182,9 +185,9 @@ def test_access_arbitrary_keys_in_authentication_status(self): self.assertEqual(auth_request.random_key, "84") def test_disabled_user_raises_correct_error(self): - api = toopher.ToopherApi('key', 'secret', api_url='https://toopher.test/v1') + api = toopher.ToopherApi('key', 'secret') api.client = HttpClientMock({ - 'https://toopher.test/v1/authentication_requests/initiate': + 'authentication_requests/initiate': ({'status': 409}, json.dumps( {'error_code': 704, 'error_message': 'disabled user'}))}) @@ -192,9 +195,9 @@ def test_disabled_user_raises_correct_error(self): auth_request = api.authenticate_by_user_name('disabled user', 'terminal name') def test_unknown_user_raises_correct_error(self): - api = toopher.ToopherApi('key', 'secret', api_url='https://toopher.test/v1') + api = toopher.ToopherApi('key', 'secret') api.client = HttpClientMock({ - 'https://toopher.test/v1/authentication_requests/initiate': + 'authentication_requests/initiate': ({'status': 409}, json.dumps( {'error_code': 705, 'error_message': 'unknown user'}))}) @@ -202,19 +205,19 @@ def test_unknown_user_raises_correct_error(self): auth_request = api.authenticate_by_user_name('unknown user', 'terminal name') def test_unknown_terminal_raises_correct_error(self): - api = toopher.ToopherApi('key', 'secret', api_url='https://toopher.test/v1') + api = toopher.ToopherApi('key', 'secret') api.client = HttpClientMock({ - 'https://toopher.test/v1/authentication_requests/initiate': + 'authentication_requests/initiate': ({'status': 409}, json.dumps( {'error_code': 706, 'error_message': 'unknown terminal'}))}) with self.assertRaises(toopher.TerminalUnknownError): auth_request = api.authenticate_by_user_name('user', 'unknown terminal name') - def test_disabled_pairing_raises_correct_error(self): - api = toopher.ToopherApi('key', 'secret', api_url='https://toopher.test/v1') + def test_deactivated_pairing_raises_correct_error(self): + api = toopher.ToopherApi('key', 'secret') api.client = HttpClientMock({ - 'https://toopher.test/v1/authentication_requests/initiate': + 'authentication_requests/initiate': ({'status': 409}, json.dumps( {'error_code': 601, 'error_message': 'pairing has been deactivated'}))}) @@ -222,9 +225,9 @@ def test_disabled_pairing_raises_correct_error(self): auth_request = api.authenticate_by_user_name('user', 'terminal name') def test_unauthorized_pairing_raises_correct_error(self): - api = toopher.ToopherApi('key', 'secret', api_url='https://toopher.test/v1') + api = toopher.ToopherApi('key', 'secret') api.client = HttpClientMock({ - 'https://toopher.test/v1/authentication_requests/initiate': + 'authentication_requests/initiate': ({'status': 409}, json.dumps( {'error_code': 601, 'error_message': 'pairing has not been authorized'}))}) From 970d8ec75ee8f7fa69a30a027dcb9faa45a3d7c0 Mon Sep 17 00:00:00 2001 From: Thomas Darr Date: Wed, 4 Dec 2013 12:57:35 -0600 Subject: [PATCH 22/73] Add AuthenticationStatusTests --- tests.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests.py b/tests.py index faf7601..037ea80 100644 --- a/tests.py +++ b/tests.py @@ -234,6 +234,25 @@ def test_unauthorized_pairing_raises_correct_error(self): with self.assertRaises(toopher.PairingDeactivatedError): auth_request = api.authenticate_by_user_name('user', 'terminal name') +class ddict(dict): + def __getitem__(self, key): + try: + value = super(ddict, self).__getitem__(key) + return value + except KeyError as e: + return ddict() + +class AuthenticationStatusTests(unittest.TestCase): + def test_nonzero_when_granted(self): + response = ddict() + response['granted'] = True + allowed = toopher.AuthenticationStatus(response) + self.assertTrue(allowed) + + response['granted'] = False + denied = toopher.AuthenticationStatus(response) + self.assertFalse(denied) + def main(): unittest.main() From fdd5a7c3b69e664065fbdf365fc5d81f584b9512 Mon Sep 17 00:00:00 2001 From: Thomas Darr Date: Wed, 4 Dec 2013 12:57:47 -0600 Subject: [PATCH 23/73] Add PairingStatusTests --- tests.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests.py b/tests.py index 037ea80..589ed75 100644 --- a/tests.py +++ b/tests.py @@ -253,6 +253,22 @@ def test_nonzero_when_granted(self): denied = toopher.AuthenticationStatus(response) self.assertFalse(denied) +class PairingStatusTests(unittest.TestCase): + def test_incomplete_response_raises_exception(self): + response = {'key': 'value'} + with self.assertRaises(toopher.ToopherApiError): + toopher.PairingStatus(response) + + def test_nonzero_when_granted(self): + response = ddict() + response['enabled'] = True + allowed = toopher.PairingStatus(response) + self.assertTrue(allowed) + + response['enabled'] = False + denied = toopher.PairingStatus(response) + self.assertFalse(denied) + def main(): unittest.main() From 373d768ec5ed586c315ef517ed3be6f42cfa8dd2 Mon Sep 17 00:00:00 2001 From: Thomas Darr Date: Wed, 4 Dec 2013 13:58:16 -0600 Subject: [PATCH 24/73] Remove unnecessary (?) status dicts --- tests.py | 68 +++++++++++++++++++++++++------------------------------- 1 file changed, 30 insertions(+), 38 deletions(-) diff --git a/tests.py b/tests.py index 589ed75..219c9a4 100644 --- a/tests.py +++ b/tests.py @@ -17,12 +17,12 @@ def request(self, method, uri, data=None, headers=None, params=None): if uri in self.paths: return ResponseMock(self.paths[uri]) else: - return {'status': 400}, '' + return ResponseMock((400, '{}')) class ResponseMock(requests.Response): def __init__(self, response): self.encoding = 'utf-8' - self.status_code = int(response[0]['status']) + self.status_code = int(response[0]) self._content = response[1] class ToopherTests(unittest.TestCase): @@ -51,8 +51,7 @@ def test_version_number_in_setup(self): def test_create_pairing(self): api = toopher.ToopherApi('key', 'secret') api.client = HttpClientMock({ - 'pairings/create':( - {'status':'200'}, + 'pairings/create': (200, '{"id":"1", "enabled":true, "user":{"id":"1","name":"some user"}}' ) }) @@ -66,8 +65,7 @@ def test_create_pairing(self): def test_pairing_status(self): api = toopher.ToopherApi('key', 'secret') api.client = HttpClientMock({ - 'pairings/1':( - {'status':'200'}, + 'pairings/1': (200, '{"id":"1", "enabled":true, "user":{"id":"1","name":"some user"}}' ) }) @@ -85,8 +83,7 @@ def test_pairing_status(self): def test_create_authentication_request(self): api = toopher.ToopherApi('key', 'secret') api.client = HttpClientMock({ - 'authentication_requests/initiate':( - {'status':'200'}, + 'authentication_requests/initiate': (200, '{"id":"1", "pending":false, "granted":true, "automated":false, "reason":"its a test", "terminal":{"id":"1", "name":"test terminal"}}' ) }) @@ -100,8 +97,7 @@ def test_create_authentication_request(self): def test_authentication_status(self): api = toopher.ToopherApi('key', 'secret') api.client = HttpClientMock({ - 'authentication_requests/1':( - {'status':'200'}, + 'authentication_requests/1': (200, '{"id":"1", "pending":false, "granted":true, "automated":false, "reason":"its a test", "terminal":{"id":"1", "name":"test terminal"}}' ) }) @@ -121,8 +117,7 @@ def test_authentication_status(self): def test_pass_arbitrary_parameters_on_pair(self): api = toopher.ToopherApi('key', 'secret') api.client = HttpClientMock({ - 'pairings/create':( - {'status':'200'}, + 'pairings/create': (200, '{"id":"1", "enabled":true, "user":{"id":"1","name":"some user"}}' ) }) @@ -135,8 +130,7 @@ def test_pass_arbitrary_parameters_on_pair(self): def test_pass_arbitrary_parameters_on_authenticate(self): api = toopher.ToopherApi('key', 'secret') api.client = HttpClientMock({ - 'authentication_requests/initiate':( - {'status':'200'}, + 'authentication_requests/initiate': (200, '{"id":"1", "pending":false, "granted":true, "automated":false, "reason":"its a test", "terminal":{"id":"1", "name":"test terminal"}}' ) }) @@ -149,8 +143,7 @@ def test_pass_arbitrary_parameters_on_authenticate(self): def test_access_arbitrary_keys_in_pairing_status(self): api = toopher.ToopherApi('key', 'secret') api.client = HttpClientMock({ - 'pairings/1':( - {'status':'200'}, + 'pairings/1': (200, '{"id":"1", "enabled":true, "user":{"id":"1","name":"some user"}, "random_key":"84"}' ) }) @@ -167,8 +160,7 @@ def test_access_arbitrary_keys_in_pairing_status(self): def test_access_arbitrary_keys_in_authentication_status(self): api = toopher.ToopherApi('key', 'secret') api.client = HttpClientMock({ - 'authentication_requests/1':( - {'status':'200'}, + 'authentication_requests/1': (200, '{"id":"1", "pending":false, "granted":true, "automated":false, "reason":"its a test", "terminal":{"id":"1", "name":"test terminal"}, "random_key":"84"}' ) }) @@ -187,50 +179,50 @@ def test_access_arbitrary_keys_in_authentication_status(self): def test_disabled_user_raises_correct_error(self): api = toopher.ToopherApi('key', 'secret') api.client = HttpClientMock({ - 'authentication_requests/initiate': - ({'status': 409}, json.dumps( - {'error_code': 704, - 'error_message': 'disabled user'}))}) + 'authentication_requests/initiate': (409, + json.dumps({'error_code': 704, + 'error_message': 'disabled user'}))}) + with self.assertRaises(toopher.UserDisabledError): auth_request = api.authenticate_by_user_name('disabled user', 'terminal name') def test_unknown_user_raises_correct_error(self): api = toopher.ToopherApi('key', 'secret') api.client = HttpClientMock({ - 'authentication_requests/initiate': - ({'status': 409}, json.dumps( - {'error_code': 705, - 'error_message': 'unknown user'}))}) + 'authentication_requests/initiate': (409, + json.dumps({'error_code': 705, + 'error_message': 'unknown user'}))}) + with self.assertRaises(toopher.UserUnknownError): auth_request = api.authenticate_by_user_name('unknown user', 'terminal name') def test_unknown_terminal_raises_correct_error(self): api = toopher.ToopherApi('key', 'secret') api.client = HttpClientMock({ - 'authentication_requests/initiate': - ({'status': 409}, json.dumps( - {'error_code': 706, - 'error_message': 'unknown terminal'}))}) + 'authentication_requests/initiate': (409, + json.dumps({'error_code': 706, + 'error_message': 'unknown terminal'}))}) + with self.assertRaises(toopher.TerminalUnknownError): auth_request = api.authenticate_by_user_name('user', 'unknown terminal name') def test_deactivated_pairing_raises_correct_error(self): api = toopher.ToopherApi('key', 'secret') api.client = HttpClientMock({ - 'authentication_requests/initiate': - ({'status': 409}, json.dumps( - {'error_code': 601, - 'error_message': 'pairing has been deactivated'}))}) + 'authentication_requests/initiate': (409, + json.dumps({'error_code': 601, + 'error_message': 'pairing has been deactivated'}))}) + with self.assertRaises(toopher.PairingDeactivatedError): auth_request = api.authenticate_by_user_name('user', 'terminal name') def test_unauthorized_pairing_raises_correct_error(self): api = toopher.ToopherApi('key', 'secret') api.client = HttpClientMock({ - 'authentication_requests/initiate': - ({'status': 409}, json.dumps( - {'error_code': 601, - 'error_message': 'pairing has not been authorized'}))}) + 'authentication_requests/initiate': (409, + json.dumps({'error_code': 601, + 'error_message': 'pairing has not been authorized'}))}) + with self.assertRaises(toopher.PairingDeactivatedError): auth_request = api.authenticate_by_user_name('user', 'terminal name') From aebfc784315461e9813bf2fbd05972bbe43c8293 Mon Sep 17 00:00:00 2001 From: Thomas Darr Date: Wed, 4 Dec 2013 13:58:58 -0600 Subject: [PATCH 25/73] Add ZeroStorageTests --- tests.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests.py b/tests.py index 219c9a4..29adf2e 100644 --- a/tests.py +++ b/tests.py @@ -226,6 +226,13 @@ def test_unauthorized_pairing_raises_correct_error(self): with self.assertRaises(toopher.PairingDeactivatedError): auth_request = api.authenticate_by_user_name('user', 'terminal name') +class ZeroStorageTests(unittest.TestCase): + def test_create_user_terminal(self): + api = toopher.ToopherApi('key', 'secret') + api.client = HttpClientMock({'user_terminals/create': (200, '{}')}) + + api.create_user_terminal('user_name', 'terminal_name', 'requester_terminal_id') + class ddict(dict): def __getitem__(self, key): try: From e2870b078f4d837f728fce99bad837908906b4a9 Mon Sep 17 00:00:00 2001 From: Thomas Darr Date: Wed, 4 Dec 2013 13:59:41 -0600 Subject: [PATCH 26/73] Move zero-storage tests --- tests.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests.py b/tests.py index 29adf2e..dbb491f 100644 --- a/tests.py +++ b/tests.py @@ -176,6 +176,13 @@ def test_access_arbitrary_keys_in_authentication_status(self): self.assertEqual(auth_request.random_key, "84") +class ZeroStorageTests(unittest.TestCase): + def test_create_user_terminal(self): + api = toopher.ToopherApi('key', 'secret') + api.client = HttpClientMock({'user_terminals/create': (200, '{}')}) + + api.create_user_terminal('user_name', 'terminal_name', 'requester_terminal_id') + def test_disabled_user_raises_correct_error(self): api = toopher.ToopherApi('key', 'secret') api.client = HttpClientMock({ @@ -226,13 +233,6 @@ def test_unauthorized_pairing_raises_correct_error(self): with self.assertRaises(toopher.PairingDeactivatedError): auth_request = api.authenticate_by_user_name('user', 'terminal name') -class ZeroStorageTests(unittest.TestCase): - def test_create_user_terminal(self): - api = toopher.ToopherApi('key', 'secret') - api.client = HttpClientMock({'user_terminals/create': (200, '{}')}) - - api.create_user_terminal('user_name', 'terminal_name', 'requester_terminal_id') - class ddict(dict): def __getitem__(self, key): try: From 7a8601420852286f460088b6a9c9ea65c4f06af1 Mon Sep 17 00:00:00 2001 From: Thomas Darr Date: Wed, 4 Dec 2013 14:59:07 -0600 Subject: [PATCH 27/73] Improve test coverage (100%) --- tests.py | 89 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/tests.py b/tests.py index dbb491f..8eed8f5 100644 --- a/tests.py +++ b/tests.py @@ -94,6 +94,14 @@ def test_create_authentication_request(self): with self.assertRaises(KeyError): self.assertEqual(api.client.last_called_data['test_param'], '42') + api.authenticate('pairing_id', 'terminal_name', 'action_name') + + last_called_data = api.client.last_called_data + self.assertEqual(api.client.last_called_method, 'POST') + self.assertEqual(last_called_data['pairing_id'], 'pairing_id') + self.assertEqual(last_called_data['terminal_name'], 'terminal_name') + self.assertEqual(last_called_data['action_name'], 'action_name') + def test_authentication_status(self): api = toopher.ToopherApi('key', 'secret') api.client = HttpClientMock({ @@ -176,6 +184,52 @@ def test_access_arbitrary_keys_in_authentication_status(self): self.assertEqual(auth_request.random_key, "84") + def test_pair_sms(self): + api = toopher.ToopherApi('key', 'secret') + api.client = HttpClientMock({ + 'pairings/create/sms': (200, + json.dumps({'id': 'id', + 'enabled': True, + 'user': {'id': 'id', 'name': 'name'}}))}) + + api.pair_sms('phone_number', 'user_name') + last_called_data = api.client.last_called_data + self.assertEqual(api.client.last_called_method, 'POST') + self.assertEqual(last_called_data['phone_number'], 'phone_number') + self.assertEqual(last_called_data['user_name'], 'user_name') + + api.pair_sms('phone_number', 'user_name', 'phone_country') + last_called_data = api.client.last_called_data + self.assertEqual(api.client.last_called_method, 'POST') + self.assertEqual(last_called_data['phone_number'], 'phone_number') + self.assertEqual(last_called_data['user_name'], 'user_name') + self.assertEqual(last_called_data['phone_country'], 'phone_country') + + def test_authenticate_with_otp(self): + api = toopher.ToopherApi('key', 'secret') + api.client = HttpClientMock({ + 'authentication_requests/id/otp_auth': (200, + json.dumps({'id': 'id', + 'pending': False, + 'granted': False, + 'automated': False, + 'reason': 'it is a test', + 'terminal': {'id': 'id', 'name': 'name'}}))}) + + api.authenticate_with_otp('id', 'otp') + self.assertEqual(api.client.last_called_method, 'POST') + self.assertEqual(api.client.last_called_data['otp'], 'otp') + + def test_unrecognized_error_still_raises_error(self): + api = toopher.ToopherApi('key', 'secret') + api.client = HttpClientMock({ + 'authentication_requests/initiate': (409, + json.dumps({'error_code': 42, + 'error_message': 'what'}))}) + + with self.assertRaises(toopher.ToopherApiError): + api.authenticate_by_user_name('user_name', 'terminal_name') + class ZeroStorageTests(unittest.TestCase): def test_create_user_terminal(self): api = toopher.ToopherApi('key', 'secret') @@ -183,6 +237,41 @@ def test_create_user_terminal(self): api.create_user_terminal('user_name', 'terminal_name', 'requester_terminal_id') + last_called_data = api.client.last_called_data + self.assertEqual(api.client.last_called_method, 'POST') + self.assertEqual(last_called_data['user_name'], 'user_name') + self.assertEqual(last_called_data['name'], 'terminal_name') + self.assertEqual(last_called_data['name_extra'], 'requester_terminal_id') + + def test_enable_toopher_for_user(self): + api = toopher.ToopherApi('key', 'secret') + api.client = HttpClientMock({ + 'users': (200, json.dumps([{'id': 'user_id', 'name': 'user_name'}])), + 'users/user_id': (200, json.dumps({'name': 'user_name'}))}) + + api.set_toopher_enabled_for_user('user_name', True) + self.assertEqual(api.client.last_called_method, 'POST') + self.assertTrue(api.client.last_called_data['disable_toopher_auth']) + + api.set_toopher_enabled_for_user('user_name', False) + self.assertEqual(api.client.last_called_method, 'POST') + self.assertFalse(api.client.last_called_data['disable_toopher_auth']) + + def test_enable_toopher_multiple_users(self): + api = toopher.ToopherApi('key', 'secret') + api.client = HttpClientMock({'users': (200, + json.dumps([{'name': 'first user'}, {'name': 'second user'}]))}) + + with self.assertRaises(toopher.ToopherApiError): + api.set_toopher_enabled_for_user('multiple users', True) + + def test_enable_toopher_no_users(self): + api = toopher.ToopherApi('key', 'secret') + api.client = HttpClientMock({'users': (200, '[]')}) + + with self.assertRaises(toopher.ToopherApiError): + api.set_toopher_enabled_for_user('no users', True) + def test_disabled_user_raises_correct_error(self): api = toopher.ToopherApi('key', 'secret') api.client = HttpClientMock({ From 48eda4eb0a313659e756a268c45d1f70a2d52996 Mon Sep 17 00:00:00 2001 From: Thomas Darr Date: Wed, 4 Dec 2013 15:12:13 -0600 Subject: [PATCH 28/73] Use requirements.txt --- .travis.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 8314d74..bbf8c00 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,6 @@ python: - "2.7" # command to install dependencies install: -# - pip install -r requirements.txt --use-mirrors - - pip install toopher + - pip install -r requirements.txt --use-mirrors # command to run tests script: python tests.py From a405add8a82d5b5486276cd22abf8d0f9e249b85 Mon Sep 17 00:00:00 2001 From: Thomas Darr Date: Wed, 4 Dec 2013 15:34:06 -0600 Subject: [PATCH 29/73] Use variables instead of strings --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 957601c..9e92843 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ try: # optimistically try to authenticate against Toopher API with username and a Terminal Identifier # Terminal Identifer is typically a randomly generated secure browser cookie. It does not # need to be human-readable - auth = api.authenticate_by_user_name("username@yourservice.com", "") + auth = api.authenticate_by_user_name(user_name, requester_terminal_id) # if you got here, everything is good! poll the auth request status as described above # there are four distinct errors ToopherAPI can return if it needs more data From de08095c0da564ffacd46b2791cf11dac50ef820 Mon Sep 17 00:00:00 2001 From: Thomas Darr Date: Wed, 4 Dec 2013 15:36:17 -0600 Subject: [PATCH 30/73] =?UTF-8?q?disable=20=E2=89=A0=20enable?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- toopher/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/toopher/__init__.py b/toopher/__init__.py index d5c299d..bf8e652 100644 --- a/toopher/__init__.py +++ b/toopher/__init__.py @@ -96,7 +96,7 @@ def set_toopher_enabled_for_user(self, user_name, enabled): raise ToopherApiError('No users with name = {}'.format(user_name)) uri = self.base_url + '/users/' + users[0]['id'] - params = {'disable_toopher_auth': bool(enabled)} + params = {'disable_toopher_auth': not enabled} self._request(uri, 'POST', params) def _request(self, uri, method, params=None): From a5677018e996f659b248a2c186be62cd757e8ad1 Mon Sep 17 00:00:00 2001 From: Thomas Darr Date: Wed, 4 Dec 2013 15:42:35 -0600 Subject: [PATCH 31/73] =?UTF-8?q?True=20=E2=89=A0=20False=20and=20vice=20v?= =?UTF-8?q?ersa?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests.py b/tests.py index 8eed8f5..3a65529 100644 --- a/tests.py +++ b/tests.py @@ -251,11 +251,11 @@ def test_enable_toopher_for_user(self): api.set_toopher_enabled_for_user('user_name', True) self.assertEqual(api.client.last_called_method, 'POST') - self.assertTrue(api.client.last_called_data['disable_toopher_auth']) + self.assertFalse(api.client.last_called_data['disable_toopher_auth']) api.set_toopher_enabled_for_user('user_name', False) self.assertEqual(api.client.last_called_method, 'POST') - self.assertFalse(api.client.last_called_data['disable_toopher_auth']) + self.assertTrue(api.client.last_called_data['disable_toopher_auth']) def test_enable_toopher_multiple_users(self): api = toopher.ToopherApi('key', 'secret') From 2beb89f7934cd29132cb8fbca4296a99d13a6ed2 Mon Sep 17 00:00:00 2001 From: Thomas Darr Date: Tue, 10 Dec 2013 12:30:27 -0600 Subject: [PATCH 32/73] Update setup.py dependencies --- setup.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/setup.py b/setup.py index be32c7a..3f4d1ea 100755 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ name='toopher', version='1.1.0', author='Toopher, Inc.', - author_email='support@toopher.com', + author_email='dev@toopher.com', url='https://dev.toopher.com', description='Wrapper library for the Toopher authentication API', classifiers=[ @@ -18,9 +18,7 @@ 'Topic :: Software Development :: Libraries :: Python Modules', ], packages=['toopher'], - package_data = {'toopher': ['toopher.pem']}, + package_data={'toopher': ['toopher.pem']}, test_suite='tests', - install_requires=[ - 'oauth2', - ] + install_requires=['requests-oauthlib>=0.4.0'] ) From 0751d45608cfb70482bdaa8bec62fe19661918df Mon Sep 17 00:00:00 2001 From: Thomas Darr Date: Thu, 19 Dec 2013 12:46:09 -0600 Subject: [PATCH 33/73] Verify SSL, actually --- setup.py | 1 - toopher/__init__.py | 1 - toopher/toopher.pem | 20 -------------------- 3 files changed, 22 deletions(-) delete mode 100644 toopher/toopher.pem diff --git a/setup.py b/setup.py index 3f4d1ea..9618583 100755 --- a/setup.py +++ b/setup.py @@ -18,7 +18,6 @@ 'Topic :: Software Development :: Libraries :: Python Modules', ], packages=['toopher'], - package_data={'toopher': ['toopher.pem']}, test_suite='tests', install_requires=['requests-oauthlib>=0.4.0'] ) diff --git a/toopher/__init__.py b/toopher/__init__.py index bf8e652..806eda7 100644 --- a/toopher/__init__.py +++ b/toopher/__init__.py @@ -17,7 +17,6 @@ class PairingDeactivatedError(ToopherApiError): pass class ToopherApi(object): def __init__(self, key, secret, api_url=None): self.client = requests_oauthlib.OAuth1Session(key, client_secret=secret) - self.client.cert = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'toopher.pem') self.client.verify = True base_url = api_url if api_url else DEFAULT_BASE_URL diff --git a/toopher/toopher.pem b/toopher/toopher.pem deleted file mode 100644 index d5b5b8a..0000000 --- a/toopher/toopher.pem +++ /dev/null @@ -1,20 +0,0 @@ -GlobalSign Root CA -================== ------BEGIN CERTIFICATE----- -MIIDdTCCAl2gAwIBAgILBAAAAAABFUtaw5QwDQYJKoZIhvcNAQEFBQAwVzELMAkGA1UEBhMCQkUx -GTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2ExEDAOBgNVBAsTB1Jvb3QgQ0ExGzAZBgNVBAMTEkds -b2JhbFNpZ24gUm9vdCBDQTAeFw05ODA5MDExMjAwMDBaFw0yODAxMjgxMjAwMDBaMFcxCzAJBgNV -BAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMRAwDgYDVQQLEwdSb290IENBMRswGQYD -VQQDExJHbG9iYWxTaWduIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDa -DuaZjc6j40+Kfvvxi4Mla+pIH/EqsLmVEQS98GPR4mdmzxzdzxtIK+6NiY6arymAZavpxy0Sy6sc -THAHoT0KMM0VjU/43dSMUBUc71DuxC73/OlS8pF94G3VNTCOXkNz8kHp1Wrjsok6Vjk4bwY8iGlb -Kk3Fp1S4bInMm/k8yuX9ifUSPJJ4ltbcdG6TRGHRjcdGsnUOhugZitVtbNV4FpWi6cgKOOvyJBNP -c1STE4U6G7weNLWLBYy5d4ux2x8gkasJU26Qzns3dLlwR5EiUWMWea6xrkEmCMgZK9FGqkjWZCrX -gzT/LCrBbBlDSgeF59N89iFo7+ryUp9/k5DPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV -HRMBAf8EBTADAQH/MB0GA1UdDgQWBBRge2YaRQ2XyolQL30EzTSo//z9SzANBgkqhkiG9w0BAQUF -AAOCAQEA1nPnfE920I2/7LqivjTFKDK1fPxsnCwrvQmeU79rXqoRSLblCKOzyj1hTdNGCbM+w6Dj -Y1Ub8rrvrTnhQ7k4o+YviiY776BQVvnGCv04zcQLcFGUl5gE38NflNUVyRRBnMRddWQVDf9VMOyG -j/8N7yy5Y0b2qvzfvGn9LhJIZJrglfCm7ymPAbEVtQwdpf5pLGkkeB6zpxxxYu7KyJesF12KwvhH -hm4qxFYxldBniYUr+WymXUadDKqC5JlR3XC321Y9YeRq4VzW9v493kHMB65jUr9TU/Qr6cf9tveC -X4XSQRjbgbMEHMUfpIBvFSDJ3gyICh3WZlXi/EjJKSZp4A== ------END CERTIFICATE----- From 3a6dad2618c90b8032b76a8bdee16f0a10ebc7f3 Mon Sep 17 00:00:00 2001 From: Seth Holloway Date: Thu, 19 Dec 2013 14:27:22 -0600 Subject: [PATCH 34/73] README updates for zero-storage Uppercased true and false; updated the zero-storage exception names; added and fixed links; updated the Dependencies section based on recent changes --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 846e371..6ad702b 100644 --- a/README.md +++ b/README.md @@ -6,10 +6,10 @@ ToopherPython is a Toopher API library that simplifies the task of interfacing with the Toopher API from Python code. This project wrangles all the required OAuth and JSON functionality so you can focus on just using the API. #### Learn the Toopher API -Make sure you visit (http://dev.toopher.com) to get acquainted with the Toopher API fundamentals. The documentation there will tell you the details about the operations this API wrapper library provides. +Make sure you visit [https://dev.toopher.com](https://dev.toopher.com) to get acquainted with the Toopher API fundamentals. The documentation there will tell you the details about the operations this API wrapper library provides. #### OAuth Authentication -First off, to access the Toopher API you'll need to sign up for an account at the developers portal (http://dev.toopher.com) and create a "requester". When that process is complete, your requester is issued OAuth 1.0a credentials in the form of a consumer key and secret. Your key is used to identify your requester when Toopher interacts with your customers, and the secret is used to sign each request so that we know it is generated by you. This library properly formats each request with your credentials automatically. +First off, to access the Toopher API you'll need to sign up for an account at the [Toopher developers portal](https://dev.toopher.com) and create a "requester". When that process is complete, your requester is issued OAuth 1.0a credentials in the form of a consumer key and secret. Your key is used to identify your requester when Toopher interacts with your customers, and the secret is used to sign each request so that we know it is generated by you. This library properly formats each request with your credentials automatically. #### The Toopher Two-Step Interacting with the Toopher web service involves two steps: pairing, and authenticating. @@ -37,7 +37,7 @@ auth = api.authenticate(pairing_status.id, "my computer") # Once they've responded you can then check the status auth_status = api.get_authentication_status(auth.id) -if (auth_status.pending == false and auth_status.granted == true): +if (auth_status.pending == False and auth_status.granted == True): # Success! ``` @@ -60,10 +60,10 @@ try: # there are four distinct errors ToopherAPI can return if it needs more data except UserDisabledError: # you have marked this user as disabled in the Toopher API. -except UnknownUserError: +except UserUnknownError: # This user has not yet paired a mobile device with their account. Pair them # using api.pair() as described above, then re-try authentication -except UnknownTerminalError: +except TerminalUnknownError: # This user has not assigned a "Friendly Name" to this terminal identifier. # Prompt them to enter a terminal name, then submit that "friendly name" to # the Toopher API: @@ -76,7 +76,7 @@ except PairingDeactivatedError: ``` #### Dependencies -This library uses the python-oauth2 library to handle OAuth signing and httplib2 to make the web requests. If you install using pip (or easy_install) they'll be installed automatically for you. +This library uses the [Requests](http://docs.python-requests.org/en/latest/) library to handle OAuth signing and to make the web requests. If you install using pip (or easy_install) they'll be installed automatically for you. #### Try it out Check out `demo.py` for an example program that walks you through the whole process! Just download the contents of this repo, make sure you have the dependencies listed above installed, and then run it like-a-this: From 4f2d5c15d4103ef23ed3ae0098a04a17278717f2 Mon Sep 17 00:00:00 2001 From: Seth Holloway Date: Thu, 19 Dec 2013 15:05:26 -0600 Subject: [PATCH 35/73] Convert to a lowercase string comparison While testing the PR I found the error message was ToopherApiError: Not allowed: Pairing has not been authorized to authenticate, so Pairing did not match pairing. This should fix the case sensitive search issue. --- toopher/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/toopher/__init__.py b/toopher/__init__.py index 806eda7..d68a12d 100644 --- a/toopher/__init__.py +++ b/toopher/__init__.py @@ -121,8 +121,8 @@ def _parse_request_error(self, content): raise error(error_message) # TODO: Add an error code for PairingDeactivatedError. - if ('pairing has been deactivated' in error_message - or 'pairing has not been authorized' in error_message): + if ('pairing has been deactivated' in error_message.lower() + or 'pairing has not been authorized' in error_message.lower()): raise PairingDeactivatedError(error_message) raise ToopherApiError(error_message) From a6a899111cc066229194440c2f9e5f65e6dd069d Mon Sep 17 00:00:00 2001 From: Seth Holloway Date: Thu, 19 Dec 2013 15:35:24 -0600 Subject: [PATCH 36/73] Update the error message to match the server response The server responds with capitalized Pairing, which necessitates a case insensitive search (already committed). --- tests.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests.py b/tests.py index 3a65529..5346150 100644 --- a/tests.py +++ b/tests.py @@ -307,7 +307,7 @@ def test_deactivated_pairing_raises_correct_error(self): api.client = HttpClientMock({ 'authentication_requests/initiate': (409, json.dumps({'error_code': 601, - 'error_message': 'pairing has been deactivated'}))}) + 'error_message': 'Pairing has been deactivated'}))}) with self.assertRaises(toopher.PairingDeactivatedError): auth_request = api.authenticate_by_user_name('user', 'terminal name') @@ -317,7 +317,7 @@ def test_unauthorized_pairing_raises_correct_error(self): api.client = HttpClientMock({ 'authentication_requests/initiate': (409, json.dumps({'error_code': 601, - 'error_message': 'pairing has not been authorized'}))}) + 'error_message': 'Pairing has not been authorized'}))}) with self.assertRaises(toopher.PairingDeactivatedError): auth_request = api.authenticate_by_user_name('user', 'terminal name') From 4e7cbd10a9f1735553ef3174f4d84c1037171cbb Mon Sep 17 00:00:00 2001 From: Thomas Darr Date: Wed, 15 Jan 2014 14:21:44 -0600 Subject: [PATCH 37/73] Use ye olde '%' formatting --- demo_bells_and_whistles.py | 6 +++--- toopher/__init__.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/demo_bells_and_whistles.py b/demo_bells_and_whistles.py index eb56fb3..af924d5 100755 --- a/demo_bells_and_whistles.py +++ b/demo_bells_and_whistles.py @@ -53,10 +53,10 @@ def print_sep(char='-'): pair_type = 'pairing phrase' print pair_help - pairing_key = raw_input('Enter {0}: '.format(pair_type)) + pairing_key = raw_input('Enter %s: ' % pair_type) while not pairing_key: - print 'Please enter a {0} to continue'.format(pair_type) - pairing_key = raw_input('Enter {0}: '.format(pair_type)) + print 'Please enter a %s to continue' % pair_type + pairing_key = raw_input('Enter %s: ' % pair_type) user_name = raw_input('Enter a username for this pairing [%s]: ' % DEFAULT_USERNAME) if not user_name: diff --git a/toopher/__init__.py b/toopher/__init__.py index d68a12d..85fc8d5 100644 --- a/toopher/__init__.py +++ b/toopher/__init__.py @@ -90,9 +90,9 @@ def set_toopher_enabled_for_user(self, user_name, enabled): users = self._request(uri, 'GET', params) if len(users) > 1: - raise ToopherApiError('Multiple users with name = {}'.format(user_name)) + raise ToopherApiError('Multiple users with name = %s' % user_name) elif not len(users): - raise ToopherApiError('No users with name = {}'.format(user_name)) + raise ToopherApiError('No users with name = %s' % user_name) uri = self.base_url + '/users/' + users[0]['id'] params = {'disable_toopher_auth': not enabled} @@ -100,7 +100,7 @@ def set_toopher_enabled_for_user(self, user_name, enabled): def _request(self, uri, method, params=None): data = {'params' if method == 'GET' else 'data': params} - header_data = {'User-Agent':'Toopher-Python/{} (Python {})'.format(VERSION, sys.version.split()[0])} + header_data = {'User-Agent':'Toopher-Python/%s (Python %s)' % (VERSION, sys.version.split()[0])} response = self.client.request(method, uri, headers=header_data, **data) try: From efbf4c2a0634dc7672ba9a6a2a13ac19f9cdc307 Mon Sep 17 00:00:00 2001 From: Thomas Darr Date: Wed, 15 Jan 2014 14:37:11 -0600 Subject: [PATCH 38/73] Test against Python 2.5 (instead of 2.7) --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index bbf8c00..4375e26 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ language: python python: - - "2.7" + - "2.5" # command to install dependencies install: - pip install -r requirements.txt --use-mirrors From 5ba23ad932f9a72fab1e94d7c5decee2bc0c8292 Mon Sep 17 00:00:00 2001 From: Thomas Darr Date: Wed, 15 Jan 2014 15:31:17 -0600 Subject: [PATCH 39/73] Test against Python 2.6 instead of 2.5 --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 4375e26..f02bc79 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ language: python python: - - "2.5" + - "2.6" # command to install dependencies install: - pip install -r requirements.txt --use-mirrors From 353105fd2da997c930035a6f96a8ec62dcd836ac Mon Sep 17 00:00:00 2001 From: Thomas Darr Date: Wed, 15 Jan 2014 16:22:17 -0600 Subject: [PATCH 40/73] Use 2.6-compatible unittest.assertRaises calls --- tests.py | 44 ++++++++++++++++++++++++++++++-------------- 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/tests.py b/tests.py index 5346150..bffaaa1 100644 --- a/tests.py +++ b/tests.py @@ -29,8 +29,9 @@ class ToopherTests(unittest.TestCase): toopher.DEFAULT_BASE_URL = 'https://api.toopher.test/v1' def test_constructor(self): - with self.assertRaises(TypeError): + def fn(): api = toopher.ToopherApi() + self.assertRaises(TypeError, fn) api = toopher.ToopherApi('key', 'secret') @@ -59,8 +60,10 @@ def test_create_pairing(self): self.assertEqual(api.client.last_called_method, 'POST') self.assertEqual(api.client.last_called_data['pairing_phrase'], 'awkward turtle') - with self.assertRaises(KeyError): + + def fn(): self.assertEqual(api.client.last_called_data['test_param'], ['42']) + self.assertRaises(KeyError, fn) def test_pairing_status(self): api = toopher.ToopherApi('key', 'secret') @@ -77,8 +80,9 @@ def test_pairing_status(self): self.assertEqual(pairing.user_id, '1') self.assertTrue(pairing.enabled) - with self.assertRaises(KeyError): + def fn(): foo = pairing.random_key + self.assertRaises(KeyError, fn) def test_create_authentication_request(self): api = toopher.ToopherApi('key', 'secret') @@ -91,8 +95,10 @@ def test_create_authentication_request(self): self.assertEqual(api.client.last_called_method, 'POST') self.assertEqual(api.client.last_called_data['pairing_id'], '1') self.assertEqual(api.client.last_called_data['terminal_name'], 'test terminal') - with self.assertRaises(KeyError): + + def fn(): self.assertEqual(api.client.last_called_data['test_param'], '42') + self.assertRaises(KeyError, fn) api.authenticate('pairing_id', 'terminal_name', 'action_name') @@ -119,8 +125,9 @@ def test_authentication_status(self): self.assertEqual(auth_request.terminal_id, '1') self.assertEqual(auth_request.terminal_name, 'test terminal') - with self.assertRaises(KeyError): + def fn(): foo = auth_request.random_key + self.assertRaises(KeyError, fn) def test_pass_arbitrary_parameters_on_pair(self): api = toopher.ToopherApi('key', 'secret') @@ -227,8 +234,9 @@ def test_unrecognized_error_still_raises_error(self): json.dumps({'error_code': 42, 'error_message': 'what'}))}) - with self.assertRaises(toopher.ToopherApiError): + def fn(): api.authenticate_by_user_name('user_name', 'terminal_name') + self.assertRaises(toopher.ToopherApiError, fn) class ZeroStorageTests(unittest.TestCase): def test_create_user_terminal(self): @@ -262,15 +270,17 @@ def test_enable_toopher_multiple_users(self): api.client = HttpClientMock({'users': (200, json.dumps([{'name': 'first user'}, {'name': 'second user'}]))}) - with self.assertRaises(toopher.ToopherApiError): + def fn(): api.set_toopher_enabled_for_user('multiple users', True) + self.assertRaises(toopher.ToopherApiError, fn) def test_enable_toopher_no_users(self): api = toopher.ToopherApi('key', 'secret') api.client = HttpClientMock({'users': (200, '[]')}) - with self.assertRaises(toopher.ToopherApiError): + def fn(): api.set_toopher_enabled_for_user('no users', True) + self.assertRaises(toopher.ToopherApiError, fn) def test_disabled_user_raises_correct_error(self): api = toopher.ToopherApi('key', 'secret') @@ -279,8 +289,9 @@ def test_disabled_user_raises_correct_error(self): json.dumps({'error_code': 704, 'error_message': 'disabled user'}))}) - with self.assertRaises(toopher.UserDisabledError): + def fn(): auth_request = api.authenticate_by_user_name('disabled user', 'terminal name') + self.assertRaises(toopher.UserDisabledError, fn) def test_unknown_user_raises_correct_error(self): api = toopher.ToopherApi('key', 'secret') @@ -289,8 +300,9 @@ def test_unknown_user_raises_correct_error(self): json.dumps({'error_code': 705, 'error_message': 'unknown user'}))}) - with self.assertRaises(toopher.UserUnknownError): + def fn(): auth_request = api.authenticate_by_user_name('unknown user', 'terminal name') + self.assertRaises(toopher.UserUnknownError, fn) def test_unknown_terminal_raises_correct_error(self): api = toopher.ToopherApi('key', 'secret') @@ -299,8 +311,9 @@ def test_unknown_terminal_raises_correct_error(self): json.dumps({'error_code': 706, 'error_message': 'unknown terminal'}))}) - with self.assertRaises(toopher.TerminalUnknownError): + def fn(): auth_request = api.authenticate_by_user_name('user', 'unknown terminal name') + self.assertRaises(toopher.TerminalUnknownError, fn) def test_deactivated_pairing_raises_correct_error(self): api = toopher.ToopherApi('key', 'secret') @@ -309,8 +322,9 @@ def test_deactivated_pairing_raises_correct_error(self): json.dumps({'error_code': 601, 'error_message': 'Pairing has been deactivated'}))}) - with self.assertRaises(toopher.PairingDeactivatedError): + def fn(): auth_request = api.authenticate_by_user_name('user', 'terminal name') + self.assertRaises(toopher.PairingDeactivatedError, fn) def test_unauthorized_pairing_raises_correct_error(self): api = toopher.ToopherApi('key', 'secret') @@ -319,8 +333,9 @@ def test_unauthorized_pairing_raises_correct_error(self): json.dumps({'error_code': 601, 'error_message': 'Pairing has not been authorized'}))}) - with self.assertRaises(toopher.PairingDeactivatedError): + def fn(): auth_request = api.authenticate_by_user_name('user', 'terminal name') + self.assertRaises(toopher.PairingDeactivatedError, fn) class ddict(dict): def __getitem__(self, key): @@ -344,8 +359,9 @@ def test_nonzero_when_granted(self): class PairingStatusTests(unittest.TestCase): def test_incomplete_response_raises_exception(self): response = {'key': 'value'} - with self.assertRaises(toopher.ToopherApiError): + def fn(): toopher.PairingStatus(response) + self.assertRaises(toopher.ToopherApiError, fn) def test_nonzero_when_granted(self): response = ddict() From 18425c40db20be2c373fe93fb6aa04fc3262a02f Mon Sep 17 00:00:00 2001 From: Thomas Darr Date: Wed, 15 Jan 2014 16:51:24 -0600 Subject: [PATCH 41/73] Use assertTrue instead of assertGreaterEqual --- tests.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests.py b/tests.py index bffaaa1..fefb79a 100644 --- a/tests.py +++ b/tests.py @@ -37,9 +37,9 @@ def fn(): def test_version_number_in_library(self): major, minor, patch = toopher.VERSION.split('.') - self.assertGreaterEqual(int(major), 1) - self.assertGreaterEqual(int(minor), 0) - self.assertGreaterEqual(int(patch), 0) + self.assertTrue(int(major) >= 1) + self.assertTrue(int(minor) >= 0) + self.assertTrue(int(patch) >= 0) def test_version_number_in_setup(self): ''' Ensure that the setup.py file has the same version number as the toopher/__init__.py file ''' From 5705308f19f76f8921ff2a2635f72e4e7f7d2399 Mon Sep 17 00:00:00 2001 From: Thomas Darr Date: Tue, 12 Nov 2013 14:03:43 -0600 Subject: [PATCH 42/73] Add QR pairing creation method --- toopher/__init__.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/toopher/__init__.py b/toopher/__init__.py index 85fc8d5..c2b2516 100644 --- a/toopher/__init__.py +++ b/toopher/__init__.py @@ -1,4 +1,4 @@ -import os +import os.path import requests_oauthlib import sys @@ -32,6 +32,13 @@ def pair(self, pairing_phrase, user_name, **kwargs): result = self._request(uri, "POST", params) return PairingStatus(result) + def pair_qr(self, user_name, **kwargs): + uri = self.base_url + '/pairings/create/qr' + params = {'user_name': user_name} + params.update(kwargs) + result = self._request(uri, 'POST', params) + return PairingStatus(result) + def pair_sms(self, phone_number, user_name, phone_country=None): uri = self.base_url + "/pairings/create/sms" params = {'phone_number': phone_number, From 64ef611134e084efeaee3528f737a85be5a0ce21 Mon Sep 17 00:00:00 2001 From: Thomas Darr Date: Wed, 4 Dec 2013 17:29:44 -0600 Subject: [PATCH 43/73] Add tests --- tests.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests.py b/tests.py index fefb79a..18e664e 100644 --- a/tests.py +++ b/tests.py @@ -191,6 +191,18 @@ def test_access_arbitrary_keys_in_authentication_status(self): self.assertEqual(auth_request.random_key, "84") + def test_pair_qr(self): + api = toopher.ToopherApi('key', 'secret') + api.client = HttpClientMock({ + 'pairings/create/qr': (200, + json.dumps({'id': 'id', + 'enabled': True, + 'user': {'id': 'id', 'name': 'name'}}))}) + + api.pair_qr('user_name') + self.assertEqual(api.client.last_called_method, 'POST') + self.assertEqual(api.client.last_called_data['user_name'], 'user_name') + def test_pair_sms(self): api = toopher.ToopherApi('key', 'secret') api.client = HttpClientMock({ From e05b2315241022a0260fe5c1e1415f057aad3397 Mon Sep 17 00:00:00 2001 From: Evan Grim Date: Wed, 22 Jan 2014 16:04:29 -0600 Subject: [PATCH 44/73] Fix __getattr__ implementation to raise correct exception If an unknown attribute name access was attempted on `PairingStatus` or `AuthenticationStatus` objects they would raise a KeyError instead of an AttributeError. This breach of contract breaks things like pickling - so this commit fixes that. --- toopher/__init__.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/toopher/__init__.py b/toopher/__init__.py index 85fc8d5..bf21285 100644 --- a/toopher/__init__.py +++ b/toopher/__init__.py @@ -145,7 +145,10 @@ def __nonzero__(self): return self.enabled def __getattr__(self, name): - return self._raw_data[name] + try: + return self._raw_data[name] + except KeyError: + raise AttributeError(name) class AuthenticationStatus(object): @@ -169,4 +172,10 @@ def __nonzero__(self): return self.granted def __getattr__(self, name): - return self._raw_data[name] + try: + return self._raw_data[name] + except KeyError: + raise AttributeError(name) + + +class ToopherApiError(Exception): pass From 128d8ad7f199cb30c363bedaa3096bf18aa780dd Mon Sep 17 00:00:00 2001 From: Evan Grim Date: Wed, 22 Jan 2014 16:38:16 -0600 Subject: [PATCH 45/73] Fix the tests that verified the breach-of-contract for __getattr__ --- tests.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests.py b/tests.py index fefb79a..85783a1 100644 --- a/tests.py +++ b/tests.py @@ -82,7 +82,7 @@ def test_pairing_status(self): def fn(): foo = pairing.random_key - self.assertRaises(KeyError, fn) + self.assertRaises(AttributeError, fn) def test_create_authentication_request(self): api = toopher.ToopherApi('key', 'secret') @@ -127,7 +127,7 @@ def test_authentication_status(self): def fn(): foo = auth_request.random_key - self.assertRaises(KeyError, fn) + self.assertRaises(AttributeError, fn) def test_pass_arbitrary_parameters_on_pair(self): api = toopher.ToopherApi('key', 'secret') From 839b9cf4dafe1a32167953e4c73edddaa2b3571a Mon Sep 17 00:00:00 2001 From: Evan Grim Date: Wed, 22 Jan 2014 18:07:42 -0600 Subject: [PATCH 46/73] Fix infinite recursion when unpickling PairingStatus and AuthenticationStatus --- toopher/__init__.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/toopher/__init__.py b/toopher/__init__.py index bf21285..dd99cf2 100644 --- a/toopher/__init__.py +++ b/toopher/__init__.py @@ -145,10 +145,10 @@ def __nonzero__(self): return self.enabled def __getattr__(self, name): - try: + if name.startswith('__') or name not in self._raw_data: # Exclude 'magic' methods to allow for (un)pickling + return super(PairingStatus, self).__getattr__(name) + else: return self._raw_data[name] - except KeyError: - raise AttributeError(name) class AuthenticationStatus(object): @@ -172,10 +172,10 @@ def __nonzero__(self): return self.granted def __getattr__(self, name): - try: + if name.startswith('__') or name not in self._raw_data: # Exclude 'magic' methods to allow for (un)pickling + return super(AuthenticationStatus, self).__getattr__(name) + else: return self._raw_data[name] - except KeyError: - raise AttributeError(name) class ToopherApiError(Exception): pass From 1b93ff2d178fe7807b70e9ed78afbb030d4771a8 Mon Sep 17 00:00:00 2001 From: Drew Shafer Date: Mon, 24 Mar 2014 18:56:40 -0500 Subject: [PATCH 47/73] Add iframe libs --- tests.py | 35 +++++++++++++++++++ toopher/__init__.py | 84 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 119 insertions(+) diff --git a/tests.py b/tests.py index 85783a1..57476b5 100644 --- a/tests.py +++ b/tests.py @@ -2,6 +2,7 @@ import toopher import requests import unittest +import time class HttpClientMock(object): def __init__(self, paths): @@ -25,6 +26,40 @@ def __init__(self, response): self.status_code = int(response[0]) self._content = response[1] +class ToopherIframeTests(unittest.TestCase): + request_token = 's9s7vsb' + + def setUp(self): + self.iframe_api = toopher.ToopherIframe('abcdefg', 'hijklmnop') + self.old_time = time.time + time.time = lambda:1000 + + def tearDown(self): + time.time = self.old_time + + + def test_validate_good_signature_is_successful(self): + data = { + 'foo':'bar', + 'timestamp':'1000', + 'session_token':ToopherIframeTests.request_token, + 'toopher_sig':'6d2c7GlQssGmeYYGpcf+V/kirOI=' + } + try: + self.iframe_api.validate(data, ToopherIframeTests.request_token) + except toopher.SignatureValidationError: + self.fail() + + def test_get_pair_uri(self): + expected = 'https://api.toopher.test/v1/web/pair?username=jdoe&reset_email=jdoe%40example.com&expires=1100&v=2&oauth_nonce=12345678&oauth_timestamp=1000&oauth_version=1.0&oauth_signature_method=HMAC-SHA1&oauth_consumer_key=abcdefg&oauth_signature=UGlgBEUF6UZEhYPxevJeagqy6D4%3D' + self.assertEqual(expected, self.iframe_api.pair_uri('jdoe', 'jdoe@example.com')) + + def test_get_login_uri(self): + expected = 'https://api.toopher.test/v1/web/auth?username=jdoe&automation_allowed=True&reset_email=jdoe%40example.com&session_token=s9s7vsb&v=2&requester_metadata=None&challenge_required=False&expires=1100&action_name=Log+In&oauth_nonce=12345678&oauth_timestamp=1000&oauth_version=1.0&oauth_signature_method=HMAC-SHA1&oauth_consumer_key=abcdefg&oauth_signature=bpgdxhHLDwpYsbru%2Bnz2p9pFlr4%3D' + self.assertEqual(expected, self.iframe_api.login_uri('jdoe', 'jdoe@example.com', ToopherIframeTests.request_token)) + + + class ToopherTests(unittest.TestCase): toopher.DEFAULT_BASE_URL = 'https://api.toopher.test/v1' diff --git a/toopher/__init__.py b/toopher/__init__.py index dd99cf2..5f27169 100644 --- a/toopher/__init__.py +++ b/toopher/__init__.py @@ -1,8 +1,16 @@ import os +from oauthlib import oauth1 +import urllib +import hashlib +import hmac +import base64 +import time import requests_oauthlib import sys DEFAULT_BASE_URL = "https://api.toopher.com/v1" +DEFAULT_IFRAME_TTL = 100 +IFRAME_VERSION = '2' VERSION = '1.1.0' class ToopherApiError(Exception): pass @@ -14,6 +22,82 @@ class PairingDeactivatedError(ToopherApiError): pass 705: UserUnknownError, 706: TerminalUnknownError} +class SignatureValidationError(Exception): pass + +class ToopherIframe(object): + + def __init__(self, key, secret, api_uri=None): + self.secret = secret + self.client = oauth1.Client(key, client_secret=secret, signature_type=oauth1.SIGNATURE_TYPE_QUERY) + self.client.nonce = '12345678' + api_uri = api_uri if api_uri else DEFAULT_BASE_URL + self.base_uri = api_uri.rstrip('/') + + def pair_uri(self, username, reset_email, ttl = DEFAULT_IFRAME_TTL): + params = { + 'v':IFRAME_VERSION, + 'username':username, + 'reset_email':reset_email + } + return self.get_oauth_uri(self.base_uri + '/web/pair', params, ttl)[0] + + def auth_uri(self, username, reset_email, action_name, automation_allowed, challenge_required, request_token, requester_metadata, ttl=DEFAULT_IFRAME_TTL): + params = { + 'v':IFRAME_VERSION, + 'username':username, + 'reset_email':reset_email, + 'action_name':action_name, + 'automation_allowed':automation_allowed, + 'challenge_required':challenge_required, + 'session_token':request_token, + 'requester_metadata':requester_metadata + } + return self.get_oauth_uri(self.base_uri + '/web/auth', params, ttl)[0] + + def login_uri(self, username, reset_email, request_token): + return self.auth_uri(username, reset_email, 'Log In', True, False, request_token, 'None', DEFAULT_IFRAME_TTL) + + def validate(self, data, request_token=None, ttl=DEFAULT_IFRAME_TTL): + missing_keys = [] + for required_key in ('toopher_sig', 'timestamp', 'session_token'): + if not required_key in data: + missing_keys.append(required_key) + + if missing_keys: + raise SignatureValidationError("Missing required keys: {0}".format(missing_keys)) + + if request_token: + if request_token != data.get('session_token'): + raise SignatureValidationError("Session token does not match expected value!") + + maybe_sig = data['toopher_sig'] + del data['toopher_sig'] + signature_valid = False + try: + computed_signature =self.signature(data) + signature_valid = maybe_sig == computed_signature + except Exception, e: + raise SignatureValidationError("Error while calculating signature", e) + + if not signature_valid: + raise SignatureValidationError("Computed signature does not match submitted signature: {0} vs {1}".format(computed_signature, maybe_sig)) + + ttl_valid = int(time.time()) - int(data['timestamp']) < ttl + if not ttl_valid: + raise SignatureValidationError("TTL expired") + + return data + + def signature(self, data): + to_sign = urllib.urlencode(sorted(data.items())).encode('utf-8') + secret = self.client.client_secret.encode('utf-8') + return base64.b64encode(hmac.new(secret, to_sign, hashlib.sha1).digest()) + + def get_oauth_uri(self, uri, params, ttl): + params['expires'] = str(int(time.time()) + ttl) + return self.client.sign(uri + '?' + urllib.urlencode(params)) + + class ToopherApi(object): def __init__(self, key, secret, api_url=None): self.client = requests_oauthlib.OAuth1Session(key, client_secret=secret) From a883484da45c877685f72e8d7362e2f8cf0e5932 Mon Sep 17 00:00:00 2001 From: Drew Shafer Date: Tue, 25 Mar 2014 09:37:15 -0500 Subject: [PATCH 48/73] PR feedback - make return type consistent with method name --- toopher/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/toopher/__init__.py b/toopher/__init__.py index 5f27169..0f5da1a 100644 --- a/toopher/__init__.py +++ b/toopher/__init__.py @@ -39,7 +39,7 @@ def pair_uri(self, username, reset_email, ttl = DEFAULT_IFRAME_TTL): 'username':username, 'reset_email':reset_email } - return self.get_oauth_uri(self.base_uri + '/web/pair', params, ttl)[0] + return self.get_oauth_uri(self.base_uri + '/web/pair', params, ttl) def auth_uri(self, username, reset_email, action_name, automation_allowed, challenge_required, request_token, requester_metadata, ttl=DEFAULT_IFRAME_TTL): params = { @@ -52,7 +52,7 @@ def auth_uri(self, username, reset_email, action_name, automation_allowed, chall 'session_token':request_token, 'requester_metadata':requester_metadata } - return self.get_oauth_uri(self.base_uri + '/web/auth', params, ttl)[0] + return self.get_oauth_uri(self.base_uri + '/web/auth', params, ttl) def login_uri(self, username, reset_email, request_token): return self.auth_uri(username, reset_email, 'Log In', True, False, request_token, 'None', DEFAULT_IFRAME_TTL) @@ -95,7 +95,7 @@ def signature(self, data): def get_oauth_uri(self, uri, params, ttl): params['expires'] = str(int(time.time()) + ttl) - return self.client.sign(uri + '?' + urllib.urlencode(params)) + return self.client.sign(uri + '?' + urllib.urlencode(params))[0] class ToopherApi(object): From b2c36c9b0daf0bf31deaf363021c348fca080d79 Mon Sep 17 00:00:00 2001 From: Drew Shafer Date: Tue, 25 Mar 2014 09:38:16 -0500 Subject: [PATCH 49/73] PR feedback --- tests.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests.py b/tests.py index 57476b5..b7de4df 100644 --- a/tests.py +++ b/tests.py @@ -37,7 +37,6 @@ def setUp(self): def tearDown(self): time.time = self.old_time - def test_validate_good_signature_is_successful(self): data = { 'foo':'bar', @@ -58,7 +57,6 @@ def test_get_login_uri(self): expected = 'https://api.toopher.test/v1/web/auth?username=jdoe&automation_allowed=True&reset_email=jdoe%40example.com&session_token=s9s7vsb&v=2&requester_metadata=None&challenge_required=False&expires=1100&action_name=Log+In&oauth_nonce=12345678&oauth_timestamp=1000&oauth_version=1.0&oauth_signature_method=HMAC-SHA1&oauth_consumer_key=abcdefg&oauth_signature=bpgdxhHLDwpYsbru%2Bnz2p9pFlr4%3D' self.assertEqual(expected, self.iframe_api.login_uri('jdoe', 'jdoe@example.com', ToopherIframeTests.request_token)) - class ToopherTests(unittest.TestCase): toopher.DEFAULT_BASE_URL = 'https://api.toopher.test/v1' From 045b0b5e104ae4cb7bc2cf8dfae9f60fcd162461 Mon Sep 17 00:00:00 2001 From: Drew Shafer Date: Tue, 25 Mar 2014 10:19:21 -0500 Subject: [PATCH 50/73] PR Feedback - this is why we have code review, people --- tests.py | 1 + toopher/__init__.py | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/tests.py b/tests.py index b7de4df..b1ffc90 100644 --- a/tests.py +++ b/tests.py @@ -31,6 +31,7 @@ class ToopherIframeTests(unittest.TestCase): def setUp(self): self.iframe_api = toopher.ToopherIframe('abcdefg', 'hijklmnop') + self.iframe_api.client.nonce = '12345678' self.old_time = time.time time.time = lambda:1000 diff --git a/toopher/__init__.py b/toopher/__init__.py index 0f5da1a..b84fb63 100644 --- a/toopher/__init__.py +++ b/toopher/__init__.py @@ -29,7 +29,6 @@ class ToopherIframe(object): def __init__(self, key, secret, api_uri=None): self.secret = secret self.client = oauth1.Client(key, client_secret=secret, signature_type=oauth1.SIGNATURE_TYPE_QUERY) - self.client.nonce = '12345678' api_uri = api_uri if api_uri else DEFAULT_BASE_URL self.base_uri = api_uri.rstrip('/') From d38509810f6dd12c6e0088ebc27dbf086ca8cff2 Mon Sep 17 00:00:00 2001 From: Drew Shafer Date: Tue, 25 Mar 2014 12:40:58 -0500 Subject: [PATCH 51/73] update to reflect new pairing deactivated error code 707 --- tests.py | 2 +- toopher/__init__.py | 17 +++++++++++------ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/tests.py b/tests.py index b1ffc90..ad9d8a5 100644 --- a/tests.py +++ b/tests.py @@ -353,7 +353,7 @@ def test_deactivated_pairing_raises_correct_error(self): api = toopher.ToopherApi('key', 'secret') api.client = HttpClientMock({ 'authentication_requests/initiate': (409, - json.dumps({'error_code': 601, + json.dumps({'error_code': 707, 'error_message': 'Pairing has been deactivated'}))}) def fn(): diff --git a/toopher/__init__.py b/toopher/__init__.py index b84fb63..665c77c 100644 --- a/toopher/__init__.py +++ b/toopher/__init__.py @@ -18,9 +18,16 @@ class UserDisabledError(ToopherApiError): pass class UserUnknownError(ToopherApiError): pass class TerminalUnknownError(ToopherApiError): pass class PairingDeactivatedError(ToopherApiError): pass -error_codes_to_errors = {704: UserDisabledError, - 705: UserUnknownError, - 706: TerminalUnknownError} + +ERROR_CODE_USER_DISABLED = 704 +ERROR_CODE_USER_UNKNOWN = 705 +ERROR_CODE_TERMINAL_UNKNOWN = 706 +ERROR_CODE_PAIRING_DEACTIVATED = 707 + +error_codes_to_errors = {ERROR_CODE_USER_DISABLED: UserDisabledError, + ERROR_CODE_USER_UNKNOWN: UserUnknownError, + ERROR_CODE_TERMINAL_UNKNOWN: TerminalUnknownError, + ERROR_CODE_PAIRING_DEACTIVATED: PairingDeactivatedError} class SignatureValidationError(Exception): pass @@ -203,9 +210,7 @@ def _parse_request_error(self, content): error = error_codes_to_errors[error_code] raise error(error_message) - # TODO: Add an error code for PairingDeactivatedError. - if ('pairing has been deactivated' in error_message.lower() - or 'pairing has not been authorized' in error_message.lower()): + if 'pairing has not been authorized' in error_message.lower(): raise PairingDeactivatedError(error_message) raise ToopherApiError(error_message) From 427b8e8e61511f356c82a2b90f3cdc94942dc7e7 Mon Sep 17 00:00:00 2001 From: Drew Shafer Date: Tue, 25 Mar 2014 12:47:51 -0500 Subject: [PATCH 52/73] Add iframe README --- README-Iframe.md | 106 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 README-Iframe.md diff --git a/README-Iframe.md b/README-Iframe.md new file mode 100644 index 0000000..7568732 --- /dev/null +++ b/README-Iframe.md @@ -0,0 +1,106 @@ +Authenticating using the Toopher ` + +There is no difference in the markup required for a Pairing vs. an Authentication iframe request (the generated URI embeds all relevant information). + +# Examples + +#### Generating an Authentication iframe URI +Every Toopher Authentication session should include a unique `request_token` - a randomized string that is included in the signed request to the Toopher API and returned in the signed response from the Toopher ` +```html + + + + +``` There is no difference in the markup required for a Pairing vs. an Authentication iframe request (the generated URI embeds all relevant information). @@ -46,61 +48,72 @@ Every Toopher Authentication session should include a unique `request_token` - a Creating a random request token and storing it in the server-side session using Django: - import random, string - request_token = ''.join(random.choice(string.lowercase + string.digits) for i in range(15)) - request.session['ToopherRequestToken'] = request_token +```python +import random, string +request_token = ''.join(random.choice(string.lowercase + string.digits) for i in range(15)) +request.session['ToopherRequestToken'] = request_token +``` The Toopher Authentication API provides the requester a rich set of controls over authentication parameters. - auth_iframe_url = iframe_api.auth_uri(username, reset_email, action_name, automation_allowed, challenge_required, request_token, requester_metadata, ttl); +```python +auth_iframe_url = iframe_api.auth_uri(username, reset_email, action_name, automation_allowed, challenge_required, request_token, requester_metadata, ttl); +``` For the simple case of authenticating a user at login, a `login_uri` helper method is available: - login_iframe_url = iframe_api.login_uri(username, reset_email, request_token) +```python +login_iframe_url = iframe_api.login_uri(username, reset_email, request_token) +``` #### Generating a Pairing iframe URI - pair_iframe_url = iframe_api.pair_uri(username, reset_email) +```python +pair_iframe_url = iframe_api.pair_uri(username, reset_email) +``` #### Validating postback data from Authentication iframe and parsing API errors In this example, `data` is a `dict` of the form data POSTed to your server from the Toopher Authentication iframe. You should replace the commented blocks with code appropriate for the condition described in the comment. - request_token = request.session.get('ToopherRequestToken') - # invalidate the Request Token to guard against replay attacks - if 'ToopherRequestToken' in request.session: - del request.session['ToopherRequestToken'] - - try: - validated_data = iframe_api.validate(data, request_token) - if 'error_code' in validated_data: - error_code = validated_data['error_code'] - # check for API errors - - if error_code == ToopherIframe.ERROR_CODE_PAIRING_DEACTIVATED: - # User deleted the pairing on their mobile device. - # - # Your server should display a Toopher Pairing iframe so their account can be re-paired - # - elif error_code == ToopherIframe.ERROR_CODE_USER_OPT_OUT: - # User has been marked as "Opt-Out" in the Toopher API - # - # If your service allows opt-out, the user should be granted access. - # - elif error_code == ToopherIframe.ERROR_CODE_USER_UNKNOWN: - # User has never authenticated with Toopher on this server - # - # Your server should display a Toopher Pairing iframe so their account can be paired - # - else: - # signature is valid, and no api errors. check authentication result - auth_pending = validated_data.get('pending').lower() == 'true' - auth_granted = validated_data.get('granted').lower() == 'true' - - # authentication_result is the ultimate result of Toopher second-factor authentication - authentication_result = auth_granted and not auth_pending - except toopher.SignatureValidationError, e: - # signature was invalid. User should not authenticated - # - # e.message will return more information about what specifically - # went wrong (incorrect session token, expired TTL, invalid signature) - # +```python + +request_token = request.session.get('ToopherRequestToken') +# invalidate the Request Token to guard against replay attacks +if 'ToopherRequestToken' in request.session: + del request.session['ToopherRequestToken'] + +try: + validated_data = iframe_api.validate(data, request_token) + if 'error_code' in validated_data: + error_code = validated_data['error_code'] + # check for API errors + + if error_code == ToopherIframe.ERROR_CODE_PAIRING_DEACTIVATED: + # User deleted the pairing on their mobile device. + # + # Your server should display a Toopher Pairing iframe so their account can be re-paired + # + elif error_code == ToopherIframe.ERROR_CODE_USER_OPT_OUT: + # User has been marked as "Opt-Out" in the Toopher API + # + # If your service allows opt-out, the user should be granted access. + # + elif error_code == ToopherIframe.ERROR_CODE_USER_UNKNOWN: + # User has never authenticated with Toopher on this server + # + # Your server should display a Toopher Pairing iframe so their account can be paired + # + else: + # signature is valid, and no api errors. check authentication result + auth_pending = validated_data.get('pending').lower() == 'true' + auth_granted = validated_data.get('granted').lower() == 'true' + + # authentication_result is the ultimate result of Toopher second-factor authentication + authentication_result = auth_granted and not auth_pending +except toopher.SignatureValidationError, e: + # signature was invalid. User should not authenticated + # + # e.message will return more information about what specifically + # went wrong (incorrect session token, expired TTL, invalid signature) + # +``` From 0faa99bd7bdac7118b9f38ea6546240f49d1ff9a Mon Sep 17 00:00:00 2001 From: Seth Holloway Date: Sun, 13 Apr 2014 23:12:50 -0500 Subject: [PATCH 60/73] Fix namespace of errors They're in the `toopher` module--not the `ToopherIframe` --- README-Iframe.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README-Iframe.md b/README-Iframe.md index b9e820b..5aafd82 100644 --- a/README-Iframe.md +++ b/README-Iframe.md @@ -88,17 +88,17 @@ try: error_code = validated_data['error_code'] # check for API errors - if error_code == ToopherIframe.ERROR_CODE_PAIRING_DEACTIVATED: + if error_code == toopher.ERROR_CODE_PAIRING_DEACTIVATED: # User deleted the pairing on their mobile device. # # Your server should display a Toopher Pairing iframe so their account can be re-paired # - elif error_code == ToopherIframe.ERROR_CODE_USER_OPT_OUT: + elif error_code == toopher.ERROR_CODE_USER_OPT_OUT: # User has been marked as "Opt-Out" in the Toopher API # # If your service allows opt-out, the user should be granted access. # - elif error_code == ToopherIframe.ERROR_CODE_USER_UNKNOWN: + elif error_code == toopher.ERROR_CODE_USER_UNKNOWN: # User has never authenticated with Toopher on this server # # Your server should display a Toopher Pairing iframe so their account can be paired From 4a288ec42d9758e41a4a032a3f472a6c65065d98 Mon Sep 17 00:00:00 2001 From: Seth Holloway Date: Sun, 13 Apr 2014 23:19:58 -0500 Subject: [PATCH 61/73] Convert from Java error to Python error toopher-python: ERROR_CODE_USER_DISABLED = 704 toopher-java: public static final String USER_OPT_OUT = "704"; --- README-Iframe.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README-Iframe.md b/README-Iframe.md index 5aafd82..ed10347 100644 --- a/README-Iframe.md +++ b/README-Iframe.md @@ -93,7 +93,7 @@ try: # # Your server should display a Toopher Pairing iframe so their account can be re-paired # - elif error_code == toopher.ERROR_CODE_USER_OPT_OUT: + elif error_code == toopher.ERROR_CODE_USER_DISABLED: # User has been marked as "Opt-Out" in the Toopher API # # If your service allows opt-out, the user should be granted access. From 804c2368837d0f4baa05619a93b575cc56859a11 Mon Sep 17 00:00:00 2001 From: Seth Holloway Date: Mon, 14 Apr 2014 12:46:52 -0500 Subject: [PATCH 62/73] Test validate with immutable dict --- requirements.txt | 1 + tests.py | 14 ++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/requirements.txt b/requirements.txt index 6f2c3df..0e0f6ca 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,3 +3,4 @@ nose==1.3.0 oauthlib==0.6.0 requests==2.0.1 requests-oauthlib==0.4.0 +Werkzeug==0.9.4 diff --git a/tests.py b/tests.py index f485777..dba309c 100644 --- a/tests.py +++ b/tests.py @@ -3,6 +3,7 @@ import requests import unittest import time +import werkzeug.datastructures class HttpClientMock(object): def __init__(self, paths): @@ -62,6 +63,19 @@ def test_arrays_get_flattened_for_validate(self): except toopher.SignatureValidationError: self.fail() + def test_immutable_dictionaries_get_copied_for_validate(self): + data = werkzeug.datastructures.ImmutableMultiDict([ + ('foo', 'bar'), + ('timestamp', '1000'), + ('session_token', ToopherIframeTests.request_token), + ('toopher_sig', '6d2c7GlQssGmeYYGpcf+V/kirOI=') + ]) + import pdb; pdb.set_trace() + try: + self.iframe_api.validate(data, ToopherIframeTests.request_token) + except toopher.SignatureValidationError: + self.fail() + def test_get_pair_uri(self): expected = 'https://api.toopher.test/v1/web/pair?username=jdoe&reset_email=jdoe%40example.com&expires=1100&v=2&oauth_nonce=12345678&oauth_timestamp=1000&oauth_version=1.0&oauth_signature_method=HMAC-SHA1&oauth_consumer_key=abcdefg&oauth_signature=UGlgBEUF6UZEhYPxevJeagqy6D4%3D' self.assertEqual(expected, self.iframe_api.pair_uri('jdoe', 'jdoe@example.com')) From ad1e99e3b22f9b1daa17a3665e4d0d3d7ebae5ec Mon Sep 17 00:00:00 2001 From: Seth Holloway Date: Mon, 14 Apr 2014 12:47:11 -0500 Subject: [PATCH 63/73] Create a mutable copy of data to satisfy test --- toopher/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/toopher/__init__.py b/toopher/__init__.py index 5aacf3f..4d8ebf1 100644 --- a/toopher/__init__.py +++ b/toopher/__init__.py @@ -64,6 +64,9 @@ def login_uri(self, username, reset_email, request_token): return self.auth_uri(username, reset_email, 'Log In', True, False, request_token, 'None', DEFAULT_IFRAME_TTL) def validate(self, data, request_token=None, ttl=DEFAULT_IFRAME_TTL): + # make a mutable copy of the data + data = dict(data) + # flatten data if necessary if hasattr(data.values()[0], '__iter__'): data = dict((k,v[0]) for (k,v) in data.items()) @@ -75,7 +78,7 @@ def validate(self, data, request_token=None, ttl=DEFAULT_IFRAME_TTL): if missing_keys: raise SignatureValidationError("Missing required keys: {0}".format(missing_keys)) - + if request_token: if request_token != data.get('session_token'): raise SignatureValidationError("Session token does not match expected value!") From 137b8aeb03b70ba5cebbfa099d7703ca6533a3ad Mon Sep 17 00:00:00 2001 From: Seth Holloway Date: Mon, 14 Apr 2014 12:49:00 -0500 Subject: [PATCH 64/73] Remove debug breakpoint --- tests.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests.py b/tests.py index dba309c..6f7cb90 100644 --- a/tests.py +++ b/tests.py @@ -70,7 +70,6 @@ def test_immutable_dictionaries_get_copied_for_validate(self): ('session_token', ToopherIframeTests.request_token), ('toopher_sig', '6d2c7GlQssGmeYYGpcf+V/kirOI=') ]) - import pdb; pdb.set_trace() try: self.iframe_api.validate(data, ToopherIframeTests.request_token) except toopher.SignatureValidationError: From 9bbf814929f14152bf10f27b3b7110c3ed3b7ccb Mon Sep 17 00:00:00 2001 From: Seth Holloway Date: Sun, 18 May 2014 16:07:24 -0500 Subject: [PATCH 65/73] Ignore the tests file --- .coveragerc | 1 + 1 file changed, 1 insertion(+) diff --git a/.coveragerc b/.coveragerc index 812fc3b..d09fe9c 100644 --- a/.coveragerc +++ b/.coveragerc @@ -2,3 +2,4 @@ omit = */python?.?/* */site-packages/nose/* + tests.py From 34eae22ab98913199b30b21dc0739486a2698b5d Mon Sep 17 00:00:00 2001 From: Seth Holloway Date: Mon, 19 May 2014 10:53:14 -0500 Subject: [PATCH 66/73] Switch to using the new web/authenticate endpoint --- tests.py | 2 +- toopher/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests.py b/tests.py index f485777..687cf2b 100644 --- a/tests.py +++ b/tests.py @@ -67,7 +67,7 @@ def test_get_pair_uri(self): self.assertEqual(expected, self.iframe_api.pair_uri('jdoe', 'jdoe@example.com')) def test_get_login_uri(self): - expected = 'https://api.toopher.test/v1/web/auth?username=jdoe&automation_allowed=True&reset_email=jdoe%40example.com&session_token=s9s7vsb&v=2&requester_metadata=None&challenge_required=False&expires=1100&action_name=Log+In&oauth_nonce=12345678&oauth_timestamp=1000&oauth_version=1.0&oauth_signature_method=HMAC-SHA1&oauth_consumer_key=abcdefg&oauth_signature=bpgdxhHLDwpYsbru%2Bnz2p9pFlr4%3D' + expected = 'https://api.toopher.test/v1/web/authenticate?username=jdoe&automation_allowed=True&reset_email=jdoe%40example.com&session_token=s9s7vsb&v=2&requester_metadata=None&challenge_required=False&expires=1100&action_name=Log+In&oauth_nonce=12345678&oauth_timestamp=1000&oauth_version=1.0&oauth_signature_method=HMAC-SHA1&oauth_consumer_key=abcdefg&oauth_signature=PykRbVHUP2OTTjGF0GJaS5TTu54%3D' self.assertEqual(expected, self.iframe_api.login_uri('jdoe', 'jdoe@example.com', ToopherIframeTests.request_token)) diff --git a/toopher/__init__.py b/toopher/__init__.py index 5aacf3f..4458e3f 100644 --- a/toopher/__init__.py +++ b/toopher/__init__.py @@ -58,7 +58,7 @@ def auth_uri(self, username, reset_email, action_name, automation_allowed, chall 'session_token':request_token, 'requester_metadata':requester_metadata } - return self.get_oauth_uri(self.base_uri + '/web/auth', params, ttl) + return self.get_oauth_uri(self.base_uri + '/web/authenticate', params, ttl) def login_uri(self, username, reset_email, request_token): return self.auth_uri(username, reset_email, 'Log In', True, False, request_token, 'None', DEFAULT_IFRAME_TTL) From c72e48c40e1e8f7406c93d5722a860f77482aa67 Mon Sep 17 00:00:00 2001 From: Drew Shafer Date: Sun, 21 Sep 2014 19:20:51 -0500 Subject: [PATCH 67/73] add helper method for manage_user iframe endpoint --- tests.py | 4 ++++ toopher/__init__.py | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/tests.py b/tests.py index 371d29d..bcd4263 100644 --- a/tests.py +++ b/tests.py @@ -79,6 +79,10 @@ def test_get_pair_uri(self): expected = 'https://api.toopher.test/v1/web/pair?username=jdoe&reset_email=jdoe%40example.com&expires=1100&v=2&oauth_nonce=12345678&oauth_timestamp=1000&oauth_version=1.0&oauth_signature_method=HMAC-SHA1&oauth_consumer_key=abcdefg&oauth_signature=UGlgBEUF6UZEhYPxevJeagqy6D4%3D' self.assertEqual(expected, self.iframe_api.pair_uri('jdoe', 'jdoe@example.com')) + def test_get_manage_user_uri(self): + expected = 'https://api.toopher.test/v1/web/manage_user?username=jdoe&reset_email=jdoe%40example.com&expires=1100&v=2&oauth_nonce=12345678&oauth_timestamp=1000&oauth_version=1.0&oauth_signature_method=HMAC-SHA1&oauth_consumer_key=abcdefg&oauth_signature=sV8qoKnxJ3fxfP6AHNa0eNFxzJs%3D' + self.assertEqual(expected, self.iframe_api.manage_user_uri('jdoe', 'jdoe@example.com')) + def test_get_login_uri(self): expected = 'https://api.toopher.test/v1/web/authenticate?username=jdoe&automation_allowed=True&reset_email=jdoe%40example.com&session_token=s9s7vsb&v=2&requester_metadata=None&challenge_required=False&expires=1100&action_name=Log+In&oauth_nonce=12345678&oauth_timestamp=1000&oauth_version=1.0&oauth_signature_method=HMAC-SHA1&oauth_consumer_key=abcdefg&oauth_signature=PykRbVHUP2OTTjGF0GJaS5TTu54%3D' self.assertEqual(expected, self.iframe_api.login_uri('jdoe', 'jdoe@example.com', ToopherIframeTests.request_token)) diff --git a/toopher/__init__.py b/toopher/__init__.py index 2359789..5c2ab02 100644 --- a/toopher/__init__.py +++ b/toopher/__init__.py @@ -47,6 +47,14 @@ def pair_uri(self, username, reset_email, ttl = DEFAULT_IFRAME_TTL): } return self.get_oauth_uri(self.base_uri + '/web/pair', params, ttl) + def manage_user_uri(self, username, reset_email, ttl = DEFAULT_IFRAME_TTL): + params = { + 'v':IFRAME_VERSION, + 'username':username, + 'reset_email':reset_email + } + return self.get_oauth_uri(self.base_uri + '/web/manage_user', params, ttl) + def auth_uri(self, username, reset_email, action_name, automation_allowed, challenge_required, request_token, requester_metadata, ttl=DEFAULT_IFRAME_TTL): params = { 'v':IFRAME_VERSION, From ff8b272937c0f89e62f2bc8d6d6adce51562c495 Mon Sep 17 00:00:00 2001 From: Seth Holloway Date: Mon, 22 Sep 2014 08:19:27 -0500 Subject: [PATCH 68/73] Remove spaces in default parameter --- toopher/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/toopher/__init__.py b/toopher/__init__.py index 5c2ab02..201be57 100644 --- a/toopher/__init__.py +++ b/toopher/__init__.py @@ -47,7 +47,7 @@ def pair_uri(self, username, reset_email, ttl = DEFAULT_IFRAME_TTL): } return self.get_oauth_uri(self.base_uri + '/web/pair', params, ttl) - def manage_user_uri(self, username, reset_email, ttl = DEFAULT_IFRAME_TTL): + def manage_user_uri(self, username, reset_email, ttl=DEFAULT_IFRAME_TTL): params = { 'v':IFRAME_VERSION, 'username':username, From 3e5664ac57523c93ecf236fd005663373f87b50c Mon Sep 17 00:00:00 2001 From: Drew Shafer Date: Thu, 25 Sep 2014 13:48:00 -0500 Subject: [PATCH 69/73] modify tests for allow_inline_pairing parameter in iframe --- tests.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests.py b/tests.py index bcd4263..c849a51 100644 --- a/tests.py +++ b/tests.py @@ -84,9 +84,13 @@ def test_get_manage_user_uri(self): self.assertEqual(expected, self.iframe_api.manage_user_uri('jdoe', 'jdoe@example.com')) def test_get_login_uri(self): - expected = 'https://api.toopher.test/v1/web/authenticate?username=jdoe&automation_allowed=True&reset_email=jdoe%40example.com&session_token=s9s7vsb&v=2&requester_metadata=None&challenge_required=False&expires=1100&action_name=Log+In&oauth_nonce=12345678&oauth_timestamp=1000&oauth_version=1.0&oauth_signature_method=HMAC-SHA1&oauth_consumer_key=abcdefg&oauth_signature=PykRbVHUP2OTTjGF0GJaS5TTu54%3D' + expected = 'https://api.toopher.test/v1/web/authenticate?username=jdoe&automation_allowed=True&reset_email=jdoe%40example.com&session_token=s9s7vsb&v=2&allow_inline_pairing=True&requester_metadata=None&challenge_required=False&expires=1100&action_name=Log+In&oauth_nonce=12345678&oauth_timestamp=1000&oauth_version=1.0&oauth_signature_method=HMAC-SHA1&oauth_consumer_key=abcdefg&oauth_signature=URVngBe35eP%2FiFOSQ5ZpuGEYcJs%3D' self.assertEqual(expected, self.iframe_api.login_uri('jdoe', 'jdoe@example.com', ToopherIframeTests.request_token)) + def test_get_login_uri_without_inline_pairing(self): + expected = 'https://api.toopher.test/v1/web/authenticate?username=jdoe&automation_allowed=True&reset_email=jdoe%40example.com&session_token=s9s7vsb&v=2&allow_inline_pairing=False&requester_metadata=None&challenge_required=False&expires=1100&action_name=Log+In&oauth_nonce=12345678&oauth_timestamp=1000&oauth_version=1.0&oauth_signature_method=HMAC-SHA1&oauth_consumer_key=abcdefg&oauth_signature=lbz2kuYG3BM2Y0mZLElbTiWPv8A%3D' + self.assertEqual(expected, self.iframe_api.login_uri('jdoe', 'jdoe@example.com', ToopherIframeTests.request_token, allow_inline_pairing=False)) + class ToopherTests(unittest.TestCase): toopher.DEFAULT_BASE_URL = 'https://api.toopher.test/v1' From 581de0e07eee26cba1d989a068ff3654d4574e48 Mon Sep 17 00:00:00 2001 From: Drew Shafer Date: Thu, 25 Sep 2014 13:51:53 -0500 Subject: [PATCH 70/73] add allow_inline_pairing kwarg to authenticate iframe --- toopher/__init__.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/toopher/__init__.py b/toopher/__init__.py index 201be57..5afa6e3 100644 --- a/toopher/__init__.py +++ b/toopher/__init__.py @@ -55,7 +55,7 @@ def manage_user_uri(self, username, reset_email, ttl=DEFAULT_IFRAME_TTL): } return self.get_oauth_uri(self.base_uri + '/web/manage_user', params, ttl) - def auth_uri(self, username, reset_email, action_name, automation_allowed, challenge_required, request_token, requester_metadata, ttl=DEFAULT_IFRAME_TTL): + def auth_uri(self, username, reset_email, action_name, automation_allowed, challenge_required, request_token, requester_metadata, ttl=DEFAULT_IFRAME_TTL, allow_inline_pairing=True): params = { 'v':IFRAME_VERSION, 'username':username, @@ -64,12 +64,13 @@ def auth_uri(self, username, reset_email, action_name, automation_allowed, chall 'automation_allowed':automation_allowed, 'challenge_required':challenge_required, 'session_token':request_token, - 'requester_metadata':requester_metadata + 'requester_metadata':requester_metadata, + 'allow_inline_pairing':allow_inline_pairing } return self.get_oauth_uri(self.base_uri + '/web/authenticate', params, ttl) - def login_uri(self, username, reset_email, request_token): - return self.auth_uri(username, reset_email, 'Log In', True, False, request_token, 'None', DEFAULT_IFRAME_TTL) + def login_uri(self, username, reset_email, request_token, **kwargs): + return self.auth_uri(username, reset_email, 'Log In', True, False, request_token, 'None', DEFAULT_IFRAME_TTL, **kwargs) def validate(self, data, request_token=None, ttl=DEFAULT_IFRAME_TTL): # make a mutable copy of the data From 0ce408b868519b3612ad4fa7a9e13f384d952e59 Mon Sep 17 00:00:00 2001 From: Grace Yim Date: Fri, 21 Nov 2014 09:48:40 -0600 Subject: [PATCH 71/73] Add CONTRIBUTING.md --- CONTRIBUTING.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..5a28fc6 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,15 @@ +# ToopherAPI Python Client + +#### Installing Dependencies +Toopher uses [pip](https://pypi.python.org/pypi/pip) to install packages. + +To install using pip run: +```shell +$ pip install -r requirements.txt +``` + +#### Tests +To run tests enter: +```shell +$ python tests.py +``` From 042315de9cd8685fa82bd57ddf93e3112f246314 Mon Sep 17 00:00:00 2001 From: Grace Yim Date: Fri, 21 Nov 2014 09:48:40 -0600 Subject: [PATCH 72/73] Add CONTRIBUTING.md --- CONTRIBUTING.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..7e099f7 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,15 @@ +# ToopherAPI Python Client + +#### Installing Dependencies +Toopher uses [pip](https://pypi.python.org/pypi/pip) to install packages. + +To ensure all dependencies are up-to-date run: +```shell +$ pip install -r requirements.txt +``` + +#### Tests +To run tests enter: +```shell +$ python tests.py +``` From 54cc1cbf20ad1040216867fe154902f04b3ada3c Mon Sep 17 00:00:00 2001 From: Grace Yim Date: Fri, 21 Nov 2014 17:33:26 -0600 Subject: [PATCH 73/73] Add Python version --- CONTRIBUTING.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7e099f7..dc36c5f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,5 +1,8 @@ # ToopherAPI Python Client +#### Python Version +>=2.6.0 + #### Installing Dependencies Toopher uses [pip](https://pypi.python.org/pypi/pip) to install packages.