Skip to content

Commit 8a14e8d

Browse files
committed
Add tests
1 parent c583e0f commit 8a14e8d

File tree

8 files changed

+279
-29
lines changed

8 files changed

+279
-29
lines changed

adafruit_requests.py

Lines changed: 28 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -360,19 +360,19 @@ def __init__(
360360
self._session_id = session_id
361361
self._last_response = None
362362

363-
def _build_boundary_data(self, files: dict):
363+
def _build_boundary_data(self, files: dict): # pylint: disable=too-many-locals
364364
boundary_string = self._build_boundary_string()
365365
content_length = 0
366366
boundary_objects = []
367367

368368
for field_name, field_values in files.items():
369369
file_name = field_values[0]
370-
file_data = field_values[1]
370+
file_handle = field_values[1]
371371

372372
boundary_data = f"--{boundary_string}\r\n"
373-
boundary_data += f'Content-Disposition: form-data; name="{field_name}"; '
373+
boundary_data += f'Content-Disposition: form-data; name="{field_name}"'
374374
if file_name is not None:
375-
boundary_data += f'filename="{file_name}"'
375+
boundary_data += f'; filename="{file_name}"'
376376
boundary_data += "\r\n"
377377
if len(field_values) >= 3:
378378
file_content_type = field_values[2]
@@ -386,20 +386,30 @@ def _build_boundary_data(self, files: dict):
386386
content_length += len(boundary_data)
387387
boundary_objects.append(boundary_data)
388388

389-
if file_name is not None:
390-
file_data.seek(0, SEEK_END)
391-
content_length += file_data.tell()
392-
file_data.seek(0)
393-
boundary_objects.append(file_data)
389+
if hasattr(file_handle, "read"):
390+
is_binary = False
391+
try:
392+
content = file_handle.read(1)
393+
is_binary = isinstance(content, bytes)
394+
except UnicodeError:
395+
is_binary = False
396+
397+
if not is_binary:
398+
raise AttributeError("Files must be opened in binary mode")
399+
400+
file_handle.seek(0, SEEK_END)
401+
content_length += file_handle.tell()
402+
file_handle.seek(0)
403+
boundary_objects.append(file_handle)
394404
boundary_data = ""
395405
else:
396-
boundary_data = file_data
406+
boundary_data = file_handle
397407

398408
boundary_data += "\r\n"
399409
content_length += len(boundary_data)
400410
boundary_objects.append(boundary_data)
401411

402-
boundary_data = f"--{boundary_string}--"
412+
boundary_data = f"--{boundary_string}--\r\n"
403413

404414
content_length += len(boundary_data)
405415
boundary_objects.append(boundary_data)
@@ -417,7 +427,7 @@ def _build_boundary_string():
417427
@staticmethod
418428
def _check_headers(headers: Dict[str, str]):
419429
if not isinstance(headers, dict):
420-
raise AttributeError("headers must be in dict format")
430+
raise AttributeError("Headers must be in dict format")
421431

422432
for key, value in headers.items():
423433
if isinstance(value, (str, bytes)) or value is None:
@@ -447,7 +457,6 @@ def _send(socket: SocketType, data: bytes):
447457
# Not EAGAIN; that was already handled.
448458
raise OSError(errno.EIO)
449459
total_sent += sent
450-
return total_sent
451460

452461
def _send_as_bytes(self, socket: SocketType, data: str):
453462
return self._send(socket, bytes(data, "utf-8"))
@@ -458,19 +467,12 @@ def _send_boundary_objects(self, socket: SocketType, boundary_objects: Any):
458467
self._send_as_bytes(socket, boundary_object)
459468
else:
460469
chunk_size = 32
461-
if hasattr(boundary_object, "readinto"):
462-
b = bytearray(chunk_size)
463-
while True:
464-
size = boundary_object.readinto(b)
465-
if size == 0:
466-
break
467-
self._send(socket, b[:size])
468-
else:
469-
while True:
470-
b = boundary_object.read(chunk_size)
471-
if len(b) == 0:
472-
break
473-
self._send(socket, b)
470+
b = bytearray(chunk_size)
471+
while True:
472+
size = boundary_object.readinto(b)
473+
if size == 0:
474+
break
475+
self._send(socket, b[:size])
474476

475477
def _send_header(self, socket, header, value):
476478
if value is None:

tests/files/green_red.png

125 Bytes
Loading

tests/files/green_red.png.license

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# SPDX-FileCopyrightText: 2024 Justin Myers
2+
# SPDX-License-Identifier: Unlicense

tests/files/red_green.png

123 Bytes
Loading

tests/files/red_green.png.license

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# SPDX-FileCopyrightText: 2024 Justin Myers
2+
# SPDX-License-Identifier: Unlicense

tests/header_test.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
def test_check_headers_not_dict(requests):
1212
with pytest.raises(AttributeError) as context:
1313
requests._check_headers("")
14-
assert "headers must be in dict format" in str(context)
14+
assert "Headers must be in dict format" in str(context)
1515

1616

1717
def test_check_headers_not_valid(requests):

tests/method_files.py

Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
# SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries
2+
#
3+
# SPDX-License-Identifier: Unlicense
4+
5+
""" Post Tests """
6+
# pylint: disable=line-too-long
7+
8+
from unittest import mock
9+
10+
import mocket
11+
import pytest
12+
13+
"""
14+
For building tests, you can use CPython requests with logging to see what should actuall get sent.
15+
16+
import logging
17+
import http.client
18+
import requests
19+
20+
def httpclient_logging_patch(level=logging.DEBUG):
21+
logging.basicConfig(level=level)
22+
23+
httpclient_logger = logging.getLogger("http.client")
24+
25+
def httpclient_log(*args):
26+
httpclient_logger.log(level, " ".join(args))
27+
28+
http.client.print = httpclient_log
29+
http.client.HTTPConnection.debuglevel = 1
30+
31+
httpclient_logging_patch()
32+
33+
URL = "https://httpbin.org/post"
34+
35+
with open("tests/files/red_green.png", "rb") as file_1:
36+
file_data = {
37+
"file_1": (
38+
"red_green.png",
39+
file_1,
40+
"image/png",
41+
{
42+
"Key_1": "Value 1",
43+
"Key_2": "Value 2",
44+
"Key_3": "Value 3",
45+
},
46+
),
47+
}
48+
49+
print(requests.post(URL, files=file_data).json())
50+
"""
51+
52+
53+
def test_post_files_text(sock, requests):
54+
file_data = {
55+
"key_4": (None, "Value 5"),
56+
}
57+
58+
requests._build_boundary_string = mock.Mock(
59+
return_value="8cd45159712eeb914c049c717d3f4d75"
60+
)
61+
requests.post("http://" + mocket.MOCK_HOST_1 + "/post", files=file_data)
62+
63+
sock.connect.assert_called_once_with((mocket.MOCK_POOL_IP, 80))
64+
sock.send.assert_has_calls(
65+
[
66+
mock.call(b"Content-Type"),
67+
mock.call(b": "),
68+
mock.call(
69+
b"multipart/form-data; boundary=8cd45159712eeb914c049c717d3f4d75"
70+
),
71+
mock.call(b"\r\n"),
72+
]
73+
)
74+
sock.send.assert_has_calls(
75+
[
76+
mock.call(
77+
b'--8cd45159712eeb914c049c717d3f4d75\r\nContent-Disposition: form-data; name="key_4"\r\n\r\n'
78+
),
79+
mock.call(b"Value 5\r\n"),
80+
mock.call(b"--8cd45159712eeb914c049c717d3f4d75--\r\n"),
81+
]
82+
)
83+
84+
85+
def test_post_files_file(sock, requests):
86+
with open("tests/files/red_green.png", "rb") as file_1:
87+
file_data = {
88+
"file_1": (
89+
"red_green.png",
90+
file_1,
91+
"image/png",
92+
{
93+
"Key_1": "Value 1",
94+
"Key_2": "Value 2",
95+
"Key_3": "Value 3",
96+
},
97+
),
98+
}
99+
100+
requests._build_boundary_string = mock.Mock(
101+
return_value="e663061c5bfcc53139c8f68d016cbef3"
102+
)
103+
requests.post("http://" + mocket.MOCK_HOST_1 + "/post", files=file_data)
104+
105+
sock.connect.assert_called_once_with((mocket.MOCK_POOL_IP, 80))
106+
sock.send.assert_has_calls(
107+
[
108+
mock.call(b"Content-Type"),
109+
mock.call(b": "),
110+
mock.call(
111+
b"multipart/form-data; boundary=e663061c5bfcc53139c8f68d016cbef3"
112+
),
113+
mock.call(b"\r\n"),
114+
]
115+
)
116+
sock.send.assert_has_calls(
117+
[
118+
mock.call(
119+
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'
120+
),
121+
mock.call(
122+
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"
123+
),
124+
mock.call(
125+
b"s\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\x04gAMA\x00\x00\xb1\x8f\x0b\xfca\x05\x00\x00"
126+
),
127+
mock.call(
128+
b"\x00\tpHYs\x00\x00\x0e\xc3\x00\x00\x0e\xc3\x01\xc7o\xa8d\x00\x00\x00\x10IDAT\x18Wc\xf8\xcf"
129+
),
130+
mock.call(
131+
b"\xc0\x00\xc5\xff\x19\x18\x00\x1d\xf0\x03\xfd\x8fk\x13|\x00\x00\x00\x00IEND\xaeB`\x82"
132+
),
133+
mock.call(b"\r\n"),
134+
mock.call(b"--e663061c5bfcc53139c8f68d016cbef3--\r\n"),
135+
]
136+
)
137+
138+
139+
def test_post_files_complex(sock, requests):
140+
with open("tests/files/red_green.png", "rb") as file_1, open(
141+
"tests/files/green_red.png", "rb"
142+
) as file_2:
143+
file_data = {
144+
"file_1": (
145+
"red_green.png",
146+
file_1,
147+
"image/png",
148+
{
149+
"Key_1": "Value 1",
150+
"Key_2": "Value 2",
151+
"Key_3": "Value 3",
152+
},
153+
),
154+
"key_4": (None, "Value 5"),
155+
"file_2": (
156+
"green_red.png",
157+
file_2,
158+
"image/png",
159+
),
160+
"key_6": (None, "Value 6"),
161+
}
162+
163+
requests._build_boundary_string = mock.Mock(
164+
return_value="e663061c5bfcc53139c8f68d016cbef3"
165+
)
166+
requests.post("http://" + mocket.MOCK_HOST_1 + "/post", files=file_data)
167+
168+
sock.connect.assert_called_once_with((mocket.MOCK_POOL_IP, 80))
169+
sock.send.assert_has_calls(
170+
[
171+
mock.call(b"Content-Type"),
172+
mock.call(b": "),
173+
mock.call(
174+
b"multipart/form-data; boundary=e663061c5bfcc53139c8f68d016cbef3"
175+
),
176+
mock.call(b"\r\n"),
177+
]
178+
)
179+
sock.send.assert_has_calls(
180+
[
181+
mock.call(
182+
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'
183+
),
184+
mock.call(
185+
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"
186+
),
187+
mock.call(
188+
b"s\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\x04gAMA\x00\x00\xb1\x8f\x0b\xfca\x05\x00\x00"
189+
),
190+
mock.call(
191+
b"\x00\tpHYs\x00\x00\x0e\xc3\x00\x00\x0e\xc3\x01\xc7o\xa8d\x00\x00\x00\x10IDAT\x18Wc\xf8\xcf"
192+
),
193+
mock.call(
194+
b"\xc0\x00\xc5\xff\x19\x18\x00\x1d\xf0\x03\xfd\x8fk\x13|\x00\x00\x00\x00IEND\xaeB`\x82"
195+
),
196+
mock.call(b"\r\n"),
197+
mock.call(
198+
b'--e663061c5bfcc53139c8f68d016cbef3\r\nContent-Disposition: form-data; name="key_4"\r\n\r\n'
199+
),
200+
mock.call(b"Value 5\r\n"),
201+
mock.call(
202+
b'--e663061c5bfcc53139c8f68d016cbef3\r\nContent-Disposition: form-data; name="file_2"; filename="green_red.png"\r\nContent-Type: image/png\r\n\r\n'
203+
),
204+
mock.call(
205+
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"
206+
),
207+
mock.call(
208+
b"s\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\x04gAMA\x00\x00\xb1\x8f\x0b\xfca\x05\x00\x00"
209+
),
210+
mock.call(
211+
b"\x00\tpHYs\x00\x00\x0e\xc3\x00\x00\x0e\xc3\x01\xc7o\xa8d\x00\x00\x00\x12IDAT\x18Wc`\xf8"
212+
),
213+
mock.call(
214+
b'\x0f\x84 \x92\x81\xe1?\x03\x00\x1d\xf0\x03\xfd\x88"uS\x00\x00\x00\x00IEND\xaeB`\x82'
215+
),
216+
mock.call(b"\r\n"),
217+
mock.call(
218+
b'--e663061c5bfcc53139c8f68d016cbef3\r\nContent-Disposition: form-data; name="key_6"\r\n\r\n'
219+
),
220+
mock.call(b"Value 6\r\n"),
221+
mock.call(b"--e663061c5bfcc53139c8f68d016cbef3--\r\n"),
222+
]
223+
)
224+
225+
226+
def test_post_files_not_binary(requests):
227+
with open("tests/files/red_green.png", "r") as file_1:
228+
file_data = {
229+
"file_1": (
230+
"red_green.png",
231+
file_1,
232+
"image/png",
233+
),
234+
}
235+
236+
with pytest.raises(AttributeError) as context:
237+
requests.post("http://" + mocket.MOCK_HOST_1 + "/post", files=file_data)
238+
assert "Files must be opened in binary mode" in str(context)

tests/method_test.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,10 @@ def test_post_string(sock, requests):
5252

5353

5454
def test_post_form(sock, requests):
55-
data = {"Date": "July 25, 2019", "Time": "12:00"}
55+
data = {
56+
"Date": "July 25, 2019",
57+
"Time": "12:00",
58+
}
5659
requests.post("http://" + mocket.MOCK_HOST_1 + "/post", data=data)
5760
sock.connect.assert_called_once_with((mocket.MOCK_POOL_IP, 80))
5861
sock.send.assert_has_calls(
@@ -67,7 +70,10 @@ def test_post_form(sock, requests):
6770

6871

6972
def test_post_json(sock, requests):
70-
json_data = {"Date": "July 25, 2019", "Time": "12:00"}
73+
json_data = {
74+
"Date": "July 25, 2019",
75+
"Time": "12:00",
76+
}
7177
requests.post("http://" + mocket.MOCK_HOST_1 + "/post", json=json_data)
7278
sock.connect.assert_called_once_with((mocket.MOCK_POOL_IP, 80))
7379
sock.send.assert_has_calls(

0 commit comments

Comments
 (0)