Repository: auth0/auth0-python
Pull request #198 (2019-06-13 18:36:57)

Fix request creation when headers are the default
### Changes

There was an error affecting only the `authentication` API client and methods exposed on this SDK. When the call was not specifying custom headers (headers=None) a call to dictionary#update would fail. 

This PR fixes that issue, as well as a response converting issue.

### Testing

I manually run this directly calling the requests library. Also run the AuthenticationBase#post/get calls.

- Added missing tests for the "default" scenario on those 2 methods.
- Added missing tests for the `management#put` method.

- [x] This change adds unit test coverage
- [x] This change has been tested on the latest version of the platform/language or why not

### Checklist

- [x] I have read the [Auth0 general contribution guidelines](https://github.com/auth0/open-source-template/blob/master/GENERAL-CONTRIBUTING.md)
- [x] I have read the [Auth0 Code of Conduct](https://github.com/auth0/open-source-template/blob/master/CODE-OF-CONDUCT.md)
- [x] All existing and new tests complete without errors



Patch:
diff --git a/auth0/v3/authentication/base.py b/auth0/v3/authentication/base.py
index 1b670e3..8e60ff8 100644
--- a/auth0/v3/authentication/base.py
+++ b/auth0/v3/authentication/base.py
@@ -42,16 +42,15 @@ class AuthenticationBase(object):
 
     def post(self, url, data=None, headers=None):
         request_headers = self.base_headers.copy()
-        request_headers.update(headers)
-        response = requests.post(url=url, data=json.dumps(data),
-                                 headers=request_headers)
+        request_headers.update(headers or {})
+        response = requests.post(url=url, json=data, headers=request_headers)
         return self._process_response(response)
 
     def get(self, url, params=None, headers=None):
         request_headers = self.base_headers.copy()
-        request_headers.update(headers)
+        request_headers.update(headers or {})
         response = requests.get(url=url, params=params, headers=request_headers)
-        return response.text
+        return self._process_response(response)
 
     def _process_response(self, response):
         return self._parse(response).content()
diff --git a/auth0/v3/authentication/database.py b/auth0/v3/authentication/database.py
index e9740d7..f867384 100644
--- a/auth0/v3/authentication/database.py
+++ b/auth0/v3/authentication/database.py
@@ -32,8 +32,7 @@ class Database(AuthenticationBase):
                 'device': device,
                 'grant_type': grant_type,
                 'scope': scope,
-            },
-            headers={'Content-Type': 'application/json'}
+            }
         )
 
     def signup(self, client_id, email, password, connection, username=None,
@@ -67,8 +66,7 @@ class Database(AuthenticationBase):
 
         return self.post(
             'https://{}/dbconnections/signup'.format(self.domain),
-            data=body,
-            headers={'Content-Type': 'application/json'}
+            data=body
         )
 
     def change_password(self, client_id, email, connection, password=None):
@@ -82,6 +80,5 @@ class Database(AuthenticationBase):
                 'email': email,
                 'password': password,
                 'connection': connection,
-            },
-            headers={'Content-Type': 'application/json'}
+            }
         )
diff --git a/auth0/v3/authentication/delegated.py b/auth0/v3/authentication/delegated.py
index 2a568f6..5c738de 100644
--- a/auth0/v3/authentication/delegated.py
+++ b/auth0/v3/authentication/delegated.py
@@ -37,6 +37,5 @@ class Delegated(AuthenticationBase):
 
         return self.post(
             'https://{}/delegation'.format(self.domain),
-            headers={'Content-Type': 'application/json'},
             data=data
         )
diff --git a/auth0/v3/authentication/get_token.py b/auth0/v3/authentication/get_token.py
index 6d254a5..dfc0c05 100644
--- a/auth0/v3/authentication/get_token.py
+++ b/auth0/v3/authentication/get_token.py
@@ -42,8 +42,7 @@ class GetToken(AuthenticationBase):
                 'code': code,
                 'grant_type': grant_type,
                 'redirect_uri': redirect_uri,
-            },
-            headers={'Content-Type': 'application/json'}
+            }
         )
 
     def authorization_code_pkce(self, client_id, code_verifier, code,
@@ -79,8 +78,7 @@ class GetToken(AuthenticationBase):
                 'code': code,
                 'grant_type': grant_type,
                 'redirect_uri': redirect_uri,
-            },
-            headers={'Content-Type': 'application/json'}
+            }
         )
 
     def client_credentials(self, client_id, client_secret, audience,
@@ -113,8 +111,7 @@ class GetToken(AuthenticationBase):
                 'client_secret': client_secret,
                 'audience': audience,
                 'grant_type': grant_type,
-            },
-            headers={'Content-Type': 'application/json'}
+            }
         )
 
     def login(self, client_id, client_secret, username, password, scope, realm,
@@ -164,8 +161,7 @@ class GetToken(AuthenticationBase):
                 'scope': scope,
                 'audience': audience,
                 'grant_type': grant_type
-            },
-            headers={'Content-Type': 'application/json'}
+            }
         )
 
     def refresh_token(self, client_id, client_secret, refresh_token, grant_type='refresh_token'):
@@ -194,6 +190,5 @@ class GetToken(AuthenticationBase):
                 'client_secret': client_secret,
                 'refresh_token': refresh_token,
                 'grant_type': grant_type
-            },
-            headers={'Content-Type': 'application/json'}
+            }
         )
diff --git a/auth0/v3/authentication/logout.py b/auth0/v3/authentication/logout.py
index 8d38a2c..8e154e1 100644
--- a/auth0/v3/authentication/logout.py
+++ b/auth0/v3/authentication/logout.py
@@ -32,12 +32,10 @@ class Logout(AuthenticationBase):
         if federated is True:
             return self.get(
                 'https://{}/v2/logout?federated&client_id={}&returnTo={}'.format(
-                    self.domain, client_id, return_to),
-                headers={'Content-Type': 'application/json'}
+                    self.domain, client_id, return_to)
             )
         return self.get(
             'https://{}/v2/logout?client_id={}&returnTo={}'.format(self.domain,
                                                                    client_id,
-                                                                   return_to),
-            headers={'Content-Type': 'application/json'}
+                                                                   return_to)
         )
diff --git a/auth0/v3/authentication/passwordless.py b/auth0/v3/authentication/passwordless.py
index 83ed90e..dd8db97 100644
--- a/auth0/v3/authentication/passwordless.py
+++ b/auth0/v3/authentication/passwordless.py
@@ -41,8 +41,7 @@ class Passwordless(AuthenticationBase):
                 'email': email,
                 'send': send,
                 'authParams': auth_params
-            },
-            headers={'Content-Type': 'application/json'}
+            }
         )
 
     def sms(self, client_id, phone_number):
@@ -55,8 +54,7 @@ class Passwordless(AuthenticationBase):
                 'client_id': client_id,
                 'connection': 'sms',
                 'phone_number': phone_number,
-            },
-            headers={'Content-Type': 'application/json'}
+            }
         )
 
     def sms_login(self, client_id, phone_number, code, scope='openid'):
@@ -72,6 +70,5 @@ class Passwordless(AuthenticationBase):
                 'username': phone_number,
                 'password': code,
                 'scope': scope,
-            },
-            headers={'Content-Type': 'application/json'}
+            }
         )
diff --git a/auth0/v3/authentication/social.py b/auth0/v3/authentication/social.py
index 333333d..da6e117 100644
--- a/auth0/v3/authentication/social.py
+++ b/auth0/v3/authentication/social.py
@@ -35,6 +35,5 @@ class Social(AuthenticationBase):
                 'access_token': access_token,
                 'connection': connection,
                 'scope': scope,
-            },
-            headers={'Content-Type': 'application/json'}
+            }
         )
diff --git a/auth0/v3/authentication/users.py b/auth0/v3/authentication/users.py
index 56bed12..1af5e75 100644
--- a/auth0/v3/authentication/users.py
+++ b/auth0/v3/authentication/users.py
@@ -44,6 +44,5 @@ class Users(AuthenticationBase):
         warnings.warn("/tokeninfo will be deprecated in future releases", DeprecationWarning)
         return self.post(
             url='https://{}/tokeninfo'.format(self.domain),
-            data={'id_token': jwt},
-            headers={'Content-Type': 'application/json'}
+            data={'id_token': jwt}
         )
diff --git a/auth0/v3/management/rest.py b/auth0/v3/management/rest.py
index aafa25f..44d9342 100644
--- a/auth0/v3/management/rest.py
+++ b/auth0/v3/management/rest.py
@@ -46,7 +46,7 @@ class RestClient(object):
     def post(self, url, data=None):
         headers = self.base_headers.copy()
 
-        response = requests.post(url, data=json.dumps(data or {}), headers=headers)
+        response = requests.post(url, json=data, headers=headers)
         return self._process_response(response)
 
     def file_post(self, url, data=None, files=None):
@@ -59,13 +59,13 @@ class RestClient(object):
     def patch(self, url, data=None):
         headers = self.base_headers.copy()
 
-        response = requests.patch(url, data=json.dumps(data or {}), headers=headers)
+        response = requests.patch(url, json=data, headers=headers)
         return self._process_response(response)
 
     def put(self, url, data=None):
         headers = self.base_headers.copy()
 
-        response = requests.put(url, data=json.dumps(data or {}), headers=headers)
+        response = requests.put(url, json=data, headers=headers)
         return self._process_response(response)
 
     def delete(self, url, params=None):


Test patch:
diff --git a/auth0/v3/test/authentication/test_base.py b/auth0/v3/test/authentication/test_base.py
index 0f20dcf..804c459 100644
--- a/auth0/v3/test/authentication/test_base.py
+++ b/auth0/v3/test/authentication/test_base.py
@@ -50,10 +50,25 @@ class TestBase(unittest.TestCase):
 
         data = ab.post('the-url', data={'a': 'b'}, headers={'c': 'd'})
 
-        mock_post.assert_called_with(url='the-url', data='{"a": "b"}',
+        mock_post.assert_called_with(url='the-url', json={'a': 'b'},
                 headers={'c': 'd', 'Content-Type': 'application/json'})
 
         self.assertEqual(data, {'x': 'y'})
+        
+    @mock.patch('requests.post')
+    def test_post_with_defaults(self, mock_post):
+        ab = AuthenticationBase('auth0.com', telemetry=False)
+
+        mock_post.return_value.status_code = 200
+        mock_post.return_value.text = '{"x": "y"}'
+
+        # Only required params are passed
+        data = ab.post('the-url')
+
+        mock_post.assert_called_with(url='the-url', json=None,
+                headers={'Content-Type': 'application/json'})
+
+        self.assertEqual(data, {'x': 'y'})
 
     @mock.patch('requests.post')
     def test_post_includes_telemetry(self, mock_post):
@@ -67,7 +82,7 @@ class TestBase(unittest.TestCase):
         self.assertEqual(mock_post.call_count, 1)
         call_kwargs = mock_post.call_args[1]
         self.assertEqual(call_kwargs['url'], 'the-url')
-        self.assertEqual(call_kwargs['data'], '{"a": "b"}')
+        self.assertEqual(call_kwargs['json'], {'a': 'b'})
         headers = call_kwargs['headers']
         self.assertEqual(headers['c'], 'd')
         self.assertEqual(headers['Content-Type'], 'application/json')
@@ -157,6 +172,35 @@ class TestBase(unittest.TestCase):
                              'a0.sdk.internal.unknown')
             self.assertEqual(context.exception.message, '')
 
+    @mock.patch('requests.get')
+    def test_get(self, mock_get):
+        ab = AuthenticationBase('auth0.com', telemetry=False)
+
+        mock_get.return_value.status_code = 200
+        mock_get.return_value.text = '{"x": "y"}'
+
+        data = ab.get('the-url', params={'a': 'b'}, headers={'c': 'd'})
+
+        mock_get.assert_called_with(url='the-url', params={'a': 'b'},
+                headers={'c': 'd', 'Content-Type': 'application/json'})
+
+        self.assertEqual(data, {'x': 'y'})
+
+    @mock.patch('requests.get')
+    def test_get_with_defaults(self, mock_get):
+        ab = AuthenticationBase('auth0.com', telemetry=False)
+
+        mock_get.return_value.status_code = 200
+        mock_get.return_value.text = '{"x": "y"}'
+
+        # Only required params are passed
+        data = ab.get('the-url')
+
+        mock_get.assert_called_with(url='the-url', params=None,
+                headers={'Content-Type': 'application/json'})
+
+        self.assertEqual(data, {'x': 'y'})
+
     @mock.patch('requests.get')
     def test_get_includes_telemetry(self, mock_get):
         ab = AuthenticationBase('auth0.com')
@@ -176,4 +220,4 @@ class TestBase(unittest.TestCase):
         self.assertIn('User-Agent', headers)
         self.assertIn('Auth0-Client', headers)
 
-        self.assertEqual(data, '{"x": "y"}')
+        self.assertEqual(data, {"x": "y"})
diff --git a/auth0/v3/test/authentication/test_database.py b/auth0/v3/test/authentication/test_database.py
index 2fd0a3c..0291f2f 100644
--- a/auth0/v3/test/authentication/test_database.py
+++ b/auth0/v3/test/authentication/test_database.py
@@ -32,9 +32,6 @@ class TestDatabase(unittest.TestCase):
             'grant_type': 'gt',
             'scope': 'openid profile',
         })
-        self.assertEqual(kwargs['headers'], {
-            'Content-Type': 'application/json'
-        })
 
     @mock.patch('auth0.v3.authentication.database.Database.post')
     def test_signup(self, mock_post):
@@ -58,9 +55,6 @@ class TestDatabase(unittest.TestCase):
                          'username': None,
                          'user_metadata': None
                         })
-        self.assertEqual(kwargs['headers'], {
-            'Content-Type': 'application/json'
-        })
 
 
         # Using also username and metadata
@@ -88,9 +82,6 @@ class TestDatabase(unittest.TestCase):
                          'username': 'usr',
                          'user_metadata': sample_meta
                         })
-        self.assertEqual(kwargs['headers'], {
-            'Content-Type': 'application/json'
-        })
 
     @mock.patch('auth0.v3.authentication.database.Database.post')
     def test_change_password(self, mock_post):
@@ -112,6 +103,3 @@ class TestDatabase(unittest.TestCase):
             'password': 'pswd',
             'connection': 'conn',
         })
-        self.assertEqual(kwargs['headers'], {
-            'Content-Type': 'application/json'
-        })
diff --git a/auth0/v3/test/authentication/test_delegated.py b/auth0/v3/test/authentication/test_delegated.py
index 697678e..a69e346 100644
--- a/auth0/v3/test/authentication/test_delegated.py
+++ b/auth0/v3/test/authentication/test_delegated.py
@@ -28,9 +28,6 @@ class TestDelegated(unittest.TestCase):
             'scope': 'openid profile',
             'api_type': 'apt',
         })
-        self.assertEqual(kwargs['headers'], {
-            'Content-Type': 'application/json'
-        })
 
     @mock.patch('auth0.v3.authentication.delegated.Delegated.post')
     def test_get_token_refresh_token(self, mock_post):
@@ -54,9 +51,6 @@ class TestDelegated(unittest.TestCase):
             'scope': 'openid',
             'api_type': 'apt',
         })
-        self.assertEqual(kwargs['headers'], {
-            'Content-Type': 'application/json'
-        })
 
     @mock.patch('auth0.v3.authentication.delegated.Delegated.post')
     def test_get_token_value_error(self, mock_post):
diff --git a/auth0/v3/test/authentication/test_get_token.py b/auth0/v3/test/authentication/test_get_token.py
index 3feff6c..c635022 100644
--- a/auth0/v3/test/authentication/test_get_token.py
+++ b/auth0/v3/test/authentication/test_get_token.py
@@ -26,9 +26,6 @@ class TestGetToken(unittest.TestCase):
             'grant_type': 'gt',
             'redirect_uri': 'idt'
         })
-        self.assertEqual(kwargs['headers'], {
-            'Content-Type': 'application/json'
-        })
 
     @mock.patch('auth0.v3.authentication.get_token.GetToken.post')
     def test_authorization_code_pkce(self, mock_post):
@@ -51,9 +48,6 @@ class TestGetToken(unittest.TestCase):
             'grant_type': 'gt',
             'redirect_uri': 'idt'
         })
-        self.assertEqual(kwargs['headers'], {
-            'Content-Type': 'application/json'
-        })
 
     @mock.patch('auth0.v3.authentication.get_token.GetToken.post')
     def test_client_credentials(self, mock_post):
@@ -74,9 +68,6 @@ class TestGetToken(unittest.TestCase):
             'audience': 'aud',
             'grant_type': 'gt'
         })
-        self.assertEqual(kwargs['headers'], {
-            'Content-Type': 'application/json'
-        })
 
     @mock.patch('auth0.v3.authentication.get_token.GetToken.post')
     def test_login(self, mock_post):
@@ -105,9 +96,6 @@ class TestGetToken(unittest.TestCase):
             'audience': 'aud',
             'grant_type': 'gt'
         })
-        self.assertEqual(kwargs['headers'], {
-            'Content-Type': 'application/json'
-        })
 
     @mock.patch('auth0.v3.authentication.get_token.GetToken.post')
     def test_refresh_token(self, mock_post):
@@ -126,7 +114,4 @@ class TestGetToken(unittest.TestCase):
             'client_secret': 'clsec',
             'refresh_token': 'rt',
             'grant_type': 'gt'
-        })
-        self.assertEqual(kwargs['headers'], {
-            'Content-Type': 'application/json'
-        })
+        })
\ No newline at end of file
diff --git a/auth0/v3/test/authentication/test_logout.py b/auth0/v3/test/authentication/test_logout.py
index 6f83a69..7f8278e 100644
--- a/auth0/v3/test/authentication/test_logout.py
+++ b/auth0/v3/test/authentication/test_logout.py
@@ -14,11 +14,7 @@ class TestLogout(unittest.TestCase):
                  return_to='rto')
 
         args, kwargs = mock_get.call_args
-
         self.assertEqual(args[0], 'https://my.domain.com/v2/logout?client_id=cid&returnTo=rto')
-        self.assertEqual(kwargs['headers'], {
-            'Content-Type': 'application/json'
-        })
 
     @mock.patch('auth0.v3.authentication.logout.Logout.get')
     def test_federated_logout(self, mock_get):
@@ -30,8 +26,4 @@ class TestLogout(unittest.TestCase):
                  federated=True)
 
         args, kwargs = mock_get.call_args
-
         self.assertEqual(args[0], 'https://my.domain.com/v2/logout?federated&client_id=cid&returnTo=rto')
-        self.assertEqual(kwargs['headers'], {
-            'Content-Type': 'application/json'
-        })
diff --git a/auth0/v3/test/authentication/test_passwordless.py b/auth0/v3/test/authentication/test_passwordless.py
index 39deb20..4a59a7a 100644
--- a/auth0/v3/test/authentication/test_passwordless.py
+++ b/auth0/v3/test/authentication/test_passwordless.py
@@ -25,9 +25,6 @@ class TestPasswordless(unittest.TestCase):
             'authParams': {'a': 'b'},
             'connection': 'email',
         })
-        self.assertEqual(kwargs['headers'], {
-            'Content-Type': 'application/json'
-        })
 
     @mock.patch('auth0.v3.authentication.passwordless.Passwordless.post')
     def test_sms(self, mock_post):
@@ -43,10 +40,7 @@ class TestPasswordless(unittest.TestCase):
             'phone_number': '123456',
             'connection': 'sms',
         })
-        self.assertEqual(kwargs['headers'], {
-            'Content-Type': 'application/json'
-        })
-
+        
     @mock.patch('auth0.v3.authentication.passwordless.Passwordless.post')
     def test_sms_login(self, mock_post):
 
@@ -65,9 +59,6 @@ class TestPasswordless(unittest.TestCase):
             'password': 'abcd',
             'scope': 'openid',
         })
-        self.assertEqual(kwargs['headers'], {
-            'Content-Type': 'application/json'
-        })
 
     @mock.patch('auth0.v3.authentication.passwordless.Passwordless.post')
     def test_sms_login_with_scope(self, mock_post):
@@ -87,7 +78,4 @@ class TestPasswordless(unittest.TestCase):
             'username': '123456',
             'password': 'abcd',
             'scope': 'openid profile',
-        })
-        self.assertEqual(kwargs['headers'], {
-            'Content-Type': 'application/json'
-        })
+        })
\ No newline at end of file
diff --git a/auth0/v3/test/authentication/test_social.py b/auth0/v3/test/authentication/test_social.py
index eaceb0a..6874dab 100644
--- a/auth0/v3/test/authentication/test_social.py
+++ b/auth0/v3/test/authentication/test_social.py
@@ -19,9 +19,6 @@ class TestSocial(unittest.TestCase):
             'connection': 'conn',
             'scope': 'openid',
         })
-        self.assertEqual(kwargs['headers'], {
-            'Content-Type': 'application/json'
-        })
 
     @mock.patch('auth0.v3.authentication.social.Social.post')
     def test_login_with_scope(self, mock_post):
@@ -38,6 +35,3 @@ class TestSocial(unittest.TestCase):
             'connection': 'conn',
             'scope': 'openid profile',
         })
-        self.assertEqual(kwargs['headers'], {
-            'Content-Type': 'application/json'
-        })
diff --git a/auth0/v3/test/authentication/test_users.py b/auth0/v3/test/authentication/test_users.py
index a201d07..b847ebb 100644
--- a/auth0/v3/test/authentication/test_users.py
+++ b/auth0/v3/test/authentication/test_users.py
@@ -26,6 +26,5 @@ class TestUsers(unittest.TestCase):
 
         mock_post.assert_called_with(
             url='https://my.domain.com/tokeninfo',
-            data={'id_token': 'jwtoken'},
-            headers={'Content-Type': 'application/json'}
+            data={'id_token': 'jwtoken'}
         )
diff --git a/auth0/v3/test/management/test_rest.py b/auth0/v3/test/management/test_rest.py
index 9403514..e3b8619 100644
--- a/auth0/v3/test/management/test_rest.py
+++ b/auth0/v3/test/management/test_rest.py
@@ -64,7 +64,7 @@ class TestRest(unittest.TestCase):
 
         mock_post.return_value.status_code = 200
         response = rc.post('the/url', data=data)
-        mock_post.assert_called_with('the/url', data=json.dumps(data),
+        mock_post.assert_called_with('the/url', json=data,
                                      headers=headers)
 
         self.assertEqual(response, {'a': 'b'})
@@ -215,6 +215,40 @@ class TestRest(unittest.TestCase):
 
         mock_post.assert_called_once_with('the-url', data=data, files=files, headers=headers)
 
+
+    @mock.patch('requests.put')
+    def test_put(self, mock_put):
+        rc = RestClient(jwt='a-token', telemetry=False)
+        headers = {'Authorization': 'Bearer a-token',
+                   'Content-Type': 'application/json'}
+
+        mock_put.return_value.text = '["a", "b"]'
+        mock_put.return_value.status_code = 200
+
+        data = {'some': 'data'}
+
+        response = rc.put(url='the-url', data=data)
+        mock_put.assert_called_with('the-url', json=data,
+                                      headers=headers)
+
+        self.assertEqual(response, ['a', 'b'])
+
+    @mock.patch('requests.put')
+    def test_put_errors(self, mock_put):
+        rc = RestClient(jwt='a-token', telemetry=False)
+
+        mock_put.return_value.text = '{"statusCode": 999,' \
+                                       ' "errorCode": "code",' \
+                                       ' "message": "message"}'
+        mock_put.return_value.status_code = 999
+
+        with self.assertRaises(Auth0Error) as context:
+            rc.put(url='the/url')
+
+        self.assertEqual(context.exception.status_code, 999)
+        self.assertEqual(context.exception.error_code, 'code')
+        self.assertEqual(context.exception.message, 'message')
+
     @mock.patch('requests.patch')
     def test_patch(self, mock_patch):
         rc = RestClient(jwt='a-token', telemetry=False)
@@ -227,7 +261,7 @@ class TestRest(unittest.TestCase):
         data = {'some': 'data'}
 
         response = rc.patch(url='the-url', data=data)
-        mock_patch.assert_called_with('the-url', data=json.dumps(data),
+        mock_patch.assert_called_with('the-url', json=data,
                                       headers=headers)
 
         self.assertEqual(response, ['a', 'b'])