From c583e0f0ff8c3babba2059d0d9c98940d2480450 Mon Sep 17 00:00:00 2001 From: Justin Myers Date: Sun, 21 Apr 2024 15:38:34 -0700 Subject: [PATCH 1/5] Update files to chunk --- adafruit_requests.py | 160 ++++++++++-------- .../expanded/requests_wifi_file_upload.py | 4 +- 2 files changed, 96 insertions(+), 68 deletions(-) diff --git a/adafruit_requests.py b/adafruit_requests.py index a756d5e..9a69428 100644 --- a/adafruit_requests.py +++ b/adafruit_requests.py @@ -46,6 +46,8 @@ from adafruit_connection_manager import get_connection_manager +SEEK_END = 2 + if not sys.implementation.name == "circuitpython": from types import TracebackType from typing import Any, Dict, Optional, Type @@ -344,14 +346,6 @@ def iter_content(self, chunk_size: int = 1, decode_unicode: bool = False) -> byt self.close() -def _generate_boundary_str(): - hex_characters = "0123456789abcdef" - _boundary = "" - for _ in range(32): - _boundary += random.choice(hex_characters) - return _boundary - - class Session: """HTTP session that shares sockets and ssl context.""" @@ -366,6 +360,60 @@ def __init__( self._session_id = session_id self._last_response = None + def _build_boundary_data(self, files: dict): + boundary_string = self._build_boundary_string() + content_length = 0 + boundary_objects = [] + + for field_name, field_values in files.items(): + file_name = field_values[0] + file_data = field_values[1] + + boundary_data = f"--{boundary_string}\r\n" + boundary_data += f'Content-Disposition: form-data; name="{field_name}"; ' + if file_name is not None: + boundary_data += f'filename="{file_name}"' + boundary_data += "\r\n" + if len(field_values) >= 3: + file_content_type = field_values[2] + boundary_data += f"Content-Type: {file_content_type}\r\n" + if len(field_values) >= 4: + file_headers = field_values[3] + for file_header_key, file_header_value in file_headers.items(): + boundary_data += f"{file_header_key}: {file_header_value}\r\n" + boundary_data += "\r\n" + + content_length += len(boundary_data) + boundary_objects.append(boundary_data) + + if file_name is not None: + file_data.seek(0, SEEK_END) + content_length += file_data.tell() + file_data.seek(0) + boundary_objects.append(file_data) + boundary_data = "" + else: + boundary_data = file_data + + boundary_data += "\r\n" + content_length += len(boundary_data) + boundary_objects.append(boundary_data) + + boundary_data = f"--{boundary_string}--" + + content_length += len(boundary_data) + boundary_objects.append(boundary_data) + + return boundary_string, content_length, boundary_objects + + @staticmethod + def _build_boundary_string(): + hex_characters = "0123456789abcdef" + _boundary = "" + for _ in range(32): + _boundary += random.choice(hex_characters) + return _boundary + @staticmethod def _check_headers(headers: Dict[str, str]): if not isinstance(headers, dict): @@ -399,10 +447,31 @@ def _send(socket: SocketType, data: bytes): # Not EAGAIN; that was already handled. raise OSError(errno.EIO) total_sent += sent + return total_sent def _send_as_bytes(self, socket: SocketType, data: str): return self._send(socket, bytes(data, "utf-8")) + def _send_boundary_objects(self, socket: SocketType, boundary_objects: Any): + for boundary_object in boundary_objects: + if isinstance(boundary_object, str): + self._send_as_bytes(socket, boundary_object) + else: + chunk_size = 32 + if hasattr(boundary_object, "readinto"): + b = bytearray(chunk_size) + while True: + size = boundary_object.readinto(b) + if size == 0: + break + self._send(socket, b[:size]) + else: + while True: + b = boundary_object.read(chunk_size) + if len(b) == 0: + break + self._send(socket, b) + def _send_header(self, socket, header, value): if value is None: return @@ -440,6 +509,7 @@ def _send_request( # pylint: disable=too-many-arguments # If data is sent and it's a dict, set content type header and convert to string if data and isinstance(data, dict): + assert files is None content_type_header = "application/x-www-form-urlencoded" _post_data = "" for k in data: @@ -451,8 +521,18 @@ def _send_request( # pylint: disable=too-many-arguments if data and isinstance(data, str): data = bytes(data, "utf-8") - if data is None: - data = b"" + # If files are send, build data to send and calculate length + content_length = 0 + boundary_objects = None + if files and isinstance(files, dict): + boundary_string, content_length, boundary_objects = ( + self._build_boundary_data(files) + ) + content_type_header = f"multipart/form-data; boundary={boundary_string}" + else: + if data is None: + data = b"" + content_length = len(data) self._send_as_bytes(socket, method) self._send(socket, b" /") @@ -461,60 +541,6 @@ def _send_request( # pylint: disable=too-many-arguments # create lower-case supplied header list supplied_headers = {header.lower() for header in headers} - boundary_str = None - - # pylint: disable=too-many-nested-blocks - if files is not None and isinstance(files, dict): - boundary_str = _generate_boundary_str() - content_type_header = f"multipart/form-data; boundary={boundary_str}" - - for fieldname in files.keys(): - if not fieldname.endswith("-name"): - if files[fieldname][0] is not None: - file_content = files[fieldname][1].read() - - data += b"--" + boundary_str.encode() + b"\r\n" - data += ( - b'Content-Disposition: form-data; name="' - + fieldname.encode() - + b'"; filename="' - + files[fieldname][0].encode() - + b'"\r\n' - ) - if len(files[fieldname]) >= 3: - data += ( - b"Content-Type: " - + files[fieldname][2].encode() - + b"\r\n" - ) - if len(files[fieldname]) >= 4: - for custom_header_key in files[fieldname][3].keys(): - data += ( - custom_header_key.encode() - + b": " - + files[fieldname][3][custom_header_key].encode() - + b"\r\n" - ) - data += b"\r\n" - data += file_content + b"\r\n" - else: - # filename is None - data += b"--" + boundary_str.encode() + b"\r\n" - data += ( - b'Content-Disposition: form-data; name="' - + fieldname.encode() - + b'"; \r\n' - ) - if len(files[fieldname]) >= 3: - data += ( - b"Content-Type: " - + files[fieldname][2].encode() - + b"\r\n" - ) - data += b"\r\n" - data += files[fieldname][1].encode() + b"\r\n" - - data += b"--" + boundary_str.encode() + b"--" # Send headers if not "host" in supplied_headers: @@ -523,8 +549,8 @@ def _send_request( # pylint: disable=too-many-arguments self._send_header(socket, "User-Agent", "Adafruit CircuitPython") if content_type_header and not "content-type" in supplied_headers: self._send_header(socket, "Content-Type", content_type_header) - if data and not "content-length" in supplied_headers: - self._send_header(socket, "Content-Length", str(len(data))) + if (data or files) and not "content-length" in supplied_headers: + self._send_header(socket, "Content-Length", str(content_length)) # Iterate over keys to avoid tuple alloc for header in headers: self._send_header(socket, header, headers[header]) @@ -533,6 +559,8 @@ def _send_request( # pylint: disable=too-many-arguments # Send data if data: self._send(socket, bytes(data)) + elif boundary_objects: + self._send_boundary_objects(socket, boundary_objects) # pylint: disable=too-many-branches, too-many-statements, unused-argument, too-many-arguments, too-many-locals def request( diff --git a/examples/wifi/expanded/requests_wifi_file_upload.py b/examples/wifi/expanded/requests_wifi_file_upload.py index 3ceaef0..962f493 100644 --- a/examples/wifi/expanded/requests_wifi_file_upload.py +++ b/examples/wifi/expanded/requests_wifi_file_upload.py @@ -12,10 +12,10 @@ ssl_context = adafruit_connection_manager.get_radio_ssl_context(wifi.radio) requests = adafruit_requests.Session(pool, ssl_context) -with open("raspi_snip.png", "rb") as file_handle: +with open("requests_wifi_file_upload_image.png", "rb") as file_handle: files = { "file": ( - "raspi_snip.png", + "requests_wifi_file_upload_image.png", file_handle, "image/png", {"CustomHeader": "BlinkaRocks"}, From 8a14e8dd353ba8eb1b3731c62351a5a71f77a5a2 Mon Sep 17 00:00:00 2001 From: Justin Myers Date: Mon, 22 Apr 2024 10:09:55 -0700 Subject: [PATCH 2/5] Add tests --- adafruit_requests.py | 54 +++---- tests/files/green_red.png | Bin 0 -> 125 bytes tests/files/green_red.png.license | 2 + tests/files/red_green.png | Bin 0 -> 123 bytes tests/files/red_green.png.license | 2 + tests/header_test.py | 2 +- tests/method_files.py | 238 ++++++++++++++++++++++++++++++ tests/method_test.py | 10 +- 8 files changed, 279 insertions(+), 29 deletions(-) create mode 100644 tests/files/green_red.png create mode 100644 tests/files/green_red.png.license create mode 100644 tests/files/red_green.png create mode 100644 tests/files/red_green.png.license create mode 100644 tests/method_files.py diff --git a/adafruit_requests.py b/adafruit_requests.py index 9a69428..2edce6e 100644 --- a/adafruit_requests.py +++ b/adafruit_requests.py @@ -360,19 +360,19 @@ def __init__( self._session_id = session_id self._last_response = None - def _build_boundary_data(self, files: dict): + def _build_boundary_data(self, files: dict): # pylint: disable=too-many-locals boundary_string = self._build_boundary_string() content_length = 0 boundary_objects = [] for field_name, field_values in files.items(): file_name = field_values[0] - file_data = field_values[1] + file_handle = field_values[1] boundary_data = f"--{boundary_string}\r\n" - boundary_data += f'Content-Disposition: form-data; name="{field_name}"; ' + boundary_data += f'Content-Disposition: form-data; name="{field_name}"' if file_name is not None: - boundary_data += f'filename="{file_name}"' + boundary_data += f'; filename="{file_name}"' boundary_data += "\r\n" if len(field_values) >= 3: file_content_type = field_values[2] @@ -386,20 +386,30 @@ def _build_boundary_data(self, files: dict): content_length += len(boundary_data) boundary_objects.append(boundary_data) - if file_name is not None: - file_data.seek(0, SEEK_END) - content_length += file_data.tell() - file_data.seek(0) - boundary_objects.append(file_data) + if hasattr(file_handle, "read"): + is_binary = False + try: + content = file_handle.read(1) + is_binary = isinstance(content, bytes) + except UnicodeError: + is_binary = False + + if not is_binary: + raise AttributeError("Files must be opened in binary mode") + + file_handle.seek(0, SEEK_END) + content_length += file_handle.tell() + file_handle.seek(0) + boundary_objects.append(file_handle) boundary_data = "" else: - boundary_data = file_data + boundary_data = file_handle boundary_data += "\r\n" content_length += len(boundary_data) boundary_objects.append(boundary_data) - boundary_data = f"--{boundary_string}--" + boundary_data = f"--{boundary_string}--\r\n" content_length += len(boundary_data) boundary_objects.append(boundary_data) @@ -417,7 +427,7 @@ def _build_boundary_string(): @staticmethod def _check_headers(headers: Dict[str, str]): if not isinstance(headers, dict): - raise AttributeError("headers must be in dict format") + raise AttributeError("Headers must be in dict format") for key, value in headers.items(): if isinstance(value, (str, bytes)) or value is None: @@ -447,7 +457,6 @@ def _send(socket: SocketType, data: bytes): # Not EAGAIN; that was already handled. raise OSError(errno.EIO) total_sent += sent - return total_sent def _send_as_bytes(self, socket: SocketType, data: str): return self._send(socket, bytes(data, "utf-8")) @@ -458,19 +467,12 @@ def _send_boundary_objects(self, socket: SocketType, boundary_objects: Any): self._send_as_bytes(socket, boundary_object) else: chunk_size = 32 - if hasattr(boundary_object, "readinto"): - b = bytearray(chunk_size) - while True: - size = boundary_object.readinto(b) - if size == 0: - break - self._send(socket, b[:size]) - else: - while True: - b = boundary_object.read(chunk_size) - if len(b) == 0: - break - self._send(socket, b) + b = bytearray(chunk_size) + while True: + size = boundary_object.readinto(b) + if size == 0: + break + self._send(socket, b[:size]) def _send_header(self, socket, header, value): if value is None: diff --git a/tests/files/green_red.png b/tests/files/green_red.png new file mode 100644 index 0000000000000000000000000000000000000000..7d8ddb37c20bcff4cbb43154844f21966c74bc44 GIT binary patch literal 125 zcmeAS@N?(olHy`uVBq!ia0vp^Od!kwBL7~QRScvUi-X*q7}lMWc?smOq&xaLGB9lH z=l+w(3gmMZctipf@f`+X#^d=bQhmdKI;Vst0G}=&`v3p{ literal 0 HcmV?d00001 diff --git a/tests/files/red_green.png.license b/tests/files/red_green.png.license new file mode 100644 index 0000000..d41b03e --- /dev/null +++ b/tests/files/red_green.png.license @@ -0,0 +1,2 @@ +# SPDX-FileCopyrightText: 2024 Justin Myers +# SPDX-License-Identifier: Unlicense diff --git a/tests/header_test.py b/tests/header_test.py index 8bcb354..ddfd61a 100644 --- a/tests/header_test.py +++ b/tests/header_test.py @@ -11,7 +11,7 @@ def test_check_headers_not_dict(requests): with pytest.raises(AttributeError) as context: requests._check_headers("") - assert "headers must be in dict format" in str(context) + assert "Headers must be in dict format" in str(context) def test_check_headers_not_valid(requests): diff --git a/tests/method_files.py b/tests/method_files.py new file mode 100644 index 0000000..1e28242 --- /dev/null +++ b/tests/method_files.py @@ -0,0 +1,238 @@ +# SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries +# +# SPDX-License-Identifier: Unlicense + +""" Post Tests """ +# pylint: disable=line-too-long + +from unittest import mock + +import mocket +import pytest + +""" +For building tests, you can use CPython requests with logging to see what should actuall get sent. + +import logging +import http.client +import requests + +def httpclient_logging_patch(level=logging.DEBUG): + logging.basicConfig(level=level) + + httpclient_logger = logging.getLogger("http.client") + + def httpclient_log(*args): + httpclient_logger.log(level, " ".join(args)) + + http.client.print = httpclient_log + http.client.HTTPConnection.debuglevel = 1 + +httpclient_logging_patch() + +URL = "https://httpbin.org/post" + +with open("tests/files/red_green.png", "rb") as file_1: + file_data = { + "file_1": ( + "red_green.png", + file_1, + "image/png", + { + "Key_1": "Value 1", + "Key_2": "Value 2", + "Key_3": "Value 3", + }, + ), + } + + print(requests.post(URL, files=file_data).json()) +""" + + +def test_post_files_text(sock, requests): + file_data = { + "key_4": (None, "Value 5"), + } + + requests._build_boundary_string = mock.Mock( + return_value="8cd45159712eeb914c049c717d3f4d75" + ) + requests.post("http://" + mocket.MOCK_HOST_1 + "/post", files=file_data) + + sock.connect.assert_called_once_with((mocket.MOCK_POOL_IP, 80)) + sock.send.assert_has_calls( + [ + mock.call(b"Content-Type"), + mock.call(b": "), + mock.call( + b"multipart/form-data; boundary=8cd45159712eeb914c049c717d3f4d75" + ), + mock.call(b"\r\n"), + ] + ) + sock.send.assert_has_calls( + [ + mock.call( + b'--8cd45159712eeb914c049c717d3f4d75\r\nContent-Disposition: form-data; name="key_4"\r\n\r\n' + ), + mock.call(b"Value 5\r\n"), + mock.call(b"--8cd45159712eeb914c049c717d3f4d75--\r\n"), + ] + ) + + +def test_post_files_file(sock, requests): + with open("tests/files/red_green.png", "rb") as file_1: + file_data = { + "file_1": ( + "red_green.png", + file_1, + "image/png", + { + "Key_1": "Value 1", + "Key_2": "Value 2", + "Key_3": "Value 3", + }, + ), + } + + requests._build_boundary_string = mock.Mock( + return_value="e663061c5bfcc53139c8f68d016cbef3" + ) + requests.post("http://" + mocket.MOCK_HOST_1 + "/post", files=file_data) + + sock.connect.assert_called_once_with((mocket.MOCK_POOL_IP, 80)) + sock.send.assert_has_calls( + [ + mock.call(b"Content-Type"), + mock.call(b": "), + mock.call( + b"multipart/form-data; boundary=e663061c5bfcc53139c8f68d016cbef3" + ), + mock.call(b"\r\n"), + ] + ) + sock.send.assert_has_calls( + [ + mock.call( + b'--e663061c5bfcc53139c8f68d016cbef3\r\nContent-Disposition: form-data; name="file_1"; filename="red_green.png"\r\nContent-Type: image/png\r\nKey_1: Value 1\r\nKey_2: Value 2\r\nKey_3: Value 3\r\n\r\n' + ), + mock.call( + b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x02\x00\x00\x00\x02\x08\x02\x00\x00\x00\xfd\xd4\x9a" + ), + mock.call( + b"s\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\x04gAMA\x00\x00\xb1\x8f\x0b\xfca\x05\x00\x00" + ), + mock.call( + b"\x00\tpHYs\x00\x00\x0e\xc3\x00\x00\x0e\xc3\x01\xc7o\xa8d\x00\x00\x00\x10IDAT\x18Wc\xf8\xcf" + ), + mock.call( + b"\xc0\x00\xc5\xff\x19\x18\x00\x1d\xf0\x03\xfd\x8fk\x13|\x00\x00\x00\x00IEND\xaeB`\x82" + ), + mock.call(b"\r\n"), + mock.call(b"--e663061c5bfcc53139c8f68d016cbef3--\r\n"), + ] + ) + + +def test_post_files_complex(sock, requests): + with open("tests/files/red_green.png", "rb") as file_1, open( + "tests/files/green_red.png", "rb" + ) as file_2: + file_data = { + "file_1": ( + "red_green.png", + file_1, + "image/png", + { + "Key_1": "Value 1", + "Key_2": "Value 2", + "Key_3": "Value 3", + }, + ), + "key_4": (None, "Value 5"), + "file_2": ( + "green_red.png", + file_2, + "image/png", + ), + "key_6": (None, "Value 6"), + } + + requests._build_boundary_string = mock.Mock( + return_value="e663061c5bfcc53139c8f68d016cbef3" + ) + requests.post("http://" + mocket.MOCK_HOST_1 + "/post", files=file_data) + + sock.connect.assert_called_once_with((mocket.MOCK_POOL_IP, 80)) + sock.send.assert_has_calls( + [ + mock.call(b"Content-Type"), + mock.call(b": "), + mock.call( + b"multipart/form-data; boundary=e663061c5bfcc53139c8f68d016cbef3" + ), + mock.call(b"\r\n"), + ] + ) + sock.send.assert_has_calls( + [ + mock.call( + b'--e663061c5bfcc53139c8f68d016cbef3\r\nContent-Disposition: form-data; name="file_1"; filename="red_green.png"\r\nContent-Type: image/png\r\nKey_1: Value 1\r\nKey_2: Value 2\r\nKey_3: Value 3\r\n\r\n' + ), + mock.call( + b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x02\x00\x00\x00\x02\x08\x02\x00\x00\x00\xfd\xd4\x9a" + ), + mock.call( + b"s\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\x04gAMA\x00\x00\xb1\x8f\x0b\xfca\x05\x00\x00" + ), + mock.call( + b"\x00\tpHYs\x00\x00\x0e\xc3\x00\x00\x0e\xc3\x01\xc7o\xa8d\x00\x00\x00\x10IDAT\x18Wc\xf8\xcf" + ), + mock.call( + b"\xc0\x00\xc5\xff\x19\x18\x00\x1d\xf0\x03\xfd\x8fk\x13|\x00\x00\x00\x00IEND\xaeB`\x82" + ), + mock.call(b"\r\n"), + mock.call( + b'--e663061c5bfcc53139c8f68d016cbef3\r\nContent-Disposition: form-data; name="key_4"\r\n\r\n' + ), + mock.call(b"Value 5\r\n"), + mock.call( + b'--e663061c5bfcc53139c8f68d016cbef3\r\nContent-Disposition: form-data; name="file_2"; filename="green_red.png"\r\nContent-Type: image/png\r\n\r\n' + ), + mock.call( + b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x02\x00\x00\x00\x02\x08\x02\x00\x00\x00\xfd\xd4\x9a" + ), + mock.call( + b"s\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\x04gAMA\x00\x00\xb1\x8f\x0b\xfca\x05\x00\x00" + ), + mock.call( + b"\x00\tpHYs\x00\x00\x0e\xc3\x00\x00\x0e\xc3\x01\xc7o\xa8d\x00\x00\x00\x12IDAT\x18Wc`\xf8" + ), + mock.call( + b'\x0f\x84 \x92\x81\xe1?\x03\x00\x1d\xf0\x03\xfd\x88"uS\x00\x00\x00\x00IEND\xaeB`\x82' + ), + mock.call(b"\r\n"), + mock.call( + b'--e663061c5bfcc53139c8f68d016cbef3\r\nContent-Disposition: form-data; name="key_6"\r\n\r\n' + ), + mock.call(b"Value 6\r\n"), + mock.call(b"--e663061c5bfcc53139c8f68d016cbef3--\r\n"), + ] + ) + + +def test_post_files_not_binary(requests): + with open("tests/files/red_green.png", "r") as file_1: + file_data = { + "file_1": ( + "red_green.png", + file_1, + "image/png", + ), + } + + with pytest.raises(AttributeError) as context: + requests.post("http://" + mocket.MOCK_HOST_1 + "/post", files=file_data) + assert "Files must be opened in binary mode" in str(context) diff --git a/tests/method_test.py b/tests/method_test.py index d75e754..1cda6c2 100644 --- a/tests/method_test.py +++ b/tests/method_test.py @@ -52,7 +52,10 @@ def test_post_string(sock, requests): def test_post_form(sock, requests): - data = {"Date": "July 25, 2019", "Time": "12:00"} + data = { + "Date": "July 25, 2019", + "Time": "12:00", + } requests.post("http://" + mocket.MOCK_HOST_1 + "/post", data=data) sock.connect.assert_called_once_with((mocket.MOCK_POOL_IP, 80)) sock.send.assert_has_calls( @@ -67,7 +70,10 @@ def test_post_form(sock, requests): def test_post_json(sock, requests): - json_data = {"Date": "July 25, 2019", "Time": "12:00"} + json_data = { + "Date": "July 25, 2019", + "Time": "12:00", + } requests.post("http://" + mocket.MOCK_HOST_1 + "/post", json=json_data) sock.connect.assert_called_once_with((mocket.MOCK_POOL_IP, 80)) sock.send.assert_has_calls( From d5614b2d32dfdd3774aaf10e44b372a68744d026 Mon Sep 17 00:00:00 2001 From: Justin Myers Date: Mon, 22 Apr 2024 10:25:34 -0700 Subject: [PATCH 3/5] Fix test file name --- tests/{method_files.py => files_test.py} | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) rename tests/{method_files.py => files_test.py} (93%) diff --git a/tests/method_files.py b/tests/files_test.py similarity index 93% rename from tests/method_files.py rename to tests/files_test.py index 1e28242..4985cac 100644 --- a/tests/method_files.py +++ b/tests/files_test.py @@ -71,6 +71,14 @@ def test_post_files_text(sock, requests): mock.call(b"\r\n"), ] ) + sock.send.assert_has_calls( + [ + mock.call(b"Content-Length"), + mock.call(b": "), + mock.call(b"131"), + mock.call(b"\r\n"), + ] + ) sock.send.assert_has_calls( [ mock.call( @@ -113,6 +121,14 @@ def test_post_files_file(sock, requests): mock.call(b"\r\n"), ] ) + sock.send.assert_has_calls( + [ + mock.call(b"Content-Length"), + mock.call(b": "), + mock.call(b"347"), + mock.call(b"\r\n"), + ] + ) sock.send.assert_has_calls( [ mock.call( @@ -176,6 +192,14 @@ def test_post_files_complex(sock, requests): mock.call(b"\r\n"), ] ) + sock.send.assert_has_calls( + [ + mock.call(b"Content-Length"), + mock.call(b": "), + mock.call(b"796"), + mock.call(b"\r\n"), + ] + ) sock.send.assert_has_calls( [ mock.call( From a2b43acbff84adb4bd3731e86ae3e5f55b7993d2 Mon Sep 17 00:00:00 2001 From: Justin Myers Date: Wed, 24 Apr 2024 21:44:25 -0700 Subject: [PATCH 4/5] Update example to match others --- examples/wifi/expanded/requests_wifi_file_upload.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/wifi/expanded/requests_wifi_file_upload.py b/examples/wifi/expanded/requests_wifi_file_upload.py index 962f493..bd9ac2a 100644 --- a/examples/wifi/expanded/requests_wifi_file_upload.py +++ b/examples/wifi/expanded/requests_wifi_file_upload.py @@ -23,5 +23,5 @@ "othervalue": (None, "HelloWorld"), } - with requests.post(URL, files=files) as resp: - print(resp.content) + with requests.post(URL, files=files) as response: + print(response.content) From 913c4c856f859d678856ec5a7bfc4b643d43f786 Mon Sep 17 00:00:00 2001 From: foamyguy Date: Fri, 26 Apr 2024 20:54:02 -0500 Subject: [PATCH 5/5] remove files test for now --- tests/files/green_red.png | Bin 125 -> 0 bytes tests/files/green_red.png.license | 2 - tests/files/red_green.png | Bin 123 -> 0 bytes tests/files/red_green.png.license | 2 - tests/files_test.py | 262 ------------------------------ 5 files changed, 266 deletions(-) delete mode 100644 tests/files/green_red.png delete mode 100644 tests/files/green_red.png.license delete mode 100644 tests/files/red_green.png delete mode 100644 tests/files/red_green.png.license delete mode 100644 tests/files_test.py diff --git a/tests/files/green_red.png b/tests/files/green_red.png deleted file mode 100644 index 7d8ddb37c20bcff4cbb43154844f21966c74bc44..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 125 zcmeAS@N?(olHy`uVBq!ia0vp^Od!kwBL7~QRScvUi-X*q7}lMWc?smOq&xaLGB9lH z=l+w(3gmMZctipf@f`+X#^d=bQhmdKI;Vst0G}=&`v3p{ diff --git a/tests/files/red_green.png.license b/tests/files/red_green.png.license deleted file mode 100644 index d41b03e..0000000 --- a/tests/files/red_green.png.license +++ /dev/null @@ -1,2 +0,0 @@ -# SPDX-FileCopyrightText: 2024 Justin Myers -# SPDX-License-Identifier: Unlicense diff --git a/tests/files_test.py b/tests/files_test.py deleted file mode 100644 index 4985cac..0000000 --- a/tests/files_test.py +++ /dev/null @@ -1,262 +0,0 @@ -# SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries -# -# SPDX-License-Identifier: Unlicense - -""" Post Tests """ -# pylint: disable=line-too-long - -from unittest import mock - -import mocket -import pytest - -""" -For building tests, you can use CPython requests with logging to see what should actuall get sent. - -import logging -import http.client -import requests - -def httpclient_logging_patch(level=logging.DEBUG): - logging.basicConfig(level=level) - - httpclient_logger = logging.getLogger("http.client") - - def httpclient_log(*args): - httpclient_logger.log(level, " ".join(args)) - - http.client.print = httpclient_log - http.client.HTTPConnection.debuglevel = 1 - -httpclient_logging_patch() - -URL = "https://httpbin.org/post" - -with open("tests/files/red_green.png", "rb") as file_1: - file_data = { - "file_1": ( - "red_green.png", - file_1, - "image/png", - { - "Key_1": "Value 1", - "Key_2": "Value 2", - "Key_3": "Value 3", - }, - ), - } - - print(requests.post(URL, files=file_data).json()) -""" - - -def test_post_files_text(sock, requests): - file_data = { - "key_4": (None, "Value 5"), - } - - requests._build_boundary_string = mock.Mock( - return_value="8cd45159712eeb914c049c717d3f4d75" - ) - requests.post("http://" + mocket.MOCK_HOST_1 + "/post", files=file_data) - - sock.connect.assert_called_once_with((mocket.MOCK_POOL_IP, 80)) - sock.send.assert_has_calls( - [ - mock.call(b"Content-Type"), - mock.call(b": "), - mock.call( - b"multipart/form-data; boundary=8cd45159712eeb914c049c717d3f4d75" - ), - mock.call(b"\r\n"), - ] - ) - sock.send.assert_has_calls( - [ - mock.call(b"Content-Length"), - mock.call(b": "), - mock.call(b"131"), - mock.call(b"\r\n"), - ] - ) - sock.send.assert_has_calls( - [ - mock.call( - b'--8cd45159712eeb914c049c717d3f4d75\r\nContent-Disposition: form-data; name="key_4"\r\n\r\n' - ), - mock.call(b"Value 5\r\n"), - mock.call(b"--8cd45159712eeb914c049c717d3f4d75--\r\n"), - ] - ) - - -def test_post_files_file(sock, requests): - with open("tests/files/red_green.png", "rb") as file_1: - file_data = { - "file_1": ( - "red_green.png", - file_1, - "image/png", - { - "Key_1": "Value 1", - "Key_2": "Value 2", - "Key_3": "Value 3", - }, - ), - } - - requests._build_boundary_string = mock.Mock( - return_value="e663061c5bfcc53139c8f68d016cbef3" - ) - requests.post("http://" + mocket.MOCK_HOST_1 + "/post", files=file_data) - - sock.connect.assert_called_once_with((mocket.MOCK_POOL_IP, 80)) - sock.send.assert_has_calls( - [ - mock.call(b"Content-Type"), - mock.call(b": "), - mock.call( - b"multipart/form-data; boundary=e663061c5bfcc53139c8f68d016cbef3" - ), - mock.call(b"\r\n"), - ] - ) - sock.send.assert_has_calls( - [ - mock.call(b"Content-Length"), - mock.call(b": "), - mock.call(b"347"), - mock.call(b"\r\n"), - ] - ) - sock.send.assert_has_calls( - [ - mock.call( - b'--e663061c5bfcc53139c8f68d016cbef3\r\nContent-Disposition: form-data; name="file_1"; filename="red_green.png"\r\nContent-Type: image/png\r\nKey_1: Value 1\r\nKey_2: Value 2\r\nKey_3: Value 3\r\n\r\n' - ), - mock.call( - b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x02\x00\x00\x00\x02\x08\x02\x00\x00\x00\xfd\xd4\x9a" - ), - mock.call( - b"s\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\x04gAMA\x00\x00\xb1\x8f\x0b\xfca\x05\x00\x00" - ), - mock.call( - b"\x00\tpHYs\x00\x00\x0e\xc3\x00\x00\x0e\xc3\x01\xc7o\xa8d\x00\x00\x00\x10IDAT\x18Wc\xf8\xcf" - ), - mock.call( - b"\xc0\x00\xc5\xff\x19\x18\x00\x1d\xf0\x03\xfd\x8fk\x13|\x00\x00\x00\x00IEND\xaeB`\x82" - ), - mock.call(b"\r\n"), - mock.call(b"--e663061c5bfcc53139c8f68d016cbef3--\r\n"), - ] - ) - - -def test_post_files_complex(sock, requests): - with open("tests/files/red_green.png", "rb") as file_1, open( - "tests/files/green_red.png", "rb" - ) as file_2: - file_data = { - "file_1": ( - "red_green.png", - file_1, - "image/png", - { - "Key_1": "Value 1", - "Key_2": "Value 2", - "Key_3": "Value 3", - }, - ), - "key_4": (None, "Value 5"), - "file_2": ( - "green_red.png", - file_2, - "image/png", - ), - "key_6": (None, "Value 6"), - } - - requests._build_boundary_string = mock.Mock( - return_value="e663061c5bfcc53139c8f68d016cbef3" - ) - requests.post("http://" + mocket.MOCK_HOST_1 + "/post", files=file_data) - - sock.connect.assert_called_once_with((mocket.MOCK_POOL_IP, 80)) - sock.send.assert_has_calls( - [ - mock.call(b"Content-Type"), - mock.call(b": "), - mock.call( - b"multipart/form-data; boundary=e663061c5bfcc53139c8f68d016cbef3" - ), - mock.call(b"\r\n"), - ] - ) - sock.send.assert_has_calls( - [ - mock.call(b"Content-Length"), - mock.call(b": "), - mock.call(b"796"), - mock.call(b"\r\n"), - ] - ) - sock.send.assert_has_calls( - [ - mock.call( - b'--e663061c5bfcc53139c8f68d016cbef3\r\nContent-Disposition: form-data; name="file_1"; filename="red_green.png"\r\nContent-Type: image/png\r\nKey_1: Value 1\r\nKey_2: Value 2\r\nKey_3: Value 3\r\n\r\n' - ), - mock.call( - b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x02\x00\x00\x00\x02\x08\x02\x00\x00\x00\xfd\xd4\x9a" - ), - mock.call( - b"s\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\x04gAMA\x00\x00\xb1\x8f\x0b\xfca\x05\x00\x00" - ), - mock.call( - b"\x00\tpHYs\x00\x00\x0e\xc3\x00\x00\x0e\xc3\x01\xc7o\xa8d\x00\x00\x00\x10IDAT\x18Wc\xf8\xcf" - ), - mock.call( - b"\xc0\x00\xc5\xff\x19\x18\x00\x1d\xf0\x03\xfd\x8fk\x13|\x00\x00\x00\x00IEND\xaeB`\x82" - ), - mock.call(b"\r\n"), - mock.call( - b'--e663061c5bfcc53139c8f68d016cbef3\r\nContent-Disposition: form-data; name="key_4"\r\n\r\n' - ), - mock.call(b"Value 5\r\n"), - mock.call( - b'--e663061c5bfcc53139c8f68d016cbef3\r\nContent-Disposition: form-data; name="file_2"; filename="green_red.png"\r\nContent-Type: image/png\r\n\r\n' - ), - mock.call( - b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x02\x00\x00\x00\x02\x08\x02\x00\x00\x00\xfd\xd4\x9a" - ), - mock.call( - b"s\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\x04gAMA\x00\x00\xb1\x8f\x0b\xfca\x05\x00\x00" - ), - mock.call( - b"\x00\tpHYs\x00\x00\x0e\xc3\x00\x00\x0e\xc3\x01\xc7o\xa8d\x00\x00\x00\x12IDAT\x18Wc`\xf8" - ), - mock.call( - b'\x0f\x84 \x92\x81\xe1?\x03\x00\x1d\xf0\x03\xfd\x88"uS\x00\x00\x00\x00IEND\xaeB`\x82' - ), - mock.call(b"\r\n"), - mock.call( - b'--e663061c5bfcc53139c8f68d016cbef3\r\nContent-Disposition: form-data; name="key_6"\r\n\r\n' - ), - mock.call(b"Value 6\r\n"), - mock.call(b"--e663061c5bfcc53139c8f68d016cbef3--\r\n"), - ] - ) - - -def test_post_files_not_binary(requests): - with open("tests/files/red_green.png", "r") as file_1: - file_data = { - "file_1": ( - "red_green.png", - file_1, - "image/png", - ), - } - - with pytest.raises(AttributeError) as context: - requests.post("http://" + mocket.MOCK_HOST_1 + "/post", files=file_data) - assert "Files must be opened in binary mode" in str(context)