You've already forked httpie-cli
mirror of
https://github.com/httpie/cli.git
synced 2025-08-10 22:42:05 +02:00
Fix incorrect separators and introduce assert_output_matches()
(close #1027)
This commit is contained in:
311
tests/utils/__init__.py
Normal file
311
tests/utils/__init__.py
Normal file
@@ -0,0 +1,311 @@
|
||||
# coding=utf-8
|
||||
"""Utilities for HTTPie test suite."""
|
||||
import re
|
||||
import shlex
|
||||
import sys
|
||||
import time
|
||||
import json
|
||||
import tempfile
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
from typing import Optional, Union, List
|
||||
|
||||
from httpie.status import ExitStatus
|
||||
from httpie.config import Config
|
||||
from httpie.context import Environment
|
||||
from httpie.core import main
|
||||
|
||||
|
||||
# pytest-httpbin currently does not support chunked requests:
|
||||
# <https://github.com/kevin1024/pytest-httpbin/issues/33>
|
||||
# <https://github.com/kevin1024/pytest-httpbin/issues/28>
|
||||
HTTPBIN_WITH_CHUNKED_SUPPORT = 'http://pie.dev'
|
||||
|
||||
|
||||
TESTS_ROOT = Path(__file__).parent.parent
|
||||
CRLF = '\r\n'
|
||||
COLOR = '\x1b['
|
||||
HTTP_OK = '200 OK'
|
||||
# noinspection GrazieInspection
|
||||
HTTP_OK_COLOR = (
|
||||
'HTTP\x1b[39m\x1b[38;5;245m/\x1b[39m\x1b'
|
||||
'[38;5;37m1.1\x1b[39m\x1b[38;5;245m \x1b[39m\x1b[38;5;37m200'
|
||||
'\x1b[39m\x1b[38;5;245m \x1b[39m\x1b[38;5;136mOK'
|
||||
)
|
||||
|
||||
|
||||
def mk_config_dir() -> Path:
|
||||
dirname = tempfile.mkdtemp(prefix='httpie_config_')
|
||||
return Path(dirname)
|
||||
|
||||
|
||||
def add_auth(url, auth):
|
||||
proto, rest = url.split('://', 1)
|
||||
return proto + '://' + auth + '@' + rest
|
||||
|
||||
|
||||
class StdinBytesIO(BytesIO):
|
||||
"""To be used for `MockEnvironment.stdin`"""
|
||||
len = 0 # See `prepare_request_body()`
|
||||
|
||||
|
||||
class MockEnvironment(Environment):
|
||||
"""Environment subclass with reasonable defaults for testing."""
|
||||
colors = 0 # For easier debugging
|
||||
stdin_isatty = True,
|
||||
stdout_isatty = True
|
||||
is_windows = False
|
||||
|
||||
def __init__(self, create_temp_config_dir=True, **kwargs):
|
||||
if 'stdout' not in kwargs:
|
||||
kwargs['stdout'] = tempfile.TemporaryFile(
|
||||
mode='w+b',
|
||||
prefix='httpie_stdout'
|
||||
)
|
||||
if 'stderr' not in kwargs:
|
||||
kwargs['stderr'] = tempfile.TemporaryFile(
|
||||
mode='w+t',
|
||||
prefix='httpie_stderr'
|
||||
)
|
||||
super().__init__(**kwargs)
|
||||
self._create_temp_config_dir = create_temp_config_dir
|
||||
self._delete_config_dir = False
|
||||
self._temp_dir = Path(tempfile.gettempdir())
|
||||
|
||||
@property
|
||||
def config(self) -> Config:
|
||||
if (self._create_temp_config_dir
|
||||
and self._temp_dir not in self.config_dir.parents):
|
||||
self.create_temp_config_dir()
|
||||
return super().config
|
||||
|
||||
def create_temp_config_dir(self):
|
||||
self.config_dir = mk_config_dir()
|
||||
self._delete_config_dir = True
|
||||
|
||||
def cleanup(self):
|
||||
self.stdout.close()
|
||||
self.stderr.close()
|
||||
if self._delete_config_dir:
|
||||
assert self._temp_dir in self.config_dir.parents
|
||||
from shutil import rmtree
|
||||
rmtree(self.config_dir, ignore_errors=True)
|
||||
|
||||
def __del__(self):
|
||||
# noinspection PyBroadException
|
||||
try:
|
||||
self.cleanup()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
class BaseCLIResponse:
|
||||
"""
|
||||
Represents the result of simulated `$ http' invocation via `http()`.
|
||||
|
||||
Holds and provides access to:
|
||||
|
||||
- stdout output: print(self)
|
||||
- stderr output: print(self.stderr)
|
||||
- devnull output: print(self.devnull)
|
||||
- exit_status output: print(self.exit_status)
|
||||
|
||||
"""
|
||||
stderr: str = None
|
||||
devnull: str = None
|
||||
json: dict = None
|
||||
exit_status: ExitStatus = None
|
||||
command: str = None
|
||||
args: List[str] = []
|
||||
complete_args: List[str] = []
|
||||
|
||||
@property
|
||||
def command(self):
|
||||
cmd = ' '.join(shlex.quote(arg) for arg in ['http', *self.args])
|
||||
# pytest-httpbin to real httpbin.
|
||||
return re.sub(r'127\.0\.0\.1:\d+', 'httpbin.org', cmd)
|
||||
|
||||
|
||||
class BytesCLIResponse(bytes, BaseCLIResponse):
|
||||
"""
|
||||
Used as a fallback when a StrCLIResponse cannot be used.
|
||||
|
||||
E.g. when the output contains binary data or when it is colorized.
|
||||
|
||||
`.json` will always be None.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
class StrCLIResponse(str, BaseCLIResponse):
|
||||
|
||||
@property
|
||||
def json(self) -> Optional[dict]:
|
||||
"""
|
||||
Return deserialized the request or response JSON body,
|
||||
if one (and only one) included in the output and is parsable.
|
||||
|
||||
"""
|
||||
if not hasattr(self, '_json'):
|
||||
self._json = None
|
||||
# De-serialize JSON body if possible.
|
||||
if COLOR in self:
|
||||
# Colorized output cannot be parsed.
|
||||
pass
|
||||
elif self.strip().startswith('{'):
|
||||
# Looks like JSON body.
|
||||
self._json = json.loads(self)
|
||||
elif self.count('Content-Type:') == 1:
|
||||
# Looks like a HTTP message,
|
||||
# try to extract JSON from its body.
|
||||
try:
|
||||
j = self.strip()[self.strip().rindex('\r\n\r\n'):]
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
# noinspection PyAttributeOutsideInit
|
||||
self._json = json.loads(j)
|
||||
except ValueError:
|
||||
pass
|
||||
return self._json
|
||||
|
||||
|
||||
class ExitStatusError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def http(
|
||||
*args,
|
||||
program_name='http',
|
||||
tolerate_error_exit_status=False,
|
||||
**kwargs,
|
||||
) -> Union[StrCLIResponse, BytesCLIResponse]:
|
||||
# noinspection PyUnresolvedReferences
|
||||
"""
|
||||
Run HTTPie and capture stderr/out and exit status.
|
||||
Content writtent to devnull will be captured only if
|
||||
env.devnull is set manually.
|
||||
|
||||
Invoke `httpie.core.main()` with `args` and `kwargs`,
|
||||
and return a `CLIResponse` subclass instance.
|
||||
|
||||
The return value is either a `StrCLIResponse`, or `BytesCLIResponse`
|
||||
if unable to decode the output. Devnull is string when possible,
|
||||
bytes otherwise.
|
||||
|
||||
The response has the following attributes:
|
||||
|
||||
`stdout` is represented by the instance itself (print r)
|
||||
`stderr`: text written to stderr
|
||||
`devnull` text written to devnull.
|
||||
`exit_status`: the exit status
|
||||
`json`: decoded JSON (if possible) or `None`
|
||||
|
||||
Exceptions are propagated.
|
||||
|
||||
If you pass ``tolerate_error_exit_status=True``, then error exit statuses
|
||||
won't result into an exception.
|
||||
|
||||
Example:
|
||||
|
||||
$ http --auth=user:password GET pie.dev/basic-auth/user/password
|
||||
|
||||
>>> httpbin = getfixture('httpbin')
|
||||
>>> r = http('-a', 'user:pw', httpbin.url + '/basic-auth/user/pw')
|
||||
>>> type(r) == StrCLIResponse
|
||||
True
|
||||
>>> r.exit_status
|
||||
<ExitStatus.SUCCESS: 0>
|
||||
>>> r.stderr
|
||||
''
|
||||
>>> 'HTTP/1.1 200 OK' in r
|
||||
True
|
||||
>>> r.json == {'authenticated': True, 'user': 'user'}
|
||||
True
|
||||
|
||||
"""
|
||||
env = kwargs.get('env')
|
||||
if not env:
|
||||
env = kwargs['env'] = MockEnvironment()
|
||||
|
||||
stdout = env.stdout
|
||||
stderr = env.stderr
|
||||
devnull = env.devnull
|
||||
|
||||
args = list(args)
|
||||
args_with_config_defaults = args + env.config.default_options
|
||||
add_to_args = []
|
||||
if '--debug' not in args_with_config_defaults:
|
||||
if (not tolerate_error_exit_status
|
||||
and '--traceback' not in args_with_config_defaults):
|
||||
add_to_args.append('--traceback')
|
||||
if not any('--timeout' in arg for arg in args_with_config_defaults):
|
||||
add_to_args.append('--timeout=3')
|
||||
|
||||
complete_args = [program_name, *add_to_args, *args]
|
||||
# print(' '.join(complete_args))
|
||||
|
||||
def dump_stderr():
|
||||
stderr.seek(0)
|
||||
sys.stderr.write(stderr.read())
|
||||
|
||||
try:
|
||||
try:
|
||||
exit_status = main(args=complete_args, **kwargs)
|
||||
if '--download' in args:
|
||||
# Let the progress reporter thread finish.
|
||||
time.sleep(.5)
|
||||
except SystemExit:
|
||||
if tolerate_error_exit_status:
|
||||
exit_status = ExitStatus.ERROR
|
||||
else:
|
||||
dump_stderr()
|
||||
raise
|
||||
except Exception:
|
||||
stderr.seek(0)
|
||||
sys.stderr.write(stderr.read())
|
||||
raise
|
||||
else:
|
||||
if (not tolerate_error_exit_status
|
||||
and exit_status != ExitStatus.SUCCESS):
|
||||
dump_stderr()
|
||||
raise ExitStatusError(
|
||||
'httpie.core.main() unexpectedly returned'
|
||||
f' a non-zero exit status: {exit_status}'
|
||||
)
|
||||
|
||||
stdout.seek(0)
|
||||
stderr.seek(0)
|
||||
devnull.seek(0)
|
||||
output = stdout.read()
|
||||
devnull_output = devnull.read()
|
||||
try:
|
||||
output = output.decode('utf8')
|
||||
except UnicodeDecodeError:
|
||||
r = BytesCLIResponse(output)
|
||||
else:
|
||||
r = StrCLIResponse(output)
|
||||
|
||||
try:
|
||||
devnull_output = devnull_output.decode('utf8')
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
r.devnull = devnull_output
|
||||
r.stderr = stderr.read()
|
||||
r.exit_status = exit_status
|
||||
r.args = args
|
||||
r.complete_args = ' '.join(complete_args)
|
||||
|
||||
if r.exit_status != ExitStatus.SUCCESS:
|
||||
sys.stderr.write(r.stderr)
|
||||
|
||||
# print(f'\n\n$ {r.command}\n')
|
||||
return r
|
||||
|
||||
finally:
|
||||
devnull.close()
|
||||
stdout.close()
|
||||
stderr.close()
|
||||
env.cleanup()
|
32
tests/utils/matching/__init__.py
Normal file
32
tests/utils/matching/__init__.py
Normal file
@@ -0,0 +1,32 @@
|
||||
from typing import Iterable
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.utils.matching.parsing import OutputMatchingError, expect_tokens, Expect
|
||||
|
||||
|
||||
__all__ = [
|
||||
'assert_output_matches',
|
||||
'assert_output_does_not_match',
|
||||
'Expect',
|
||||
]
|
||||
|
||||
|
||||
def assert_output_matches(output: str, tokens: Iterable[Expect]):
|
||||
r"""
|
||||
Check the command `output` for an exact full sequence of `tokens`.
|
||||
|
||||
>>> out = 'GET / HTTP/1.1\r\nAAA:BBB\r\n\r\nCCC\n\n'
|
||||
>>> assert_output_matches(out, [Expect.REQUEST_HEADERS, Expect.BODY, Expect.SEPARATOR])
|
||||
|
||||
"""
|
||||
# TODO: auto-remove ansi colors to allow for testing of colorized output as well.
|
||||
expect_tokens(tokens=tokens, s=output)
|
||||
|
||||
|
||||
def assert_output_does_not_match(output: str, tokens: Iterable[Expect]):
|
||||
r"""
|
||||
>>> assert_output_does_not_match('\r\n', [Expect.BODY])
|
||||
"""
|
||||
with pytest.raises(OutputMatchingError):
|
||||
assert_output_matches(output=output, tokens=tokens)
|
107
tests/utils/matching/parsing.py
Normal file
107
tests/utils/matching/parsing.py
Normal file
@@ -0,0 +1,107 @@
|
||||
import re
|
||||
from typing import Iterable
|
||||
from enum import Enum, auto
|
||||
|
||||
from httpie.output.writer import MESSAGE_SEPARATOR
|
||||
from tests.utils import CRLF
|
||||
|
||||
|
||||
class Expect(Enum):
|
||||
"""
|
||||
Predefined token types we can expect in the output.
|
||||
|
||||
"""
|
||||
REQUEST_HEADERS = auto()
|
||||
RESPONSE_HEADERS = auto()
|
||||
BODY = auto()
|
||||
SEPARATOR = auto()
|
||||
|
||||
|
||||
SEPARATOR_RE = re.compile(f'^{MESSAGE_SEPARATOR}')
|
||||
|
||||
|
||||
def make_headers_re(message_type: Expect):
|
||||
assert message_type in {Expect.REQUEST_HEADERS, Expect.RESPONSE_HEADERS}
|
||||
|
||||
# language=RegExp
|
||||
crlf = r'[\r][\n]'
|
||||
non_crlf = rf'[^{CRLF}]'
|
||||
|
||||
# language=RegExp
|
||||
http_version = r'HTTP/\d+\.\d+'
|
||||
if message_type is Expect.REQUEST_HEADERS:
|
||||
# POST /post HTTP/1.1
|
||||
start_line_re = fr'{non_crlf}*{http_version}{crlf}'
|
||||
else:
|
||||
# HTTP/1.1 200 OK
|
||||
start_line_re = fr'{http_version}{non_crlf}*{crlf}'
|
||||
|
||||
return re.compile(
|
||||
fr'''
|
||||
^
|
||||
{start_line_re}
|
||||
({non_crlf}+:{non_crlf}+{crlf})+
|
||||
{crlf}
|
||||
''',
|
||||
flags=re.VERBOSE
|
||||
)
|
||||
|
||||
|
||||
BODY_ENDINGS = [
|
||||
MESSAGE_SEPARATOR,
|
||||
CRLF, # Not really but useful for testing (just remember not to include it in a body).
|
||||
]
|
||||
TOKEN_REGEX_MAP = {
|
||||
Expect.REQUEST_HEADERS: make_headers_re(Expect.REQUEST_HEADERS),
|
||||
Expect.RESPONSE_HEADERS: make_headers_re(Expect.RESPONSE_HEADERS),
|
||||
Expect.SEPARATOR: SEPARATOR_RE,
|
||||
}
|
||||
|
||||
|
||||
class OutputMatchingError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
def expect_tokens(tokens: Iterable[Expect], s: str):
|
||||
for token in tokens:
|
||||
s = expect_token(token, s)
|
||||
if s:
|
||||
raise OutputMatchingError(f'Unmatched remaining output for {tokens} in {s!r}')
|
||||
|
||||
|
||||
def expect_token(token: Expect, s: str) -> str:
|
||||
if token is Expect.BODY:
|
||||
s = expect_body(s)
|
||||
else:
|
||||
s = expect_regex(token, s)
|
||||
return s
|
||||
|
||||
|
||||
def expect_regex(token: Expect, s: str) -> str:
|
||||
match = TOKEN_REGEX_MAP[token].match(s)
|
||||
if not match:
|
||||
raise OutputMatchingError(f'No match for {token} in {s!r}')
|
||||
return s[match.end():]
|
||||
|
||||
|
||||
def expect_body(s: str) -> str:
|
||||
"""
|
||||
We require some text, and continue to read until we find an ending or until the end of the string.
|
||||
|
||||
"""
|
||||
if 'content-disposition:' in s.lower():
|
||||
# Multipart body heuristic.
|
||||
final_boundary_re = re.compile('\r\n--[^-]+?--\r\n')
|
||||
match = final_boundary_re.search(s)
|
||||
if match:
|
||||
return s[match.end():]
|
||||
|
||||
endings = [s.index(sep) for sep in BODY_ENDINGS if sep in s]
|
||||
if not endings:
|
||||
s = '' # Only body
|
||||
else:
|
||||
end = min(endings)
|
||||
if end == 0:
|
||||
raise OutputMatchingError(f'Empty body: {s!r}')
|
||||
s = s[end:]
|
||||
return s
|
190
tests/utils/matching/test_matching.py
Normal file
190
tests/utils/matching/test_matching.py
Normal file
@@ -0,0 +1,190 @@
|
||||
"""
|
||||
Here we test our output parsing and matching implementation, not HTTPie itself.
|
||||
|
||||
"""
|
||||
from httpie.output.writer import MESSAGE_SEPARATOR
|
||||
from tests.utils import CRLF
|
||||
from tests.utils.matching import assert_output_does_not_match, assert_output_matches, Expect
|
||||
|
||||
|
||||
def test_assert_output_matches_headers_incomplete():
|
||||
assert_output_does_not_match(f'HTTP/1.1{CRLF}', [Expect.RESPONSE_HEADERS])
|
||||
|
||||
|
||||
def test_assert_output_matches_headers_unterminated():
|
||||
assert_output_does_not_match(
|
||||
(
|
||||
f'HTTP/1.1{CRLF}'
|
||||
f'AAA:BBB'
|
||||
f'{CRLF}'
|
||||
),
|
||||
[Expect.RESPONSE_HEADERS],
|
||||
)
|
||||
|
||||
|
||||
def test_assert_output_matches_response_headers():
|
||||
assert_output_matches(
|
||||
(
|
||||
f'HTTP/1.1 200 OK{CRLF}'
|
||||
f'AAA:BBB{CRLF}'
|
||||
f'{CRLF}'
|
||||
),
|
||||
[Expect.RESPONSE_HEADERS],
|
||||
)
|
||||
|
||||
|
||||
def test_assert_output_matches_request_headers():
|
||||
assert_output_matches(
|
||||
(
|
||||
f'GET / HTTP/1.1{CRLF}'
|
||||
f'AAA:BBB{CRLF}'
|
||||
f'{CRLF}'
|
||||
),
|
||||
[Expect.REQUEST_HEADERS],
|
||||
)
|
||||
|
||||
|
||||
def test_assert_output_matches_headers_and_separator():
|
||||
assert_output_matches(
|
||||
(
|
||||
f'HTTP/1.1{CRLF}'
|
||||
f'AAA:BBB{CRLF}'
|
||||
f'{CRLF}'
|
||||
f'{MESSAGE_SEPARATOR}'
|
||||
),
|
||||
[Expect.RESPONSE_HEADERS, Expect.SEPARATOR],
|
||||
)
|
||||
|
||||
|
||||
def test_assert_output_matches_body_unmatched_crlf():
|
||||
assert_output_does_not_match(f'AAA{CRLF}', [Expect.BODY])
|
||||
|
||||
|
||||
def test_assert_output_matches_body_unmatched_separator():
|
||||
assert_output_does_not_match(f'AAA{MESSAGE_SEPARATOR}', [Expect.BODY])
|
||||
|
||||
|
||||
def test_assert_output_matches_body_and_separator():
|
||||
assert_output_matches(f'AAA{MESSAGE_SEPARATOR}', [Expect.BODY, Expect.SEPARATOR])
|
||||
|
||||
|
||||
def test_assert_output_matches_body_r():
|
||||
assert_output_matches(f'AAA\r', [Expect.BODY])
|
||||
|
||||
|
||||
def test_assert_output_matches_body_n():
|
||||
assert_output_matches(f'AAA\n', [Expect.BODY])
|
||||
|
||||
|
||||
def test_assert_output_matches_body_r_body():
|
||||
assert_output_matches(f'AAA\rBBB', [Expect.BODY])
|
||||
|
||||
|
||||
def test_assert_output_matches_body_n_body():
|
||||
assert_output_matches(f'AAA\nBBB', [Expect.BODY])
|
||||
|
||||
|
||||
def test_assert_output_matches_headers_and_body():
|
||||
assert_output_matches(
|
||||
(
|
||||
f'HTTP/1.1{CRLF}'
|
||||
f'AAA:BBB{CRLF}'
|
||||
f'{CRLF}'
|
||||
f'CCC'
|
||||
),
|
||||
[Expect.RESPONSE_HEADERS, Expect.BODY]
|
||||
)
|
||||
|
||||
|
||||
def test_assert_output_matches_headers_with_body_and_separator():
|
||||
assert_output_matches(
|
||||
(
|
||||
f'HTTP/1.1 {CRLF}'
|
||||
f'AAA:BBB{CRLF}{CRLF}'
|
||||
f'CCC{MESSAGE_SEPARATOR}'
|
||||
),
|
||||
[Expect.RESPONSE_HEADERS, Expect.BODY, Expect.SEPARATOR]
|
||||
)
|
||||
|
||||
|
||||
def test_assert_output_matches_multiple_messages():
|
||||
assert_output_matches(
|
||||
(
|
||||
f'POST / HTTP/1.1{CRLF}'
|
||||
f'AAA:BBB{CRLF}'
|
||||
f'{CRLF}'
|
||||
|
||||
f'CCC'
|
||||
f'{MESSAGE_SEPARATOR}'
|
||||
|
||||
f'HTTP/1.1 200 OK{CRLF}'
|
||||
f'EEE:FFF{CRLF}'
|
||||
f'{CRLF}'
|
||||
|
||||
f'GGG'
|
||||
f'{MESSAGE_SEPARATOR}'
|
||||
), [
|
||||
Expect.REQUEST_HEADERS,
|
||||
Expect.BODY,
|
||||
Expect.SEPARATOR,
|
||||
Expect.RESPONSE_HEADERS,
|
||||
Expect.BODY,
|
||||
Expect.SEPARATOR,
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def test_assert_output_matches_multipart_body():
|
||||
output = (
|
||||
'POST / HTTP/1.1\r\n'
|
||||
'User-Agent: HTTPie/2.4.0-dev\r\n'
|
||||
'Accept-Encoding: gzip, deflate\r\n'
|
||||
'Accept: */*\r\n'
|
||||
'Connection: keep-alive\r\n'
|
||||
'Content-Type: multipart/form-data; boundary=1e22169de43e4a2e8d9e41c0a1c93cc5\r\n'
|
||||
'Content-Length: 212\r\n'
|
||||
'Host: pie.dev\r\n'
|
||||
'\r\n'
|
||||
'--1e22169de43e4a2e8d9e41c0a1c93cc5\r\n'
|
||||
'Content-Disposition: form-data; name="AAA"\r\n'
|
||||
'\r\n'
|
||||
'BBB\r\n'
|
||||
'--1e22169de43e4a2e8d9e41c0a1c93cc5\r\n'
|
||||
'Content-Disposition: form-data; name="CCC"\r\n'
|
||||
'\r\n'
|
||||
'DDD\r\n'
|
||||
'--1e22169de43e4a2e8d9e41c0a1c93cc5--\r\n'
|
||||
)
|
||||
assert_output_matches(output, [Expect.REQUEST_HEADERS, Expect.BODY])
|
||||
|
||||
|
||||
def test_assert_output_matches_multipart_body_with_separator():
|
||||
output = (
|
||||
'POST / HTTP/1.1\r\n'
|
||||
'User-Agent: HTTPie/2.4.0-dev\r\n'
|
||||
'Accept-Encoding: gzip, deflate\r\n'
|
||||
'Accept: */*\r\n'
|
||||
'Connection: keep-alive\r\n'
|
||||
'Content-Type: multipart/form-data; boundary=1e22169de43e4a2e8d9e41c0a1c93cc5\r\n'
|
||||
'Content-Length: 212\r\n'
|
||||
'Host: pie.dev\r\n'
|
||||
'\r\n'
|
||||
'--1e22169de43e4a2e8d9e41c0a1c93cc5\r\n'
|
||||
'Content-Disposition: form-data; name="AAA"\r\n'
|
||||
'\r\n'
|
||||
'BBB\r\n'
|
||||
'--1e22169de43e4a2e8d9e41c0a1c93cc5\r\n'
|
||||
'Content-Disposition: form-data; name="CCC"\r\n'
|
||||
'\r\n'
|
||||
'DDD\r\n'
|
||||
'--1e22169de43e4a2e8d9e41c0a1c93cc5--\r\n'
|
||||
f'{MESSAGE_SEPARATOR}'
|
||||
)
|
||||
assert_output_matches(output, [Expect.REQUEST_HEADERS, Expect.BODY, Expect.SEPARATOR])
|
||||
|
||||
|
||||
def test_assert_output_matches_multiple_separators():
|
||||
assert_output_matches(
|
||||
MESSAGE_SEPARATOR + MESSAGE_SEPARATOR + 'AAA' + MESSAGE_SEPARATOR + MESSAGE_SEPARATOR,
|
||||
[Expect.SEPARATOR, Expect.SEPARATOR, Expect.BODY, Expect.SEPARATOR, Expect.SEPARATOR]
|
||||
)
|
Reference in New Issue
Block a user