mirror of
https://github.com/django/daphne.git
synced 2025-04-21 01:02:06 +03:00
Add test suite for ASGI handlers
This commit is contained in:
parent
17e9824f71
commit
5cd2cbdfee
|
@ -9,6 +9,7 @@ from django.core.handlers import base
|
|||
from django.core import signals
|
||||
from django.core.urlresolvers import set_script_prefix
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils import six
|
||||
|
||||
logger = logging.getLogger('django.request')
|
||||
|
||||
|
@ -21,8 +22,10 @@ class AsgiRequest(http.HttpRequest):
|
|||
|
||||
def __init__(self, message):
|
||||
self.message = message
|
||||
self.reply_channel = self.message['reply_channel']
|
||||
self.reply_channel = self.message.reply_channel
|
||||
self._content_length = 0
|
||||
self._post_parse_error = False
|
||||
self.resolver_match = None
|
||||
# Path info
|
||||
self.path = self.message['path']
|
||||
self.script_name = self.message.get('root_path', '')
|
||||
|
@ -63,7 +66,7 @@ class AsgiRequest(http.HttpRequest):
|
|||
except (ValueError, TypeError):
|
||||
pass
|
||||
# Body handling
|
||||
self._body = message.get("body", "")
|
||||
self._body = message.get("body", b"")
|
||||
if message.get("body_channel", None):
|
||||
while True:
|
||||
# Get the next chunk from the request body channel
|
||||
|
@ -78,6 +81,7 @@ class AsgiRequest(http.HttpRequest):
|
|||
# Exit loop if this was the last
|
||||
if not chunk.get("more_content", False):
|
||||
break
|
||||
assert isinstance(self._body, six.binary_type), "Body is not bytes"
|
||||
# Other bits
|
||||
self.resolver_match = None
|
||||
|
||||
|
@ -97,7 +101,13 @@ class AsgiRequest(http.HttpRequest):
|
|||
def _set_post(self, post):
|
||||
self._post = post
|
||||
|
||||
def _get_files(self):
|
||||
if not hasattr(self, '_files'):
|
||||
self._load_post_and_files()
|
||||
return self._files
|
||||
|
||||
POST = property(_get_post, _set_post)
|
||||
FILES = property(_get_files)
|
||||
|
||||
@cached_property
|
||||
def COOKIES(self):
|
||||
|
@ -114,6 +124,9 @@ class AsgiHandler(base.BaseHandler):
|
|||
initLock = Lock()
|
||||
request_class = AsgiRequest
|
||||
|
||||
# Size to chunk response bodies into for multiple response messages
|
||||
chunk_size = 512 * 1024
|
||||
|
||||
def __call__(self, message):
|
||||
# Set up middleware if needed. We couldn't do this earlier, because
|
||||
# settings weren't available.
|
||||
|
@ -148,7 +161,9 @@ class AsgiHandler(base.BaseHandler):
|
|||
"""
|
||||
Encodes a Django HTTP response into an ASGI http.response message(s).
|
||||
"""
|
||||
# Collect cookies into headers
|
||||
# Collect cookies into headers.
|
||||
# Note that we have to preserve header case as there are some non-RFC
|
||||
# compliant clients that want things like Content-Type correct. Ugh.
|
||||
response_headers = [(str(k), str(v)) for k, v in response.items()]
|
||||
for c in response.cookies.values():
|
||||
response_headers.append((str('Set-Cookie'), str(c.output(header=''))))
|
||||
|
@ -186,14 +201,13 @@ class AsgiHandler(base.BaseHandler):
|
|||
|
||||
Yields (chunk, last_chunk) tuples.
|
||||
"""
|
||||
CHUNK_SIZE = 512 * 1024
|
||||
position = 0
|
||||
while position < len(data):
|
||||
yield (
|
||||
data[position:position+CHUNK_SIZE],
|
||||
(position + CHUNK_SIZE) >= len(data),
|
||||
data[position:position + self.chunk_size],
|
||||
(position + self.chunk_size) >= len(data),
|
||||
)
|
||||
position += CHUNK_SIZE
|
||||
position += self.chunk_size
|
||||
|
||||
|
||||
class ViewConsumer(object):
|
||||
|
@ -205,5 +219,5 @@ class ViewConsumer(object):
|
|||
self.handler = AsgiHandler()
|
||||
|
||||
def __call__(self, message):
|
||||
for reply_message in self.handler(message.content):
|
||||
for reply_message in self.handler(message):
|
||||
message.reply_channel.send(reply_message)
|
||||
|
|
|
@ -12,16 +12,20 @@ class Message(object):
|
|||
to use to reply to this message's end user, if that makes sense.
|
||||
"""
|
||||
|
||||
class Requeue(Exception):
|
||||
"""
|
||||
Raise this while processing a message to requeue it back onto the
|
||||
channel. Useful if you're manually ensuring partial ordering, etc.
|
||||
"""
|
||||
pass
|
||||
|
||||
def __init__(self, content, channel, channel_layer, reply_channel=None):
|
||||
def __init__(self, content, channel, channel_layer):
|
||||
self.content = content
|
||||
self.channel = channel
|
||||
self.channel_layer = channel_layer
|
||||
if reply_channel:
|
||||
self.reply_channel = Channel(reply_channel, channel_layer=self.channel_layer)
|
||||
if content.get("reply_channel", None):
|
||||
self.reply_channel = Channel(
|
||||
content["reply_channel"],
|
||||
channel_layer=self.channel_layer,
|
||||
)
|
||||
else:
|
||||
self.reply_channel = None
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self.content[key]
|
||||
|
||||
def get(self, key, default=None):
|
||||
return self.content.get(key, default)
|
||||
|
|
90
channels/tests/test_handler.py
Normal file
90
channels/tests/test_handler.py
Normal file
|
@ -0,0 +1,90 @@
|
|||
from __future__ import unicode_literals
|
||||
from django.test import SimpleTestCase
|
||||
from django.http import HttpResponse
|
||||
|
||||
from asgiref.inmemory import ChannelLayer
|
||||
from channels.handler import AsgiHandler
|
||||
from channels.message import Message
|
||||
|
||||
|
||||
class FakeAsgiHandler(AsgiHandler):
|
||||
"""
|
||||
Handler subclass that just returns a premade response rather than
|
||||
go into the view subsystem.
|
||||
"""
|
||||
|
||||
chunk_size = 30
|
||||
|
||||
def __init__(self, response):
|
||||
assert isinstance(response, HttpResponse)
|
||||
self._response = response
|
||||
super(FakeAsgiHandler, self).__init__()
|
||||
|
||||
def get_response(self, request):
|
||||
return self._response
|
||||
|
||||
|
||||
class HandlerTests(SimpleTestCase):
|
||||
"""
|
||||
Tests that the handler works correctly and round-trips things into a
|
||||
correct response.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Make an in memory channel layer for testing
|
||||
"""
|
||||
self.channel_layer = ChannelLayer()
|
||||
self.make_message = lambda m, c: Message(m, c, self.channel_layer)
|
||||
|
||||
def test_basic(self):
|
||||
"""
|
||||
Tests a simple request
|
||||
"""
|
||||
# Make stub request and desired response
|
||||
message = self.make_message({
|
||||
"reply_channel": "test",
|
||||
"http_version": "1.1",
|
||||
"method": "GET",
|
||||
"path": "/test/",
|
||||
}, "test")
|
||||
response = HttpResponse(b"Hi there!", content_type="text/plain")
|
||||
# Run the handler
|
||||
handler = FakeAsgiHandler(response)
|
||||
reply_messages = list(handler(message))
|
||||
# Make sure we got the right number of messages
|
||||
self.assertEqual(len(reply_messages), 1)
|
||||
reply_message = reply_messages[0]
|
||||
# Make sure the message looks correct
|
||||
self.assertEqual(reply_message["content"], b"Hi there!")
|
||||
self.assertEqual(reply_message["status"], 200)
|
||||
self.assertEqual(reply_message["status_text"], "OK")
|
||||
self.assertEqual(reply_message.get("more_content", False), False)
|
||||
self.assertEqual(
|
||||
reply_message["headers"],
|
||||
[("Content-Type", b"text/plain")],
|
||||
)
|
||||
|
||||
def test_large(self):
|
||||
"""
|
||||
Tests a large response (will need chunking)
|
||||
"""
|
||||
# Make stub request and desired response
|
||||
message = self.make_message({
|
||||
"reply_channel": "test",
|
||||
"http_version": "1.1",
|
||||
"method": "GET",
|
||||
"path": "/test/",
|
||||
}, "test")
|
||||
response = HttpResponse(b"Thefirstthirtybytesisrighthereandhereistherest")
|
||||
# Run the handler
|
||||
handler = FakeAsgiHandler(response)
|
||||
reply_messages = list(handler(message))
|
||||
# Make sure we got the right number of messages
|
||||
self.assertEqual(len(reply_messages), 2)
|
||||
# Make sure the messages look correct
|
||||
self.assertEqual(reply_messages[0]["content"], b"Thefirstthirtybytesisrighthere")
|
||||
self.assertEqual(reply_messages[0]["status"], 200)
|
||||
self.assertEqual(reply_messages[0]["more_content"], True)
|
||||
self.assertEqual(reply_messages[1]["content"], b"andhereistherest")
|
||||
self.assertEqual(reply_messages[1].get("more_content", False), False)
|
178
channels/tests/test_request.py
Normal file
178
channels/tests/test_request.py
Normal file
|
@ -0,0 +1,178 @@
|
|||
from __future__ import unicode_literals
|
||||
from django.test import SimpleTestCase
|
||||
from django.utils import six
|
||||
|
||||
from asgiref.inmemory import ChannelLayer
|
||||
from channels.handler import AsgiRequest
|
||||
from channels.message import Message
|
||||
|
||||
|
||||
class RequestTests(SimpleTestCase):
|
||||
"""
|
||||
Tests that ASGI request handling correctly decodes HTTP requests.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Make an in memory channel layer for testing
|
||||
"""
|
||||
self.channel_layer = ChannelLayer()
|
||||
self.make_message = lambda m, c: Message(m, c, self.channel_layer)
|
||||
|
||||
def test_basic(self):
|
||||
"""
|
||||
Tests that the handler can decode the most basic request message,
|
||||
with all optional fields omitted.
|
||||
"""
|
||||
message = self.make_message({
|
||||
"reply_channel": "test-reply",
|
||||
"http_version": "1.1",
|
||||
"method": "GET",
|
||||
"path": "/test/",
|
||||
}, "test")
|
||||
request = AsgiRequest(message)
|
||||
self.assertEqual(request.path, "/test/")
|
||||
self.assertEqual(request.method, "GET")
|
||||
self.assertFalse(request.body)
|
||||
self.assertNotIn("HTTP_HOST", request.META)
|
||||
self.assertNotIn("REMOTE_ADDR", request.META)
|
||||
self.assertNotIn("REMOTE_HOST", request.META)
|
||||
self.assertNotIn("REMOTE_PORT", request.META)
|
||||
self.assertNotIn("SERVER_NAME", request.META)
|
||||
self.assertNotIn("SERVER_PORT", request.META)
|
||||
self.assertFalse(request.GET)
|
||||
self.assertFalse(request.POST)
|
||||
self.assertFalse(request.COOKIES)
|
||||
|
||||
def test_extended(self):
|
||||
"""
|
||||
Tests a more fully-featured GET request
|
||||
"""
|
||||
message = self.make_message({
|
||||
"reply_channel": "test",
|
||||
"http_version": "1.1",
|
||||
"method": "GET",
|
||||
"path": "/test2/",
|
||||
"query_string": b"x=1&y=foo%20bar+baz",
|
||||
"headers": {
|
||||
"host": b"example.com",
|
||||
"cookie": b"test-time=1448995585123; test-value=yeah",
|
||||
},
|
||||
"client": ["10.0.0.1", 1234],
|
||||
"server": ["10.0.0.2", 80],
|
||||
}, "test")
|
||||
request = AsgiRequest(message)
|
||||
self.assertEqual(request.path, "/test2/")
|
||||
self.assertEqual(request.method, "GET")
|
||||
self.assertFalse(request.body)
|
||||
self.assertEqual(request.META["HTTP_HOST"], "example.com")
|
||||
self.assertEqual(request.META["REMOTE_ADDR"], "10.0.0.1")
|
||||
self.assertEqual(request.META["REMOTE_HOST"], "10.0.0.1")
|
||||
self.assertEqual(request.META["REMOTE_PORT"], 1234)
|
||||
self.assertEqual(request.META["SERVER_NAME"], "10.0.0.2")
|
||||
self.assertEqual(request.META["SERVER_PORT"], 80)
|
||||
self.assertEqual(request.GET["x"], "1")
|
||||
self.assertEqual(request.GET["y"], "foo bar baz")
|
||||
self.assertEqual(request.COOKIES["test-time"], "1448995585123")
|
||||
self.assertEqual(request.COOKIES["test-value"], "yeah")
|
||||
self.assertFalse(request.POST)
|
||||
|
||||
def test_post_single(self):
|
||||
"""
|
||||
Tests a POST body contained within a single message.
|
||||
"""
|
||||
message = self.make_message({
|
||||
"reply_channel": "test",
|
||||
"http_version": "1.1",
|
||||
"method": "POST",
|
||||
"path": "/test2/",
|
||||
"query_string": b"django=great",
|
||||
"body": b"ponies=are+awesome",
|
||||
"headers": {
|
||||
"host": b"example.com",
|
||||
"content-type": b"application/x-www-form-urlencoded",
|
||||
"content-length": b"18",
|
||||
},
|
||||
}, "test")
|
||||
request = AsgiRequest(message)
|
||||
self.assertEqual(request.path, "/test2/")
|
||||
self.assertEqual(request.method, "POST")
|
||||
self.assertEqual(request.body, b"ponies=are+awesome")
|
||||
self.assertEqual(request.META["HTTP_HOST"], "example.com")
|
||||
self.assertEqual(request.META["CONTENT_TYPE"], "application/x-www-form-urlencoded")
|
||||
self.assertEqual(request.GET["django"], "great")
|
||||
self.assertEqual(request.POST["ponies"], "are awesome")
|
||||
with self.assertRaises(KeyError):
|
||||
request.POST["django"]
|
||||
with self.assertRaises(KeyError):
|
||||
request.GET["ponies"]
|
||||
|
||||
def test_post_multiple(self):
|
||||
"""
|
||||
Tests a POST body across multiple messages (first part in 'body').
|
||||
"""
|
||||
message = self.make_message({
|
||||
"reply_channel": "test",
|
||||
"http_version": "1.1",
|
||||
"method": "POST",
|
||||
"path": "/test/",
|
||||
"body": b"there_a",
|
||||
"body_channel": "test-input",
|
||||
"headers": {
|
||||
"host": b"example.com",
|
||||
"content-type": b"application/x-www-form-urlencoded",
|
||||
"content-length": b"21",
|
||||
},
|
||||
}, "test")
|
||||
self.channel_layer.send("test-input", {
|
||||
"content": b"re=fou",
|
||||
"more_content": True,
|
||||
})
|
||||
self.channel_layer.send("test-input", {
|
||||
"content": b"r+lights",
|
||||
})
|
||||
request = AsgiRequest(message)
|
||||
self.assertEqual(request.method, "POST")
|
||||
self.assertEqual(request.body, b"there_are=four+lights")
|
||||
self.assertEqual(request.META["CONTENT_TYPE"], "application/x-www-form-urlencoded")
|
||||
self.assertEqual(request.POST["there_are"], "four lights")
|
||||
|
||||
def test_post_files(self):
|
||||
"""
|
||||
Tests POSTing files using multipart form data and multiple messages,
|
||||
with no body in the initial message.
|
||||
"""
|
||||
body = (
|
||||
b'--BOUNDARY\r\n' +
|
||||
b'Content-Disposition: form-data; name="title"\r\n\r\n' +
|
||||
b'My First Book\r\n' +
|
||||
b'--BOUNDARY\r\n' +
|
||||
b'Content-Disposition: form-data; name="pdf"; filename="book.pdf"\r\n\r\n' +
|
||||
b'FAKEPDFBYTESGOHERE' +
|
||||
b'--BOUNDARY--'
|
||||
)
|
||||
message = self.make_message({
|
||||
"reply_channel": "test",
|
||||
"http_version": "1.1",
|
||||
"method": "POST",
|
||||
"path": "/test/",
|
||||
"body_channel": "test-input",
|
||||
"headers": {
|
||||
"content-type": b"multipart/form-data; boundary=BOUNDARY",
|
||||
"content-length": six.binary_type(len(body)),
|
||||
},
|
||||
}, "test")
|
||||
self.channel_layer.send("test-input", {
|
||||
"content": body[:20],
|
||||
"more_content": True,
|
||||
})
|
||||
self.channel_layer.send("test-input", {
|
||||
"content": body[20:],
|
||||
})
|
||||
request = AsgiRequest(message)
|
||||
self.assertEqual(request.method, "POST")
|
||||
self.assertEqual(len(request.body), len(body))
|
||||
self.assertTrue(request.META["CONTENT_TYPE"].startswith("multipart/form-data"))
|
||||
self.assertFalse(request._post_parse_error)
|
||||
self.assertEqual(request.POST["title"], "My First Book")
|
||||
self.assertEqual(request.FILES["pdf"].read(), "FAKEPDFBYTESGOHERE")
|
|
@ -35,7 +35,6 @@ class Worker(object):
|
|||
content=content,
|
||||
channel=channel,
|
||||
channel_layer=self.channel_layer,
|
||||
reply_channel=content.get("reply_channel", None),
|
||||
)
|
||||
# Handle the message
|
||||
consumer = self.channel_layer.registry.consumer_for_channel(channel)
|
||||
|
|
|
@ -431,30 +431,30 @@ Keys:
|
|||
* ``method``: Unicode string HTTP method name, uppercased.
|
||||
|
||||
* ``scheme``: Unicode string URL scheme portion (likely ``http`` or ``https``).
|
||||
Optional (but must not be empty), default is ``http``.
|
||||
Optional (but must not be empty), default is ``"http"``.
|
||||
|
||||
* ``path``: Byte string HTTP path from URL.
|
||||
|
||||
* ``query_string``: Byte string URL portion after the ``?``. Optional, default
|
||||
is empty string.
|
||||
is ``""``.
|
||||
|
||||
* ``root_path``: Byte string that indicates the root path this application
|
||||
is mounted at; same as ``SCRIPT_NAME`` in WSGI. Optional, defaults
|
||||
to empty string.
|
||||
to ``""``.
|
||||
|
||||
* ``headers``: Dict of ``{name: value}``, where ``name`` is the lowercased
|
||||
HTTP header name as byte string and ``value`` is the header value as a byte
|
||||
HTTP header name as unicode string and ``value`` is the header value as a byte
|
||||
string. If multiple headers with the same name are received, they should
|
||||
be concatenated into a single header as per RFC 2616. Header names containing
|
||||
underscores should be discarded by the server.
|
||||
underscores should be discarded by the server. Optional, defaults to ``{}``.
|
||||
|
||||
* ``body``: Body of the request, as a byte string. Optional, defaults to empty
|
||||
string. If ``body_channel`` is set, treat as start of body and concatenate
|
||||
* ``body``: Body of the request, as a byte string. Optional, defaults to ``""``.
|
||||
If ``body_channel`` is set, treat as start of body and concatenate
|
||||
on further chunks.
|
||||
|
||||
* ``body_channel``: Single-reader channel name that contains
|
||||
Request Body Chunk messages representing a large request body.
|
||||
Optional, defaults to None. Chunks append to ``body`` if set. Presence of
|
||||
Optional, defaults to ``None``. Chunks append to ``body`` if set. Presence of
|
||||
a channel indicates at least one Request Body Chunk message needs to be read,
|
||||
and then further consumption keyed off of the ``more_content`` key in those
|
||||
messages.
|
||||
|
@ -501,9 +501,9 @@ Keys:
|
|||
Ignored for HTTP/2 clients. Optional, default should be based on ``status``
|
||||
or left as empty string if no default found.
|
||||
|
||||
* ``headers``: A list of ``[name, value]`` pairs, where ``name`` is the byte
|
||||
string header name, and ``value`` is the byte string header value. Order
|
||||
should be preserved in the HTTP response.
|
||||
* ``headers``: A list of ``[name, value]`` pairs, where ``name`` is the
|
||||
unicode string header name, and ``value`` is the byte string
|
||||
header value. Order should be preserved in the HTTP response.
|
||||
|
||||
* ``content``: Byte string of HTTP body content.
|
||||
Optional, defaults to empty string.
|
||||
|
|
Loading…
Reference in New Issue
Block a user