From 7c8d2b241e34dd15b7f9ae4065268a0d36bf17fa Mon Sep 17 00:00:00 2001 From: Justin Myers Date: Sat, 27 Apr 2024 11:23:46 -0700 Subject: [PATCH 1/3] Add tests --- requirements.txt | 1 + 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/files_test.py | 205 ++++++++++++++++++++++++++++++ 6 files changed, 210 insertions(+) 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/files_test.py diff --git a/requirements.txt b/requirements.txt index 2505288..ae35f68 100755 --- a/requirements.txt +++ b/requirements.txt @@ -4,3 +4,4 @@ Adafruit-Blinka Adafruit-Circuitpython-ConnectionManager +requests 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/files_test.py b/tests/files_test.py new file mode 100644 index 0000000..32f69ae --- /dev/null +++ b/tests/files_test.py @@ -0,0 +1,205 @@ +# SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries +# +# SPDX-License-Identifier: Unlicense + +""" Post Tests """ +# pylint: disable=line-too-long + +import re +from unittest import mock + +import mocket +import pytest +import requests as python_requests + + +@pytest.fixture +def log_stream(): + return [] + + +@pytest.fixture +def post_url(): + return "https://httpbin.org/post" + + +@pytest.fixture +def request_logging(log_stream): + """Reset the ConnectionManager, since it's a singlton and will hold data""" + import http.client # pylint: disable=import-outside-toplevel + + def httpclient_log(*args): + log_stream.append(args) + + http.client.print = httpclient_log + http.client.HTTPConnection.debuglevel = 1 + + +def get_actual_request_data(log_stream): + boundary_pattern = r"(?<=boundary=)(.\w*)" + + boundary = "" + actual_request_post = "" + for log in log_stream: + for log_arg in log: + boundary_search = re.findall(boundary_pattern, log_arg) + if boundary_search: + boundary = boundary_search[0] + elif "Content-Disposition" in log_arg: + # this will look like: + # b\'{content}\' + # and escapped characters look like: + # \\r + post_data = log_arg[2:-1] + post_bytes = post_data.encode("utf-8") + post_unescaped = post_bytes.decode("unicode_escape") + actual_request_post = post_unescaped.encode("latin1") + + return boundary, actual_request_post + + +def test_post_files_text( # pylint: disable=unused-argument + sock, requests, log_stream, post_url, request_logging +): + file_data = { + "key_4": (None, "Value 5"), + } + + python_requests.post(post_url, files=file_data) + boundary, actual_request_post = get_actual_request_data(log_stream) + + requests._build_boundary_string = mock.Mock(return_value=boundary) + 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(f"multipart/form-data; boundary={boundary}".encode()), + 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"), + ] + ) + + sent = b"".join(sock.sent_data) + assert sent.endswith(actual_request_post) + + +def test_post_files_file( # pylint: disable=unused-argument + sock, requests, log_stream, post_url, request_logging +): + 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", + }, + ), + } + + python_requests.post(post_url, files=file_data) + boundary, actual_request_post = get_actual_request_data(log_stream) + + requests._build_boundary_string = mock.Mock(return_value=boundary) + 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(f"multipart/form-data; boundary={boundary}".encode()), + 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"), + ] + ) + sent = b"".join(sock.sent_data) + assert sent.endswith(actual_request_post) + + +def test_post_files_complex( # pylint: disable=unused-argument + sock, requests, log_stream, post_url, request_logging +): + 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"), + } + + python_requests.post(post_url, files=file_data) + boundary, actual_request_post = get_actual_request_data(log_stream) + + requests._build_boundary_string = mock.Mock(return_value=boundary) + 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(f"multipart/form-data; boundary={boundary}".encode()), + 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"), + ] + ) + sent = b"".join(sock.sent_data) + assert sent.endswith(actual_request_post) + + +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) From 1eaa1a816e568573578197e3f89f153c1318e1fb Mon Sep 17 00:00:00 2001 From: Justin Myers Date: Sat, 27 Apr 2024 11:38:26 -0700 Subject: [PATCH 2/3] Use actual content length too --- tests/files_test.py | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/tests/files_test.py b/tests/files_test.py index 32f69ae..fe6e77c 100644 --- a/tests/files_test.py +++ b/tests/files_test.py @@ -37,15 +37,20 @@ def httpclient_log(*args): def get_actual_request_data(log_stream): boundary_pattern = r"(?<=boundary=)(.\w*)" + content_length_pattern = r"(?<=Content-Length: )(.\d*)" boundary = "" actual_request_post = "" + content_length = "" for log in log_stream: for log_arg in log: boundary_search = re.findall(boundary_pattern, log_arg) + content_length_search = re.findall(content_length_pattern, log_arg) if boundary_search: boundary = boundary_search[0] - elif "Content-Disposition" in log_arg: + if content_length_search: + content_length = content_length_search[0] + if "Content-Disposition" in log_arg: # this will look like: # b\'{content}\' # and escapped characters look like: @@ -55,7 +60,7 @@ def get_actual_request_data(log_stream): post_unescaped = post_bytes.decode("unicode_escape") actual_request_post = post_unescaped.encode("latin1") - return boundary, actual_request_post + return boundary, content_length, actual_request_post def test_post_files_text( # pylint: disable=unused-argument @@ -66,7 +71,7 @@ def test_post_files_text( # pylint: disable=unused-argument } python_requests.post(post_url, files=file_data) - boundary, actual_request_post = get_actual_request_data(log_stream) + boundary, content_length, actual_request_post = get_actual_request_data(log_stream) requests._build_boundary_string = mock.Mock(return_value=boundary) requests.post("http://" + mocket.MOCK_HOST_1 + "/post", files=file_data) @@ -84,7 +89,7 @@ def test_post_files_text( # pylint: disable=unused-argument [ mock.call(b"Content-Length"), mock.call(b": "), - mock.call(b"131"), + mock.call(content_length.encode()), mock.call(b"\r\n"), ] ) @@ -111,7 +116,9 @@ def test_post_files_file( # pylint: disable=unused-argument } python_requests.post(post_url, files=file_data) - boundary, actual_request_post = get_actual_request_data(log_stream) + boundary, content_length, actual_request_post = get_actual_request_data( + log_stream + ) requests._build_boundary_string = mock.Mock(return_value=boundary) requests.post("http://" + mocket.MOCK_HOST_1 + "/post", files=file_data) @@ -129,7 +136,7 @@ def test_post_files_file( # pylint: disable=unused-argument [ mock.call(b"Content-Length"), mock.call(b": "), - mock.call(b"347"), + mock.call(content_length.encode()), mock.call(b"\r\n"), ] ) @@ -164,7 +171,9 @@ def test_post_files_complex( # pylint: disable=unused-argument } python_requests.post(post_url, files=file_data) - boundary, actual_request_post = get_actual_request_data(log_stream) + boundary, content_length, actual_request_post = get_actual_request_data( + log_stream + ) requests._build_boundary_string = mock.Mock(return_value=boundary) requests.post("http://" + mocket.MOCK_HOST_1 + "/post", files=file_data) @@ -182,7 +191,7 @@ def test_post_files_complex( # pylint: disable=unused-argument [ mock.call(b"Content-Length"), mock.call(b": "), - mock.call(b"796"), + mock.call(content_length.encode()), mock.call(b"\r\n"), ] ) From 9b9f7bcd6f04baec298bd3b676914f22afb08ac7 Mon Sep 17 00:00:00 2001 From: foamyguy Date: Mon, 29 Apr 2024 08:31:22 -0500 Subject: [PATCH 3/3] move requests to optional. change copyright name in new test file. add timeouts to python_requests.post. --- optional_requirements.txt | 2 ++ requirements.txt | 1 - tests/files_test.py | 12 ++++++------ 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/optional_requirements.txt b/optional_requirements.txt index d4e27c4..38e5c0c 100644 --- a/optional_requirements.txt +++ b/optional_requirements.txt @@ -1,3 +1,5 @@ # SPDX-FileCopyrightText: 2022 Alec Delaney, for Adafruit Industries # # SPDX-License-Identifier: Unlicense + +requests diff --git a/requirements.txt b/requirements.txt index ae35f68..2505288 100755 --- a/requirements.txt +++ b/requirements.txt @@ -4,4 +4,3 @@ Adafruit-Blinka Adafruit-Circuitpython-ConnectionManager -requests diff --git a/tests/files_test.py b/tests/files_test.py index fe6e77c..8299b1b 100644 --- a/tests/files_test.py +++ b/tests/files_test.py @@ -1,8 +1,8 @@ -# SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries +# SPDX-FileCopyrightText: 2024 Justin Myers # # SPDX-License-Identifier: Unlicense -""" Post Tests """ +""" Post Files Tests """ # pylint: disable=line-too-long import re @@ -53,7 +53,7 @@ def get_actual_request_data(log_stream): if "Content-Disposition" in log_arg: # this will look like: # b\'{content}\' - # and escapped characters look like: + # and escaped characters look like: # \\r post_data = log_arg[2:-1] post_bytes = post_data.encode("utf-8") @@ -70,7 +70,7 @@ def test_post_files_text( # pylint: disable=unused-argument "key_4": (None, "Value 5"), } - python_requests.post(post_url, files=file_data) + python_requests.post(post_url, files=file_data, timeout=30) boundary, content_length, actual_request_post = get_actual_request_data(log_stream) requests._build_boundary_string = mock.Mock(return_value=boundary) @@ -115,7 +115,7 @@ def test_post_files_file( # pylint: disable=unused-argument ), } - python_requests.post(post_url, files=file_data) + python_requests.post(post_url, files=file_data, timeout=30) boundary, content_length, actual_request_post = get_actual_request_data( log_stream ) @@ -170,7 +170,7 @@ def test_post_files_complex( # pylint: disable=unused-argument "key_6": (None, "Value 6"), } - python_requests.post(post_url, files=file_data) + python_requests.post(post_url, files=file_data, timeout=30) boundary, content_length, actual_request_post = get_actual_request_data( log_stream )