0

I'm so confused why my middleware isn't correctly getting a user based token query by key.

First I checked in my consumer, however the user in self.scope was an AnonymousUser (seen below in the code). This is reiterated when I set a foreign key in my receiver function, which raises the error: ValueError: Cannot assign "<django.contrib.auth.models.AnonymousUser object at 0x10943cc10>": "ChatMessage.user" must be a "User" instance.

Hence, I'm assuming the error is my middleware. I experimented with putting a print statement in middleware.TokenAuthMiddleware.__call__ after the get_user function is awaited and called, however, unexpectedly, the function returns an AnonymousUser. Both the user and the associated Token are created in the testcase's setUp and it's that token's key which is passed to the function.

Is the token string becoming malformed in format_querystring? The function is tested and passed, so I do not believe the token is becoming malformed there.

One thought is because the user and the token are being created in setUp, which is not async, it somehow is not aligned with the processes of the database look up in get_user. However changing that to async and wrapping the database calls in database_sync_to_async did not fix it, so I don't believe it's that either.

One idea is that not setting up my application correctly in the test, however based on their tests, it seems correct.

# middleware.py

@database_sync_to_async
def get_user(token_key):
    """
    Get token from token key
    Parameters:
        token_key (str): Token key string
    Returns:
        if found, return associated User. Else, AnonymousUser
    """
    try:
        token = Token.objects.get(key=token_key)
    except Token.DoesNotExist:
        return AnonymousUser()
    else:
        return token.user


class TokenAuthMiddleware(BaseMiddleware):
    """
    Token authorization middleware for Django Channels
    E.g.
    ws://localhost:8000/<route>/?token=<token_of_the_user>

    Based on:
    https://stackoverflow.com/a/49372334/5574063
    https://stackoverflow.com/a/65654519/5574063
    https://gist.github.com/rluts/22e05ed8f53f97bdd02eafdf38f3d60a#gistcomment-3166469

    TODO:
    implement:
    https://gist.github.com/rluts/22e05ed8f53f97bdd02eafdf38f3d60a#gistcomment-3174829
    https://stackoverflow.com/a/4361358/5574063
    """

    def __init__(self, inner):
        self.inner = inner

    async def __call__(self, scope, receive, send):
        close_old_connections()
        query = self.format_querystring(scope['query_string'])
        token_key = query.get('token')
        scope['user'] = await get_user(token_key)
        return await super(TokenAuthMiddleware, self).__call__(scope, receive, send)

    def format_querystring(self, qs):
        """
        Convert byte querystring into dictionary.
        NOTE: will only take the first value.

        Parameters:
            qs (bstr): Querystring as bytes
        Returns:
            Dict of querystring
        """
        return {k: v[0] for k, v in dict(parse_qs(qs.decode().lower())).items()}


def TokenAuthMiddlewareStack(inner):
    return TokenAuthMiddleware(inner)


# consumer.py
    ...
    async def connect(self):
        good_user = 'user' in self.scope and self.scope['user'].is_authenticated
        print(good_user)
        ...

>>> False

# test.py
async def test_receive(self):
   # self.user and rest_framework Token created
   self.application = TokenAuthMiddlewareStack(URLRouter(websocket_urlpatterns))
   self.route = '/ws/chat/test-room/?token={}'.format(self.user.auth_token.key)
   communicator = WebsocketCommunicator(self.application, self.route)
   connected, _ = await communicator.connect()
   ...

I'm using channels==3.0.3 and Django==3.2.5


EDIT: Narrowed down the problem. The Token queryset is empty when it reaches get_user. E.i.

# printed from the test
<QuerySet [<Token: bca50384e55d88fdc109c73b29c5d0745c5dd379>]>
# printed from within the function
<QuerySet []>

Why would the queryset empty for the middleware/consumer?

af3ld
  • 782
  • 8
  • 30

1 Answers1

0

using Django's TransactionTestCase solved the issue. TransactionTestCase is further explained here.

The questions Database errors in Django when using threading, Test database not keeping data during Django Channels Pytest test, and Django channels 2, accessing db in tests led me to a working solution.

I don't fully understand why forcing transactions to be atomic is the solution, but oh well.

af3ld
  • 782
  • 8
  • 30