From 7b8e3624b8a57519a883bf3439b77e0a4613c4a7 Mon Sep 17 00:00:00 2001 From: Adam Hopkins Date: Wed, 22 May 2019 01:42:19 +0300 Subject: [PATCH] Prepare initial websocket support --- examples/run_asgi.py | 37 ++ sanic/app.py | 39 +- sanic/asgi.py | 83 ++- sanic/server.py | 2 + sanic/testing.py | 11 +- sanic/websocket.py | 49 ++ tests/test_asgi.py | 951 ------------------------------- tests/test_keep_alive_timeout.py | 4 - tests/test_requests.py | 22 + tests/test_response.py | 4 +- 10 files changed, 207 insertions(+), 995 deletions(-) create mode 100644 examples/run_asgi.py diff --git a/examples/run_asgi.py b/examples/run_asgi.py new file mode 100644 index 00000000..4e7e838c --- /dev/null +++ b/examples/run_asgi.py @@ -0,0 +1,37 @@ +""" +1. Create a simple Sanic app +2. Run with an ASGI server: + $ uvicorn run_asgi:app + or + $ hypercorn run_asgi:app +""" + +from sanic import Sanic +from sanic.response import text + + +app = Sanic(__name__) + +@app.route("/") +def handler(request): + return text("Hello") + +@app.route("/foo") +def handler_foo(request): + return text("bar") + + +@app.websocket('/feed') +async def feed(request, ws): + name = "" + while True: + data = f"Hello {name}" + await ws.send(data) + name = await ws.recv() + + if not name: + break + + +if __name__ == '__main__': + app.run(debug=True) diff --git a/sanic/app.py b/sanic/app.py index 5e309452..5e1b87c3 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -8,7 +8,6 @@ from asyncio import CancelledError, Protocol, ensure_future, get_event_loop from collections import defaultdict, deque from functools import partial from inspect import getmodulename, isawaitable, signature, stack -from multidict import CIMultiDict from socket import socket from ssl import Purpose, SSLContext, create_default_context from traceback import format_exc @@ -24,11 +23,10 @@ from sanic.exceptions import SanicException, ServerError, URLBuildError from sanic.handlers import ErrorHandler from sanic.log import LOGGING_CONFIG_DEFAULTS, error_logger, logger from sanic.response import HTTPResponse, StreamingHTTPResponse -from sanic.request import Request from sanic.router import Router from sanic.server import HttpProtocol, Signal, serve, serve_multiple from sanic.static import register as static_register -from sanic.testing import SanicTestClient, SanicASGITestClient +from sanic.testing import SanicASGITestClient, SanicTestClient from sanic.views import CompositionView from sanic.websocket import ConnectionClosed, WebSocketProtocol @@ -56,6 +54,7 @@ class Sanic: logging.config.dictConfig(log_config or LOGGING_CONFIG_DEFAULTS) self.name = name + self.asgi = True self.router = router or Router() self.request_class = request_class self.error_handler = error_handler or ErrorHandler() @@ -468,13 +467,23 @@ class Sanic: getattr(handler, "__blueprintname__", "") + handler.__name__ ) - try: - protocol = request.transport.get_protocol() - except AttributeError: - # On Python3.5 the Transport classes in asyncio do not - # have a get_protocol() method as in uvloop - protocol = request.transport._protocol - ws = await protocol.websocket_handshake(request, subprotocols) + + pass + + if self.asgi: + ws = request.transport.get_websocket_connection() + else: + try: + protocol = request.transport.get_protocol() + except AttributeError: + # On Python3.5 the Transport classes in asyncio do not + # have a get_protocol() method as in uvloop + protocol = request.transport._protocol + protocol.app = self + + ws = await protocol.websocket_handshake( + request, subprotocols + ) # schedule the application handler # its future is kept in self.websocket_tasks in case it @@ -985,7 +994,13 @@ class Sanic: if write_callback is None or isinstance( response, StreamingHTTPResponse ): - await stream_callback(response) + if stream_callback: + await stream_callback(response) + else: + # Should only end here IF it is an ASGI websocket. + # TODO: + # - Add exception handling + pass else: write_callback(response) @@ -1374,5 +1389,5 @@ class Sanic: # -------------------------------------------------------------------- # async def __call__(self, scope, receive, send): - asgi_app = ASGIApp(self, scope, receive, send) + asgi_app = await ASGIApp.create(self, scope, receive, send) await asgi_app() diff --git a/sanic/asgi.py b/sanic/asgi.py index 8e2693f4..6e9be0e7 100644 --- a/sanic/asgi.py +++ b/sanic/asgi.py @@ -1,24 +1,50 @@ -from sanic.request import Request -from multidict import CIMultiDict -from sanic.response import StreamingHTTPResponse +from typing import Any, Awaitable, Callable, MutableMapping, Union +from multidict import CIMultiDict + +from sanic.request import Request +from sanic.response import HTTPResponse, StreamingHTTPResponse +from sanic.websocket import WebSocketConnection + + +ASGIScope = MutableMapping[str, Any] +ASGIMessage = MutableMapping[str, Any] +ASGISend = Callable[[ASGIMessage], Awaitable[None]] +ASGIReceive = Callable[[], Awaitable[ASGIMessage]] class MockTransport: - def __init__(self, scope): + def __init__(self, scope: ASGIScope) -> None: self.scope = scope - def get_extra_info(self, info): + def get_extra_info(self, info: str) -> Union[str, bool]: if info == "peername": return self.scope.get("server") elif info == "sslcontext": return self.scope.get("scheme") in ["https", "wss"] + def get_websocket_connection(self) -> WebSocketConnection: + return self._websocket_connection + + def create_websocket_connection( + self, + send: ASGISend, + receive: ASGIReceive, + ) -> WebSocketConnection: + self._websocket_connection = WebSocketConnection(send, receive) + return self._websocket_connection + class ASGIApp: - def __init__(self, sanic_app, scope, receive, send): - self.sanic_app = sanic_app - self.receive = receive - self.send = send + def __init__(self) -> None: + self.ws = None + + @classmethod + async def create(cls, sanic_app, scope: ASGIScope, receive: ASGIReceive, send: ASGISend) -> "ASGIApp": + instance = cls() + instance.sanic_app = sanic_app + instance.receive = receive + instance.send = send + url_bytes = scope.get("root_path", "") + scope["path"] url_bytes = url_bytes.encode("latin-1") url_bytes += scope["query_string"] @@ -28,18 +54,30 @@ class ASGIApp: for key, value in scope.get("headers", []) ] ) - version = scope["http_version"] - method = scope["method"] - self.request = Request( - url_bytes, - headers, - version, - method, - MockTransport(scope), - sanic_app, + + transport = MockTransport(scope) + + if scope["type"] == "http": + version = scope["http_version"] + method = scope["method"] + elif scope["type"] == "websocket": + version = "1.1" + method = "GET" + + instance.ws = transport.create_websocket_connection(send, receive) + await instance.ws.accept() + else: + pass + # TODO: + # - close connection + + instance.request = Request( + url_bytes, headers, version, method, transport, sanic_app ) - async def read_body(self): + return instance + + async def read_body(self) -> bytes: """ Read and return the entire body from an incoming ASGI message. """ @@ -53,15 +91,16 @@ class ASGIApp: return body - async def __call__(self): + async def __call__(self) -> None: """ Handle the incoming request. """ self.request.body = await self.read_body() handler = self.sanic_app.handle_request - await handler(self.request, None, self.stream_callback) + callback = None if self.ws else self.stream_callback + await handler(self.request, None, callback) - async def stream_callback(self, response): + async def stream_callback(self, response: HTTPResponse) -> None: """ Write the response. """ diff --git a/sanic/server.py b/sanic/server.py index a2038e3c..4f0bea37 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -708,6 +708,8 @@ def serve( if debug: loop.set_debug(debug) + app.asgi = False + connections = connections if connections is not None else set() server = partial( protocol, diff --git a/sanic/testing.py b/sanic/testing.py index 77dd274d..0e86db9b 100644 --- a/sanic/testing.py +++ b/sanic/testing.py @@ -1,16 +1,21 @@ +import typing +import types +import asyncio + from json import JSONDecodeError from socket import socket -from urllib.parse import unquote, urljoin, urlsplit +from urllib.parse import unquote, urlsplit import httpcore import requests_async as requests -import typing import websockets + from sanic.asgi import ASGIApp from sanic.exceptions import MethodNotSupported from sanic.log import logger from sanic.response import text + HOST = "127.0.0.1" PORT = 42101 @@ -314,7 +319,7 @@ class TestASGIApp(ASGIApp): async def app_call_with_return(self, scope, receive, send): - asgi_app = TestASGIApp(self, scope, receive, send) + asgi_app = await TestASGIApp.create(self, scope, receive, send) return await asgi_app() diff --git a/sanic/websocket.py b/sanic/websocket.py index e9279871..e4c693ff 100644 --- a/sanic/websocket.py +++ b/sanic/websocket.py @@ -1,3 +1,5 @@ +from typing import Any, Awaitable, Callable, MutableMapping, Optional, Union + from httptools import HttpParserUpgrade from websockets import ConnectionClosed # noqa from websockets import InvalidHandshake, WebSocketCommonProtocol, handshake @@ -6,6 +8,9 @@ from sanic.exceptions import InvalidUsage from sanic.server import HttpProtocol +ASIMessage = MutableMapping[str, Any] + + class WebSocketProtocol(HttpProtocol): def __init__( self, @@ -19,6 +24,7 @@ class WebSocketProtocol(HttpProtocol): ): super().__init__(*args, **kwargs) self.websocket = None + self.app = None self.websocket_timeout = websocket_timeout self.websocket_max_size = websocket_max_size self.websocket_max_queue = websocket_max_queue @@ -103,3 +109,46 @@ class WebSocketProtocol(HttpProtocol): self.websocket.connection_made(request.transport) self.websocket.connection_open() return self.websocket + + +class WebSocketConnection: + + # TODO + # - Implement ping/pong + + def __init__( + self, + send: Callable[[ASIMessage], Awaitable[None]], + receive: Callable[[], Awaitable[ASIMessage]], + ) -> None: + self._send = send + self._receive = receive + + async def send(self, data: Union[str, bytes], *args, **kwargs) -> None: + message = {"type": "websocket.send"} + + try: + data.decode() + except AttributeError: + message.update({"text": str(data)}) + else: + message.update({"bytes": data}) + + await self._send(message) + + async def recv(self, *args, **kwargs) -> Optional[str]: + message = await self._receive() + + if message["type"] == "websocket.receive": + return message["text"] + elif message["type"] == "websocket.disconnect": + pass + # await self._send({ + # "type": "websocket.close" + # }) + + async def accept(self) -> None: + await self._send({"type": "websocket.accept", "subprotocol": ""}) + + async def close(self) -> None: + pass diff --git a/tests/test_asgi.py b/tests/test_asgi.py index fcda40de..d51b4f2f 100644 --- a/tests/test_asgi.py +++ b/tests/test_asgi.py @@ -1,956 +1,5 @@ -import pytest - from sanic.testing import SanicASGITestClient -from sanic.response import text def asgi_client_instantiation(app): assert isinstance(app.asgi_client, SanicASGITestClient) - - -# import logging -# import os -# import ssl - -# from json import dumps as json_dumps -# from json import loads as json_loads -# from urllib.parse import urlparse - -# import pytest - -# from sanic import Blueprint, Sanic -# from sanic.exceptions import ServerError -# from sanic.request import DEFAULT_HTTP_CONTENT_TYPE, RequestParameters -# from sanic.response import json, text -# from sanic.testing import HOST, PORT - - -# ------------------------------------------------------------ # -# GET - Adapted from test_requests.py -# ------------------------------------------------------------ # - - -@pytest.mark.asyncio -async def test_basic_request(app): - @app.route("/") - def handler(request): - return text("Hello") - - _, response = await app.asgi_client.get("/") - assert response.text == "Hello" - - -@pytest.mark.asyncio -async def test_ip(app): - @app.route("/") - def handler(request): - return text("{}".format(request.ip)) - - request, response = await app.asgi_client.get("/") - - assert response.text == "mockserver" - - -@pytest.mark.asyncio -def test_text(app): - @app.route("/") - async def handler(request): - return text("Hello") - - request, response = await app.asgi_client.get("/") - - assert response.text == "Hello" - - -# def test_headers(app): -# @app.route("/") -# async def handler(request): -# headers = {"spam": "great"} -# return text("Hello", headers=headers) - -# request, response = app.asgi_client.get("/") - -# assert response.headers.get("spam") == "great" - - -# def test_non_str_headers(app): -# @app.route("/") -# async def handler(request): -# headers = {"answer": 42} -# return text("Hello", headers=headers) - -# request, response = app.asgi_client.get("/") - -# assert response.headers.get("answer") == "42" - - -# def test_invalid_response(app): -# @app.exception(ServerError) -# def handler_exception(request, exception): -# return text("Internal Server Error.", 500) - -# @app.route("/") -# async def handler(request): -# return "This should fail" - -# request, response = app.asgi_client.get("/") -# assert response.status == 500 -# assert response.text == "Internal Server Error." - - -# def test_json(app): -# @app.route("/") -# async def handler(request): -# return json({"test": True}) - -# request, response = app.asgi_client.get("/") - -# results = json_loads(response.text) - -# assert results.get("test") is True - - -# def test_empty_json(app): -# @app.route("/") -# async def handler(request): -# assert request.json is None -# return json(request.json) - -# request, response = app.asgi_client.get("/") -# assert response.status == 200 -# assert response.text == "null" - - -# def test_invalid_json(app): -# @app.route("/") -# async def handler(request): -# return json(request.json) - -# data = "I am not json" -# request, response = app.asgi_client.get("/", data=data) - -# assert response.status == 400 - - -# def test_query_string(app): -# @app.route("/") -# async def handler(request): -# return text("OK") - -# request, response = app.asgi_client.get( -# "/", params=[("test1", "1"), ("test2", "false"), ("test2", "true")] -# ) - -# assert request.args.get("test1") == "1" -# assert request.args.get("test2") == "false" -# assert request.args.getlist("test2") == ["false", "true"] -# assert request.args.getlist("test1") == ["1"] -# assert request.args.get("test3", default="My value") == "My value" - - -# def test_uri_template(app): -# @app.route("/foo//bar/") -# async def handler(request, id, name): -# return text("OK") - -# request, response = app.asgi_client.get("/foo/123/bar/baz") -# assert request.uri_template == "/foo//bar/" - - -# def test_token(app): -# @app.route("/") -# async def handler(request): -# return text("OK") - -# # uuid4 generated token. -# token = "a1d895e0-553a-421a-8e22-5ff8ecb48cbf" -# headers = { -# "content-type": "application/json", -# "Authorization": "{}".format(token), -# } - -# request, response = app.asgi_client.get("/", headers=headers) - -# assert request.token == token - -# token = "a1d895e0-553a-421a-8e22-5ff8ecb48cbf" -# headers = { -# "content-type": "application/json", -# "Authorization": "Token {}".format(token), -# } - -# request, response = app.asgi_client.get("/", headers=headers) - -# assert request.token == token - -# token = "a1d895e0-553a-421a-8e22-5ff8ecb48cbf" -# headers = { -# "content-type": "application/json", -# "Authorization": "Bearer {}".format(token), -# } - -# request, response = app.asgi_client.get("/", headers=headers) - -# assert request.token == token - -# # no Authorization headers -# headers = {"content-type": "application/json"} - -# request, response = app.asgi_client.get("/", headers=headers) - -# assert request.token is None - - -# def test_content_type(app): -# @app.route("/") -# async def handler(request): -# return text(request.content_type) - -# request, response = app.asgi_client.get("/") -# assert request.content_type == DEFAULT_HTTP_CONTENT_TYPE -# assert response.text == DEFAULT_HTTP_CONTENT_TYPE - -# headers = {"content-type": "application/json"} -# request, response = app.asgi_client.get("/", headers=headers) -# assert request.content_type == "application/json" -# assert response.text == "application/json" - - -# def test_remote_addr_with_two_proxies(app): -# app.config.PROXIES_COUNT = 2 - -# @app.route("/") -# async def handler(request): -# return text(request.remote_addr) - -# headers = {"X-Real-IP": "127.0.0.2", "X-Forwarded-For": "127.0.1.1"} -# request, response = app.asgi_client.get("/", headers=headers) -# assert request.remote_addr == "127.0.0.2" -# assert response.text == "127.0.0.2" - -# headers = {"X-Forwarded-For": "127.0.1.1"} -# request, response = app.asgi_client.get("/", headers=headers) -# assert request.remote_addr == "" -# assert response.text == "" - -# headers = {"X-Forwarded-For": "127.0.0.1, 127.0.1.2"} -# request, response = app.asgi_client.get("/", headers=headers) -# assert request.remote_addr == "127.0.0.1" -# assert response.text == "127.0.0.1" - -# request, response = app.asgi_client.get("/") -# assert request.remote_addr == "" -# assert response.text == "" - -# headers = {"X-Forwarded-For": "127.0.0.1, , ,,127.0.1.2"} -# request, response = app.asgi_client.get("/", headers=headers) -# assert request.remote_addr == "127.0.0.1" -# assert response.text == "127.0.0.1" - -# headers = { -# "X-Forwarded-For": ", 127.0.2.2, , ,127.0.0.1, , ,,127.0.1.2" -# } -# request, response = app.asgi_client.get("/", headers=headers) -# assert request.remote_addr == "127.0.0.1" -# assert response.text == "127.0.0.1" - - -# def test_remote_addr_with_infinite_number_of_proxies(app): -# app.config.PROXIES_COUNT = -1 - -# @app.route("/") -# async def handler(request): -# return text(request.remote_addr) - -# headers = {"X-Real-IP": "127.0.0.2", "X-Forwarded-For": "127.0.1.1"} -# request, response = app.asgi_client.get("/", headers=headers) -# assert request.remote_addr == "127.0.0.2" -# assert response.text == "127.0.0.2" - -# headers = {"X-Forwarded-For": "127.0.1.1"} -# request, response = app.asgi_client.get("/", headers=headers) -# assert request.remote_addr == "127.0.1.1" -# assert response.text == "127.0.1.1" - -# headers = { -# "X-Forwarded-For": "127.0.0.5, 127.0.0.4, 127.0.0.3, 127.0.0.2, 127.0.0.1" -# } -# request, response = app.asgi_client.get("/", headers=headers) -# assert request.remote_addr == "127.0.0.5" -# assert response.text == "127.0.0.5" - - -# def test_remote_addr_without_proxy(app): -# app.config.PROXIES_COUNT = 0 - -# @app.route("/") -# async def handler(request): -# return text(request.remote_addr) - -# headers = {"X-Real-IP": "127.0.0.2", "X-Forwarded-For": "127.0.1.1"} -# request, response = app.asgi_client.get("/", headers=headers) -# assert request.remote_addr == "" -# assert response.text == "" - -# headers = {"X-Forwarded-For": "127.0.1.1"} -# request, response = app.asgi_client.get("/", headers=headers) -# assert request.remote_addr == "" -# assert response.text == "" - -# headers = {"X-Forwarded-For": "127.0.0.1, 127.0.1.2"} -# request, response = app.asgi_client.get("/", headers=headers) -# assert request.remote_addr == "" -# assert response.text == "" - - -# def test_remote_addr_custom_headers(app): -# app.config.PROXIES_COUNT = 1 -# app.config.REAL_IP_HEADER = "Client-IP" -# app.config.FORWARDED_FOR_HEADER = "Forwarded" - -# @app.route("/") -# async def handler(request): -# return text(request.remote_addr) - -# headers = {"X-Real-IP": "127.0.0.2", "Forwarded": "127.0.1.1"} -# request, response = app.asgi_client.get("/", headers=headers) -# assert request.remote_addr == "127.0.1.1" -# assert response.text == "127.0.1.1" - -# headers = {"X-Forwarded-For": "127.0.1.1"} -# request, response = app.asgi_client.get("/", headers=headers) -# assert request.remote_addr == "" -# assert response.text == "" - -# headers = {"Client-IP": "127.0.0.2", "Forwarded": "127.0.1.1"} -# request, response = app.asgi_client.get("/", headers=headers) -# assert request.remote_addr == "127.0.0.2" -# assert response.text == "127.0.0.2" - - -# def test_match_info(app): -# @app.route("/api/v1/user//") -# async def handler(request, user_id): -# return json(request.match_info) - -# request, response = app.asgi_client.get("/api/v1/user/sanic_user/") - -# assert request.match_info == {"user_id": "sanic_user"} -# assert json_loads(response.text) == {"user_id": "sanic_user"} - - -# # ------------------------------------------------------------ # -# # POST -# # ------------------------------------------------------------ # - - -# def test_post_json(app): -# @app.route("/", methods=["POST"]) -# async def handler(request): -# return text("OK") - -# payload = {"test": "OK"} -# headers = {"content-type": "application/json"} - -# request, response = app.asgi_client.post( -# "/", data=json_dumps(payload), headers=headers -# ) - -# assert request.json.get("test") == "OK" -# assert request.json.get("test") == "OK" # for request.parsed_json -# assert response.text == "OK" - - -# def test_post_form_urlencoded(app): -# @app.route("/", methods=["POST"]) -# async def handler(request): -# return text("OK") - -# payload = "test=OK" -# headers = {"content-type": "application/x-www-form-urlencoded"} - -# request, response = app.asgi_client.post( -# "/", data=payload, headers=headers -# ) - -# assert request.form.get("test") == "OK" -# assert request.form.get("test") == "OK" # For request.parsed_form - - -# @pytest.mark.parametrize( -# "payload", -# [ -# "------sanic\r\n" -# 'Content-Disposition: form-data; name="test"\r\n' -# "\r\n" -# "OK\r\n" -# "------sanic--\r\n", -# "------sanic\r\n" -# 'content-disposition: form-data; name="test"\r\n' -# "\r\n" -# "OK\r\n" -# "------sanic--\r\n", -# ], -# ) -# def test_post_form_multipart_form_data(app, payload): -# @app.route("/", methods=["POST"]) -# async def handler(request): -# return text("OK") - -# headers = {"content-type": "multipart/form-data; boundary=----sanic"} - -# request, response = app.asgi_client.post(data=payload, headers=headers) - -# assert request.form.get("test") == "OK" - - -# @pytest.mark.parametrize( -# "path,query,expected_url", -# [ -# ("/foo", "", "http://{}:{}/foo"), -# ("/bar/baz", "", "http://{}:{}/bar/baz"), -# ("/moo/boo", "arg1=val1", "http://{}:{}/moo/boo?arg1=val1"), -# ], -# ) -# def test_url_attributes_no_ssl(app, path, query, expected_url): -# async def handler(request): -# return text("OK") - -# app.add_route(handler, path) - -# request, response = app.asgi_client.get(path + "?{}".format(query)) -# assert request.url == expected_url.format(HOST, PORT) - -# parsed = urlparse(request.url) - -# assert parsed.scheme == request.scheme -# assert parsed.path == request.path -# assert parsed.query == request.query_string -# assert parsed.netloc == request.host - - -# @pytest.mark.parametrize( -# "path,query,expected_url", -# [ -# ("/foo", "", "https://{}:{}/foo"), -# ("/bar/baz", "", "https://{}:{}/bar/baz"), -# ("/moo/boo", "arg1=val1", "https://{}:{}/moo/boo?arg1=val1"), -# ], -# ) -# def test_url_attributes_with_ssl_context(app, path, query, expected_url): -# current_dir = os.path.dirname(os.path.realpath(__file__)) -# context = ssl.create_default_context(purpose=ssl.Purpose.CLIENT_AUTH) -# context.load_cert_chain( -# os.path.join(current_dir, "certs/selfsigned.cert"), -# keyfile=os.path.join(current_dir, "certs/selfsigned.key"), -# ) - -# async def handler(request): -# return text("OK") - -# app.add_route(handler, path) - -# request, response = app.asgi_client.get( -# "https://{}:{}".format(HOST, PORT) + path + "?{}".format(query), -# server_kwargs={"ssl": context}, -# ) -# assert request.url == expected_url.format(HOST, PORT) - -# parsed = urlparse(request.url) - -# assert parsed.scheme == request.scheme -# assert parsed.path == request.path -# assert parsed.query == request.query_string -# assert parsed.netloc == request.host - - -# @pytest.mark.parametrize( -# "path,query,expected_url", -# [ -# ("/foo", "", "https://{}:{}/foo"), -# ("/bar/baz", "", "https://{}:{}/bar/baz"), -# ("/moo/boo", "arg1=val1", "https://{}:{}/moo/boo?arg1=val1"), -# ], -# ) -# def test_url_attributes_with_ssl_dict(app, path, query, expected_url): - -# current_dir = os.path.dirname(os.path.realpath(__file__)) -# ssl_cert = os.path.join(current_dir, "certs/selfsigned.cert") -# ssl_key = os.path.join(current_dir, "certs/selfsigned.key") - -# ssl_dict = {"cert": ssl_cert, "key": ssl_key} - -# async def handler(request): -# return text("OK") - -# app.add_route(handler, path) - -# request, response = app.asgi_client.get( -# "https://{}:{}".format(HOST, PORT) + path + "?{}".format(query), -# server_kwargs={"ssl": ssl_dict}, -# ) -# assert request.url == expected_url.format(HOST, PORT) - -# parsed = urlparse(request.url) - -# assert parsed.scheme == request.scheme -# assert parsed.path == request.path -# assert parsed.query == request.query_string -# assert parsed.netloc == request.host - - -# def test_invalid_ssl_dict(app): -# @app.get("/test") -# async def handler(request): -# return text("ssl test") - -# ssl_dict = {"cert": None, "key": None} - -# with pytest.raises(ValueError) as excinfo: -# request, response = app.asgi_client.get( -# "/test", server_kwargs={"ssl": ssl_dict} -# ) - -# assert str(excinfo.value) == "SSLContext or certificate and key required." - - -# def test_form_with_multiple_values(app): -# @app.route("/", methods=["POST"]) -# async def handler(request): -# return text("OK") - -# payload = "selectedItems=v1&selectedItems=v2&selectedItems=v3" - -# headers = {"content-type": "application/x-www-form-urlencoded"} - -# request, response = app.asgi_client.post( -# "/", data=payload, headers=headers -# ) - -# assert request.form.getlist("selectedItems") == ["v1", "v2", "v3"] - - -# def test_request_string_representation(app): -# @app.route("/", methods=["GET"]) -# async def get(request): -# return text("OK") - -# request, _ = app.asgi_client.get("/") -# assert repr(request) == "" - - -# @pytest.mark.parametrize( -# "payload,filename", -# [ -# ( -# "------sanic\r\n" -# 'Content-Disposition: form-data; filename="filename"; name="test"\r\n' -# "\r\n" -# "OK\r\n" -# "------sanic--\r\n", -# "filename", -# ), -# ( -# "------sanic\r\n" -# 'content-disposition: form-data; filename="filename"; name="test"\r\n' -# "\r\n" -# 'content-type: application/json; {"field": "value"}\r\n' -# "------sanic--\r\n", -# "filename", -# ), -# ( -# "------sanic\r\n" -# 'Content-Disposition: form-data; filename=""; name="test"\r\n' -# "\r\n" -# "OK\r\n" -# "------sanic--\r\n", -# "", -# ), -# ( -# "------sanic\r\n" -# 'content-disposition: form-data; filename=""; name="test"\r\n' -# "\r\n" -# 'content-type: application/json; {"field": "value"}\r\n' -# "------sanic--\r\n", -# "", -# ), -# ( -# "------sanic\r\n" -# 'Content-Disposition: form-data; filename*="utf-8\'\'filename_%C2%A0_test"; name="test"\r\n' -# "\r\n" -# "OK\r\n" -# "------sanic--\r\n", -# "filename_\u00A0_test", -# ), -# ( -# "------sanic\r\n" -# 'content-disposition: form-data; filename*="utf-8\'\'filename_%C2%A0_test"; name="test"\r\n' -# "\r\n" -# 'content-type: application/json; {"field": "value"}\r\n' -# "------sanic--\r\n", -# "filename_\u00A0_test", -# ), -# ], -# ) -# def test_request_multipart_files(app, payload, filename): -# @app.route("/", methods=["POST"]) -# async def post(request): -# return text("OK") - -# headers = {"content-type": "multipart/form-data; boundary=----sanic"} - -# request, _ = app.asgi_client.post(data=payload, headers=headers) -# assert request.files.get("test").name == filename - - -# def test_request_multipart_file_with_json_content_type(app): -# @app.route("/", methods=["POST"]) -# async def post(request): -# return text("OK") - -# payload = ( -# "------sanic\r\n" -# 'Content-Disposition: form-data; name="file"; filename="test.json"\r\n' -# "Content-Type: application/json\r\n" -# "Content-Length: 0" -# "\r\n" -# "\r\n" -# "------sanic--" -# ) - -# headers = {"content-type": "multipart/form-data; boundary=------sanic"} - -# request, _ = app.asgi_client.post(data=payload, headers=headers) -# assert request.files.get("file").type == "application/json" - - -# def test_request_multipart_file_without_field_name(app, caplog): -# @app.route("/", methods=["POST"]) -# async def post(request): -# return text("OK") - -# payload = ( -# '------sanic\r\nContent-Disposition: form-data; filename="test.json"' -# "\r\nContent-Type: application/json\r\n\r\n\r\n------sanic--" -# ) - -# headers = {"content-type": "multipart/form-data; boundary=------sanic"} - -# request, _ = app.asgi_client.post( -# data=payload, headers=headers, debug=True -# ) -# with caplog.at_level(logging.DEBUG): -# request.form - -# assert caplog.record_tuples[-1] == ( -# "sanic.root", -# logging.DEBUG, -# "Form-data field does not have a 'name' parameter " -# "in the Content-Disposition header", -# ) - - -# def test_request_multipart_file_duplicate_filed_name(app): -# @app.route("/", methods=["POST"]) -# async def post(request): -# return text("OK") - -# payload = ( -# "--e73ffaa8b1b2472b8ec848de833cb05b\r\n" -# 'Content-Disposition: form-data; name="file"\r\n' -# "Content-Type: application/octet-stream\r\n" -# "Content-Length: 15\r\n" -# "\r\n" -# '{"test":"json"}\r\n' -# "--e73ffaa8b1b2472b8ec848de833cb05b\r\n" -# 'Content-Disposition: form-data; name="file"\r\n' -# "Content-Type: application/octet-stream\r\n" -# "Content-Length: 15\r\n" -# "\r\n" -# '{"test":"json2"}\r\n' -# "--e73ffaa8b1b2472b8ec848de833cb05b--\r\n" -# ) - -# headers = { -# "Content-Type": "multipart/form-data; boundary=e73ffaa8b1b2472b8ec848de833cb05b" -# } - -# request, _ = app.asgi_client.post( -# data=payload, headers=headers, debug=True -# ) -# assert request.form.getlist("file") == [ -# '{"test":"json"}', -# '{"test":"json2"}', -# ] - - -# def test_request_multipart_with_multiple_files_and_type(app): -# @app.route("/", methods=["POST"]) -# async def post(request): -# return text("OK") - -# payload = ( -# '------sanic\r\nContent-Disposition: form-data; name="file"; filename="test.json"' -# "\r\nContent-Type: application/json\r\n\r\n\r\n" -# '------sanic\r\nContent-Disposition: form-data; name="file"; filename="some_file.pdf"\r\n' -# "Content-Type: application/pdf\r\n\r\n\r\n------sanic--" -# ) -# headers = {"content-type": "multipart/form-data; boundary=------sanic"} - -# request, _ = app.asgi_client.post(data=payload, headers=headers) -# assert len(request.files.getlist("file")) == 2 -# assert request.files.getlist("file")[0].type == "application/json" -# assert request.files.getlist("file")[1].type == "application/pdf" - - -# def test_request_repr(app): -# @app.get("/") -# def handler(request): -# return text("pass") - -# request, response = app.asgi_client.get("/") -# assert repr(request) == "" - -# request.method = None -# assert repr(request) == "" - - -# def test_request_bool(app): -# @app.get("/") -# def handler(request): -# return text("pass") - -# request, response = app.asgi_client.get("/") -# assert bool(request) - -# request.transport = False -# assert not bool(request) - - -# def test_request_parsing_form_failed(app, caplog): -# @app.route("/", methods=["POST"]) -# async def handler(request): -# return text("OK") - -# payload = "test=OK" -# headers = {"content-type": "multipart/form-data"} - -# request, response = app.asgi_client.post( -# "/", data=payload, headers=headers -# ) - -# with caplog.at_level(logging.ERROR): -# request.form - -# assert caplog.record_tuples[-1] == ( -# "sanic.error", -# logging.ERROR, -# "Failed when parsing form", -# ) - - -# def test_request_args_no_query_string(app): -# @app.get("/") -# def handler(request): -# return text("pass") - -# request, response = app.asgi_client.get("/") - -# assert request.args == {} - - -# def test_request_raw_args(app): - -# params = {"test": "OK"} - -# @app.get("/") -# def handler(request): -# return text("pass") - -# request, response = app.asgi_client.get("/", params=params) - -# assert request.raw_args == params - - -# def test_request_query_args(app): -# # test multiple params with the same key -# params = [("test", "value1"), ("test", "value2")] - -# @app.get("/") -# def handler(request): -# return text("pass") - -# request, response = app.asgi_client.get("/", params=params) - -# assert request.query_args == params - -# # test cached value -# assert ( -# request.parsed_not_grouped_args[(False, False, "utf-8", "replace")] -# == request.query_args -# ) - -# # test params directly in the url -# request, response = app.asgi_client.get("/?test=value1&test=value2") - -# assert request.query_args == params - -# # test unique params -# params = [("test1", "value1"), ("test2", "value2")] - -# request, response = app.asgi_client.get("/", params=params) - -# assert request.query_args == params - -# # test no params -# request, response = app.asgi_client.get("/") - -# assert not request.query_args - - -# def test_request_query_args_custom_parsing(app): -# @app.get("/") -# def handler(request): -# return text("pass") - -# request, response = app.asgi_client.get( -# "/?test1=value1&test2=&test3=value3" -# ) - -# assert request.get_query_args(keep_blank_values=True) == [ -# ("test1", "value1"), -# ("test2", ""), -# ("test3", "value3"), -# ] -# assert request.query_args == [("test1", "value1"), ("test3", "value3")] -# assert request.get_query_args(keep_blank_values=False) == [ -# ("test1", "value1"), -# ("test3", "value3"), -# ] - -# assert request.get_args(keep_blank_values=True) == RequestParameters( -# {"test1": ["value1"], "test2": [""], "test3": ["value3"]} -# ) - -# assert request.args == RequestParameters( -# {"test1": ["value1"], "test3": ["value3"]} -# ) - -# assert request.get_args(keep_blank_values=False) == RequestParameters( -# {"test1": ["value1"], "test3": ["value3"]} -# ) - - -# def test_request_cookies(app): - -# cookies = {"test": "OK"} - -# @app.get("/") -# def handler(request): -# return text("OK") - -# request, response = app.asgi_client.get("/", cookies=cookies) - -# assert request.cookies == cookies -# assert request.cookies == cookies # For request._cookies - - -# def test_request_cookies_without_cookies(app): -# @app.get("/") -# def handler(request): -# return text("OK") - -# request, response = app.asgi_client.get("/") - -# assert request.cookies == {} - - -# def test_request_port(app): -# @app.get("/") -# def handler(request): -# return text("OK") - -# request, response = app.asgi_client.get("/") - -# port = request.port -# assert isinstance(port, int) - -# delattr(request, "_socket") -# delattr(request, "_port") - -# port = request.port -# assert isinstance(port, int) -# assert hasattr(request, "_socket") -# assert hasattr(request, "_port") - - -# def test_request_socket(app): -# @app.get("/") -# def handler(request): -# return text("OK") - -# request, response = app.asgi_client.get("/") - -# socket = request.socket -# assert isinstance(socket, tuple) - -# ip = socket[0] -# port = socket[1] - -# assert ip == request.ip -# assert port == request.port - -# delattr(request, "_socket") - -# socket = request.socket -# assert isinstance(socket, tuple) -# assert hasattr(request, "_socket") - - -# def test_request_form_invalid_content_type(app): -# @app.route("/", methods=["POST"]) -# async def post(request): -# return text("OK") - -# request, response = app.asgi_client.post("/", json={"test": "OK"}) - -# assert request.form == {} - - -# def test_endpoint_basic(): -# app = Sanic() - -# @app.route("/") -# def my_unique_handler(request): -# return text("Hello") - -# request, response = app.asgi_client.get("/") - -# assert request.endpoint == "test_requests.my_unique_handler" - - -# def test_endpoint_named_app(): -# app = Sanic("named") - -# @app.route("/") -# def my_unique_handler(request): -# return text("Hello") - -# request, response = app.asgi_client.get("/") - -# assert request.endpoint == "named.my_unique_handler" - - -# def test_endpoint_blueprint(): -# bp = Blueprint("my_blueprint", url_prefix="/bp") - -# @bp.route("/") -# async def bp_root(request): -# return text("Hello") - -# app = Sanic("named") -# app.blueprint(bp) - -# request, response = app.asgi_client.get("/bp") - -# assert request.endpoint == "named.my_blueprint.bp_root" diff --git a/tests/test_keep_alive_timeout.py b/tests/test_keep_alive_timeout.py index 1d6de63e..603b4fe8 100644 --- a/tests/test_keep_alive_timeout.py +++ b/tests/test_keep_alive_timeout.py @@ -19,10 +19,6 @@ from sanic.testing import HOST, PORT, SanicTestClient # import traceback - - - - CONFIG_FOR_TESTS = {"KEEP_ALIVE_TIMEOUT": 2, "KEEP_ALIVE": True} old_conn = None diff --git a/tests/test_requests.py b/tests/test_requests.py index 2d854a73..64a919e8 100644 --- a/tests/test_requests.py +++ b/tests/test_requests.py @@ -30,6 +30,17 @@ def test_sync(app): assert response.text == "Hello" +@pytest.mark.asyncio +async def test_sync_asgi(app): + @app.route("/") + def handler(request): + return text("Hello") + + request, response = await app.asgi_client.get("/") + + assert response.text == "Hello" + + def test_ip(app): @app.route("/") def handler(request): @@ -40,6 +51,17 @@ def test_ip(app): assert response.text == "127.0.0.1" +@pytest.mark.asyncio +async def test_ip_asgi(app): + @app.route("/") + def handler(request): + return text("{}".format(request.ip)) + + request, response = await app.asgi_client.get("/") + + assert response.text == "mockserver" + + def test_text(app): @app.route("/") async def handler(request): diff --git a/tests/test_response.py b/tests/test_response.py index 4e305191..c47dd1db 100644 --- a/tests/test_response.py +++ b/tests/test_response.py @@ -231,9 +231,7 @@ def test_chunked_streaming_returns_correct_content(streaming_app): assert response.text == "foo,bar" -def test_non_chunked_streaming_adds_correct_headers( - non_chunked_streaming_app -): +def test_non_chunked_streaming_adds_correct_headers(non_chunked_streaming_app): request, response = non_chunked_streaming_app.test_client.get("/") assert "Transfer-Encoding" not in response.headers assert response.headers["Content-Type"] == "text/csv"