from __future__ import annotations from typing import Any from typing import AnyStr from typing import cast from typing import Literal from typing import overload from typing import TYPE_CHECKING from urllib.parse import unquote from urllib.parse import urlencode from hypercorn.typing import HTTPScope from hypercorn.typing import Scope from hypercorn.typing import WebsocketScope from werkzeug.datastructures import Authorization from werkzeug.datastructures import Headers from werkzeug.sansio.multipart import Data from werkzeug.sansio.multipart import Epilogue from werkzeug.sansio.multipart import Field from werkzeug.sansio.multipart import File from werkzeug.sansio.multipart import MultipartEncoder from werkzeug.sansio.multipart import Preamble from werkzeug.urls import iri_to_uri from ..datastructures import FileStorage from ..json import dumps from ..utils import encode_headers if TYPE_CHECKING: from ..app import Quart # noqa sentinel = object() def make_test_headers_path_and_query_string( app: Quart, path: str, headers: dict | Headers | None = None, query_string: dict | None = None, auth: Authorization | tuple[str, str] | None = None, subdomain: str | None = None, ) -> tuple[Headers, str, bytes]: """Make the headers and path with defaults for testing. Arguments: app: The application to test against. path: The path to request. If the query_string argument is not defined this argument will be partitioned on a '?' with the following part being considered the query_string. headers: Initial headers to send. query_string: To send as a dictionary, alternatively the query_string can be determined from the path. """ if headers is None: headers = Headers() elif isinstance(headers, Headers): headers = headers elif headers is not None: headers = Headers(headers) if auth is not None: if isinstance(auth, tuple): auth = Authorization("basic", {"username": auth[0], "password": auth[1]}) headers.setdefault("Authorization", auth.to_header()) headers.setdefault("User-Agent", "Quart") host = app.config["SERVER_NAME"] or "localhost" if subdomain is not None: host = f"{subdomain}.{host}" headers.setdefault("host", host) if "?" in path and query_string is not None: raise ValueError("Query string is defined in the path and as an argument") if query_string is None: path, _, query_string_raw = path.partition("?") else: query_string_raw = urlencode(query_string, doseq=True) query_string_bytes = query_string_raw.encode("ascii") return headers, unquote(path), query_string_bytes def make_test_body_with_headers( *, data: AnyStr | None = None, form: dict | None = None, files: dict[str, FileStorage] | None = None, json: Any = sentinel, app: Quart | None = None, ) -> tuple[bytes, Headers]: """Make the body bytes with associated headers. Arguments: data: Raw data to send in the request body. form: Key value paired data to send form encoded in the request body. files: Key FileStorage paired data to send as file encoded in the request body. json: Data to send json encoded in the request body. """ if [json is not sentinel, form is not None, data is not None].count(True) > 1: raise ValueError( "Quart test args 'json', 'form', and 'data' are mutually exclusive" ) if [json is not sentinel, files is not None, data is not None].count(True) > 1: raise ValueError( "Quart test args 'files', 'json', and 'data' are mutually exclusive" ) request_data = b"" headers = Headers() if isinstance(data, str): request_data = data.encode("utf-8") elif isinstance(data, bytes): request_data = data if json is not sentinel: request_data = dumps(json).encode("utf-8") headers["Content-Type"] = "application/json" elif files is not None: boundary = "----QuartBoundary" headers["Content-Type"] = f"multipart/form-data; boundary={boundary}" encoder = MultipartEncoder(boundary.encode()) request_data += encoder.send_event(Preamble(data=b"")) for key, file_storage in files.items(): request_data += encoder.send_event( File( name=key, filename=file_storage.filename, headers=file_storage.headers, ) ) chunk = file_storage.read(16384) while chunk != b"": request_data += encoder.send_event(Data(data=chunk, more_data=True)) chunk = file_storage.read(16384) request_data += encoder.send_event(Data(data=b"", more_data=False)) if form is not None: for key, value in form.items(): request_data += encoder.send_event(Field(name=key, headers=Headers())) request_data += encoder.send_event( Data(data=value.encode("utf-8"), more_data=False) ) request_data += encoder.send_event(Epilogue(data=b"")) elif form is not None: request_data = urlencode(form).encode("utf-8") headers["Content-Type"] = "application/x-www-form-urlencoded" return request_data, headers @overload def make_test_scope( type_: Literal["http"], path: str, method: str, headers: Headers, query_string: bytes, scheme: str, root_path: str, http_version: str, scope_base: dict | None, *, _preserve_context: bool = False, ) -> HTTPScope: ... @overload def make_test_scope( type_: Literal["websocket"], path: str, method: str, headers: Headers, query_string: bytes, scheme: str, root_path: str, http_version: str, scope_base: dict | None, *, _preserve_context: bool = False, ) -> WebsocketScope: ... def make_test_scope( type_: str, path: str, method: str, headers: Headers, query_string: bytes, scheme: str, root_path: str, http_version: str, scope_base: dict | None, *, _preserve_context: bool = False, ) -> Scope: scope = { "type": type_, "http_version": http_version, "asgi": {"spec_version": "2.1"}, "method": method, "scheme": scheme, "path": path, "raw_path": iri_to_uri(path).encode("ascii"), "query_string": query_string, "root_path": root_path, "headers": encode_headers(headers), "extensions": {}, "_quart._preserve_context": _preserve_context, } if scope_base is not None: scope.update(scope_base) if type_ == "http" and http_version in {"2", "3"}: scope["extensions"] = {"http.response.push": {}} elif type_ == "websocket": scope["extensions"] = {"websocket.http.response": {}} return cast(Scope, scope) async def no_op_push(path: str, headers: Headers) -> None: """A push promise sender that does nothing. This is best used when creating Request instances for testing outside of the QuartClient. The Request instance must know what to do with push promises, and this gives it the option of doing nothing. """ pass