|
7 | 7 | import unittest |
8 | 8 | from io import BytesIO |
9 | 9 | from typing import TYPE_CHECKING |
10 | | -from unittest.mock import Mock |
| 10 | +from unittest.mock import Mock, patch |
11 | 11 |
|
12 | 12 | import pytest |
13 | 13 | import yaml |
@@ -1009,13 +1009,45 @@ def test_upload_delete_tmp_config(self) -> None: |
1009 | 1009 | uploaded_file = self.files[0] |
1010 | 1010 | assert uploaded_file.actual_file_name is not None |
1011 | 1011 | actual_file_name = uploaded_file.actual_file_name.decode(sys.getfilesystemencoding()) |
| 1012 | + self.f.close() |
| 1013 | + |
| 1014 | + self.assertFalse(uploaded_file.file_object.closed) |
1012 | 1015 | uploaded_file.close() |
1013 | 1016 |
|
1014 | 1017 | try: |
1015 | 1018 | self.assertTrue(os.path.exists(actual_file_name)) |
1016 | 1019 | finally: |
1017 | 1020 | os.unlink(actual_file_name) |
1018 | 1021 |
|
| 1022 | + def test_close_closes_incomplete_upload(self) -> None: |
| 1023 | + for transfer_encoding, data in ((b"binary", b"test"), (b"base64", b"dGVzdA=="), (b"quoted-printable", b"test")): |
| 1024 | + with ( |
| 1025 | + self.subTest(transfer_encoding=transfer_encoding), |
| 1026 | + tempfile.TemporaryDirectory() as upload_dir, |
| 1027 | + patch.object(File, "close", autospec=True, side_effect=File.close) as close, |
| 1028 | + ): |
| 1029 | + self.make( |
| 1030 | + "boundary", config={"UPLOAD_DIR": upload_dir, "UPLOAD_DELETE_TMP": False, "MAX_MEMORY_FILE_SIZE": 1} |
| 1031 | + ) |
| 1032 | + body = ( |
| 1033 | + b"--boundary\r\n" |
| 1034 | + b'Content-Disposition: form-data; name="file"; filename="test.txt"\r\n' |
| 1035 | + b"Content-Type: text/plain\r\n" |
| 1036 | + b"Content-Transfer-Encoding: " + transfer_encoding + b"\r\n\r\n" + data |
| 1037 | + ) |
| 1038 | + |
| 1039 | + self.f.write(body) |
| 1040 | + paths = os.listdir(upload_dir) |
| 1041 | + self.assertEqual(len(paths), 1) |
| 1042 | + |
| 1043 | + self.f.close() |
| 1044 | + self.f.close() |
| 1045 | + |
| 1046 | + close.assert_called_once() |
| 1047 | + self.assertTrue(close.call_args.args[0].file_object.closed) |
| 1048 | + self.assertEqual(os.listdir(upload_dir), paths) |
| 1049 | + self.assertEqual(self.files, []) |
| 1050 | + |
1019 | 1051 | @parametrize("param", [t for t in http_tests if t["name"] in single_byte_tests]) |
1020 | 1052 | def test_feed_single_bytes(self, param: TestParams) -> None: |
1021 | 1053 | """ |
@@ -1294,6 +1326,33 @@ def on_file(f: File) -> None: |
1294 | 1326 | self.assert_file_data(files[0], b"test1234") |
1295 | 1327 | self.assertTrue(on_end.called) |
1296 | 1328 |
|
| 1329 | + f.close() |
| 1330 | + self.assertFalse(files[0].file_object.closed) |
| 1331 | + files[0].close() |
| 1332 | + |
| 1333 | + def test_close_closes_incomplete_octet_stream_upload(self) -> None: |
| 1334 | + with ( |
| 1335 | + tempfile.TemporaryDirectory() as upload_dir, |
| 1336 | + patch.object(File, "close", autospec=True, side_effect=File.close) as close, |
| 1337 | + ): |
| 1338 | + f = FormParser( |
| 1339 | + "application/octet-stream", |
| 1340 | + None, |
| 1341 | + None, |
| 1342 | + file_name=b"test.txt", |
| 1343 | + config={"UPLOAD_DIR": upload_dir, "UPLOAD_DELETE_TMP": False, "MAX_MEMORY_FILE_SIZE": 1}, |
| 1344 | + ) |
| 1345 | + f.write(b"test") |
| 1346 | + paths = os.listdir(upload_dir) |
| 1347 | + self.assertEqual(len(paths), 1) |
| 1348 | + |
| 1349 | + f.close() |
| 1350 | + f.close() |
| 1351 | + |
| 1352 | + close.assert_called_once() |
| 1353 | + self.assertTrue(close.call_args.args[0].file_object.closed) |
| 1354 | + self.assertEqual(os.listdir(upload_dir), paths) |
| 1355 | + |
1297 | 1356 | def test_querystring(self) -> None: |
1298 | 1357 | fields: list[Field] = [] |
1299 | 1358 |
|
@@ -1390,6 +1449,34 @@ def on_file(f: File) -> None: |
1390 | 1449 | f.finalize() |
1391 | 1450 | self.assert_file_data(files[0], b"Test") |
1392 | 1451 |
|
| 1452 | + def test_bad_content_transfer_encoding_closes_current_file(self) -> None: |
| 1453 | + data = ( |
| 1454 | + b"--boundary\r\n" |
| 1455 | + b'Content-Disposition: form-data; name="first"; filename="first.txt"\r\n\r\n' |
| 1456 | + b"first\r\n" |
| 1457 | + b"--boundary\r\n" |
| 1458 | + b'Content-Disposition: form-data; name="second"; filename="second.txt"\r\n' |
| 1459 | + b"Content-Transfer-Encoding: badstuff\r\n\r\n" |
| 1460 | + ) |
| 1461 | + files: list[File] = [] |
| 1462 | + f = FormParser( |
| 1463 | + "multipart/form-data", None, files.append, boundary="boundary", config={"UPLOAD_ERROR_ON_BAD_CTE": True} |
| 1464 | + ) |
| 1465 | + |
| 1466 | + with patch.object(File, "close", autospec=True, side_effect=File.close) as close: |
| 1467 | + with self.assertRaises(FormParserError): |
| 1468 | + f.write(data) |
| 1469 | + |
| 1470 | + self.assertEqual(len(files), 1) |
| 1471 | + f.close() |
| 1472 | + |
| 1473 | + close.assert_called_once() |
| 1474 | + self.assertIsNot(close.call_args.args[0], files[0]) |
| 1475 | + self.assertTrue(close.call_args.args[0].file_object.closed) |
| 1476 | + self.assertFalse(files[0].file_object.closed) |
| 1477 | + |
| 1478 | + files[0].close() |
| 1479 | + |
1393 | 1480 | def test_bad_content_disposition(self) -> None: |
1394 | 1481 | # Field name is required per RFC 7578 §4.2. |
1395 | 1482 | data = ( |
|
0 commit comments