diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..142b3c3 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,24 @@ +[run] +branch = True +source = channels, django.http.response +omit = channels/tests/* + +[report] +show_missing = True +skip_covered = True +omit = channels/tests/* + +[html] +directory = coverage_html + +[paths] +django_19 = + .tox/py27-django-18/lib/python2.7 + .tox/py34-django-18/lib/python3.4 + .tox/py35-django-18/lib/python3.5 + +django_18 = + .tox/py27-django-19/lib/python2.7 + .tox/py34-django-19/lib/python3.4 + .tox/py35-django-19/lib/python3.5 + diff --git a/channels/handler.py b/channels/handler.py index 8902cdf..0e0f4e1 100644 --- a/channels/handler.py +++ b/channels/handler.py @@ -277,7 +277,9 @@ class AsgiHandler(base.BaseHandler): } # Streaming responses need to be pinned to their iterator if response.streaming: - for part in response.streaming_content: + # Access `__iter__` and not `streaming_content` directly in case + # it has been overridden in a subclass. + for part in response: for chunk, more in cls.chunk_bytes(part): message['content'] = chunk # We ignore "more" as there may be more parts; instead, diff --git a/channels/tests/a_file b/channels/tests/a_file new file mode 100644 index 0000000..207ed20 --- /dev/null +++ b/channels/tests/a_file @@ -0,0 +1,5 @@ +thi is +a file +sdaf +sadf + diff --git a/channels/tests/test_handler.py b/channels/tests/test_handler.py index 450f06b..255f0b1 100644 --- a/channels/tests/test_handler.py +++ b/channels/tests/test_handler.py @@ -1,5 +1,15 @@ from __future__ import unicode_literals -from django.http import HttpResponse + +import os +import unittest +from datetime import datetime +from itertools import islice + +from django.http import ( + FileResponse, HttpResponse, HttpResponseRedirect, JsonResponse, + StreamingHttpResponse, +) +from six import BytesIO, StringIO from channels import Channel from channels.handler import AsgiHandler @@ -15,7 +25,7 @@ class FakeAsgiHandler(AsgiHandler): chunk_size = 30 def __init__(self, response): - assert isinstance(response, HttpResponse) + assert isinstance(response, (HttpResponse, StreamingHttpResponse)) self._response = response super(FakeAsgiHandler, self).__init__() @@ -43,7 +53,8 @@ class HandlerTests(ChannelTestCase): response = HttpResponse(b"Hi there!", content_type="text/plain") # Run the handler handler = FakeAsgiHandler(response) - reply_messages = list(handler(self.get_next_message("test", require=True))) + reply_messages = list( + handler(self.get_next_message("test", require=True))) # Make sure we got the right number of messages self.assertEqual(len(reply_messages), 1) reply_message = reply_messages[0] @@ -53,9 +64,58 @@ class HandlerTests(ChannelTestCase): self.assertEqual(reply_message.get("more_content", False), False) self.assertEqual( reply_message["headers"], - [(b"Content-Type", b"text/plain")], + [ + (b"Content-Type", b"text/plain"), + ], ) + def test_cookies(self): + Channel("test").send({ + "reply_channel": "test", + "http_version": "1.1", + "method": "GET", + "path": b"/test/", + }) + response = HttpResponse(b"Hi there!", content_type="text/plain") + response.set_signed_cookie('foo', '1', expires=datetime.now()) + # Run the handler + handler = FakeAsgiHandler(response) + reply_messages = list( + handler(self.get_next_message("test", require=True))) + # 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.get("more_content", False), False) + self.assertEqual(reply_message["headers"][0], (b'Content-Type', b'text/plain')) + self.assertIn('foo=', reply_message["headers"][1][1].decode()) + + def test_headers(self): + Channel("test").send({ + "reply_channel": "test", + "http_version": "1.1", + "method": "GET", + "path": b"/test/", + }) + response = HttpResponse(b"Hi there!", content_type="text/plain") + response['foo'] = 1 + response['bar'] = 1 + del response['bar'] + del response['nonexistant_key'] + handler = FakeAsgiHandler(response) + reply_messages = list( + handler(self.get_next_message("test", require=True))) + # 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!") + header_dict = dict(reply_messages[0]['headers']) + self.assertEqual(header_dict[b'foo'].decode(), '1') + self.assertNotIn('bar', header_dict) + def test_large(self): """ Tests a large response (will need chunking) @@ -67,14 +127,17 @@ class HandlerTests(ChannelTestCase): "method": "GET", "path": b"/test/", }) - response = HttpResponse(b"Thefirstthirtybytesisrighthereandhereistherest") + response = HttpResponse( + b"Thefirstthirtybytesisrighthereandhereistherest") # Run the handler handler = FakeAsgiHandler(response) - reply_messages = list(handler(self.get_next_message("test", require=True))) + reply_messages = list( + handler(self.get_next_message("test", require=True))) # 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][ + "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") @@ -90,19 +153,174 @@ class HandlerTests(ChannelTestCase): self.assertEqual(result[0][0], b"") self.assertEqual(result[0][1], True) # Below chunk size - result = list(FakeAsgiHandler.chunk_bytes(b"12345678901234567890123456789")) + result = list(FakeAsgiHandler.chunk_bytes( + b"12345678901234567890123456789")) self.assertEqual(len(result), 1) self.assertEqual(result[0][0], b"12345678901234567890123456789") self.assertEqual(result[0][1], True) # Exactly chunk size - result = list(FakeAsgiHandler.chunk_bytes(b"123456789012345678901234567890")) + result = list(FakeAsgiHandler.chunk_bytes( + b"123456789012345678901234567890")) self.assertEqual(len(result), 1) self.assertEqual(result[0][0], b"123456789012345678901234567890") self.assertEqual(result[0][1], True) # Just above chunk size - result = list(FakeAsgiHandler.chunk_bytes(b"123456789012345678901234567890a")) + result = list(FakeAsgiHandler.chunk_bytes( + b"123456789012345678901234567890a")) self.assertEqual(len(result), 2) self.assertEqual(result[0][0], b"123456789012345678901234567890") self.assertEqual(result[0][1], False) self.assertEqual(result[1][0], b"a") self.assertEqual(result[1][1], True) + + def test_iterator(self): + Channel("test").send({ + "reply_channel": "test", + "http_version": "1.1", + "method": "GET", + "path": b"/test/", + }) + response = HttpResponse(range(10)) + handler = FakeAsgiHandler(response) + reply_messages = list( + handler(self.get_next_message("test", require=True))) + self.assertEqual(len(reply_messages), 1) + self.assertEqual(reply_messages[0]["content"], b"0123456789") + + def test_streaming_data(self): + Channel("test").send({ + "reply_channel": "test", + "http_version": "1.1", + "method": "GET", + "path": b"/test/", + }) + response = StreamingHttpResponse('Line: %s' % i for i in range(10)) + handler = FakeAsgiHandler(response) + reply_messages = list( + handler(self.get_next_message("test", require=True))) + self.assertEqual(len(reply_messages), 11) + self.assertEqual(reply_messages[0]["content"], b"Line: 0") + self.assertEqual(reply_messages[9]["content"], b"Line: 9") + + def test_real_file_response(self): + Channel("test").send({ + "reply_channel": "test", + "http_version": "1.1", + "method": "GET", + "path": b"/test/", + }) + current_dir = os.path.realpath(os.path.join( + os.getcwd(), os.path.dirname(__file__))) + response = FileResponse( + open(os.path.join(current_dir, 'a_file'), 'rb')) + handler = FakeAsgiHandler(response) + reply_messages = list( + handler(self.get_next_message("test", require=True))) + self.assertEqual(len(reply_messages), 2) + self.assertEqual(response.getvalue(), b'') + + def test_bytes_file_response(self): + Channel("test").send({ + "reply_channel": "test", + "http_version": "1.1", + "method": "GET", + "path": b"/test/", + }) + response = FileResponse(BytesIO(b'sadfdasfsdfsadf')) + handler = FakeAsgiHandler(response) + reply_messages = list( + handler(self.get_next_message("test", require=True))) + self.assertEqual(len(reply_messages), 2) + + def test_string_file_response(self): + Channel("test").send({ + "reply_channel": "test", + "http_version": "1.1", + "method": "GET", + "path": b"/test/", + }) + response = FileResponse('abcd') + handler = FakeAsgiHandler(response) + reply_messages = list( + handler(self.get_next_message("test", require=True))) + self.assertEqual(len(reply_messages), 5) + + def test_non_streaming_file_response(self): + Channel("test").send({ + "reply_channel": "test", + "http_version": "1.1", + "method": "GET", + "path": b"/test/", + }) + response = FileResponse(BytesIO(b'sadfdasfsdfsadf')) + # This is to test the exception handling. This would only happening if + # the StreamingHttpResponse was incorrectly subclassed. + response.streaming = False + + handler = FakeAsgiHandler(response) + with self.assertRaises(AttributeError): + list(handler(self.get_next_message("test", require=True))) + + def test_unclosable_filelike_object(self): + Channel("test").send({ + "reply_channel": "test", + "http_version": "1.1", + "method": "GET", + "path": b"/test/", + }) + + # This is a readable object that cannot be closed. + class Unclosable: + + def read(self, n=-1): + # Nothing to see here + return b"" + + response = FileResponse(Unclosable()) + handler = FakeAsgiHandler(response) + reply_messages = list(islice(handler(self.get_next_message("test", require=True)), 5)) + self.assertEqual(len(reply_messages), 1) + response.close() + + def test_json_response(self): + Channel("test").send({ + "reply_channel": "test", + "http_version": "1.1", + "method": "GET", + "path": b"/test/", + }) + response = JsonResponse({'foo': (1, 2)}) + handler = FakeAsgiHandler(response) + reply_messages = list(handler(self.get_next_message("test", require=True))) + self.assertEqual(len(reply_messages), 1) + self.assertEqual(reply_messages[0]['content'], b'{"foo": [1, 2]}') + + def test_redirect(self): + for redirect_to in ['/', '..', 'https://example.com']: + Channel("test").send({ + "reply_channel": "test", + "http_version": "1.1", + "method": "GET", + "path": b"/test/", + }) + response = HttpResponseRedirect(redirect_to) + handler = FakeAsgiHandler(response) + reply_messages = list(handler(self.get_next_message("test", require=True))) + self.assertEqual(reply_messages[0]['status'], 302) + header_dict = dict(reply_messages[0]['headers']) + self.assertEqual(header_dict[b'Location'].decode(), redirect_to) + + @unittest.skip("failing under python 3") + def test_stringio_file_response(self): + Channel("test").send({ + "reply_channel": "test", + "http_version": "1.1", + "method": "GET", + "path": b"/test/", + }) + response = FileResponse(StringIO('sadfdasfsdfsadf')) + handler = FakeAsgiHandler(response) + # Use islice because the generator never ends. + reply_messages = list( + islice(handler(self.get_next_message("test", require=True)), 5)) + self.assertEqual(len(reply_messages), 2, reply_messages) diff --git a/tox.ini b/tox.ini index 75ae168..51b3fc2 100644 --- a/tox.ini +++ b/tox.ini @@ -12,6 +12,7 @@ setenv = PYTHONPATH = {toxinidir}:{toxinidir} deps = autobahn + coverage asgiref>=0.9 six redis==2.10.5 @@ -23,4 +24,5 @@ deps = commands = flake8: flake8 isort: isort -c -rc channels - django: python {toxinidir}/runtests.py + django: coverage run --parallel-mode {toxinidir}/runtests.py +