3

I'm looking to test my consumer, which uses scope['url_route'] but using HttpCommunicator or ApplicationCommunicator, that param isn't set. How can I set this parameter? The testing documentation is very limited and doesn't have any documentation on this https://channels.readthedocs.io/en/latest/topics/testing.html.

test.py

from channels.testing import HttpCommunicator
import pytest

from my_app import consumers

@pytest.mark.asyncio
async def test_consumers():
    communicator = HttpCommunicator(consumers.MyConsumer, 'GET', '/project/1')
    response = await communicator.get_response()
    assert response['status'] == 400

my_app.py

import json

from channels.db import database_sync_to_async
from channels.generic.http import AsyncHttpConsumer

from projects import model

class MyConsumer(AsyncHttpConsumer):
    async def handle(self, body):
        _id = int(self.scope['url_route']['kwargs'].get('project_id', 1))
        record = await database_sync_to_async(models.Project.objects.get)(id=_id)
        data = json.dumps({"id": _id})
        await self.send_response(200, data.encode('utf8'))
notorious.no
  • 4,919
  • 3
  • 20
  • 34
  • Which version of channels are you using? it's usually parsed to scope as `path` and not `url_route` as far as I know – Ken4scholars Feb 13 '19 at 22:22

2 Answers2

7

Found the solution in the docs, but it was under the Websocket testing section, not in the HTTP section. Either import your asgi application or wrap the consumer in URLRouter then use that in HttpCommunicator. Turns out there's some flow that adds scope['url_route'] in URLRouter.

notorious.no
  • 4,919
  • 3
  • 20
  • 34
  • Specifically, this is a simplified way I handled it: `communicator = WebsocketCommunicator(URLRouter(websocket_urlpatterns), "my/path/here")` – JosiahDub Jun 17 '22 at 20:44
1

You could also alter the WebsocketCommunicator provided by channels.testing to contain the extra information you need. I could not get the URLRouter option to add the correct items to the scope in my instance.

For example I needed a few objects in my session so I did this to accomodate:

import json
from urllib.parse import unquote, urlparse

from asgiref.testing import ApplicationCommunicator


class WebsocketCommunicator(ApplicationCommunicator):
    """
    ApplicationCommunicator subclass that has WebSocket shortcut methods.

    It will construct the scope for you, so you need to pass the application
    (uninstantiated) along with the initial connection parameters.
    """

    def __init__(self, application, path, headers=None, subprotocols=None, property=None, client=None):
        if not isinstance(path, str):
            raise TypeError("Expected str, got {}".format(type(path)))
        parsed = urlparse(path)
        self.scope = {
            "type": "websocket",
            "path": unquote(parsed.path),
            "query_string": parsed.query.encode("utf-8"),
            "headers": headers or [],
            "subprotocols": subprotocols or [],
            # needed for connect() in consumer
            "session": {
                # create client and property with passed objects
                'client': client,
                'property': property
            },
        }
        super().__init__(application, self.scope)

    async def connect(self, timeout=1):
        """
        Trigger the connection code.

        On an accepted connection, returns (True, <chosen-subprotocol>)
        On a rejected connection, returns (False, <close-code>)
        """
        await self.send_input({"type": "websocket.connect"})
        response = await self.receive_output(timeout)
        if response["type"] == "websocket.close":
            return (False, response.get("code", 1000))
        else:
            return (True, response.get("subprotocol", None))

    async def send_to(self, text_data=None, bytes_data=None):
        """
        Sends a WebSocket frame to the application.
        """
        # Make sure we have exactly one of the arguments
        assert bool(text_data) != bool(
            bytes_data
        ), "You must supply exactly one of text_data or bytes_data"
        # Send the right kind of event
        if text_data:
            assert isinstance(text_data, str), "The text_data argument must be a str"
            await self.send_input({"type": "websocket.receive", "text": text_data})
        else:
            assert isinstance(
                bytes_data, bytes
            ), "The bytes_data argument must be bytes"
            await self.send_input({"type": "websocket.receive", "bytes": bytes_data})

    async def send_json_to(self, data):
        """
        Sends JSON data as a text frame
        """
        await self.send_to(text_data=json.dumps(data))

    async def receive_from(self, timeout=1):
        """
        Receives a data frame from the view. Will fail if the connection
        closes instead. Returns either a bytestring or a unicode string
        depending on what sort of frame you got.
        """
        response = await self.receive_output(timeout)
        # Make sure this is a send message
        assert response["type"] == "websocket.send"
        # Make sure there's exactly one key in the response
        assert ("text" in response) != (
            "bytes" in response
        ), "The response needs exactly one of 'text' or 'bytes'"
        # Pull out the right key and typecheck it for our users
        if "text" in response:
            assert isinstance(response["text"], str), "Text frame payload is not str"
            return response["text"]
        else:
            assert isinstance(
                response["bytes"], bytes
            ), "Binary frame payload is not bytes"
            return response["bytes"]

    async def receive_json_from(self, timeout=1):
        """
        Receives a JSON text frame payload and decodes it
        """
        payload = await self.receive_from(timeout)
        assert isinstance(payload, str), "JSON data is not a text frame"
        return json.loads(payload)

    async def disconnect(self, code=1000, timeout=1):
        """
        Closes the socket
        """
        await self.send_input({"type": "websocket.disconnect", "code": code})
        await self.wait(timeout)


Then, for example, you can write a test that connects easily:

import pytest
from channels.routing import URLRouter
from django.conf import settings
from django.conf.urls import url

from apps.chat.consumers import PropertyConsumer
# this is the edited communicator
from utils.testing.WebsocketCommunicator import WebsocketCommunicator

@pytest.mark.asyncio
class TestWebsockets:
    async def test_can_connect(self, client, slug, property, admin):
        # Use in-memory channel layers for testing.
        settings.CHANNEL_LAYERS = {
            'default': {
                'BACKEND': 'channels.layers.InMemoryChannelLayer',
            },
        }
    
        application = URLRouter([
            url(r'(?P<client_url>\w+)/chat/property/(?P<property_id>\w+)/ws/$', PropertyConsumer),
        ])
        
        communicator = WebsocketCommunicator(application, f'{slug.url_base}/chat/property/{property.id}/ws/', property=property, client=slug)
        connected, subprotocol = await communicator.connect()
        assert connected
        await communicator.disconnect()
ViaTech
  • 2,143
  • 1
  • 16
  • 51