tests.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629
  1. #! -*- coding: utf-8 -*-
  2. from __future__ import unicode_literals
  3. import base64
  4. import errno
  5. import hashlib
  6. import json
  7. import os
  8. import shutil
  9. import tempfile as sys_tempfile
  10. import unittest
  11. from io import BytesIO
  12. from django.core.files import temp as tempfile
  13. from django.core.files.uploadedfile import SimpleUploadedFile
  14. from django.http.multipartparser import MultiPartParser, parse_header
  15. from django.test import SimpleTestCase, TestCase, client, override_settings
  16. from django.utils.encoding import force_bytes
  17. from django.utils.http import urlquote
  18. from django.utils.six import PY2, StringIO
  19. from . import uploadhandler
  20. from .models import FileModel
  21. UNICODE_FILENAME = 'test-0123456789_中文_Orléans.jpg'
  22. MEDIA_ROOT = sys_tempfile.mkdtemp()
  23. UPLOAD_TO = os.path.join(MEDIA_ROOT, 'test_upload')
  24. @override_settings(MEDIA_ROOT=MEDIA_ROOT, ROOT_URLCONF='file_uploads.urls', MIDDLEWARE=[])
  25. class FileUploadTests(TestCase):
  26. @classmethod
  27. def setUpClass(cls):
  28. super(FileUploadTests, cls).setUpClass()
  29. if not os.path.isdir(MEDIA_ROOT):
  30. os.makedirs(MEDIA_ROOT)
  31. @classmethod
  32. def tearDownClass(cls):
  33. shutil.rmtree(MEDIA_ROOT)
  34. super(FileUploadTests, cls).tearDownClass()
  35. def test_simple_upload(self):
  36. with open(__file__, 'rb') as fp:
  37. post_data = {
  38. 'name': 'Ringo',
  39. 'file_field': fp,
  40. }
  41. response = self.client.post('/upload/', post_data)
  42. self.assertEqual(response.status_code, 200)
  43. def test_large_upload(self):
  44. file = tempfile.NamedTemporaryFile
  45. with file(suffix=".file1") as file1, file(suffix=".file2") as file2:
  46. file1.write(b'a' * (2 ** 21))
  47. file1.seek(0)
  48. file2.write(b'a' * (10 * 2 ** 20))
  49. file2.seek(0)
  50. post_data = {
  51. 'name': 'Ringo',
  52. 'file_field1': file1,
  53. 'file_field2': file2,
  54. }
  55. for key in list(post_data):
  56. try:
  57. post_data[key + '_hash'] = hashlib.sha1(post_data[key].read()).hexdigest()
  58. post_data[key].seek(0)
  59. except AttributeError:
  60. post_data[key + '_hash'] = hashlib.sha1(force_bytes(post_data[key])).hexdigest()
  61. response = self.client.post('/verify/', post_data)
  62. self.assertEqual(response.status_code, 200)
  63. def _test_base64_upload(self, content, encode=base64.b64encode):
  64. payload = client.FakePayload("\r\n".join([
  65. '--' + client.BOUNDARY,
  66. 'Content-Disposition: form-data; name="file"; filename="test.txt"',
  67. 'Content-Type: application/octet-stream',
  68. 'Content-Transfer-Encoding: base64',
  69. '']))
  70. payload.write(b"\r\n" + encode(force_bytes(content)) + b"\r\n")
  71. payload.write('--' + client.BOUNDARY + '--\r\n')
  72. r = {
  73. 'CONTENT_LENGTH': len(payload),
  74. 'CONTENT_TYPE': client.MULTIPART_CONTENT,
  75. 'PATH_INFO': "/echo_content/",
  76. 'REQUEST_METHOD': 'POST',
  77. 'wsgi.input': payload,
  78. }
  79. response = self.client.request(**r)
  80. received = json.loads(response.content.decode('utf-8'))
  81. self.assertEqual(received['file'], content)
  82. def test_base64_upload(self):
  83. self._test_base64_upload("This data will be transmitted base64-encoded.")
  84. def test_big_base64_upload(self):
  85. self._test_base64_upload("Big data" * 68000) # > 512Kb
  86. def test_big_base64_newlines_upload(self):
  87. self._test_base64_upload(
  88. # encodestring is a deprecated alias on Python 3
  89. "Big data" * 68000, encode=base64.encodestring if PY2 else base64.encodebytes)
  90. def test_unicode_file_name(self):
  91. tdir = sys_tempfile.mkdtemp()
  92. self.addCleanup(shutil.rmtree, tdir, True)
  93. # This file contains Chinese symbols and an accented char in the name.
  94. with open(os.path.join(tdir, UNICODE_FILENAME), 'w+b') as file1:
  95. file1.write(b'b' * (2 ** 10))
  96. file1.seek(0)
  97. post_data = {
  98. 'file_unicode': file1,
  99. }
  100. response = self.client.post('/unicode_name/', post_data)
  101. self.assertEqual(response.status_code, 200)
  102. def test_unicode_file_name_rfc2231(self):
  103. """
  104. Test receiving file upload when filename is encoded with RFC2231
  105. (#22971).
  106. """
  107. payload = client.FakePayload()
  108. payload.write('\r\n'.join([
  109. '--' + client.BOUNDARY,
  110. 'Content-Disposition: form-data; name="file_unicode"; filename*=UTF-8\'\'%s' % urlquote(UNICODE_FILENAME),
  111. 'Content-Type: application/octet-stream',
  112. '',
  113. 'You got pwnd.\r\n',
  114. '\r\n--' + client.BOUNDARY + '--\r\n'
  115. ]))
  116. r = {
  117. 'CONTENT_LENGTH': len(payload),
  118. 'CONTENT_TYPE': client.MULTIPART_CONTENT,
  119. 'PATH_INFO': "/unicode_name/",
  120. 'REQUEST_METHOD': 'POST',
  121. 'wsgi.input': payload,
  122. }
  123. response = self.client.request(**r)
  124. self.assertEqual(response.status_code, 200)
  125. def test_unicode_name_rfc2231(self):
  126. """
  127. Test receiving file upload when filename is encoded with RFC2231
  128. (#22971).
  129. """
  130. payload = client.FakePayload()
  131. payload.write(
  132. '\r\n'.join([
  133. '--' + client.BOUNDARY,
  134. 'Content-Disposition: form-data; name*=UTF-8\'\'file_unicode; filename*=UTF-8\'\'%s' % urlquote(
  135. UNICODE_FILENAME
  136. ),
  137. 'Content-Type: application/octet-stream',
  138. '',
  139. 'You got pwnd.\r\n',
  140. '\r\n--' + client.BOUNDARY + '--\r\n'
  141. ])
  142. )
  143. r = {
  144. 'CONTENT_LENGTH': len(payload),
  145. 'CONTENT_TYPE': client.MULTIPART_CONTENT,
  146. 'PATH_INFO': "/unicode_name/",
  147. 'REQUEST_METHOD': 'POST',
  148. 'wsgi.input': payload,
  149. }
  150. response = self.client.request(**r)
  151. self.assertEqual(response.status_code, 200)
  152. def test_blank_filenames(self):
  153. """
  154. Receiving file upload when filename is blank (before and after
  155. sanitization) should be okay.
  156. """
  157. # The second value is normalized to an empty name by
  158. # MultiPartParser.IE_sanitize()
  159. filenames = ['', 'C:\\Windows\\']
  160. payload = client.FakePayload()
  161. for i, name in enumerate(filenames):
  162. payload.write('\r\n'.join([
  163. '--' + client.BOUNDARY,
  164. 'Content-Disposition: form-data; name="file%s"; filename="%s"' % (i, name),
  165. 'Content-Type: application/octet-stream',
  166. '',
  167. 'You got pwnd.\r\n'
  168. ]))
  169. payload.write('\r\n--' + client.BOUNDARY + '--\r\n')
  170. r = {
  171. 'CONTENT_LENGTH': len(payload),
  172. 'CONTENT_TYPE': client.MULTIPART_CONTENT,
  173. 'PATH_INFO': '/echo/',
  174. 'REQUEST_METHOD': 'POST',
  175. 'wsgi.input': payload,
  176. }
  177. response = self.client.request(**r)
  178. self.assertEqual(response.status_code, 200)
  179. # Empty filenames should be ignored
  180. received = json.loads(response.content.decode('utf-8'))
  181. for i, name in enumerate(filenames):
  182. self.assertIsNone(received.get('file%s' % i))
  183. def test_dangerous_file_names(self):
  184. """Uploaded file names should be sanitized before ever reaching the view."""
  185. # This test simulates possible directory traversal attacks by a
  186. # malicious uploader We have to do some monkeybusiness here to construct
  187. # a malicious payload with an invalid file name (containing os.sep or
  188. # os.pardir). This similar to what an attacker would need to do when
  189. # trying such an attack.
  190. scary_file_names = [
  191. "/tmp/hax0rd.txt", # Absolute path, *nix-style.
  192. "C:\\Windows\\hax0rd.txt", # Absolute path, win-style.
  193. "C:/Windows/hax0rd.txt", # Absolute path, broken-style.
  194. "\\tmp\\hax0rd.txt", # Absolute path, broken in a different way.
  195. "/tmp\\hax0rd.txt", # Absolute path, broken by mixing.
  196. "subdir/hax0rd.txt", # Descendant path, *nix-style.
  197. "subdir\\hax0rd.txt", # Descendant path, win-style.
  198. "sub/dir\\hax0rd.txt", # Descendant path, mixed.
  199. "../../hax0rd.txt", # Relative path, *nix-style.
  200. "..\\..\\hax0rd.txt", # Relative path, win-style.
  201. "../..\\hax0rd.txt" # Relative path, mixed.
  202. ]
  203. payload = client.FakePayload()
  204. for i, name in enumerate(scary_file_names):
  205. payload.write('\r\n'.join([
  206. '--' + client.BOUNDARY,
  207. 'Content-Disposition: form-data; name="file%s"; filename="%s"' % (i, name),
  208. 'Content-Type: application/octet-stream',
  209. '',
  210. 'You got pwnd.\r\n'
  211. ]))
  212. payload.write('\r\n--' + client.BOUNDARY + '--\r\n')
  213. r = {
  214. 'CONTENT_LENGTH': len(payload),
  215. 'CONTENT_TYPE': client.MULTIPART_CONTENT,
  216. 'PATH_INFO': "/echo/",
  217. 'REQUEST_METHOD': 'POST',
  218. 'wsgi.input': payload,
  219. }
  220. response = self.client.request(**r)
  221. # The filenames should have been sanitized by the time it got to the view.
  222. received = json.loads(response.content.decode('utf-8'))
  223. for i, name in enumerate(scary_file_names):
  224. got = received["file%s" % i]
  225. self.assertEqual(got, "hax0rd.txt")
  226. def test_filename_overflow(self):
  227. """File names over 256 characters (dangerous on some platforms) get fixed up."""
  228. long_str = 'f' * 300
  229. cases = [
  230. # field name, filename, expected
  231. ('long_filename', '%s.txt' % long_str, '%s.txt' % long_str[:251]),
  232. ('long_extension', 'foo.%s' % long_str, '.%s' % long_str[:254]),
  233. ('no_extension', long_str, long_str[:255]),
  234. ('no_filename', '.%s' % long_str, '.%s' % long_str[:254]),
  235. ('long_everything', '%s.%s' % (long_str, long_str), '.%s' % long_str[:254]),
  236. ]
  237. payload = client.FakePayload()
  238. for name, filename, _ in cases:
  239. payload.write("\r\n".join([
  240. '--' + client.BOUNDARY,
  241. 'Content-Disposition: form-data; name="{}"; filename="{}"',
  242. 'Content-Type: application/octet-stream',
  243. '',
  244. 'Oops.',
  245. ''
  246. ]).format(name, filename))
  247. payload.write('\r\n--' + client.BOUNDARY + '--\r\n')
  248. r = {
  249. 'CONTENT_LENGTH': len(payload),
  250. 'CONTENT_TYPE': client.MULTIPART_CONTENT,
  251. 'PATH_INFO': "/echo/",
  252. 'REQUEST_METHOD': 'POST',
  253. 'wsgi.input': payload,
  254. }
  255. response = self.client.request(**r)
  256. result = json.loads(response.content.decode('utf-8'))
  257. for name, _, expected in cases:
  258. got = result[name]
  259. self.assertEqual(expected, got, 'Mismatch for {}'.format(name))
  260. self.assertLess(len(got), 256,
  261. "Got a long file name (%s characters)." % len(got))
  262. def test_file_content(self):
  263. file = tempfile.NamedTemporaryFile
  264. with file(suffix=".ctype_extra") as no_content_type, file(suffix=".ctype_extra") as simple_file:
  265. no_content_type.write(b'no content')
  266. no_content_type.seek(0)
  267. simple_file.write(b'text content')
  268. simple_file.seek(0)
  269. simple_file.content_type = 'text/plain'
  270. string_io = StringIO('string content')
  271. bytes_io = BytesIO(b'binary content')
  272. response = self.client.post('/echo_content/', {
  273. 'no_content_type': no_content_type,
  274. 'simple_file': simple_file,
  275. 'string': string_io,
  276. 'binary': bytes_io,
  277. })
  278. received = json.loads(response.content.decode('utf-8'))
  279. self.assertEqual(received['no_content_type'], 'no content')
  280. self.assertEqual(received['simple_file'], 'text content')
  281. self.assertEqual(received['string'], 'string content')
  282. self.assertEqual(received['binary'], 'binary content')
  283. def test_content_type_extra(self):
  284. """Uploaded files may have content type parameters available."""
  285. file = tempfile.NamedTemporaryFile
  286. with file(suffix=".ctype_extra") as no_content_type, file(suffix=".ctype_extra") as simple_file:
  287. no_content_type.write(b'something')
  288. no_content_type.seek(0)
  289. simple_file.write(b'something')
  290. simple_file.seek(0)
  291. simple_file.content_type = 'text/plain; test-key=test_value'
  292. response = self.client.post('/echo_content_type_extra/', {
  293. 'no_content_type': no_content_type,
  294. 'simple_file': simple_file,
  295. })
  296. received = json.loads(response.content.decode('utf-8'))
  297. self.assertEqual(received['no_content_type'], {})
  298. self.assertEqual(received['simple_file'], {'test-key': 'test_value'})
  299. def test_truncated_multipart_handled_gracefully(self):
  300. """
  301. If passed an incomplete multipart message, MultiPartParser does not
  302. attempt to read beyond the end of the stream, and simply will handle
  303. the part that can be parsed gracefully.
  304. """
  305. payload_str = "\r\n".join([
  306. '--' + client.BOUNDARY,
  307. 'Content-Disposition: form-data; name="file"; filename="foo.txt"',
  308. 'Content-Type: application/octet-stream',
  309. '',
  310. 'file contents'
  311. '--' + client.BOUNDARY + '--',
  312. '',
  313. ])
  314. payload = client.FakePayload(payload_str[:-10])
  315. r = {
  316. 'CONTENT_LENGTH': len(payload),
  317. 'CONTENT_TYPE': client.MULTIPART_CONTENT,
  318. 'PATH_INFO': '/echo/',
  319. 'REQUEST_METHOD': 'POST',
  320. 'wsgi.input': payload,
  321. }
  322. got = json.loads(self.client.request(**r).content.decode('utf-8'))
  323. self.assertEqual(got, {})
  324. def test_empty_multipart_handled_gracefully(self):
  325. """
  326. If passed an empty multipart message, MultiPartParser will return
  327. an empty QueryDict.
  328. """
  329. r = {
  330. 'CONTENT_LENGTH': 0,
  331. 'CONTENT_TYPE': client.MULTIPART_CONTENT,
  332. 'PATH_INFO': '/echo/',
  333. 'REQUEST_METHOD': 'POST',
  334. 'wsgi.input': client.FakePayload(b''),
  335. }
  336. got = json.loads(self.client.request(**r).content.decode('utf-8'))
  337. self.assertEqual(got, {})
  338. def test_custom_upload_handler(self):
  339. file = tempfile.NamedTemporaryFile
  340. with file() as smallfile, file() as bigfile:
  341. # A small file (under the 5M quota)
  342. smallfile.write(b'a' * (2 ** 21))
  343. smallfile.seek(0)
  344. # A big file (over the quota)
  345. bigfile.write(b'a' * (10 * 2 ** 20))
  346. bigfile.seek(0)
  347. # Small file posting should work.
  348. response = self.client.post('/quota/', {'f': smallfile})
  349. got = json.loads(response.content.decode('utf-8'))
  350. self.assertIn('f', got)
  351. # Large files don't go through.
  352. response = self.client.post("/quota/", {'f': bigfile})
  353. got = json.loads(response.content.decode('utf-8'))
  354. self.assertNotIn('f', got)
  355. def test_broken_custom_upload_handler(self):
  356. with tempfile.NamedTemporaryFile() as file:
  357. file.write(b'a' * (2 ** 21))
  358. file.seek(0)
  359. # AttributeError: You cannot alter upload handlers after the upload has been processed.
  360. with self.assertRaises(AttributeError):
  361. self.client.post('/quota/broken/', {'f': file})
  362. def test_fileupload_getlist(self):
  363. file = tempfile.NamedTemporaryFile
  364. with file() as file1, file() as file2, file() as file2a:
  365. file1.write(b'a' * (2 ** 23))
  366. file1.seek(0)
  367. file2.write(b'a' * (2 * 2 ** 18))
  368. file2.seek(0)
  369. file2a.write(b'a' * (5 * 2 ** 20))
  370. file2a.seek(0)
  371. response = self.client.post('/getlist_count/', {
  372. 'file1': file1,
  373. 'field1': 'test',
  374. 'field2': 'test3',
  375. 'field3': 'test5',
  376. 'field4': 'test6',
  377. 'field5': 'test7',
  378. 'file2': (file2, file2a)
  379. })
  380. got = json.loads(response.content.decode('utf-8'))
  381. self.assertEqual(got.get('file1'), 1)
  382. self.assertEqual(got.get('file2'), 2)
  383. def test_fileuploads_closed_at_request_end(self):
  384. file = tempfile.NamedTemporaryFile
  385. with file() as f1, file() as f2a, file() as f2b:
  386. response = self.client.post('/fd_closing/t/', {
  387. 'file': f1,
  388. 'file2': (f2a, f2b),
  389. })
  390. request = response.wsgi_request
  391. # Check that the files got actually parsed.
  392. self.assertTrue(hasattr(request, '_files'))
  393. file = request._files['file']
  394. self.assertTrue(file.closed)
  395. files = request._files.getlist('file2')
  396. self.assertTrue(files[0].closed)
  397. self.assertTrue(files[1].closed)
  398. def test_no_parsing_triggered_by_fd_closing(self):
  399. file = tempfile.NamedTemporaryFile
  400. with file() as f1, file() as f2a, file() as f2b:
  401. response = self.client.post('/fd_closing/f/', {
  402. 'file': f1,
  403. 'file2': (f2a, f2b),
  404. })
  405. request = response.wsgi_request
  406. # Check that the fd closing logic doesn't trigger parsing of the stream
  407. self.assertFalse(hasattr(request, '_files'))
  408. def test_file_error_blocking(self):
  409. """
  410. The server should not block when there are upload errors (bug #8622).
  411. This can happen if something -- i.e. an exception handler -- tries to
  412. access POST while handling an error in parsing POST. This shouldn't
  413. cause an infinite loop!
  414. """
  415. class POSTAccessingHandler(client.ClientHandler):
  416. """A handler that'll access POST during an exception."""
  417. def handle_uncaught_exception(self, request, resolver, exc_info):
  418. ret = super(POSTAccessingHandler, self).handle_uncaught_exception(request, resolver, exc_info)
  419. request.POST # evaluate
  420. return ret
  421. # Maybe this is a little more complicated that it needs to be; but if
  422. # the django.test.client.FakePayload.read() implementation changes then
  423. # this test would fail. So we need to know exactly what kind of error
  424. # it raises when there is an attempt to read more than the available bytes:
  425. try:
  426. client.FakePayload(b'a').read(2)
  427. except Exception as err:
  428. reference_error = err
  429. # install the custom handler that tries to access request.POST
  430. self.client.handler = POSTAccessingHandler()
  431. with open(__file__, 'rb') as fp:
  432. post_data = {
  433. 'name': 'Ringo',
  434. 'file_field': fp,
  435. }
  436. try:
  437. self.client.post('/upload_errors/', post_data)
  438. except reference_error.__class__ as err:
  439. self.assertFalse(
  440. str(err) == str(reference_error),
  441. "Caught a repeated exception that'll cause an infinite loop in file uploads."
  442. )
  443. except Exception as err:
  444. # CustomUploadError is the error that should have been raised
  445. self.assertEqual(err.__class__, uploadhandler.CustomUploadError)
  446. def test_filename_case_preservation(self):
  447. """
  448. The storage backend shouldn't mess with the case of the filenames
  449. uploaded.
  450. """
  451. # Synthesize the contents of a file upload with a mixed case filename
  452. # so we don't have to carry such a file in the Django tests source code
  453. # tree.
  454. vars = {'boundary': 'oUrBoUnDaRyStRiNg'}
  455. post_data = [
  456. '--%(boundary)s',
  457. 'Content-Disposition: form-data; name="file_field"; filename="MiXeD_cAsE.txt"',
  458. 'Content-Type: application/octet-stream',
  459. '',
  460. 'file contents\n'
  461. '',
  462. '--%(boundary)s--\r\n',
  463. ]
  464. response = self.client.post(
  465. '/filename_case/',
  466. '\r\n'.join(post_data) % vars,
  467. 'multipart/form-data; boundary=%(boundary)s' % vars
  468. )
  469. self.assertEqual(response.status_code, 200)
  470. id = int(response.content)
  471. obj = FileModel.objects.get(pk=id)
  472. # The name of the file uploaded and the file stored in the server-side
  473. # shouldn't differ.
  474. self.assertEqual(os.path.basename(obj.testfile.path), 'MiXeD_cAsE.txt')
  475. @override_settings(MEDIA_ROOT=MEDIA_ROOT)
  476. class DirectoryCreationTests(SimpleTestCase):
  477. """
  478. Tests for error handling during directory creation
  479. via _save_FIELD_file (ticket #6450)
  480. """
  481. @classmethod
  482. def setUpClass(cls):
  483. super(DirectoryCreationTests, cls).setUpClass()
  484. if not os.path.isdir(MEDIA_ROOT):
  485. os.makedirs(MEDIA_ROOT)
  486. @classmethod
  487. def tearDownClass(cls):
  488. shutil.rmtree(MEDIA_ROOT)
  489. super(DirectoryCreationTests, cls).tearDownClass()
  490. def setUp(self):
  491. self.obj = FileModel()
  492. def test_readonly_root(self):
  493. """Permission errors are not swallowed"""
  494. os.chmod(MEDIA_ROOT, 0o500)
  495. self.addCleanup(os.chmod, MEDIA_ROOT, 0o700)
  496. try:
  497. self.obj.testfile.save('foo.txt', SimpleUploadedFile('foo.txt', b'x'), save=False)
  498. except OSError as err:
  499. self.assertEqual(err.errno, errno.EACCES)
  500. except Exception:
  501. self.fail("OSError [Errno %s] not raised." % errno.EACCES)
  502. def test_not_a_directory(self):
  503. """The correct IOError is raised when the upload directory name exists but isn't a directory"""
  504. # Create a file with the upload directory name
  505. open(UPLOAD_TO, 'wb').close()
  506. self.addCleanup(os.remove, UPLOAD_TO)
  507. with self.assertRaises(IOError) as exc_info:
  508. with SimpleUploadedFile('foo.txt', b'x') as file:
  509. self.obj.testfile.save('foo.txt', file, save=False)
  510. # The test needs to be done on a specific string as IOError
  511. # is raised even without the patch (just not early enough)
  512. self.assertEqual(exc_info.exception.args[0], "%s exists and is not a directory." % UPLOAD_TO)
  513. class MultiParserTests(unittest.TestCase):
  514. def test_empty_upload_handlers(self):
  515. # We're not actually parsing here; just checking if the parser properly
  516. # instantiates with empty upload handlers.
  517. MultiPartParser({
  518. 'CONTENT_TYPE': 'multipart/form-data; boundary=_foo',
  519. 'CONTENT_LENGTH': '1'
  520. }, StringIO('x'), [], 'utf-8')
  521. def test_rfc2231_parsing(self):
  522. test_data = (
  523. (b"Content-Type: application/x-stuff; title*=us-ascii'en-us'This%20is%20%2A%2A%2Afun%2A%2A%2A",
  524. "This is ***fun***"),
  525. (b"Content-Type: application/x-stuff; title*=UTF-8''foo-%c3%a4.html",
  526. "foo-ä.html"),
  527. (b"Content-Type: application/x-stuff; title*=iso-8859-1''foo-%E4.html",
  528. "foo-ä.html"),
  529. )
  530. for raw_line, expected_title in test_data:
  531. parsed = parse_header(raw_line)
  532. self.assertEqual(parsed[1]['title'], expected_title)
  533. def test_rfc2231_wrong_title(self):
  534. """
  535. Test wrongly formatted RFC 2231 headers (missing double single quotes).
  536. Parsing should not crash (#24209).
  537. """
  538. test_data = (
  539. (b"Content-Type: application/x-stuff; title*='This%20is%20%2A%2A%2Afun%2A%2A%2A",
  540. b"'This%20is%20%2A%2A%2Afun%2A%2A%2A"),
  541. (b"Content-Type: application/x-stuff; title*='foo.html",
  542. b"'foo.html"),
  543. (b"Content-Type: application/x-stuff; title*=bar.html",
  544. b"bar.html"),
  545. )
  546. for raw_line, expected_title in test_data:
  547. parsed = parse_header(raw_line)
  548. self.assertEqual(parsed[1]['title'], expected_title)