]> git.ipfire.org Git - thirdparty/httpx.git/commitdiff
Multipart data values encoding (#121)
authorambrozic <ambrozic@users.noreply.github.com>
Thu, 18 Jul 2019 11:00:02 +0000 (12:00 +0100)
committerTom Christie <tom@tomchristie.com>
Thu, 18 Jul 2019 11:00:02 +0000 (12:00 +0100)
* multipart data values encoding (#119)

* Update test_multipart.py

http3/multipart.py
tests/test_multipart.py

index 53fb68fd66b6b94e260676d4c13e4ae2ac91a19d..74805d14396a9da1e0311d8967221a1ff5b48910 100644 (file)
@@ -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:
index fbccab769a662c0acfa2a370dc83590df1bac9f3..4749ffa879faae2c141a3392727c688b1fbbfa80 100644 (file)
@@ -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"<file content>")}
     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"<file content>"]
 
 
+@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"<file content>")}
+    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"<file content>")}
+    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"<file content>"]
+
+
+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"<file content>"))}
+
+    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<file content>\r\n"
+            "--{0}--\r\n"
+            "".format(boundary).encode("ascii")
+        )