2014-04-24 14:07:31 +02:00
|
|
|
import os
|
2021-12-29 10:41:44 +01:00
|
|
|
import json
|
2022-01-12 15:07:34 +01:00
|
|
|
import sys
|
|
|
|
import subprocess
|
|
|
|
import time
|
|
|
|
import contextlib
|
|
|
|
import httpie.__main__ as main
|
2014-04-24 14:07:31 +02:00
|
|
|
|
2014-04-24 17:08:40 +02:00
|
|
|
import pytest
|
|
|
|
|
2019-08-31 15:17:10 +02:00
|
|
|
from httpie.cli.exceptions import ParseError
|
2020-08-19 10:22:42 +02:00
|
|
|
from httpie.client import FORM_CONTENT_TYPE
|
2022-01-12 15:07:34 +01:00
|
|
|
from httpie.compat import is_windows
|
2020-01-23 15:54:43 +01:00
|
|
|
from httpie.status import ExitStatus
|
2021-05-05 14:13:39 +02:00
|
|
|
from .utils import (
|
2021-05-27 19:30:36 +02:00
|
|
|
MockEnvironment, StdinBytesIO, http,
|
2020-09-28 12:16:57 +02:00
|
|
|
HTTP_OK,
|
|
|
|
)
|
2021-05-05 14:13:39 +02:00
|
|
|
from .fixtures import FILE_PATH_ARG, FILE_PATH, FILE_CONTENT
|
2014-04-24 14:07:31 +02:00
|
|
|
|
|
|
|
|
2021-05-27 19:30:36 +02:00
|
|
|
def test_chunked_json(httpbin_with_chunked_support):
|
2020-09-28 12:16:57 +02:00
|
|
|
r = http(
|
|
|
|
'--verbose',
|
|
|
|
'--chunked',
|
2021-05-27 19:30:36 +02:00
|
|
|
httpbin_with_chunked_support + '/post',
|
2020-09-28 12:16:57 +02:00
|
|
|
'hello=world',
|
|
|
|
)
|
|
|
|
assert HTTP_OK in r
|
|
|
|
assert 'Transfer-Encoding: chunked' in r
|
|
|
|
assert r.count('hello') == 3
|
|
|
|
|
|
|
|
|
2021-05-27 19:30:36 +02:00
|
|
|
def test_chunked_form(httpbin_with_chunked_support):
|
2020-09-28 12:16:57 +02:00
|
|
|
r = http(
|
|
|
|
'--verbose',
|
|
|
|
'--chunked',
|
|
|
|
'--form',
|
2021-05-27 19:30:36 +02:00
|
|
|
httpbin_with_chunked_support + '/post',
|
2020-09-28 12:16:57 +02:00
|
|
|
'hello=world',
|
|
|
|
)
|
|
|
|
assert HTTP_OK in r
|
|
|
|
assert 'Transfer-Encoding: chunked' in r
|
|
|
|
assert r.count('hello') == 2
|
|
|
|
|
|
|
|
|
2021-05-27 19:30:36 +02:00
|
|
|
def test_chunked_stdin(httpbin_with_chunked_support):
|
2020-09-28 12:16:57 +02:00
|
|
|
r = http(
|
|
|
|
'--verbose',
|
|
|
|
'--chunked',
|
2021-05-27 19:30:36 +02:00
|
|
|
httpbin_with_chunked_support + '/post',
|
2020-09-28 12:16:57 +02:00
|
|
|
env=MockEnvironment(
|
|
|
|
stdin=StdinBytesIO(FILE_PATH.read_bytes()),
|
|
|
|
stdin_isatty=False,
|
|
|
|
)
|
|
|
|
)
|
|
|
|
assert HTTP_OK in r
|
|
|
|
assert 'Transfer-Encoding: chunked' in r
|
|
|
|
assert r.count(FILE_CONTENT) == 2
|
|
|
|
|
|
|
|
|
2021-05-27 19:30:36 +02:00
|
|
|
def test_chunked_stdin_multiple_chunks(httpbin_with_chunked_support):
|
2021-05-29 12:06:06 +02:00
|
|
|
data = FILE_PATH.read_bytes()
|
|
|
|
stdin_bytes = data + b'\n' + data
|
2020-10-25 20:39:01 +01:00
|
|
|
r = http(
|
|
|
|
'--verbose',
|
|
|
|
'--chunked',
|
2021-05-27 19:30:36 +02:00
|
|
|
httpbin_with_chunked_support + '/post',
|
2020-10-25 20:39:01 +01:00
|
|
|
env=MockEnvironment(
|
|
|
|
stdin=StdinBytesIO(stdin_bytes),
|
|
|
|
stdin_isatty=False,
|
|
|
|
stdout_isatty=True,
|
|
|
|
)
|
|
|
|
)
|
|
|
|
assert HTTP_OK in r
|
|
|
|
assert 'Transfer-Encoding: chunked' in r
|
|
|
|
assert r.count(FILE_CONTENT) == 4
|
|
|
|
|
|
|
|
|
2021-12-29 10:41:44 +01:00
|
|
|
def test_chunked_raw(httpbin_with_chunked_support):
|
|
|
|
r = http(
|
|
|
|
'--verbose',
|
|
|
|
'--chunked',
|
|
|
|
httpbin_with_chunked_support + '/post',
|
|
|
|
'--raw',
|
|
|
|
json.dumps({'a': 1, 'b': '2fafds', 'c': '🥰'}),
|
|
|
|
)
|
|
|
|
assert HTTP_OK in r
|
|
|
|
assert 'Transfer-Encoding: chunked' in r
|
|
|
|
|
|
|
|
|
2022-01-12 15:07:34 +01:00
|
|
|
@contextlib.contextmanager
|
|
|
|
def stdin_processes(httpbin, *args):
|
|
|
|
process_1 = subprocess.Popen(
|
|
|
|
[
|
|
|
|
"cat"
|
|
|
|
],
|
|
|
|
stdin=subprocess.PIPE,
|
|
|
|
stdout=subprocess.PIPE
|
|
|
|
)
|
|
|
|
process_2 = subprocess.Popen(
|
|
|
|
[
|
|
|
|
sys.executable,
|
|
|
|
main.__file__,
|
|
|
|
"POST",
|
|
|
|
httpbin + "/post",
|
|
|
|
*args
|
|
|
|
],
|
|
|
|
stdin=process_1.stdout,
|
|
|
|
stderr=subprocess.PIPE,
|
|
|
|
env={
|
|
|
|
**os.environ,
|
|
|
|
"HTTPIE_STDIN_READ_WARN_THRESHOLD": "0.1"
|
|
|
|
}
|
|
|
|
)
|
|
|
|
try:
|
|
|
|
yield process_1, process_2
|
|
|
|
finally:
|
|
|
|
process_1.terminate()
|
|
|
|
process_2.terminate()
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.parametrize("wait", (True, False))
|
|
|
|
@pytest.mark.skipif(is_windows, reason="Windows doesn't support select() calls into files")
|
|
|
|
def test_reading_from_stdin(httpbin, wait):
|
|
|
|
with stdin_processes(httpbin) as (process_1, process_2):
|
|
|
|
process_1.communicate(timeout=0.1, input=b"bleh")
|
|
|
|
# Since there is data, it doesn't matter if there
|
|
|
|
# you wait or not.
|
|
|
|
if wait:
|
2022-01-13 13:04:30 +01:00
|
|
|
time.sleep(1)
|
2022-01-12 15:07:34 +01:00
|
|
|
|
|
|
|
try:
|
|
|
|
_, errs = process_2.communicate(timeout=0.25)
|
|
|
|
except subprocess.TimeoutExpired:
|
|
|
|
errs = b''
|
|
|
|
|
|
|
|
assert b'> warning: no stdin data read in 0.1s' not in errs
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.skipif(is_windows, reason="Windows doesn't support select() calls into files")
|
|
|
|
def test_stdin_read_warning(httpbin):
|
|
|
|
with stdin_processes(httpbin) as (process_1, process_2):
|
|
|
|
# Wait before sending any data
|
2022-01-13 13:04:30 +01:00
|
|
|
time.sleep(1)
|
2022-01-12 15:07:34 +01:00
|
|
|
process_1.communicate(timeout=0.1, input=b"bleh\n")
|
|
|
|
|
|
|
|
try:
|
|
|
|
_, errs = process_2.communicate(timeout=0.25)
|
|
|
|
except subprocess.TimeoutExpired:
|
|
|
|
errs = b''
|
|
|
|
|
|
|
|
assert b'> warning: no stdin data read in 0.1s' in errs
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.skipif(is_windows, reason="Windows doesn't support select() calls into files")
|
|
|
|
def test_stdin_read_warning_with_quiet(httpbin):
|
|
|
|
with stdin_processes(httpbin, "-qq") as (process_1, process_2):
|
|
|
|
# Wait before sending any data
|
2022-01-13 13:04:30 +01:00
|
|
|
time.sleep(1)
|
2022-01-12 15:07:34 +01:00
|
|
|
process_1.communicate(timeout=0.1, input=b"bleh\n")
|
|
|
|
|
|
|
|
try:
|
|
|
|
_, errs = process_2.communicate(timeout=0.25)
|
|
|
|
except subprocess.TimeoutExpired:
|
|
|
|
errs = b''
|
|
|
|
|
|
|
|
assert b'> warning: no stdin data read in 0.1s' not in errs
|
|
|
|
|
|
|
|
|
2014-04-25 11:39:59 +02:00
|
|
|
class TestMultipartFormDataFileUpload:
|
2014-06-28 16:35:57 +02:00
|
|
|
|
|
|
|
def test_non_existent_file_raises_parse_error(self, httpbin):
|
2014-04-24 17:08:40 +02:00
|
|
|
with pytest.raises(ParseError):
|
2014-06-28 16:35:57 +02:00
|
|
|
http('--form',
|
|
|
|
'POST', httpbin.url + '/post', 'foo@/__does_not_exist__')
|
2014-04-24 14:07:31 +02:00
|
|
|
|
2014-06-28 16:35:57 +02:00
|
|
|
def test_upload_ok(self, httpbin):
|
|
|
|
r = http('--form', '--verbose', 'POST', httpbin.url + '/post',
|
2020-06-08 17:59:41 +02:00
|
|
|
f'test-file@{FILE_PATH_ARG}', 'foo=bar')
|
2014-04-24 14:58:15 +02:00
|
|
|
assert HTTP_OK in r
|
|
|
|
assert 'Content-Disposition: form-data; name="foo"' in r
|
|
|
|
assert 'Content-Disposition: form-data; name="test-file";' \
|
2020-06-08 17:59:41 +02:00
|
|
|
f' filename="{os.path.basename(FILE_PATH)}"' in r
|
2014-06-28 16:35:57 +02:00
|
|
|
assert FILE_CONTENT in r
|
2014-04-24 14:58:15 +02:00
|
|
|
assert '"foo": "bar"' in r
|
2016-02-28 08:45:45 +01:00
|
|
|
assert 'Content-Type: text/plain' in r
|
2014-04-24 14:07:31 +02:00
|
|
|
|
2014-10-20 14:40:55 +02:00
|
|
|
def test_upload_multiple_fields_with_the_same_name(self, httpbin):
|
|
|
|
r = http('--form', '--verbose', 'POST', httpbin.url + '/post',
|
2020-06-08 17:59:41 +02:00
|
|
|
f'test-file@{FILE_PATH_ARG}',
|
|
|
|
f'test-file@{FILE_PATH_ARG}')
|
2014-10-20 14:40:55 +02:00
|
|
|
assert HTTP_OK in r
|
|
|
|
assert r.count('Content-Disposition: form-data; name="test-file";'
|
2020-06-08 17:59:41 +02:00
|
|
|
f' filename="{os.path.basename(FILE_PATH)}"') == 2
|
2014-10-20 14:40:55 +02:00
|
|
|
# Should be 4, but is 3 because httpbin
|
|
|
|
# doesn't seem to support filed field lists
|
|
|
|
assert r.count(FILE_CONTENT) in [3, 4]
|
2016-02-28 08:45:45 +01:00
|
|
|
assert r.count('Content-Type: text/plain') == 2
|
2014-10-20 14:40:55 +02:00
|
|
|
|
2020-06-08 17:59:41 +02:00
|
|
|
def test_upload_custom_content_type(self, httpbin):
|
2020-08-15 17:50:00 +02:00
|
|
|
r = http(
|
|
|
|
'--form',
|
|
|
|
'--verbose',
|
|
|
|
httpbin.url + '/post',
|
|
|
|
f'test-file@{FILE_PATH_ARG};type=image/vnd.microsoft.icon'
|
|
|
|
)
|
2020-06-08 17:59:41 +02:00
|
|
|
assert HTTP_OK in r
|
|
|
|
# Content type is stripped from the filename
|
|
|
|
assert 'Content-Disposition: form-data; name="test-file";' \
|
|
|
|
f' filename="{os.path.basename(FILE_PATH)}"' in r
|
2020-08-15 17:50:00 +02:00
|
|
|
assert r.count(FILE_CONTENT) == 2
|
2020-06-08 17:59:41 +02:00
|
|
|
assert 'Content-Type: image/vnd.microsoft.icon' in r
|
|
|
|
|
2020-08-19 10:22:42 +02:00
|
|
|
def test_form_no_files_urlencoded(self, httpbin):
|
|
|
|
r = http(
|
|
|
|
'--form',
|
|
|
|
'--verbose',
|
|
|
|
httpbin.url + '/post',
|
|
|
|
'AAAA=AAA',
|
|
|
|
'BBB=BBB',
|
|
|
|
)
|
|
|
|
assert HTTP_OK in r
|
|
|
|
assert FORM_CONTENT_TYPE in r
|
|
|
|
|
2020-09-25 14:44:22 +02:00
|
|
|
def test_multipart(self, httpbin):
|
2020-08-19 10:22:42 +02:00
|
|
|
r = http(
|
|
|
|
'--verbose',
|
|
|
|
'--multipart',
|
|
|
|
httpbin.url + '/post',
|
|
|
|
'AAAA=AAA',
|
|
|
|
'BBB=BBB',
|
|
|
|
)
|
|
|
|
assert HTTP_OK in r
|
|
|
|
assert FORM_CONTENT_TYPE not in r
|
|
|
|
assert 'multipart/form-data' in r
|
|
|
|
|
|
|
|
def test_form_multipart_custom_boundary(self, httpbin):
|
|
|
|
boundary = 'HTTPIE_FTW'
|
|
|
|
r = http(
|
|
|
|
'--print=HB',
|
|
|
|
'--check-status',
|
|
|
|
'--multipart',
|
|
|
|
f'--boundary={boundary}',
|
|
|
|
httpbin.url + '/post',
|
|
|
|
'AAAA=AAA',
|
|
|
|
'BBB=BBB',
|
|
|
|
)
|
|
|
|
assert f'multipart/form-data; boundary={boundary}' in r
|
|
|
|
assert r.count(boundary) == 4
|
|
|
|
|
|
|
|
def test_multipart_custom_content_type_boundary_added(self, httpbin):
|
|
|
|
boundary = 'HTTPIE_FTW'
|
|
|
|
r = http(
|
|
|
|
'--print=HB',
|
|
|
|
'--check-status',
|
|
|
|
'--multipart',
|
|
|
|
f'--boundary={boundary}',
|
|
|
|
httpbin.url + '/post',
|
|
|
|
'Content-Type: multipart/magic',
|
|
|
|
'AAAA=AAA',
|
|
|
|
'BBB=BBB',
|
|
|
|
)
|
|
|
|
assert f'multipart/magic; boundary={boundary}' in r
|
|
|
|
assert r.count(boundary) == 4
|
|
|
|
|
|
|
|
def test_multipart_custom_content_type_boundary_preserved(self, httpbin):
|
|
|
|
# Allow explicit nonsense requests.
|
|
|
|
boundary_in_header = 'HEADER_BOUNDARY'
|
|
|
|
boundary_in_body = 'BODY_BOUNDARY'
|
|
|
|
r = http(
|
|
|
|
'--print=HB',
|
|
|
|
'--check-status',
|
|
|
|
'--multipart',
|
|
|
|
f'--boundary={boundary_in_body}',
|
|
|
|
httpbin.url + '/post',
|
|
|
|
f'Content-Type: multipart/magic; boundary={boundary_in_header}',
|
|
|
|
'AAAA=AAA',
|
|
|
|
'BBB=BBB',
|
|
|
|
)
|
|
|
|
assert f'multipart/magic; boundary={boundary_in_header}' in r
|
|
|
|
assert r.count(boundary_in_body) == 3
|
|
|
|
|
2021-05-27 19:30:36 +02:00
|
|
|
def test_multipart_chunked(self, httpbin_with_chunked_support):
|
2020-09-28 12:16:57 +02:00
|
|
|
r = http(
|
|
|
|
'--verbose',
|
|
|
|
'--multipart',
|
|
|
|
'--chunked',
|
2021-05-27 19:30:36 +02:00
|
|
|
httpbin_with_chunked_support + '/post',
|
2020-09-28 12:16:57 +02:00
|
|
|
'AAA=AAA',
|
|
|
|
)
|
|
|
|
assert 'Transfer-Encoding: chunked' in r
|
|
|
|
assert 'multipart/form-data' in r
|
|
|
|
assert 'name="AAA"' in r # in request
|
|
|
|
assert '"AAA": "AAA"', r # in response
|
|
|
|
|
2020-09-28 16:22:34 +02:00
|
|
|
def test_multipart_preserve_order(self, httpbin):
|
|
|
|
r = http(
|
|
|
|
'--form',
|
|
|
|
'--offline',
|
|
|
|
httpbin + '/post',
|
|
|
|
'text_field=foo',
|
|
|
|
f'file_field@{FILE_PATH_ARG}',
|
|
|
|
)
|
|
|
|
assert r.index('text_field') < r.index('file_field')
|
|
|
|
|
|
|
|
r = http(
|
|
|
|
'--form',
|
|
|
|
'--offline',
|
|
|
|
httpbin + '/post',
|
|
|
|
f'file_field@{FILE_PATH_ARG}',
|
|
|
|
'text_field=foo',
|
|
|
|
)
|
|
|
|
assert r.index('text_field') > r.index('file_field')
|
|
|
|
|
2014-04-24 14:07:31 +02:00
|
|
|
|
2014-04-25 11:39:59 +02:00
|
|
|
class TestRequestBodyFromFilePath:
|
2014-04-24 14:07:31 +02:00
|
|
|
"""
|
|
|
|
`http URL @file'
|
|
|
|
|
|
|
|
"""
|
2014-04-24 15:48:01 +02:00
|
|
|
|
2014-06-28 16:35:57 +02:00
|
|
|
def test_request_body_from_file_by_path(self, httpbin):
|
2020-09-28 12:16:57 +02:00
|
|
|
r = http(
|
|
|
|
'--verbose',
|
|
|
|
'POST', httpbin.url + '/post',
|
|
|
|
'@' + FILE_PATH_ARG,
|
|
|
|
)
|
|
|
|
assert HTTP_OK in r
|
|
|
|
assert r.count(FILE_CONTENT) == 2
|
|
|
|
assert '"Content-Type": "text/plain"' in r
|
|
|
|
|
2021-05-27 19:30:36 +02:00
|
|
|
def test_request_body_from_file_by_path_chunked(self, httpbin_with_chunked_support):
|
2020-09-28 12:16:57 +02:00
|
|
|
r = http(
|
|
|
|
'--verbose', '--chunked',
|
2021-05-27 19:30:36 +02:00
|
|
|
httpbin_with_chunked_support + '/post',
|
2020-09-28 12:16:57 +02:00
|
|
|
'@' + FILE_PATH_ARG,
|
|
|
|
)
|
2014-04-24 14:58:15 +02:00
|
|
|
assert HTTP_OK in r
|
2020-09-28 12:16:57 +02:00
|
|
|
assert 'Transfer-Encoding: chunked' in r
|
2014-04-24 14:58:15 +02:00
|
|
|
assert '"Content-Type": "text/plain"' in r
|
2020-09-28 12:16:57 +02:00
|
|
|
assert r.count(FILE_CONTENT) == 2
|
2014-04-24 14:07:31 +02:00
|
|
|
|
2014-06-28 16:35:57 +02:00
|
|
|
def test_request_body_from_file_by_path_with_explicit_content_type(
|
|
|
|
self, httpbin):
|
|
|
|
r = http('--verbose',
|
|
|
|
'POST', httpbin.url + '/post', '@' + FILE_PATH_ARG,
|
2021-08-05 20:58:43 +02:00
|
|
|
'Content-Type:text/plain; charset=UTF-8')
|
2014-04-24 14:58:15 +02:00
|
|
|
assert HTTP_OK in r
|
|
|
|
assert FILE_CONTENT in r
|
2021-08-05 20:58:43 +02:00
|
|
|
assert 'Content-Type: text/plain; charset=UTF-8' in r
|
2014-04-24 14:07:31 +02:00
|
|
|
|
2014-06-28 16:35:57 +02:00
|
|
|
def test_request_body_from_file_by_path_no_field_name_allowed(
|
|
|
|
self, httpbin):
|
2017-12-28 18:17:48 +01:00
|
|
|
env = MockEnvironment(stdin_isatty=True)
|
2014-06-28 16:35:57 +02:00
|
|
|
r = http('POST', httpbin.url + '/post', 'field-name@' + FILE_PATH_ARG,
|
2019-09-03 17:14:39 +02:00
|
|
|
env=env, tolerate_error_exit_status=True)
|
2014-04-24 14:58:15 +02:00
|
|
|
assert 'perhaps you meant --form?' in r.stderr
|
2014-04-24 14:07:31 +02:00
|
|
|
|
2014-06-28 16:35:57 +02:00
|
|
|
def test_request_body_from_file_by_path_no_data_items_allowed(
|
|
|
|
self, httpbin):
|
2017-12-28 18:17:48 +01:00
|
|
|
env = MockEnvironment(stdin_isatty=False)
|
2019-09-03 17:14:39 +02:00
|
|
|
r = http(
|
|
|
|
'POST',
|
|
|
|
httpbin.url + '/post',
|
|
|
|
'@' + FILE_PATH_ARG, 'foo=bar',
|
|
|
|
env=env,
|
|
|
|
tolerate_error_exit_status=True,
|
|
|
|
)
|
2020-01-23 15:54:43 +01:00
|
|
|
assert r.exit_status == ExitStatus.ERROR
|
2014-04-24 14:58:15 +02:00
|
|
|
assert 'cannot be mixed' in r.stderr
|
2021-04-15 09:35:50 +02:00
|
|
|
|
|
|
|
def test_multiple_request_bodies_from_file_by_path(self, httpbin):
|
|
|
|
env = MockEnvironment(stdin_isatty=True)
|
|
|
|
r = http(
|
|
|
|
'--verbose',
|
|
|
|
'POST', httpbin.url + '/post',
|
|
|
|
'@' + FILE_PATH_ARG,
|
|
|
|
'@' + FILE_PATH_ARG,
|
|
|
|
env=env,
|
|
|
|
tolerate_error_exit_status=True,
|
|
|
|
)
|
|
|
|
assert r.exit_status == ExitStatus.ERROR
|
|
|
|
assert 'from multiple files' in r.stderr
|