Repository: auth0/auth0-python
Pull request #506 (2023-07-10 09:44:47)

Fix asyncify for users client where token is not required
### Changes

This PR fixes a bug that is currently in the Auth0 SDK `asyncify` wrapper where it falls through to the AsyncManagementClient when the class being wrapped doesn't inherit from the `AuthenticationBase` class. This doesn't work for the `Users` client, as that shouldn't require a management token.

Currently, using the `Users` client with the async wrapper fails with the following error:

```python
TypeError: asyncify.<locals>.AsyncManagementClient.__init__() missing 1 required positional argument: 'token'
```

The approach I took was to implement a new `BareAsyncClient` for this case, however I'm not 100% wedded to this approach so feel free to tell me to have another go!

### Testing

I've added a test for this functionality to check that we don't need to add the token when calling the Users client when wrapped in an async context.

- [x] This change adds integration test coverage

### 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/asyncify.py b/auth0/asyncify.py
index 091d049..fb88424 100644
--- a/auth0/asyncify.py
+++ b/auth0/asyncify.py
@@ -1,5 +1,6 @@
 import aiohttp
 
+from auth0.authentication import Users
 from auth0.authentication.base import AuthenticationBase
 from auth0.rest import RestClientOptions
 from auth0.rest_async import AsyncRestClient
@@ -21,6 +22,17 @@ def asyncify(cls):
         if callable(getattr(cls, func)) and not func.startswith("_")
     ]
 
+    class UsersAsyncClient(cls):
+        def __init__(
+            self,
+            domain,
+            telemetry=True,
+            timeout=5.0,
+            protocol="https",
+        ):
+            super().__init__(domain, telemetry, timeout, protocol)
+            self.client = AsyncRestClient(None, telemetry=telemetry, timeout=timeout)
+
     class AsyncManagementClient(cls):
         def __init__(
             self,
@@ -68,7 +80,9 @@ def asyncify(cls):
     class Wrapper(cls):
         def __init__(self, *args, **kwargs):
             super().__init__(*args, **kwargs)
-            if AuthenticationBase in cls.__bases__:
+            if cls == Users:
+                self._async_client = UsersAsyncClient(*args, **kwargs)
+            elif AuthenticationBase in cls.__bases__:
                 self._async_client = AsyncAuthenticationClient(*args, **kwargs)
             else:
                 self._async_client = AsyncManagementClient(*args, **kwargs)
diff --git a/auth0/authentication/users.py b/auth0/authentication/users.py
index 9535eda..f0231fd 100644
--- a/auth0/authentication/users.py
+++ b/auth0/authentication/users.py
@@ -46,7 +46,6 @@ class Users:
         Returns:
             The user profile.
         """
-
         data: dict[str, Any] = self.client.get(
             url=f"{self.protocol}://{self.domain}/userinfo",
             headers={"Authorization": f"Bearer {access_token}"},


Test patch:
diff --git a/auth0/test_async/test_asyncify.py b/auth0/test_async/test_asyncify.py
index 8a80bef..4871e94 100644
--- a/auth0/test_async/test_asyncify.py
+++ b/auth0/test_async/test_asyncify.py
@@ -12,11 +12,12 @@ from aioresponses import CallbackResult, aioresponses
 from callee import Attrs
 
 from auth0.asyncify import asyncify
-from auth0.authentication import GetToken
+from auth0.authentication import GetToken, Users
 from auth0.management import Clients, Guardian, Jobs
 
 clients = re.compile(r"^https://example\.com/api/v2/clients.*")
 token = re.compile(r"^https://example\.com/oauth/token.*")
+user_info = re.compile(r"^https://example\.com/userinfo.*")
 factors = re.compile(r"^https://example\.com/api/v2/guardian/factors.*")
 users_imports = re.compile(r"^https://example\.com/api/v2/jobs/users-imports.*")
 payload = {"foo": "bar"}
@@ -111,6 +112,22 @@ class TestAsyncify(getattr(unittest, "IsolatedAsyncioTestCase", object)):
             timeout=ANY,
         )
 
+    @aioresponses()
+    async def test_user_info(self, mocked):
+        callback, mock = get_callback()
+        mocked.get(user_info, callback=callback)
+        c = asyncify(Users)(domain="example.com")
+        self.assertEqual(
+            await c.userinfo_async(access_token="access-token-example"), payload
+        )
+        mock.assert_called_with(
+            Attrs(path="/userinfo"),
+            headers={**headers, "Authorization": "Bearer access-token-example"},
+            timeout=ANY,
+            allow_redirects=True,
+            params=None,
+        )
+
     @aioresponses()
     async def test_file_post(self, mocked):
         callback, mock = get_callback()