You've already forked httpie-cli
mirror of
https://github.com/httpie/cli.git
synced 2025-08-10 22:42:05 +02:00
Add support for streamed uploads, --chunked, finish --multipart, etc.
Close #201 Close #753 Close #684 Close #903 Related: #452
This commit is contained in:
33
tests/fixtures/__init__.py
vendored
33
tests/fixtures/__init__.py
vendored
@@ -1,6 +1,5 @@
|
||||
"""Test data"""
|
||||
from os import path
|
||||
import codecs
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def patharg(path):
|
||||
@@ -9,32 +8,24 @@ def patharg(path):
|
||||
even in Windows paths.
|
||||
|
||||
"""
|
||||
return path.replace('\\', '\\\\\\')
|
||||
return str(path).replace('\\', '\\\\\\')
|
||||
|
||||
|
||||
FIXTURES_ROOT = path.join(path.abspath(path.dirname(__file__)))
|
||||
FILE_PATH = path.join(FIXTURES_ROOT, 'test.txt')
|
||||
JSON_FILE_PATH = path.join(FIXTURES_ROOT, 'test.json')
|
||||
BIN_FILE_PATH = path.join(FIXTURES_ROOT, 'test.bin')
|
||||
|
||||
FIXTURES_ROOT = Path(__file__).parent
|
||||
FILE_PATH = FIXTURES_ROOT / 'test.txt'
|
||||
JSON_FILE_PATH = FIXTURES_ROOT / 'test.json'
|
||||
BIN_FILE_PATH = FIXTURES_ROOT / 'test.bin'
|
||||
|
||||
FILE_PATH_ARG = patharg(FILE_PATH)
|
||||
BIN_FILE_PATH_ARG = patharg(BIN_FILE_PATH)
|
||||
JSON_FILE_PATH_ARG = patharg(JSON_FILE_PATH)
|
||||
|
||||
|
||||
with codecs.open(FILE_PATH, encoding='utf8') as f:
|
||||
# Strip because we don't want new lines in the data so that we can
|
||||
# easily count occurrences also when embedded in JSON (where the new
|
||||
# line would be escaped).
|
||||
FILE_CONTENT = f.read().strip()
|
||||
# Strip because we don't want new lines in the data so that we can
|
||||
# easily count occurrences also when embedded in JSON (where the new
|
||||
# line would be escaped).
|
||||
FILE_CONTENT = FILE_PATH.read_text().strip()
|
||||
|
||||
|
||||
with codecs.open(JSON_FILE_PATH, encoding='utf8') as f:
|
||||
JSON_FILE_CONTENT = f.read()
|
||||
|
||||
|
||||
with open(BIN_FILE_PATH, 'rb') as f:
|
||||
BIN_FILE_CONTENT = f.read()
|
||||
|
||||
JSON_FILE_CONTENT = JSON_FILE_PATH.read_text()
|
||||
BIN_FILE_CONTENT = BIN_FILE_PATH.read_bytes()
|
||||
UNICODE = FILE_CONTENT
|
||||
|
@@ -15,7 +15,7 @@ from httpie.cli import constants
|
||||
from httpie.cli.definition import parser
|
||||
from httpie.cli.argtypes import KeyValueArg, KeyValueArgType
|
||||
from httpie.cli.requestitems import RequestItems
|
||||
from utils import HTTP_OK, MockEnvironment, http
|
||||
from utils import HTTP_OK, MockEnvironment, StdinBytesIO, http
|
||||
|
||||
|
||||
class TestItemParsing:
|
||||
@@ -312,10 +312,11 @@ class TestNoOptions:
|
||||
class TestStdin:
|
||||
|
||||
def test_ignore_stdin(self, httpbin):
|
||||
with open(FILE_PATH) as f:
|
||||
env = MockEnvironment(stdin=f, stdin_isatty=False)
|
||||
r = http('--ignore-stdin', '--verbose', httpbin.url + '/get',
|
||||
env=env)
|
||||
env = MockEnvironment(
|
||||
stdin=StdinBytesIO(FILE_PATH.read_bytes()),
|
||||
stdin_isatty=False,
|
||||
)
|
||||
r = http('--ignore-stdin', '--verbose', httpbin.url + '/get', env=env)
|
||||
assert HTTP_OK in r
|
||||
assert 'GET /get HTTP' in r, "Don't default to POST."
|
||||
assert FILE_CONTENT not in r, "Don't send stdin data."
|
||||
|
@@ -12,7 +12,8 @@ import base64
|
||||
import zlib
|
||||
|
||||
from fixtures import FILE_PATH, FILE_CONTENT
|
||||
from utils import http, HTTP_OK, MockEnvironment
|
||||
from httpie.status import ExitStatus
|
||||
from utils import StdinBytesIO, http, HTTP_OK, MockEnvironment
|
||||
|
||||
|
||||
def assert_decompressed_equal(base64_compressed_data, expected_str):
|
||||
@@ -27,6 +28,20 @@ def assert_decompressed_equal(base64_compressed_data, expected_str):
|
||||
assert actual_str == expected_str
|
||||
|
||||
|
||||
def test_cannot_combine_compress_with_chunked(httpbin):
|
||||
r = http('--compress', '--chunked', httpbin.url + '/get',
|
||||
tolerate_error_exit_status=True)
|
||||
assert r.exit_status == ExitStatus.ERROR
|
||||
assert 'cannot combine --compress and --chunked' in r.stderr
|
||||
|
||||
|
||||
def test_cannot_combine_compress_with_multipart(httpbin):
|
||||
r = http('--compress', '--multipart', httpbin.url + '/get',
|
||||
tolerate_error_exit_status=True)
|
||||
assert r.exit_status == ExitStatus.ERROR
|
||||
assert 'cannot combine --compress and --multipart' in r.stderr
|
||||
|
||||
|
||||
def test_compress_skip_negative_ratio(httpbin_both):
|
||||
r = http(
|
||||
'--compress',
|
||||
@@ -78,15 +93,17 @@ def test_compress_form(httpbin_both):
|
||||
|
||||
|
||||
def test_compress_stdin(httpbin_both):
|
||||
with open(FILE_PATH) as f:
|
||||
env = MockEnvironment(stdin=f, stdin_isatty=False)
|
||||
r = http(
|
||||
'--compress',
|
||||
'--compress',
|
||||
'PATCH',
|
||||
httpbin_both + '/patch',
|
||||
env=env,
|
||||
)
|
||||
env = MockEnvironment(
|
||||
stdin=StdinBytesIO(FILE_PATH.read_bytes()),
|
||||
stdin_isatty=False,
|
||||
)
|
||||
r = http(
|
||||
'--compress',
|
||||
'--compress',
|
||||
'PATCH',
|
||||
httpbin_both + '/patch',
|
||||
env=env,
|
||||
)
|
||||
assert HTTP_OK in r
|
||||
assert r.json['headers']['Content-Encoding'] == 'deflate'
|
||||
assert_decompressed_equal(r.json['data'], FILE_CONTENT.strip())
|
||||
@@ -100,7 +117,7 @@ def test_compress_file(httpbin_both):
|
||||
'--compress',
|
||||
'PUT',
|
||||
httpbin_both + '/put',
|
||||
'file@' + FILE_PATH,
|
||||
f'file@{FILE_PATH}',
|
||||
)
|
||||
assert HTTP_OK in r
|
||||
assert r.json['headers']['Content-Encoding'] == 'deflate'
|
||||
|
@@ -2,6 +2,8 @@
|
||||
Tests for the provided defaults regarding HTTP method, and --json vs. --form.
|
||||
|
||||
"""
|
||||
from io import BytesIO
|
||||
|
||||
from httpie.client import JSON_ACCEPT
|
||||
from utils import MockEnvironment, http, HTTP_OK
|
||||
from fixtures import FILE_PATH
|
||||
@@ -44,9 +46,11 @@ class TestImplicitHTTPMethod:
|
||||
assert r.json['form'] == {'foo': 'bar'}
|
||||
|
||||
def test_implicit_POST_stdin(self, httpbin):
|
||||
with open(FILE_PATH) as f:
|
||||
env = MockEnvironment(stdin_isatty=False, stdin=f)
|
||||
r = http('--form', httpbin.url + '/post', env=env)
|
||||
env = MockEnvironment(
|
||||
stdin_isatty=False,
|
||||
stdin=BytesIO(FILE_PATH.read_bytes())
|
||||
)
|
||||
r = http('--form', httpbin.url + '/post', env=env)
|
||||
assert HTTP_OK in r
|
||||
|
||||
|
||||
|
@@ -9,7 +9,7 @@ import httpie.__main__
|
||||
from httpie.context import Environment
|
||||
from httpie.status import ExitStatus
|
||||
from httpie.cli.exceptions import ParseError
|
||||
from utils import MockEnvironment, http, HTTP_OK
|
||||
from utils import MockEnvironment, StdinBytesIO, http, HTTP_OK
|
||||
from fixtures import FILE_PATH, FILE_CONTENT
|
||||
|
||||
import httpie
|
||||
@@ -104,15 +104,17 @@ def test_POST_form_multiple_values(httpbin_both):
|
||||
|
||||
|
||||
def test_POST_stdin(httpbin_both):
|
||||
with open(FILE_PATH) as f:
|
||||
env = MockEnvironment(stdin=f, stdin_isatty=False)
|
||||
r = http('--form', 'POST', httpbin_both + '/post', env=env)
|
||||
env = MockEnvironment(
|
||||
stdin=StdinBytesIO(FILE_PATH.read_bytes()),
|
||||
stdin_isatty=False,
|
||||
)
|
||||
r = http('--form', 'POST', httpbin_both + '/post', env=env)
|
||||
assert HTTP_OK in r
|
||||
assert FILE_CONTENT in r
|
||||
|
||||
|
||||
def test_POST_file(httpbin_both):
|
||||
r = http('--form', 'POST', httpbin_both + '/post', 'file@' + FILE_PATH)
|
||||
r = http('--form', 'POST', httpbin_both + '/post', f'file@{FILE_PATH}')
|
||||
assert HTTP_OK in r
|
||||
assert FILE_CONTENT in r
|
||||
|
||||
@@ -127,10 +129,10 @@ def test_form_POST_file_redirected_stdin(httpbin):
|
||||
'--form',
|
||||
'POST',
|
||||
httpbin + '/post',
|
||||
'file@' + FILE_PATH,
|
||||
f'file@{FILE_PATH}',
|
||||
tolerate_error_exit_status=True,
|
||||
env=MockEnvironment(
|
||||
stdin=f,
|
||||
stdin=StdinBytesIO(FILE_PATH.read_bytes()),
|
||||
stdin_isatty=False,
|
||||
),
|
||||
)
|
||||
|
@@ -2,7 +2,7 @@ import pytest
|
||||
|
||||
from httpie.compat import is_windows
|
||||
from httpie.output.streams import BINARY_SUPPRESSED_NOTICE
|
||||
from utils import http, MockEnvironment
|
||||
from utils import StdinBytesIO, http, MockEnvironment
|
||||
from fixtures import BIN_FILE_CONTENT, BIN_FILE_PATH
|
||||
|
||||
|
||||
@@ -13,32 +13,37 @@ from fixtures import BIN_FILE_CONTENT, BIN_FILE_PATH
|
||||
reason='Pretty redirect not supported under Windows')
|
||||
def test_pretty_redirected_stream(httpbin):
|
||||
"""Test that --stream works with prettified redirected output."""
|
||||
with open(BIN_FILE_PATH, 'rb') as f:
|
||||
env = MockEnvironment(colors=256, stdin=f,
|
||||
stdin_isatty=False,
|
||||
stdout_isatty=False)
|
||||
r = http('--verbose', '--pretty=all', '--stream', 'GET',
|
||||
httpbin.url + '/get', env=env)
|
||||
env = MockEnvironment(
|
||||
colors=256,
|
||||
stdin=StdinBytesIO(BIN_FILE_PATH.read_bytes()),
|
||||
stdin_isatty=False,
|
||||
stdout_isatty=False,
|
||||
)
|
||||
r = http('--verbose', '--pretty=all', '--stream', 'GET',
|
||||
httpbin.url + '/get', env=env)
|
||||
assert BINARY_SUPPRESSED_NOTICE.decode() in r
|
||||
|
||||
|
||||
def test_encoded_stream(httpbin):
|
||||
"""Test that --stream works with non-prettified
|
||||
redirected terminal output."""
|
||||
with open(BIN_FILE_PATH, 'rb') as f:
|
||||
env = MockEnvironment(stdin=f, stdin_isatty=False)
|
||||
r = http('--pretty=none', '--stream', '--verbose', 'GET',
|
||||
httpbin.url + '/get', env=env)
|
||||
env = MockEnvironment(
|
||||
stdin=StdinBytesIO(BIN_FILE_PATH.read_bytes()),
|
||||
stdin_isatty=False,
|
||||
)
|
||||
r = http('--pretty=none', '--stream', '--verbose', 'GET',
|
||||
httpbin.url + '/get', env=env)
|
||||
assert BINARY_SUPPRESSED_NOTICE.decode() in r
|
||||
|
||||
|
||||
def test_redirected_stream(httpbin):
|
||||
"""Test that --stream works with non-prettified
|
||||
redirected terminal output."""
|
||||
with open(BIN_FILE_PATH, 'rb') as f:
|
||||
env = MockEnvironment(stdout_isatty=False,
|
||||
stdin_isatty=False,
|
||||
stdin=f)
|
||||
r = http('--pretty=none', '--stream', '--verbose', 'GET',
|
||||
httpbin.url + '/get', env=env)
|
||||
env = MockEnvironment(
|
||||
stdout_isatty=False,
|
||||
stdin_isatty=False,
|
||||
stdin=StdinBytesIO(BIN_FILE_PATH.read_bytes()),
|
||||
)
|
||||
r = http('--pretty=none', '--stream', '--verbose', 'GET',
|
||||
httpbin.url + '/get', env=env)
|
||||
assert BIN_FILE_CONTENT in r
|
||||
|
@@ -1,16 +1,57 @@
|
||||
import os
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
|
||||
from httpie.cli.exceptions import ParseError
|
||||
from httpie.client import FORM_CONTENT_TYPE
|
||||
from httpie.output.streams import LARGE_UPLOAD_SUPPRESSED_NOTICE
|
||||
from httpie.status import ExitStatus
|
||||
from utils import MockEnvironment, http, HTTP_OK
|
||||
from utils import (
|
||||
HTTPBIN_WITH_CHUNKED_SUPPORT, MockEnvironment, StdinBytesIO, http,
|
||||
HTTP_OK,
|
||||
)
|
||||
from fixtures import FILE_PATH_ARG, FILE_PATH, FILE_CONTENT
|
||||
|
||||
|
||||
def test_chunked_json():
|
||||
r = http(
|
||||
'--verbose',
|
||||
'--chunked',
|
||||
HTTPBIN_WITH_CHUNKED_SUPPORT + '/post',
|
||||
'hello=world',
|
||||
)
|
||||
assert HTTP_OK in r
|
||||
assert 'Transfer-Encoding: chunked' in r
|
||||
assert r.count('hello') == 3
|
||||
|
||||
|
||||
def test_chunked_form():
|
||||
r = http(
|
||||
'--verbose',
|
||||
'--chunked',
|
||||
'--form',
|
||||
HTTPBIN_WITH_CHUNKED_SUPPORT + '/post',
|
||||
'hello=world',
|
||||
)
|
||||
assert HTTP_OK in r
|
||||
assert 'Transfer-Encoding: chunked' in r
|
||||
assert r.count('hello') == 2
|
||||
|
||||
|
||||
def test_chunked_stdin():
|
||||
r = http(
|
||||
'--verbose',
|
||||
'--chunked',
|
||||
HTTPBIN_WITH_CHUNKED_SUPPORT + '/post',
|
||||
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
|
||||
|
||||
|
||||
class TestMultipartFormDataFileUpload:
|
||||
|
||||
def test_non_existent_file_raises_parse_error(self, httpbin):
|
||||
@@ -55,19 +96,6 @@ class TestMultipartFormDataFileUpload:
|
||||
assert r.count(FILE_CONTENT) == 2
|
||||
assert 'Content-Type: image/vnd.microsoft.icon' in r
|
||||
|
||||
@mock.patch('httpie.uploads.MULTIPART_UPLOAD_BUFFER', 0)
|
||||
def test_large_upload_display_suppressed(self, httpbin):
|
||||
r = http(
|
||||
'--form',
|
||||
'--verbose',
|
||||
httpbin.url + '/post',
|
||||
f'test-file@{FILE_PATH_ARG}',
|
||||
'foo=bar',
|
||||
)
|
||||
assert HTTP_OK in r
|
||||
assert r.count(FILE_CONTENT) == 1
|
||||
assert LARGE_UPLOAD_SUPPRESSED_NOTICE.decode() in r
|
||||
|
||||
def test_form_no_files_urlencoded(self, httpbin):
|
||||
r = http(
|
||||
'--form',
|
||||
@@ -91,33 +119,6 @@ class TestMultipartFormDataFileUpload:
|
||||
assert FORM_CONTENT_TYPE not in r
|
||||
assert 'multipart/form-data' in r
|
||||
|
||||
def test_multipart_too_large_for_terminal(self, httpbin):
|
||||
with mock.patch('httpie.uploads.MULTIPART_UPLOAD_BUFFER', 0):
|
||||
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_multipart_too_large_for_terminal_non_pretty(self, httpbin):
|
||||
with mock.patch('httpie.uploads.MULTIPART_UPLOAD_BUFFER', 0):
|
||||
r = http(
|
||||
'--verbose',
|
||||
'--multipart',
|
||||
'--pretty=none',
|
||||
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(
|
||||
@@ -164,6 +165,20 @@ class TestMultipartFormDataFileUpload:
|
||||
assert f'multipart/magic; boundary={boundary_in_header}' in r
|
||||
assert r.count(boundary_in_body) == 3
|
||||
|
||||
def test_multipart_chunked(self, httpbin):
|
||||
r = http(
|
||||
'--verbose',
|
||||
'--multipart',
|
||||
'--chunked',
|
||||
# '--offline',
|
||||
HTTPBIN_WITH_CHUNKED_SUPPORT + '/post',
|
||||
'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
|
||||
|
||||
|
||||
class TestRequestBodyFromFilePath:
|
||||
"""
|
||||
@@ -172,12 +187,26 @@ class TestRequestBodyFromFilePath:
|
||||
"""
|
||||
|
||||
def test_request_body_from_file_by_path(self, httpbin):
|
||||
r = http('--verbose',
|
||||
'POST', httpbin.url + '/post', '@' + FILE_PATH_ARG)
|
||||
r = http(
|
||||
'--verbose',
|
||||
'POST', httpbin.url + '/post',
|
||||
'@' + FILE_PATH_ARG,
|
||||
)
|
||||
assert HTTP_OK in r
|
||||
assert FILE_CONTENT in r, r
|
||||
assert r.count(FILE_CONTENT) == 2
|
||||
assert '"Content-Type": "text/plain"' in r
|
||||
|
||||
def test_request_body_from_file_by_path_chunked(self, httpbin):
|
||||
r = http(
|
||||
'--verbose', '--chunked',
|
||||
HTTPBIN_WITH_CHUNKED_SUPPORT + '/post',
|
||||
'@' + FILE_PATH_ARG,
|
||||
)
|
||||
assert HTTP_OK in r
|
||||
assert 'Transfer-Encoding: chunked' in r
|
||||
assert '"Content-Type": "text/plain"' in r
|
||||
assert r.count(FILE_CONTENT) == 2
|
||||
|
||||
def test_request_body_from_file_by_path_with_explicit_content_type(
|
||||
self, httpbin):
|
||||
r = http('--verbose',
|
||||
|
@@ -1,10 +1,10 @@
|
||||
# coding=utf-8
|
||||
"""Utilities for HTTPie test suite."""
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import json
|
||||
import tempfile
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
from typing import Optional, Union
|
||||
|
||||
@@ -14,6 +14,12 @@ 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://httpbin.org'
|
||||
|
||||
|
||||
TESTS_ROOT = Path(__file__).parent
|
||||
CRLF = '\r\n'
|
||||
COLOR = '\x1b['
|
||||
@@ -36,6 +42,11 @@ def add_auth(url, auth):
|
||||
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
|
||||
|
Reference in New Issue
Block a user