From: ambrozic Date: Thu, 18 Jul 2019 11:00:02 +0000 (+0100) Subject: Multipart data values encoding (#121) X-Git-Tag: 0.6.8~15 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=0e8dae4815327462345e36326f218a36dc6aa3aa;p=thirdparty%2Fhttpx.git Multipart data values encoding (#121) * multipart data values encoding (#119) * Update test_multipart.py --- diff --git a/http3/multipart.py b/http3/multipart.py index 53fb68fd..74805d14 100644 --- a/http3/multipart.py +++ b/http3/multipart.py @@ -15,7 +15,11 @@ class Field: class DataField(Field): - def __init__(self, name: str, value: str) -> None: + def __init__(self, name: str, value: typing.Union[str, bytes]) -> None: + if not isinstance(name, str): + raise TypeError("Invalid type for name. Expected str.") + if not isinstance(value, (str, bytes)): + raise TypeError("Invalid type for value. Expected str or bytes.") self.name = name self.value = value @@ -26,7 +30,9 @@ class DataField(Field): ) def render_data(self) -> bytes: - return self.value.encode("utf-8") + return ( + self.value if isinstance(self.value, bytes) else self.value.encode("utf-8") + ) class FileField(Field): @@ -73,7 +79,7 @@ class FileField(Field): def iter_fields(data: dict, files: dict) -> typing.Iterator[Field]: for name, value in data.items(): - if isinstance(value, list): + if isinstance(value, (list, dict)): for item in value: yield DataField(name=name, value=item) else: diff --git a/tests/test_multipart.py b/tests/test_multipart.py index fbccab76..4749ffa8 100644 --- a/tests/test_multipart.py +++ b/tests/test_multipart.py @@ -1,10 +1,16 @@ +import binascii import cgi import io +import os +from unittest import mock + +import pytest from http3 import ( CertTypes, Client, Dispatcher, + multipart, Request, Response, TimeoutTypes, @@ -23,11 +29,12 @@ class MockDispatch(Dispatcher): return Response(200, content=request.read()) -def test_multipart(): +@pytest.mark.parametrize(("value,output"), (("abc", b"abc"), (b"abc", b"abc"))) +def test_multipart(value, output): client = Client(dispatch=MockDispatch()) # Test with a single-value 'data' argument, and a plain file 'files' argument. - data = {"text": "abc"} + data = {"text": value} files = {"file": io.BytesIO(b"")} response = client.post("http://127.0.0.1:8000/", data=data, files=files) assert response.status_code == 200 @@ -41,10 +48,30 @@ def test_multipart(): # Note that the expected return type for text fields # appears to differs from 3.6 to 3.7+ - assert multipart["text"] == ["abc"] or multipart["text"] == [b"abc"] + assert multipart["text"] == [output.decode()] or multipart["text"] == [output] assert multipart["file"] == [b""] +@pytest.mark.parametrize(("key"), (b"abc", 1, 2.3, None)) +def test_multipart_invalid_key(key): + client = Client(dispatch=MockDispatch()) + data = {key: "abc"} + files = {"file": io.BytesIO(b"")} + with pytest.raises(TypeError) as e: + client.post("http://127.0.0.1:8000/", data=data, files=files) + assert "Invalid type for name" in str(e.value) + + +@pytest.mark.parametrize(("value"), (1, 2.3, None, [None, "abc"], {None: "abc"})) +def test_multipart_invalid_value(value): + client = Client(dispatch=MockDispatch()) + data = {"text": value} + files = {"file": io.BytesIO(b"")} + with pytest.raises(TypeError) as e: + client.post("http://127.0.0.1:8000/", data=data, files=files) + assert "Invalid type for value" in str(e.value) + + def test_multipart_file_tuple(): client = Client(dispatch=MockDispatch()) @@ -65,3 +92,33 @@ def test_multipart_file_tuple(): # appears to differs from 3.6 to 3.7+ assert multipart["text"] == ["abc"] or multipart["text"] == [b"abc"] assert multipart["file"] == [b""] + + +def test_multipart_encode(): + data = { + "a": "1", + "b": b"C", + "c": ["11", "22", "33"], + "d": {"ff": ["1", b"2", "3"], "fff": ["11", b"22", "33"]}, + "f": "", + } + files = {"file": ("name.txt", io.BytesIO(b""))} + + with mock.patch("os.urandom", return_value=os.urandom(16)): + boundary = binascii.hexlify(os.urandom(16)).decode("ascii") + body, content_type = multipart.multipart_encode(data=data, files=files) + assert content_type == f"multipart/form-data; boundary={boundary}" + assert body == ( + '--{0}\r\nContent-Disposition: form-data; name="a"\r\n\r\n1\r\n' + '--{0}\r\nContent-Disposition: form-data; name="b"\r\n\r\nC\r\n' + '--{0}\r\nContent-Disposition: form-data; name="c"\r\n\r\n11\r\n' + '--{0}\r\nContent-Disposition: form-data; name="c"\r\n\r\n22\r\n' + '--{0}\r\nContent-Disposition: form-data; name="c"\r\n\r\n33\r\n' + '--{0}\r\nContent-Disposition: form-data; name="d"\r\n\r\nff\r\n' + '--{0}\r\nContent-Disposition: form-data; name="d"\r\n\r\nfff\r\n' + '--{0}\r\nContent-Disposition: form-data; name="f"\r\n\r\n\r\n' + '--{0}\r\nContent-Disposition: form-data; name="file"; filename="name.txt"\r\n' + "Content-Type: text/plain\r\n\r\n\r\n" + "--{0}--\r\n" + "".format(boundary).encode("ascii") + )