mirror of
https://github.com/django/daphne.git
synced 2024-11-21 15:36:33 +03:00
Add HTTP response test suite
This commit is contained in:
parent
e0e60e4117
commit
1ca1c67032
|
@ -152,7 +152,7 @@ class Server(object):
|
|||
"""
|
||||
Coroutine that jumps the reply message from asyncio to Twisted
|
||||
"""
|
||||
reactor.callLater(0, protocol.handle_reply, message)
|
||||
protocol.handle_reply(message)
|
||||
|
||||
### Utility
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import msgpack
|
||||
import os
|
||||
import pickle
|
||||
import tempfile
|
||||
|
||||
|
||||
|
@ -19,12 +19,15 @@ class TestApplication:
|
|||
async def __call__(self, send, receive):
|
||||
# Load setup info
|
||||
setup = self.load_setup()
|
||||
# Receive input and send output
|
||||
try:
|
||||
for _ in range(setup["receive_messages"]):
|
||||
self.messages.append(await receive())
|
||||
for message in setup["response_messages"]:
|
||||
await send(message)
|
||||
finally:
|
||||
except Exception as e:
|
||||
self.save_exception(e)
|
||||
else:
|
||||
self.save_result()
|
||||
|
||||
@classmethod
|
||||
|
@ -33,13 +36,13 @@ class TestApplication:
|
|||
Stores setup information.
|
||||
"""
|
||||
with open(cls.setup_storage, "wb") as fh:
|
||||
fh.write(msgpack.packb(
|
||||
pickle.dump(
|
||||
{
|
||||
"response_messages": response_messages,
|
||||
"receive_messages": receive_messages,
|
||||
},
|
||||
use_bin_type=True,
|
||||
))
|
||||
fh,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def load_setup(cls):
|
||||
|
@ -47,7 +50,7 @@ class TestApplication:
|
|||
Returns setup details.
|
||||
"""
|
||||
with open(cls.setup_storage, "rb") as fh:
|
||||
return msgpack.unpackb(fh.read(), encoding="utf-8")
|
||||
return pickle.load(fh)
|
||||
|
||||
def save_result(self):
|
||||
"""
|
||||
|
@ -55,13 +58,26 @@ class TestApplication:
|
|||
We could use pickle here, but that seems wrong, still, somehow.
|
||||
"""
|
||||
with open(self.result_storage, "wb") as fh:
|
||||
fh.write(msgpack.packb(
|
||||
pickle.dump(
|
||||
{
|
||||
"scope": self.scope,
|
||||
"messages": self.messages,
|
||||
},
|
||||
use_bin_type=True,
|
||||
))
|
||||
fh,
|
||||
)
|
||||
|
||||
def save_exception(self, exception):
|
||||
"""
|
||||
Saves details of what happened to the result storage.
|
||||
We could use pickle here, but that seems wrong, still, somehow.
|
||||
"""
|
||||
with open(self.result_storage, "wb") as fh:
|
||||
pickle.dump(
|
||||
{
|
||||
"exception": exception,
|
||||
},
|
||||
fh,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def load_result(cls):
|
||||
|
@ -69,7 +85,7 @@ class TestApplication:
|
|||
Returns result details.
|
||||
"""
|
||||
with open(cls.result_storage, "rb") as fh:
|
||||
return msgpack.unpackb(fh.read(), encoding="utf-8")
|
||||
return pickle.load(fh)
|
||||
|
||||
@classmethod
|
||||
def clear_storage(cls):
|
||||
|
|
|
@ -1,128 +0,0 @@
|
|||
# coding: utf8
|
||||
"""
|
||||
Tests for the HTTP response section of the ASGI spec
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from unittest import TestCase
|
||||
|
||||
from asgiref.inmemory import ChannelLayer
|
||||
from hypothesis import given, settings
|
||||
from twisted.test import proto_helpers
|
||||
|
||||
from daphne.http_protocol import HTTPFactory
|
||||
from . import factories, http_strategies, testcases
|
||||
|
||||
|
||||
class TestHTTPResponseSpec(testcases.ASGIHTTPTestCase):
|
||||
|
||||
def test_minimal_response(self):
|
||||
"""
|
||||
Smallest viable example. Mostly verifies that our response building works.
|
||||
"""
|
||||
message = {"status": 200}
|
||||
response = factories.response_for_message(message)
|
||||
self.assert_valid_http_response_message(message, response)
|
||||
self.assertIn(b"200 OK", response)
|
||||
# Assert that the response is the last of the chunks.
|
||||
# N.b. at the time of writing, Daphne did not support multiple response chunks,
|
||||
# but still sends with Transfer-Encoding: chunked if no Content-Length header
|
||||
# is specified (and maybe even if specified).
|
||||
self.assertTrue(response.endswith(b"0\r\n\r\n"))
|
||||
|
||||
def test_status_code_required(self):
|
||||
"""
|
||||
Asserts that passing in the 'status' key is required.
|
||||
|
||||
Previous versions of Daphne did not enforce this, so this test is here
|
||||
to make sure it stays required.
|
||||
"""
|
||||
with self.assertRaises(ValueError):
|
||||
factories.response_for_message({})
|
||||
|
||||
def test_status_code_is_transmitted(self):
|
||||
"""
|
||||
Tests that a custom status code is present in the response.
|
||||
|
||||
We can't really use hypothesis to test all sorts of status codes, because a lot
|
||||
of them have meaning that is respected by Twisted. E.g. setting 204 (No Content)
|
||||
as a status code results in Twisted discarding the body.
|
||||
"""
|
||||
message = {"status": 201} # 'Created'
|
||||
response = factories.response_for_message(message)
|
||||
self.assert_valid_http_response_message(message, response)
|
||||
self.assertIn(b"201 Created", response)
|
||||
|
||||
@given(body=http_strategies.http_body())
|
||||
def test_body_is_transmitted(self, body):
|
||||
message = {"status": 200, "content": body.encode("ascii")}
|
||||
response = factories.response_for_message(message)
|
||||
self.assert_valid_http_response_message(message, response)
|
||||
|
||||
@given(headers=http_strategies.headers())
|
||||
def test_headers(self, headers):
|
||||
# The ASGI spec requires us to lowercase our header names
|
||||
message = {"status": 200, "headers": [(name.lower(), value) for name, value in headers]}
|
||||
response = factories.response_for_message(message)
|
||||
# The assert_ method does the heavy lifting of checking that headers are
|
||||
# as expected.
|
||||
self.assert_valid_http_response_message(message, response)
|
||||
|
||||
@given(
|
||||
headers=http_strategies.headers(),
|
||||
body=http_strategies.http_body(),
|
||||
)
|
||||
@settings(perform_health_check=False)
|
||||
def test_kitchen_sink(self, headers, body):
|
||||
"""
|
||||
This tests tries to let Hypothesis find combinations of variables that result
|
||||
in breaking our assumptions. But responses are less exciting than responses,
|
||||
so there's not a lot going on here.
|
||||
"""
|
||||
message = {
|
||||
"status": 202, # 'Accepted'
|
||||
"headers": [(name.lower(), value) for name, value in headers],
|
||||
"content": body.encode("ascii")
|
||||
}
|
||||
response = factories.response_for_message(message)
|
||||
self.assert_valid_http_response_message(message, response)
|
||||
|
||||
|
||||
class TestHTTPResponse(TestCase):
|
||||
"""
|
||||
Tests that the HTTP protocol class correctly generates and parses messages.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.channel_layer = ChannelLayer()
|
||||
self.factory = HTTPFactory(self.channel_layer, send_channel="test!")
|
||||
self.proto = self.factory.buildProtocol(("127.0.0.1", 0))
|
||||
self.tr = proto_helpers.StringTransport()
|
||||
self.proto.makeConnection(self.tr)
|
||||
|
||||
def test_http_disconnect_sets_path_key(self):
|
||||
"""
|
||||
Tests http disconnect has the path key set, see https://channels.readthedocs.io/en/latest/asgi.html#disconnect
|
||||
"""
|
||||
# Send a simple request to the protocol
|
||||
self.proto.dataReceived(
|
||||
b"GET /te%20st-%C3%A0/?foo=bar HTTP/1.1\r\n" +
|
||||
b"Host: anywhere.com\r\n" +
|
||||
b"\r\n"
|
||||
)
|
||||
# Get the request message
|
||||
_, message = self.channel_layer.receive(["http.request"])
|
||||
|
||||
# Send back an example response
|
||||
self.factory.dispatch_reply(
|
||||
message["reply_channel"],
|
||||
{
|
||||
"status": 200,
|
||||
"status_text": b"OK",
|
||||
"content": b"DISCO",
|
||||
}
|
||||
)
|
||||
|
||||
# Get the disconnection notification
|
||||
_, disconnect_message = self.channel_layer.receive(["http.disconnect"])
|
||||
self.assertEqual(disconnect_message["path"], "/te st-à/")
|
|
@ -82,7 +82,19 @@ class DaphneTestCase(unittest.TestCase):
|
|||
conn.endheaders(message_body=body)
|
||||
else:
|
||||
conn.endheaders()
|
||||
response = conn.getresponse()
|
||||
try:
|
||||
response = conn.getresponse()
|
||||
except socket.timeout:
|
||||
# See if they left an exception for us to load
|
||||
try:
|
||||
exception_result = TestApplication.load_result()
|
||||
except OSError:
|
||||
raise RuntimeError("Daphne timed out handling request, no result file")
|
||||
else:
|
||||
if "exception" in exception_result:
|
||||
raise exception_result["exception"]
|
||||
else:
|
||||
raise RuntimeError("Daphne timed out handling request, no exception found: %r" % exception_result)
|
||||
finally:
|
||||
# Shut down daphne
|
||||
process.terminate()
|
||||
|
@ -107,6 +119,20 @@ class DaphneTestCase(unittest.TestCase):
|
|||
)
|
||||
return inner_result["scope"], inner_result["messages"]
|
||||
|
||||
def run_daphne_response(self, response_messages):
|
||||
"""
|
||||
Convenience method for just testing response handling.
|
||||
Returns (scope, messages)
|
||||
"""
|
||||
_, response = self.run_daphne(
|
||||
method="GET",
|
||||
path="/",
|
||||
params={},
|
||||
body=b"",
|
||||
responses=response_messages,
|
||||
)
|
||||
return response
|
||||
|
||||
def tearDown(self):
|
||||
"""
|
||||
Ensures any storage files are cleared.
|
||||
|
@ -155,36 +181,6 @@ class DaphneTestCase(unittest.TestCase):
|
|||
self.assertIsInstance(port, int)
|
||||
|
||||
|
||||
# class ASGIHTTPTestCase(ASGITestCaseBase):
|
||||
# """
|
||||
# Test case with helpers for verifying HTTP channel messages
|
||||
# """
|
||||
|
||||
|
||||
# def assert_valid_http_response_message(self, message, response):
|
||||
# self.assertTrue(message)
|
||||
# self.assertTrue(response.startswith(b"HTTP"))
|
||||
|
||||
# status_code_bytes = str(message["status"]).encode("ascii")
|
||||
# self.assertIn(status_code_bytes, response)
|
||||
|
||||
# if "content" in message:
|
||||
# self.assertIn(message["content"], response)
|
||||
|
||||
# # Check that headers are in the given order.
|
||||
# # N.b. HTTP spec only enforces that the order of header values is kept, but
|
||||
# # the ASGI spec requires that order of all headers is kept. This code
|
||||
# # checks conformance with the stricter ASGI spec.
|
||||
# if "headers" in message:
|
||||
# for name, value in message["headers"]:
|
||||
# expected_header = factories.header_line(name, value)
|
||||
# # Daphne or Twisted turn our lower cased header names ('foo-bar') into title
|
||||
# # case ('Foo-Bar'). So technically we want to to match that the header name is
|
||||
# # present while ignoring casing, and want to ensure the value is present without
|
||||
# # altered casing. The approach below does this well enough.
|
||||
# self.assertIn(expected_header.lower(), response.lower())
|
||||
# self.assertIn(value.encode("ascii"), response)
|
||||
|
||||
|
||||
# class ASGIWebSocketTestCase(ASGITestCaseBase):
|
||||
# """
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
import collections
|
||||
from urllib import parse
|
||||
|
||||
from hypothesis import given, assume, settings, HealthCheck
|
||||
from hypothesis import given, assume, settings
|
||||
|
||||
import http_strategies
|
||||
from http_base import DaphneTestCase
|
||||
|
|
101
tests/test_http_response.py
Normal file
101
tests/test_http_response.py
Normal file
|
@ -0,0 +1,101 @@
|
|||
# coding: utf8
|
||||
|
||||
from hypothesis import given, assume, settings
|
||||
|
||||
import http_strategies
|
||||
from http_base import DaphneTestCase
|
||||
|
||||
|
||||
class TestHTTPResponseSpec(DaphneTestCase):
|
||||
"""
|
||||
Tests which try to pour the HTTP request section of the ASGI spec into code.
|
||||
The heavy lifting is done by the assert_valid_http_request_message function,
|
||||
the tests mostly serve to wire up hypothesis so that it exercise it's power to find
|
||||
edge cases.
|
||||
"""
|
||||
|
||||
def normalize_headers(self, headers):
|
||||
"""
|
||||
Lowercases and sorts headers, and strips transfer-encoding ones.
|
||||
"""
|
||||
return sorted([
|
||||
(name.lower(), value)
|
||||
for name, value in headers
|
||||
if name.lower() != "transfer-encoding"
|
||||
])
|
||||
|
||||
def test_minimal_response(self):
|
||||
"""
|
||||
Smallest viable example. Mostly verifies that our response building works.
|
||||
"""
|
||||
response = self.run_daphne_response([
|
||||
{
|
||||
"type": "http.response",
|
||||
"status": 200,
|
||||
"content": b"hello world",
|
||||
},
|
||||
])
|
||||
self.assertEqual(response.status, 200)
|
||||
self.assertEqual(response.read(), b"hello world")
|
||||
|
||||
def test_status_code_required(self):
|
||||
"""
|
||||
Asserts that passing in the 'status' key is required.
|
||||
|
||||
Previous versions of Daphne did not enforce this, so this test is here
|
||||
to make sure it stays required.
|
||||
"""
|
||||
with self.assertRaises(ValueError):
|
||||
self.run_daphne_response([
|
||||
{
|
||||
"type": "http.response",
|
||||
"content": b"hello world",
|
||||
},
|
||||
])
|
||||
|
||||
def test_custom_status_code(self):
|
||||
"""
|
||||
Tries a non-default status code.
|
||||
"""
|
||||
response = self.run_daphne_response([
|
||||
{
|
||||
"type": "http.response",
|
||||
"status": 201,
|
||||
"content": b"i made a thing!",
|
||||
},
|
||||
])
|
||||
self.assertEqual(response.status, 201)
|
||||
self.assertEqual(response.read(), b"i made a thing!")
|
||||
|
||||
@given(body=http_strategies.http_body())
|
||||
@settings(max_examples=5, deadline=2000)
|
||||
def test_body(self, body):
|
||||
"""
|
||||
Tries body variants.
|
||||
"""
|
||||
response = self.run_daphne_response([
|
||||
{
|
||||
"type": "http.response",
|
||||
"status": 200,
|
||||
"content": body,
|
||||
},
|
||||
])
|
||||
self.assertEqual(response.status, 200)
|
||||
self.assertEqual(response.read(), body)
|
||||
|
||||
@given(headers=http_strategies.headers())
|
||||
@settings(max_examples=5, deadline=2000)
|
||||
def test_headers(self, headers):
|
||||
# The ASGI spec requires us to lowercase our header names
|
||||
response = self.run_daphne_response([
|
||||
{
|
||||
"type": "http.response",
|
||||
"status": 200,
|
||||
"headers": self.normalize_headers(headers),
|
||||
},
|
||||
])
|
||||
# Check headers in a sensible way. Ignore transfer-encoding.
|
||||
self.assertEqual(
|
||||
self.normalize_headers(response.getheaders()),
|
||||
self.normalize_headers(headers),
|
||||
)
|
Loading…
Reference in New Issue
Block a user