Repository: auth0/auth0-python
Pull request #230 (2020-07-07 19:36:12)

Skip sending optional parameters on POST request when unspecified
### Changes

The python request library when it creates the query parameters, it will remove any parameter on the passed dict that has a value of `None`. In our library, these are only used for GET requests.
For the case of POST or PATCH requests, the dict that conforms the body will not suffer the same filtering.

In some cases it makes sense to send an explicit null value, like when removing a property from the user's `user_metadata` property, the check should be performed on each POST/PATCH function instead of in the base client class.


The PR also includes some linting fixes, related to doc blocks and formatting. 
### References
 Fixes https://github.com/auth0/auth0-python/issues/228

Docs on the params filtering https://requests.readthedocs.io/en/master/user/quickstart/#passing-parameters-in-urls. Note it doesn't explicitly mention how the body params are being handled.

### Testing

Please describe how this can be tested by reviewers. Be specific about anything not tested and reasons why. If this library has unit and/or integration testing, tests should be added for new functionality and existing tests should complete without errors. 

- [x] This change adds unit test coverage
- [ ] This change adds integration 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/database.py b/auth0/v3/authentication/database.py
index bc57de8..e89a825 100644
--- a/auth0/v3/authentication/database.py
+++ b/auth0/v3/authentication/database.py
@@ -21,19 +21,20 @@ class Database(AuthenticationBase):
         Windows Azure AD and ADFS.
         """
         warnings.warn("/oauth/ro will be deprecated in future releases", DeprecationWarning)
-        return self.post(
-            'https://{}/oauth/ro'.format(self.domain),
-            data={
-                'client_id': client_id,
-                'username': username,
-                'password': password,
-                'id_token': id_token,
-                'connection': connection,
-                'device': device,
-                'grant_type': grant_type,
-                'scope': scope,
-            }
-        )
+
+        body = {
+            'client_id': client_id,
+            'username': username,
+            'password': password,
+            'connection': connection,
+            'grant_type': grant_type,
+            'scope': scope,
+        }
+        if id_token:
+            body.update({'id_token': id_token})
+        if device:
+            body.update({'device': device})
+        return self.post('https://{}/oauth/ro'.format(self.domain), data=body)
 
     def signup(self, client_id, email, password, connection, username=None, user_metadata=None,
                given_name=None, family_name=None, name=None, nickname=None, picture=None):
@@ -71,10 +72,7 @@ class Database(AuthenticationBase):
             'email': email,
             'password': password,
             'connection': connection,
-            'username': username,
-            'user_metadata': user_metadata
         }
-
         if username:
             body.update({'username': username})
         if user_metadata:
@@ -90,21 +88,21 @@ class Database(AuthenticationBase):
         if picture:
             body.update({'picture': picture})
 
-        return self.post(
-            'https://{}/dbconnections/signup'.format(self.domain),
-            data=body
-        )
+        return self.post('https://{}/dbconnections/signup'.format(self.domain), data=body)
 
     def change_password(self, client_id, email, connection, password=None):
         """Asks to change a password for a given user.
-        """
+            
+           client_id (str): ID of the application to use.
+
+           email (str): The user's email address.
 
-        return self.post(
-            'https://{}/dbconnections/change_password'.format(self.domain),
-            data={
-                'client_id': client_id,
-                'email': email,
-                'password': password,
-                'connection': connection,
-            }
-        )
+           connection (str): The name of the database connection where this user should be created.
+        """
+        body = {
+            'client_id': client_id,
+            'email': email,
+            'connection': connection,
+        }
+        
+        return self.post('https://{}/dbconnections/change_password'.format(self.domain), data=body)
diff --git a/auth0/v3/authentication/delegated.py b/auth0/v3/authentication/delegated.py
index 5c738de..f84cef2 100644
--- a/auth0/v3/authentication/delegated.py
+++ b/auth0/v3/authentication/delegated.py
@@ -2,7 +2,6 @@ from .base import AuthenticationBase
 
 
 class Delegated(AuthenticationBase):
-
     """Delegated authentication endpoints.
 
     Args:
@@ -35,7 +34,4 @@ class Delegated(AuthenticationBase):
             raise ValueError('Either id_token or refresh_token must '
                              'have a value')
 
-        return self.post(
-            'https://{}/delegation'.format(self.domain),
-            data=data
-        )
+        return self.post('https://{}/delegation'.format(self.domain), data=data)
diff --git a/auth0/v3/authentication/revoke_token.py b/auth0/v3/authentication/revoke_token.py
index 1705bb1..9523c5f 100644
--- a/auth0/v3/authentication/revoke_token.py
+++ b/auth0/v3/authentication/revoke_token.py
@@ -2,7 +2,6 @@ from .base import AuthenticationBase
 
 
 class RevokeToken(AuthenticationBase):
-
     """Revoke Refresh Token endpoint
 
     Args:
@@ -30,8 +29,9 @@ class RevokeToken(AuthenticationBase):
         body = {
             'client_id': client_id,
             'token': token,
-            'client_secret': client_secret
         }
 
-        return self.post(
-            'https://{}/oauth/revoke'.format(self.domain), data=body)
+        if client_secret:
+            body.update({'client_secret': client_secret})
+
+        return self.post('https://{}/oauth/revoke'.format(self.domain), data=body)
diff --git a/auth0/v3/management/blacklists.py b/auth0/v3/management/blacklists.py
index 0b8a76c..219a0fd 100644
--- a/auth0/v3/management/blacklists.py
+++ b/auth0/v3/management/blacklists.py
@@ -39,7 +39,7 @@ class Blacklists(object):
 
         return self.client.get(self.url, params=params)
 
-    def create(self, jti, aud=''):
+    def create(self, jti, aud=None):
         """Adds a token to the blacklist.
 
         Args:
@@ -50,5 +50,11 @@ class Blacklists(object):
 
         See: https://auth0.com/docs/api/management/v2#!/Blacklists/post_tokens
         """
+        body = {
+            'jti': jti,
+        }
+
+        if aud:
+            body.update({'aud': aud})
 
-        return self.client.post(self.url, data={'jti': jti, 'aud': aud})
+        return self.client.post(self.url, data=body)


Test patch:
diff --git a/auth0/v3/test/authentication/test_database.py b/auth0/v3/test/authentication/test_database.py
index 3966b41..d614fd1 100644
--- a/auth0/v3/test/authentication/test_database.py
+++ b/auth0/v3/test/authentication/test_database.py
@@ -52,8 +52,6 @@ class TestDatabase(unittest.TestCase):
             'email': 'a@b.com',
             'password': 'pswd',
             'connection': 'conn',
-            'username': None,
-            'user_metadata': None
         })
 
         # Using also optional properties
@@ -96,6 +94,7 @@ class TestDatabase(unittest.TestCase):
     def test_change_password(self, mock_post):
         d = Database('my.domain.com')
 
+        # ignores the password argument
         d.change_password(client_id='cid',
                           email='a@b.com',
                           password='pswd',
@@ -108,6 +107,5 @@ class TestDatabase(unittest.TestCase):
         self.assertEqual(kwargs['data'], {
             'client_id': 'cid',
             'email': 'a@b.com',
-            'password': 'pswd',
             'connection': 'conn',
         })
diff --git a/auth0/v3/test/authentication/test_revoke_token.py b/auth0/v3/test/authentication/test_revoke_token.py
index 0aa40fd..57a1d21 100644
--- a/auth0/v3/test/authentication/test_revoke_token.py
+++ b/auth0/v3/test/authentication/test_revoke_token.py
@@ -11,16 +11,14 @@ class TestRevokeToken(unittest.TestCase):
         a = RevokeToken('my.domain.com')
 
         # regular apps
-        a.revoke_refresh_token(client_id='cid',
-                    token='tkn')
+        a.revoke_refresh_token(client_id='cid', token='tkn')
 
         args, kwargs = mock_post.call_args
 
         self.assertEqual(args[0], 'https://my.domain.com/oauth/revoke')
         self.assertEqual(kwargs['data'], {
             'client_id': 'cid',
-            'token': 'tkn',
-            'client_secret': None
+            'token': 'tkn'
         })
 
         # confidential apps
diff --git a/auth0/v3/test/management/test_blacklists.py b/auth0/v3/test/management/test_blacklists.py
index cbf74e9..7c62e57 100644
--- a/auth0/v3/test/management/test_blacklists.py
+++ b/auth0/v3/test/management/test_blacklists.py
@@ -28,6 +28,16 @@ class TestBlacklists(unittest.TestCase):
         mock_instance = mock_rc.return_value
 
         t = Blacklists(domain='domain', token='jwttoken')
+
+        # create without audience
+        t.create(jti='the-jti')
+
+        args, kwargs = mock_instance.post.call_args
+
+        self.assertEqual('https://domain/api/v2/blacklists/tokens', args[0])
+        self.assertEqual(kwargs['data'], {'jti': 'the-jti'})
+
+        # create with audience
         t.create(jti='the-jti', aud='the-aud')
 
         args, kwargs = mock_instance.post.call_args