Repository: django/channels

Issue:
AuthMiddleware not called during tests
Hello channels devs team,

I am writing the tests for the websocket consumers of my application and I cannot successfully test my authentication process. I use a custom  AuthMiddleware to use tokens instead of cookies ([this one](https://gist.github.com/rluts/22e05ed8f53f97bdd02eafdf38f3d60a)) and it seems that the middleware is not called at all during tests, whereas it is actually called with real-world testing (*i.e.* when running the app).

I cannot say if my problem is related to #903, as I process the login by another way with Django REST Framework. The authentication only works if the user is already connected.

Here is the test:
```python
import pytest
from channels.testing import WebsocketCommunicator
from rest_framework.authtoken.models import Token
from my_app.routing import application

@pytest.mark.asyncio                                                           
@pytest.mark.django_db(transaction=True)                                       
async def test_authentication():
    # create a token                                                           
    user = get_an_user_somehow()
    token = Token.objects.create(user=user)                         
    headers = [(b'authorization', "Token {}".format(token.key).encode())]      
                                                                               
    # create communicator and connect
    communicator = WebsocketCommunicator(application, "/ws/my/path/",  
                                         headers=headers)                      
    connected, _ = await communicator.connect()                                
                                                                               
    assert connected  # fails here
    await communicator.disconnect()                                            
```

According to the AuthMiddleware code, a user will always be added to the scope, even if the authentication fails (an anonymous user is added in this case). But there is no user in my scope after awaiting connect, this is why I believe that the AuthMIddleware is not called by the WebsocketCommunicatior. This does not seem to be a problem from the custom AuthMiddleware side.

What do you think?

* **OS and runtime environment** Linux kernel 4.17.11-1-default, Python 3.6.5;
* **Versions** Channels 2.1.2, Daphne 2.2.0, Django 1.11.13, Django REST Framework 3.6.4, Twisted none, ASGI backend in memory;
* **Running** in dev mode with runserver or with pytest.


Discussion:
Did you verify the AuthMiddleware is being run at all? Put a print or something in it? As long as the middleware is in the `routing` you inherit from, it will run, otherwise it will not.
Using prints is how I figured out that the AuthMiddleware is run when using the runserver command, but not during a test with pytest. More precisely, the `__init__` method is called in both cases (obviously) and the `__call__` method only using runserver.

The documentation states that I can pass an application built with URLRouter. The application I use is built with ProtocolTypeRouter (on top of URLRouter), like this one:

```python
# my_app.routing.py
from channels.routing import ProtocolTypeRouter, URLRouter         
from channels.security.websocket import AllowedHostsOriginValidator
from django.conf.urls import url
from my_app.token_auth import TokenAuthMiddlewareStack  # the custom AuthMiddleware
from my_module import consumers

application = ProtocolTypeRouter({
    'websocket': AllowedHostsOriginValidator(
        TokenAuthMiddlewareStack(  # the custom AuthMiddleware
            URLRouter([
                url(r'^ws/my/path/$', consumers.MyConsumer),
            ])
        )
    )
})
```

Maybe such a stack is not valid for the WebsocketCommunicator?
Hmm, that's totally how you're meant to do it. When you say it "fails here" on that first line, do you mean that it never gets accepted? Raises a traceback? Or something else?

I'm trying to work out what's happening because there's not even a reference to the inner consumer in the test at all - it would _have_ to go through the auth middleware stack somehow, so the question is how it's skipping the middleware down a later, or if that's what you're saying it does.
The assert simply fails, meaning that the WebsocketCommunicator did not connect.

Technically, in the test, I try to test the `connect` method of my consumer and the AuthMiddleware. I will create a MWE to isolate the problem.
It seems that AllowedHostOriginValidator rejects the connection during tests, even if I have `ALLOWED_HOSTS = ['*']` or `DEBUG = True` in my `settings.py`. If I remove it, the communicator can connect and the test passes. Same behavior if I use an OriginValidator with `['*']` as second argument.
I did some digging of the source code of OriginValidator:

https://github.com/django/channels/blob/e7295efdfd2a191f5865018d08abb95cf061afeb/channels/security/websocket.py#L24-L31

The parsed origin extracted during a test is `None`. This value is not allowed by the `valid_origin` method:

https://github.com/django/channels/blob/e7295efdfd2a191f5865018d08abb95cf061afeb/channels/security/websocket.py#L48-L50

 So for OriginValidator, it appears that a connection performed during a test is intrinsically invalid.
Ahh, yes, that explains it. That's actually how Django's tests behave normally - they validate host names during tests (https://docs.djangoproject.com/en/2.1/topics/testing/advanced/#topics-testing-advanced-multiple-hosts)

Given that, I don't think I can change Channels here; instead, you should change the ALLOWED_HOSTS setting for the duration of the tests, or always pass a hostname header that matches one you allow.
There is no `origin` in the headers of the test connections, and my `ALLOWED_HOSTS` for tests is already `['*']`, so the only way is to manually add one in the headers of the communicator:

```python
    headers = [
        (b'authorization', "Token {}".format(token.key).encode()),
        (b'origin', b'localhost'),
    ]
```

I think there should be at least one line or two in the tests documentation about that. Personally, I never had to edit my `origin` header with Django tests. Or maybe this could be a default argument for the WebsocketCommunicator?
Oh, the problem is that the middleware reads the setting at declaration time, not when the test is run. Hmm. You're right, there should be a better solution for this, I'll reopen it and get round to it eventually (or let someone else fix it - my suggestion would be for the middleware to somehow detect a change in ALLOWED_HOSTS and re-import it in the `__call__` if there was no explicit argument passed).
I do not think this is the problem. The problem is there is no `origin` in the headers of the test connections. Whatever the value of `ALLOWED_HOSTS` is, the connection will be rejected.
I thought you said:

>  If I remove it, the communicator can connect and the test passes. Same behavior if I use an OriginValidator with ['*'] as second argument.

I read that as "Giving OriginValidator a `*` value for the allowed hosts makes the tests pass".
Ah, my bad, the sentence was troublesome. I wanted to say that OriginValidator with `['*']` works like AllowedHostOriginValidator.
No problem! Anyway, that means I'd probably say the fix is to allow a hostname value of `None` as part of `*`.

Resolving patch:
diff --git a/channels/security/websocket.py b/channels/security/websocket.py
--- a/channels/security/websocket.py
+++ b/channels/security/websocket.py
@@ -45,8 +45,8 @@ def valid_origin(self, parsed_origin):
 
         Returns ``True`` if validation function was successful, ``False`` otherwise.
         """
-        # None is not allowed
-        if parsed_origin is None:
+        # None is not allowed unless all hosts are allowed
+        if parsed_origin is None and "*" not in self.allowed_origins:
             return False
         return self.validate_origin(parsed_origin)
 
@@ -85,9 +85,12 @@ def match_allowed_origin(self, parsed_origin, pattern):
         ``http://exapmple.com``). After the domain there must be a port,
         but it can be omitted.
 
-        Note. This function assumes that the given origin has a schema, domain and port,
-        or only domain. Port is optional.
+        Note. This function assumes that the given origin is either None, a
+        schema-domain-port string, or just a domain string
         """
+        if parsed_origin is None:
+            return False
+
         # Get ResultParse object
         parsed_pattern = urlparse(pattern.lower(), scheme=None)
         if parsed_pattern.scheme is None:


Test patch:
diff --git a/tests/security/test_websocket.py b/tests/security/test_websocket.py
--- a/tests/security/test_websocket.py
+++ b/tests/security/test_websocket.py
@@ -48,3 +48,17 @@ async def test_origin_validator():
     connected, _ = await communicator.connect()
     assert not connected
     await communicator.disconnect()
+    # Make our test application, with all hosts allowed
+    application = OriginValidator(AsyncWebsocketConsumer, ["*"])
+    # Test a connection without any headers
+    communicator = WebsocketCommunicator(application, "/", headers=[])
+    connected, _ = await communicator.connect()
+    assert connected
+    await communicator.disconnect()
+    # Make our test application, with no hosts allowed
+    application = OriginValidator(AsyncWebsocketConsumer, [])
+    # Test a connection without any headers
+    communicator = WebsocketCommunicator(application, "/", headers=[])
+    connected, _ = await communicator.connect()
+    assert not connected
+    await communicator.disconnect()