|
|
|
|
|
|
|
"""Testing utils.""" |
|
from __future__ import annotations |
|
|
|
import json |
|
import os |
|
import sys |
|
from http.cookies import SimpleCookie |
|
from pathlib import Path |
|
from urllib.parse import parse_qs, urlparse |
|
|
|
import tornado.httpclient |
|
import tornado.web |
|
from openapi_core import V30RequestValidator, V30ResponseValidator |
|
from openapi_core.spec.paths import Spec |
|
from openapi_core.validation.request.datatypes import RequestParameters |
|
from tornado.httpclient import HTTPRequest, HTTPResponse |
|
from werkzeug.datastructures import Headers, ImmutableMultiDict |
|
|
|
from jupyterlab_server.spec import get_openapi_spec |
|
|
|
HERE = Path(os.path.dirname(__file__)).resolve() |
|
|
|
with open(HERE / "test_data" / "app-settings" / "overrides.json", encoding="utf-8") as fid: |
|
big_unicode_string = json.load(fid)["@jupyterlab/unicode-extension:plugin"]["comment"] |
|
|
|
|
|
class TornadoOpenAPIRequest: |
|
""" |
|
Converts a torando request to an OpenAPI one |
|
""" |
|
|
|
def __init__(self, request: HTTPRequest, spec: Spec): |
|
"""Initialize the request.""" |
|
self.request = request |
|
self.spec = spec |
|
if request.url is None: |
|
msg = "Request URL is missing" |
|
raise RuntimeError(msg) |
|
self._url_parsed = urlparse(request.url) |
|
|
|
cookie: SimpleCookie = SimpleCookie() |
|
cookie.load(request.headers.get("Set-Cookie", "")) |
|
cookies = {} |
|
for key, morsel in cookie.items(): |
|
cookies[key] = morsel.value |
|
|
|
|
|
o = urlparse(request.url) |
|
|
|
|
|
path: dict = {} |
|
|
|
self.parameters = RequestParameters( |
|
query=ImmutableMultiDict(parse_qs(o.query)), |
|
header=dict(request.headers), |
|
cookie=ImmutableMultiDict(cookies), |
|
path=path, |
|
) |
|
|
|
@property |
|
def content_type(self) -> str: |
|
return "application/json" |
|
|
|
@property |
|
def host_url(self) -> str: |
|
url = self.request.url |
|
return url[: url.index("/lab")] |
|
|
|
@property |
|
def path(self) -> str: |
|
|
|
|
|
|
|
url = None |
|
o = urlparse(self.request.url) |
|
for path_ in self.spec["paths"]: |
|
if url: |
|
continue |
|
has_arg = "{" in path_ |
|
path = path_[: path_.index("{")] if has_arg else path_ |
|
if path in o.path: |
|
u = o.path[o.path.index(path) :] |
|
if not has_arg and len(u) == len(path): |
|
url = u |
|
if has_arg and not u.endswith("/"): |
|
url = u[: len(path)] + r"foo" |
|
|
|
if url is None: |
|
msg = f"Could not find matching pattern for {o.path}" |
|
raise ValueError(msg) |
|
return url |
|
|
|
@property |
|
def method(self) -> str: |
|
method = self.request.method |
|
return method and method.lower() or "" |
|
|
|
@property |
|
def body(self) -> bytes | None: |
|
if self.request.body is None: |
|
return None |
|
if not isinstance(self.request.body, bytes): |
|
msg = "Request body is invalid" |
|
raise AssertionError(msg) |
|
return self.request.body |
|
|
|
@property |
|
def mimetype(self) -> str: |
|
|
|
|
|
request = self.request |
|
return ( |
|
request.headers.get("Content-Type") |
|
or request.headers.get("Accept") |
|
or "application/json" |
|
) |
|
|
|
|
|
class TornadoOpenAPIResponse: |
|
"""A tornado open API response.""" |
|
|
|
def __init__(self, response: HTTPResponse): |
|
"""Initialize the response.""" |
|
self.response = response |
|
|
|
@property |
|
def data(self) -> bytes | None: |
|
if not isinstance(self.response.body, bytes): |
|
msg = "Response body is invalid" |
|
raise AssertionError(msg) |
|
return self.response.body |
|
|
|
@property |
|
def status_code(self) -> int: |
|
return int(self.response.code) |
|
|
|
@property |
|
def content_type(self) -> str: |
|
return "application/json" |
|
|
|
@property |
|
def mimetype(self) -> str: |
|
return str(self.response.headers.get("Content-Type", "application/json")) |
|
|
|
@property |
|
def headers(self) -> Headers: |
|
return Headers(dict(self.response.headers)) |
|
|
|
|
|
def validate_request(response: HTTPResponse) -> None: |
|
"""Validate an API request""" |
|
openapi_spec = get_openapi_spec() |
|
|
|
request = TornadoOpenAPIRequest(response.request, openapi_spec) |
|
V30RequestValidator(openapi_spec).validate(request) |
|
|
|
torn_response = TornadoOpenAPIResponse(response) |
|
V30ResponseValidator(openapi_spec).validate(request, torn_response) |
|
|
|
|
|
def maybe_patch_ioloop() -> None: |
|
"""a windows 3.8+ patch for the asyncio loop""" |
|
if ( |
|
sys.platform.startswith("win") |
|
and tornado.version_info < (6, 1) |
|
and sys.version_info >= (3, 8) |
|
): |
|
try: |
|
from asyncio import WindowsProactorEventLoopPolicy, WindowsSelectorEventLoopPolicy |
|
except ImportError: |
|
pass |
|
|
|
else: |
|
from asyncio import get_event_loop_policy, set_event_loop_policy |
|
|
|
if type(get_event_loop_policy()) is WindowsProactorEventLoopPolicy: |
|
|
|
|
|
set_event_loop_policy(WindowsSelectorEventLoopPolicy()) |
|
|
|
|
|
def expected_http_error( |
|
error: Exception, expected_code: int, expected_message: str | None = None |
|
) -> bool: |
|
"""Check that the error matches the expected output error.""" |
|
e = error.value |
|
if isinstance(e, tornado.web.HTTPError): |
|
if expected_code != e.status_code: |
|
return False |
|
if expected_message is not None and expected_message != str(e): |
|
return False |
|
return True |
|
if any( |
|
[ |
|
isinstance(e, tornado.httpclient.HTTPClientError), |
|
isinstance(e, tornado.httpclient.HTTPError), |
|
] |
|
): |
|
if expected_code != e.code: |
|
return False |
|
if expected_message: |
|
message = json.loads(e.response.body.decode())["message"] |
|
if expected_message != message: |
|
return False |
|
return True |
|
|
|
return False |
|
|