/tests/test_asgi.py

https://github.com/pgjones/quart · Python · 228 lines · 177 code · 41 blank · 10 comment · 5 complexity · 9e36bd6d93a559f1440905483e1fade5 MD5 · raw file

  1. import asyncio
  2. from typing import Optional
  3. import pytest
  4. from werkzeug.datastructures import Headers
  5. from quart import Quart
  6. from quart.asgi import (
  7. _convert_version,
  8. _encode_headers,
  9. ASGIHTTPConnection,
  10. ASGIWebsocketConnection,
  11. )
  12. try:
  13. from unittest.mock import AsyncMock
  14. except ImportError:
  15. # Python < 3.8
  16. from mock import AsyncMock
  17. @pytest.mark.asyncio
  18. @pytest.mark.parametrize("headers, expected", [([(b"host", b"quart")], "quart"), ([], "")])
  19. async def test_http_1_0_host_header(headers: list, expected: str) -> None:
  20. app = Quart(__name__)
  21. scope = {
  22. "headers": headers,
  23. "http_version": "1.0",
  24. "method": "GET",
  25. "scheme": "https",
  26. "path": "/",
  27. "query_string": b"",
  28. }
  29. connection = ASGIHTTPConnection(app, scope)
  30. request = connection._create_request_from_scope(lambda: None)
  31. assert request.headers["host"] == expected
  32. @pytest.mark.asyncio
  33. async def test_http_completion() -> None:
  34. # Ensure that the connecion callable returns on completion
  35. app = Quart(__name__)
  36. scope = {
  37. "headers": [(b"host", b"quart")],
  38. "http_version": "1.1",
  39. "method": "GET",
  40. "scheme": "https",
  41. "path": "/",
  42. "query_string": b"",
  43. }
  44. connection = ASGIHTTPConnection(app, scope)
  45. queue: asyncio.Queue = asyncio.Queue()
  46. queue.put_nowait({"type": "http.request", "body": b"", "more_body": False})
  47. async def receive() -> dict:
  48. # This will block after returning the first and only entry
  49. return await queue.get()
  50. async def send(message: dict) -> None:
  51. pass
  52. # This test fails if a timeout error is raised here
  53. await asyncio.wait_for(connection(receive, send), timeout=1)
  54. @pytest.mark.asyncio
  55. @pytest.mark.parametrize(
  56. "request_message",
  57. [
  58. {"type": "http.request", "body": b"", "more_body": False},
  59. {"type": "http.request", "more_body": False},
  60. ],
  61. )
  62. async def test_http_request_without_body(request_message: dict) -> None:
  63. app = Quart(__name__)
  64. scope = {
  65. "headers": [(b"host", b"quart")],
  66. "http_version": "1.0",
  67. "method": "GET",
  68. "scheme": "https",
  69. "path": "/",
  70. "query_string": b"",
  71. }
  72. connection = ASGIHTTPConnection(app, scope)
  73. request = connection._create_request_from_scope(lambda: None)
  74. queue: asyncio.Queue = asyncio.Queue()
  75. queue.put_nowait(request_message)
  76. async def receive() -> dict:
  77. # This will block after returning the first and only entry
  78. return await queue.get()
  79. # This test fails with a timeout error if the request body is not received
  80. # within 1 second
  81. receiver_task = asyncio.ensure_future(connection.handle_messages(request, receive))
  82. body = await asyncio.wait_for(request.body, timeout=1)
  83. receiver_task.cancel()
  84. assert body == b""
  85. @pytest.mark.asyncio
  86. async def test_websocket_completion() -> None:
  87. # Ensure that the connecion callable returns on completion
  88. app = Quart(__name__)
  89. scope = {
  90. "headers": [(b"host", b"quart")],
  91. "http_version": "1.1",
  92. "method": "GET",
  93. "scheme": "wss",
  94. "path": "/",
  95. "query_string": b"",
  96. "subprotocols": [],
  97. "extensions": {"websocket.http.response": {}},
  98. }
  99. connection = ASGIWebsocketConnection(app, scope)
  100. queue: asyncio.Queue = asyncio.Queue()
  101. queue.put_nowait({"type": "websocket.connect"})
  102. async def receive() -> dict:
  103. # This will block after returning the first and only entry
  104. return await queue.get()
  105. async def send(message: dict) -> None:
  106. pass
  107. # This test fails if a timeout error is raised here
  108. await asyncio.wait_for(connection(receive, send), timeout=1)
  109. def test_http_path_from_absolute_target() -> None:
  110. app = Quart(__name__)
  111. scope = {
  112. "headers": [(b"host", b"quart")],
  113. "http_version": "1.1",
  114. "method": "GET",
  115. "scheme": "https",
  116. "path": "http://quart/path",
  117. "query_string": b"",
  118. }
  119. connection = ASGIHTTPConnection(app, scope)
  120. request = connection._create_request_from_scope(lambda: None)
  121. assert request.path == "/path"
  122. def test_websocket_path_from_absolute_target() -> None:
  123. app = Quart(__name__)
  124. scope = {
  125. "headers": [(b"host", b"quart")],
  126. "http_version": "1.1",
  127. "method": "GET",
  128. "scheme": "wss",
  129. "path": "ws://quart/path",
  130. "query_string": b"",
  131. "subprotocols": [],
  132. "extensions": {"websocket.http.response": {}},
  133. }
  134. connection = ASGIWebsocketConnection(app, scope)
  135. websocket = connection._create_websocket_from_scope(lambda: None)
  136. assert websocket.path == "/path"
  137. @pytest.mark.asyncio
  138. @pytest.mark.parametrize(
  139. "scope, headers, subprotocol, has_headers",
  140. [
  141. ({}, Headers(), None, False),
  142. ({}, Headers(), "abc", False),
  143. ({"asgi": {"spec_version": "2.1"}}, Headers({"a": "b"}), None, True),
  144. ({"asgi": {"spec_version": "2.1.1"}}, Headers({"a": "b"}), None, True),
  145. ],
  146. )
  147. async def test_websocket_accept_connection(
  148. scope: dict, headers: Headers, subprotocol: Optional[str], has_headers: bool
  149. ) -> None:
  150. connection = ASGIWebsocketConnection(Quart(__name__), scope)
  151. mock_send = AsyncMock()
  152. await connection.accept_connection(mock_send, headers, subprotocol)
  153. if has_headers:
  154. mock_send.assert_called_with(
  155. {
  156. "subprotocol": subprotocol,
  157. "type": "websocket.accept",
  158. "headers": _encode_headers(headers),
  159. }
  160. )
  161. else:
  162. mock_send.assert_called_with({"subprotocol": subprotocol, "type": "websocket.accept"})
  163. @pytest.mark.asyncio
  164. async def test_websocket_accept_connection_warns() -> None:
  165. connection = ASGIWebsocketConnection(Quart(__name__), {})
  166. async def mock_send(message: dict) -> None:
  167. pass
  168. with pytest.warns(None):
  169. await connection.accept_connection(mock_send, Headers({"a": "b"}), None)
  170. def test__encode_headers() -> None:
  171. assert _encode_headers(Headers({"Foo": "Bar"})) == [(b"foo", b"Bar")]
  172. def test__convert_version() -> None:
  173. assert _convert_version("2.1") == [2, 1]
  174. def test_http_asgi_scope_from_request() -> None:
  175. app = Quart(__name__)
  176. scope = {
  177. "headers": [(b"host", b"quart")],
  178. "http_version": "1.0",
  179. "method": "GET",
  180. "scheme": "https",
  181. "path": "/",
  182. "query_string": b"",
  183. "test_result": "PASSED",
  184. }
  185. connection = ASGIHTTPConnection(app, scope)
  186. request = connection._create_request_from_scope(lambda: None)
  187. assert request.scope["test_result"] == "PASSED"