8CONTENT_IN_GET_REQUEST_ERROR = (
9 'GET requests cannot have content, but Content-Length header was sent.'
11CHUNKED_CONTENT_IN_GET_REQUEST_ERROR = (
12 "GET requests cannot have content, but 'Transfer-Encoding: chunked' "
15MULTIPART_MIME_PATTERN =
"""MIME-Version: 1.0
26 """Base class for mockserver mocked errors."""
28 error_code =
'unknown'
32 """Exception used to mock HTTP client timeout errors.
34 Requires service side support.
36 Available as ``mockserver.TimeoutError`` alias
37 or by full name ``testsuite.utils.http.TimeoutError``.
40 error_code =
'timeout'
44 """Exception used to mock HTTP client network errors.
46 Requires service side support.
48 Available as ``mockserver.NetworkError`` alias
49 or by full name ``testsuite.utils.http.NetworkError``.
52 error_code =
'network'
56 def __init__(self, *, url: str, status: int):
59 super().__init__(f
"status={self.status}, url='{self.url}'")
63 """Invalid request which cannot be wrapped"""
67 """Adapts aiohttp.web.BaseRequest to mimic a frequently used subset of
68 werkzeug.Request interface. ``data`` property is not supported,
69 use get_data() instead.
72 def __init__(self, request: aiohttp.web.BaseRequest, data: bytes):
73 self._request = request
74 self._data: bytes = data
75 self._json: object =
None
76 self._form: dict[str, str] |
None =
None
79 def method(self) -> str:
80 return self._request.method
84 return str(self._request.url)
87 def path(self) -> str:
88 return self._request.path
92 def path_qs(self) -> str:
93 return self._request.raw_path
96 def query_string(self) -> bytes:
97 path_and_query = self._request.raw_path.split(
'?')
98 if len(path_and_query) < 2:
100 return path_and_query[1].encode()
104 return self._request.headers
107 def content_type(self):
108 return self._request.content_type
110 def get_data(self) -> bytes:
115 if self._form
is None:
116 if self._request.content_type
in (
118 'application/x-www-form-urlencoded',
120 charset = self._request.charset
or 'utf-8'
121 items = urllib.parse.parse_qsl(
122 self._data.rstrip().decode(charset),
123 keep_blank_values=
True,
126 self._form = dict(items)
127 elif self._request.content_type.startswith(
'multipart/form-data'):
128 charset = self._request.charset
or 'utf-8'
129 epost_data = MULTIPART_MIME_PATTERN % (
130 self._request.headers[
'content-type'],
131 self._data.rstrip().decode(charset),
133 data = email.message_from_string(epost_data)
134 assert data.is_multipart()
137 for part
in data.get_payload():
138 name = part.get_param(
'name', header=
'content-disposition')
139 payload = part.get_payload(decode=
True).decode(charset)
141 payload = int(payload)
144 self._form[name] = payload
152 def json(self) -> typing.Any:
153 if self._json
is None:
154 bytes_body = self.get_data()
155 encoding = self._request.charset
or 'utf-8'
156 str_body = bytes_body.decode(encoding)
157 self._json = json.loads(str_body)
161 def cookies(self) -> typing.Mapping[str, str]:
162 return self._request.cookies
166 return self._request.query
171 return self._request.query
178async def wrap_request(request: aiohttp.web.BaseRequest) -> Request:
179 if request.method ==
'GET':
180 if request.content_length:
181 raise InvalidRequestError(CONTENT_IN_GET_REQUEST_ERROR)
182 if request.headers.get(
'Transfer-Encoding',
'') ==
'chunked':
183 raise InvalidRequestError(CHUNKED_CONTENT_IN_GET_REQUEST_ERROR)
184 if request.headers.get(
'expect') ==
'100-continue':
185 await request.writer.write(b
'HTTP/1.1 100 Continue\r\n\r\n')
186 await request.writer.drain()
187 data = await request.content.read()
188 return Request(request, data)
194 body: bytes | bytearray |
None =
None,
195 text: str |
None =
None,
197 headers: typing.Mapping[str, str] |
None =
None,
198 content_type: str |
None =
None,
199 charset: str |
None =
None,
203 'Response params "body" and "text" can not be used at the same time'
215 f
'<{self.__class__.__name__} body={self._body!r} '
216 f
'text={self._text} status={self._status} content_type={self._content_type} charset={self._charset}>'
219 def to_aiohttp(self) -> aiohttp.web.Response:
220 return aiohttp.web.Response(
233 response: aiohttp.ClientResponse,
240 self.
_text: str |
None =
None
241 self.
_form: dict[str, str] |
None =
None
246 f
'<{self.__class__.__name__} method={self._response.method} '
247 f
'url={self._response.url} status={self.status} content={self.content!r}>'
251 def status_code(self) -> int:
256 def status(self) -> int:
260 def reason(self) -> str | None:
264 def content(self) -> bytes:
268 def text(self) -> str:
269 if self.
_text is None:
274 def json(self) -> typing.Any:
279 if self.
_form is None:
280 if self.
content_type in (
'',
'application/x-www-form-urlencoded'):
281 items = urllib.parse.parse_qsl(
283 keep_blank_values=
True,
286 self.
_form = dict(items)
297 def content_type(self):
308 def raise_for_status(self) -> None:
313 url=str(self.
_response.request_info.url),
318async def wrap_client_response(
319 response: aiohttp.ClientResponse,
321 json_loads=json.loads,
323 content = await response.read()
324 wrapped =
ClientResponse(response, content, json_loads=json_loads)
329 response: str | bytes | bytearray |
None =
None,
331 headers: typing.Mapping[str, str] |
None =
None,
332 content_type: str |
None =
None,
333 charset: str |
None =
None,
339 Create HTTP response object. Returns ``Response`` instance.
341 :param response: response content
342 :param status: HTTP status code
343 :param headers: HTTP headers dictionary
344 :param content_type: HTTP Content-Type header
345 :param charset: Response character set
346 :param json: JSON response shortcut
347 :param form: x-www-form-urlencoded response shortcut
349 if json
is not _NoValue
and form
is not _NoValue:
351 'Response params "json" and "form" can not be used '
354 if json
is not _NoValue:
355 response = _json_response(json)
356 if content_type
is None:
357 content_type =
'application/json'
358 if form
is not _NoValue:
359 response = _form_response(form)
360 if content_type
is None:
361 content_type =
'application/x-www-form-urlencoded'
363 if isinstance(response, (bytes, bytearray)):
368 content_type=content_type,
371 if isinstance(response, str):
376 content_type=content_type,
383 content_type=content_type,
386 raise RuntimeError(f
'Unsupported response {response!r} given')
389def _json_response(data: typing.Any) -> bytes:
390 text = json.dumps(data, ensure_ascii=
False)
391 return text.encode(
'utf-8')
394def _form_response(data: typing.Any) -> bytes:
395 text = urllib.parse.urlencode(data)
396 return text.encode(
'utf-8')