ยปCore Development>Code coverage>Lib/test/test_cgi.py

Python code coverage for Lib/test/test_cgi.py

#countcontent
1n/afrom test.support import check_warnings
2n/aimport cgi
3n/aimport os
4n/aimport sys
5n/aimport tempfile
6n/aimport unittest
7n/aimport warnings
8n/afrom collections import namedtuple
9n/afrom io import StringIO, BytesIO
10n/afrom test import support
11n/a
12n/aclass HackedSysModule:
13n/a # The regression test will have real values in sys.argv, which
14n/a # will completely confuse the test of the cgi module
15n/a argv = []
16n/a stdin = sys.stdin
17n/a
18n/acgi.sys = HackedSysModule()
19n/a
20n/aclass ComparableException:
21n/a def __init__(self, err):
22n/a self.err = err
23n/a
24n/a def __str__(self):
25n/a return str(self.err)
26n/a
27n/a def __eq__(self, anExc):
28n/a if not isinstance(anExc, Exception):
29n/a return NotImplemented
30n/a return (self.err.__class__ == anExc.__class__ and
31n/a self.err.args == anExc.args)
32n/a
33n/a def __getattr__(self, attr):
34n/a return getattr(self.err, attr)
35n/a
36n/adef do_test(buf, method):
37n/a env = {}
38n/a if method == "GET":
39n/a fp = None
40n/a env['REQUEST_METHOD'] = 'GET'
41n/a env['QUERY_STRING'] = buf
42n/a elif method == "POST":
43n/a fp = BytesIO(buf.encode('latin-1')) # FieldStorage expects bytes
44n/a env['REQUEST_METHOD'] = 'POST'
45n/a env['CONTENT_TYPE'] = 'application/x-www-form-urlencoded'
46n/a env['CONTENT_LENGTH'] = str(len(buf))
47n/a else:
48n/a raise ValueError("unknown method: %s" % method)
49n/a try:
50n/a return cgi.parse(fp, env, strict_parsing=1)
51n/a except Exception as err:
52n/a return ComparableException(err)
53n/a
54n/aparse_strict_test_cases = [
55n/a ("", ValueError("bad query field: ''")),
56n/a ("&", ValueError("bad query field: ''")),
57n/a ("&&", ValueError("bad query field: ''")),
58n/a (";", ValueError("bad query field: ''")),
59n/a (";&;", ValueError("bad query field: ''")),
60n/a # Should the next few really be valid?
61n/a ("=", {}),
62n/a ("=&=", {}),
63n/a ("=;=", {}),
64n/a # This rest seem to make sense
65n/a ("=a", {'': ['a']}),
66n/a ("&=a", ValueError("bad query field: ''")),
67n/a ("=a&", ValueError("bad query field: ''")),
68n/a ("=&a", ValueError("bad query field: 'a'")),
69n/a ("b=a", {'b': ['a']}),
70n/a ("b+=a", {'b ': ['a']}),
71n/a ("a=b=a", {'a': ['b=a']}),
72n/a ("a=+b=a", {'a': [' b=a']}),
73n/a ("&b=a", ValueError("bad query field: ''")),
74n/a ("b&=a", ValueError("bad query field: 'b'")),
75n/a ("a=a+b&b=b+c", {'a': ['a b'], 'b': ['b c']}),
76n/a ("a=a+b&a=b+a", {'a': ['a b', 'b a']}),
77n/a ("x=1&y=2.0&z=2-3.%2b0", {'x': ['1'], 'y': ['2.0'], 'z': ['2-3.+0']}),
78n/a ("x=1;y=2.0&z=2-3.%2b0", {'x': ['1'], 'y': ['2.0'], 'z': ['2-3.+0']}),
79n/a ("x=1;y=2.0;z=2-3.%2b0", {'x': ['1'], 'y': ['2.0'], 'z': ['2-3.+0']}),
80n/a ("Hbc5161168c542333633315dee1182227:key_store_seqid=400006&cuyer=r&view=bustomer&order_id=0bb2e248638833d48cb7fed300000f1b&expire=964546263&lobale=en-US&kid=130003.300038&ss=env",
81n/a {'Hbc5161168c542333633315dee1182227:key_store_seqid': ['400006'],
82n/a 'cuyer': ['r'],
83n/a 'expire': ['964546263'],
84n/a 'kid': ['130003.300038'],
85n/a 'lobale': ['en-US'],
86n/a 'order_id': ['0bb2e248638833d48cb7fed300000f1b'],
87n/a 'ss': ['env'],
88n/a 'view': ['bustomer'],
89n/a }),
90n/a
91n/a ("group_id=5470&set=custom&_assigned_to=31392&_status=1&_category=100&SUBMIT=Browse",
92n/a {'SUBMIT': ['Browse'],
93n/a '_assigned_to': ['31392'],
94n/a '_category': ['100'],
95n/a '_status': ['1'],
96n/a 'group_id': ['5470'],
97n/a 'set': ['custom'],
98n/a })
99n/a ]
100n/a
101n/adef norm(seq):
102n/a return sorted(seq, key=repr)
103n/a
104n/adef first_elts(list):
105n/a return [p[0] for p in list]
106n/a
107n/adef first_second_elts(list):
108n/a return [(p[0], p[1][0]) for p in list]
109n/a
110n/adef gen_result(data, environ):
111n/a encoding = 'latin-1'
112n/a fake_stdin = BytesIO(data.encode(encoding))
113n/a fake_stdin.seek(0)
114n/a form = cgi.FieldStorage(fp=fake_stdin, environ=environ, encoding=encoding)
115n/a
116n/a result = {}
117n/a for k, v in dict(form).items():
118n/a result[k] = isinstance(v, list) and form.getlist(k) or v.value
119n/a
120n/a return result
121n/a
122n/aclass CgiTests(unittest.TestCase):
123n/a
124n/a def test_parse_multipart(self):
125n/a fp = BytesIO(POSTDATA.encode('latin1'))
126n/a env = {'boundary': BOUNDARY.encode('latin1'),
127n/a 'CONTENT-LENGTH': '558'}
128n/a result = cgi.parse_multipart(fp, env)
129n/a expected = {'submit': [b' Add '], 'id': [b'1234'],
130n/a 'file': [b'Testing 123.\n'], 'title': [b'']}
131n/a self.assertEqual(result, expected)
132n/a
133n/a def test_fieldstorage_properties(self):
134n/a fs = cgi.FieldStorage()
135n/a self.assertFalse(fs)
136n/a self.assertIn("FieldStorage", repr(fs))
137n/a self.assertEqual(list(fs), list(fs.keys()))
138n/a fs.list.append(namedtuple('MockFieldStorage', 'name')('fieldvalue'))
139n/a self.assertTrue(fs)
140n/a
141n/a def test_fieldstorage_invalid(self):
142n/a self.assertRaises(TypeError, cgi.FieldStorage, "not-a-file-obj",
143n/a environ={"REQUEST_METHOD":"PUT"})
144n/a self.assertRaises(TypeError, cgi.FieldStorage, "foo", "bar")
145n/a fs = cgi.FieldStorage(headers={'content-type':'text/plain'})
146n/a self.assertRaises(TypeError, bool, fs)
147n/a
148n/a def test_escape(self):
149n/a # cgi.escape() is deprecated.
150n/a with warnings.catch_warnings():
151n/a warnings.filterwarnings('ignore', r'cgi\.escape',
152n/a DeprecationWarning)
153n/a self.assertEqual("test & string", cgi.escape("test & string"))
154n/a self.assertEqual("&lt;test string&gt;", cgi.escape("<test string>"))
155n/a self.assertEqual("&quot;test string&quot;", cgi.escape('"test string"', True))
156n/a
157n/a def test_strict(self):
158n/a for orig, expect in parse_strict_test_cases:
159n/a # Test basic parsing
160n/a d = do_test(orig, "GET")
161n/a self.assertEqual(d, expect, "Error parsing %s method GET" % repr(orig))
162n/a d = do_test(orig, "POST")
163n/a self.assertEqual(d, expect, "Error parsing %s method POST" % repr(orig))
164n/a
165n/a env = {'QUERY_STRING': orig}
166n/a fs = cgi.FieldStorage(environ=env)
167n/a if isinstance(expect, dict):
168n/a # test dict interface
169n/a self.assertEqual(len(expect), len(fs))
170n/a self.assertCountEqual(expect.keys(), fs.keys())
171n/a ##self.assertEqual(norm(expect.values()), norm(fs.values()))
172n/a ##self.assertEqual(norm(expect.items()), norm(fs.items()))
173n/a self.assertEqual(fs.getvalue("nonexistent field", "default"), "default")
174n/a # test individual fields
175n/a for key in expect.keys():
176n/a expect_val = expect[key]
177n/a self.assertIn(key, fs)
178n/a if len(expect_val) > 1:
179n/a self.assertEqual(fs.getvalue(key), expect_val)
180n/a else:
181n/a self.assertEqual(fs.getvalue(key), expect_val[0])
182n/a
183n/a def test_log(self):
184n/a cgi.log("Testing")
185n/a
186n/a cgi.logfp = StringIO()
187n/a cgi.initlog("%s", "Testing initlog 1")
188n/a cgi.log("%s", "Testing log 2")
189n/a self.assertEqual(cgi.logfp.getvalue(), "Testing initlog 1\nTesting log 2\n")
190n/a if os.path.exists(os.devnull):
191n/a cgi.logfp = None
192n/a cgi.logfile = os.devnull
193n/a cgi.initlog("%s", "Testing log 3")
194n/a self.addCleanup(cgi.closelog)
195n/a cgi.log("Testing log 4")
196n/a
197n/a def test_fieldstorage_readline(self):
198n/a # FieldStorage uses readline, which has the capacity to read all
199n/a # contents of the input file into memory; we use readline's size argument
200n/a # to prevent that for files that do not contain any newlines in
201n/a # non-GET/HEAD requests
202n/a class TestReadlineFile:
203n/a def __init__(self, file):
204n/a self.file = file
205n/a self.numcalls = 0
206n/a
207n/a def readline(self, size=None):
208n/a self.numcalls += 1
209n/a if size:
210n/a return self.file.readline(size)
211n/a else:
212n/a return self.file.readline()
213n/a
214n/a def __getattr__(self, name):
215n/a file = self.__dict__['file']
216n/a a = getattr(file, name)
217n/a if not isinstance(a, int):
218n/a setattr(self, name, a)
219n/a return a
220n/a
221n/a f = TestReadlineFile(tempfile.TemporaryFile("wb+"))
222n/a self.addCleanup(f.close)
223n/a f.write(b'x' * 256 * 1024)
224n/a f.seek(0)
225n/a env = {'REQUEST_METHOD':'PUT'}
226n/a fs = cgi.FieldStorage(fp=f, environ=env)
227n/a self.addCleanup(fs.file.close)
228n/a # if we're not chunking properly, readline is only called twice
229n/a # (by read_binary); if we are chunking properly, it will be called 5 times
230n/a # as long as the chunksize is 1 << 16.
231n/a self.assertGreater(f.numcalls, 2)
232n/a f.close()
233n/a
234n/a def test_fieldstorage_multipart(self):
235n/a #Test basic FieldStorage multipart parsing
236n/a env = {
237n/a 'REQUEST_METHOD': 'POST',
238n/a 'CONTENT_TYPE': 'multipart/form-data; boundary={}'.format(BOUNDARY),
239n/a 'CONTENT_LENGTH': '558'}
240n/a fp = BytesIO(POSTDATA.encode('latin-1'))
241n/a fs = cgi.FieldStorage(fp, environ=env, encoding="latin-1")
242n/a self.assertEqual(len(fs.list), 4)
243n/a expect = [{'name':'id', 'filename':None, 'value':'1234'},
244n/a {'name':'title', 'filename':None, 'value':''},
245n/a {'name':'file', 'filename':'test.txt', 'value':b'Testing 123.\n'},
246n/a {'name':'submit', 'filename':None, 'value':' Add '}]
247n/a for x in range(len(fs.list)):
248n/a for k, exp in expect[x].items():
249n/a got = getattr(fs.list[x], k)
250n/a self.assertEqual(got, exp)
251n/a
252n/a def test_fieldstorage_multipart_leading_whitespace(self):
253n/a env = {
254n/a 'REQUEST_METHOD': 'POST',
255n/a 'CONTENT_TYPE': 'multipart/form-data; boundary={}'.format(BOUNDARY),
256n/a 'CONTENT_LENGTH': '560'}
257n/a # Add some leading whitespace to our post data that will cause the
258n/a # first line to not be the innerboundary.
259n/a fp = BytesIO(b"\r\n" + POSTDATA.encode('latin-1'))
260n/a fs = cgi.FieldStorage(fp, environ=env, encoding="latin-1")
261n/a self.assertEqual(len(fs.list), 4)
262n/a expect = [{'name':'id', 'filename':None, 'value':'1234'},
263n/a {'name':'title', 'filename':None, 'value':''},
264n/a {'name':'file', 'filename':'test.txt', 'value':b'Testing 123.\n'},
265n/a {'name':'submit', 'filename':None, 'value':' Add '}]
266n/a for x in range(len(fs.list)):
267n/a for k, exp in expect[x].items():
268n/a got = getattr(fs.list[x], k)
269n/a self.assertEqual(got, exp)
270n/a
271n/a def test_fieldstorage_multipart_non_ascii(self):
272n/a #Test basic FieldStorage multipart parsing
273n/a env = {'REQUEST_METHOD':'POST',
274n/a 'CONTENT_TYPE': 'multipart/form-data; boundary={}'.format(BOUNDARY),
275n/a 'CONTENT_LENGTH':'558'}
276n/a for encoding in ['iso-8859-1','utf-8']:
277n/a fp = BytesIO(POSTDATA_NON_ASCII.encode(encoding))
278n/a fs = cgi.FieldStorage(fp, environ=env,encoding=encoding)
279n/a self.assertEqual(len(fs.list), 1)
280n/a expect = [{'name':'id', 'filename':None, 'value':'\xe7\xf1\x80'}]
281n/a for x in range(len(fs.list)):
282n/a for k, exp in expect[x].items():
283n/a got = getattr(fs.list[x], k)
284n/a self.assertEqual(got, exp)
285n/a
286n/a def test_fieldstorage_multipart_maxline(self):
287n/a # Issue #18167
288n/a maxline = 1 << 16
289n/a self.maxDiff = None
290n/a def check(content):
291n/a data = """---123
292n/aContent-Disposition: form-data; name="upload"; filename="fake.txt"
293n/aContent-Type: text/plain
294n/a
295n/a%s
296n/a---123--
297n/a""".replace('\n', '\r\n') % content
298n/a environ = {
299n/a 'CONTENT_LENGTH': str(len(data)),
300n/a 'CONTENT_TYPE': 'multipart/form-data; boundary=-123',
301n/a 'REQUEST_METHOD': 'POST',
302n/a }
303n/a self.assertEqual(gen_result(data, environ),
304n/a {'upload': content.encode('latin1')})
305n/a check('x' * (maxline - 1))
306n/a check('x' * (maxline - 1) + '\r')
307n/a check('x' * (maxline - 1) + '\r' + 'y' * (maxline - 1))
308n/a
309n/a def test_fieldstorage_multipart_w3c(self):
310n/a # Test basic FieldStorage multipart parsing (W3C sample)
311n/a env = {
312n/a 'REQUEST_METHOD': 'POST',
313n/a 'CONTENT_TYPE': 'multipart/form-data; boundary={}'.format(BOUNDARY_W3),
314n/a 'CONTENT_LENGTH': str(len(POSTDATA_W3))}
315n/a fp = BytesIO(POSTDATA_W3.encode('latin-1'))
316n/a fs = cgi.FieldStorage(fp, environ=env, encoding="latin-1")
317n/a self.assertEqual(len(fs.list), 2)
318n/a self.assertEqual(fs.list[0].name, 'submit-name')
319n/a self.assertEqual(fs.list[0].value, 'Larry')
320n/a self.assertEqual(fs.list[1].name, 'files')
321n/a files = fs.list[1].value
322n/a self.assertEqual(len(files), 2)
323n/a expect = [{'name': None, 'filename': 'file1.txt', 'value': b'... contents of file1.txt ...'},
324n/a {'name': None, 'filename': 'file2.gif', 'value': b'...contents of file2.gif...'}]
325n/a for x in range(len(files)):
326n/a for k, exp in expect[x].items():
327n/a got = getattr(files[x], k)
328n/a self.assertEqual(got, exp)
329n/a
330n/a def test_fieldstorage_part_content_length(self):
331n/a BOUNDARY = "JfISa01"
332n/a POSTDATA = """--JfISa01
333n/aContent-Disposition: form-data; name="submit-name"
334n/aContent-Length: 5
335n/a
336n/aLarry
337n/a--JfISa01"""
338n/a env = {
339n/a 'REQUEST_METHOD': 'POST',
340n/a 'CONTENT_TYPE': 'multipart/form-data; boundary={}'.format(BOUNDARY),
341n/a 'CONTENT_LENGTH': str(len(POSTDATA))}
342n/a fp = BytesIO(POSTDATA.encode('latin-1'))
343n/a fs = cgi.FieldStorage(fp, environ=env, encoding="latin-1")
344n/a self.assertEqual(len(fs.list), 1)
345n/a self.assertEqual(fs.list[0].name, 'submit-name')
346n/a self.assertEqual(fs.list[0].value, 'Larry')
347n/a
348n/a def test_fieldstorage_as_context_manager(self):
349n/a fp = BytesIO(b'x' * 10)
350n/a env = {'REQUEST_METHOD': 'PUT'}
351n/a with cgi.FieldStorage(fp=fp, environ=env) as fs:
352n/a content = fs.file.read()
353n/a self.assertFalse(fs.file.closed)
354n/a self.assertTrue(fs.file.closed)
355n/a self.assertEqual(content, 'x' * 10)
356n/a with self.assertRaisesRegex(ValueError, 'I/O operation on closed file'):
357n/a fs.file.read()
358n/a
359n/a _qs_result = {
360n/a 'key1': 'value1',
361n/a 'key2': ['value2x', 'value2y'],
362n/a 'key3': 'value3',
363n/a 'key4': 'value4'
364n/a }
365n/a def testQSAndUrlEncode(self):
366n/a data = "key2=value2x&key3=value3&key4=value4"
367n/a environ = {
368n/a 'CONTENT_LENGTH': str(len(data)),
369n/a 'CONTENT_TYPE': 'application/x-www-form-urlencoded',
370n/a 'QUERY_STRING': 'key1=value1&key2=value2y',
371n/a 'REQUEST_METHOD': 'POST',
372n/a }
373n/a v = gen_result(data, environ)
374n/a self.assertEqual(self._qs_result, v)
375n/a
376n/a def testQSAndFormData(self):
377n/a data = """---123
378n/aContent-Disposition: form-data; name="key2"
379n/a
380n/avalue2y
381n/a---123
382n/aContent-Disposition: form-data; name="key3"
383n/a
384n/avalue3
385n/a---123
386n/aContent-Disposition: form-data; name="key4"
387n/a
388n/avalue4
389n/a---123--
390n/a"""
391n/a environ = {
392n/a 'CONTENT_LENGTH': str(len(data)),
393n/a 'CONTENT_TYPE': 'multipart/form-data; boundary=-123',
394n/a 'QUERY_STRING': 'key1=value1&key2=value2x',
395n/a 'REQUEST_METHOD': 'POST',
396n/a }
397n/a v = gen_result(data, environ)
398n/a self.assertEqual(self._qs_result, v)
399n/a
400n/a def testQSAndFormDataFile(self):
401n/a data = """---123
402n/aContent-Disposition: form-data; name="key2"
403n/a
404n/avalue2y
405n/a---123
406n/aContent-Disposition: form-data; name="key3"
407n/a
408n/avalue3
409n/a---123
410n/aContent-Disposition: form-data; name="key4"
411n/a
412n/avalue4
413n/a---123
414n/aContent-Disposition: form-data; name="upload"; filename="fake.txt"
415n/aContent-Type: text/plain
416n/a
417n/athis is the content of the fake file
418n/a
419n/a---123--
420n/a"""
421n/a environ = {
422n/a 'CONTENT_LENGTH': str(len(data)),
423n/a 'CONTENT_TYPE': 'multipart/form-data; boundary=-123',
424n/a 'QUERY_STRING': 'key1=value1&key2=value2x',
425n/a 'REQUEST_METHOD': 'POST',
426n/a }
427n/a result = self._qs_result.copy()
428n/a result.update({
429n/a 'upload': b'this is the content of the fake file\n'
430n/a })
431n/a v = gen_result(data, environ)
432n/a self.assertEqual(result, v)
433n/a
434n/a def test_deprecated_parse_qs(self):
435n/a # this func is moved to urllib.parse, this is just a sanity check
436n/a with check_warnings(('cgi.parse_qs is deprecated, use urllib.parse.'
437n/a 'parse_qs instead', DeprecationWarning)):
438n/a self.assertEqual({'a': ['A1'], 'B': ['B3'], 'b': ['B2']},
439n/a cgi.parse_qs('a=A1&b=B2&B=B3'))
440n/a
441n/a def test_deprecated_parse_qsl(self):
442n/a # this func is moved to urllib.parse, this is just a sanity check
443n/a with check_warnings(('cgi.parse_qsl is deprecated, use urllib.parse.'
444n/a 'parse_qsl instead', DeprecationWarning)):
445n/a self.assertEqual([('a', 'A1'), ('b', 'B2'), ('B', 'B3')],
446n/a cgi.parse_qsl('a=A1&b=B2&B=B3'))
447n/a
448n/a def test_parse_header(self):
449n/a self.assertEqual(
450n/a cgi.parse_header("text/plain"),
451n/a ("text/plain", {}))
452n/a self.assertEqual(
453n/a cgi.parse_header("text/vnd.just.made.this.up ; "),
454n/a ("text/vnd.just.made.this.up", {}))
455n/a self.assertEqual(
456n/a cgi.parse_header("text/plain;charset=us-ascii"),
457n/a ("text/plain", {"charset": "us-ascii"}))
458n/a self.assertEqual(
459n/a cgi.parse_header('text/plain ; charset="us-ascii"'),
460n/a ("text/plain", {"charset": "us-ascii"}))
461n/a self.assertEqual(
462n/a cgi.parse_header('text/plain ; charset="us-ascii"; another=opt'),
463n/a ("text/plain", {"charset": "us-ascii", "another": "opt"}))
464n/a self.assertEqual(
465n/a cgi.parse_header('attachment; filename="silly.txt"'),
466n/a ("attachment", {"filename": "silly.txt"}))
467n/a self.assertEqual(
468n/a cgi.parse_header('attachment; filename="strange;name"'),
469n/a ("attachment", {"filename": "strange;name"}))
470n/a self.assertEqual(
471n/a cgi.parse_header('attachment; filename="strange;name";size=123;'),
472n/a ("attachment", {"filename": "strange;name", "size": "123"}))
473n/a self.assertEqual(
474n/a cgi.parse_header('form-data; name="files"; filename="fo\\"o;bar"'),
475n/a ("form-data", {"name": "files", "filename": 'fo"o;bar'}))
476n/a
477n/a def test_all(self):
478n/a blacklist = {"logfile", "logfp", "initlog", "dolog", "nolog",
479n/a "closelog", "log", "maxlen", "valid_boundary"}
480n/a support.check__all__(self, cgi, blacklist=blacklist)
481n/a
482n/a
483n/aBOUNDARY = "---------------------------721837373350705526688164684"
484n/a
485n/aPOSTDATA = """-----------------------------721837373350705526688164684
486n/aContent-Disposition: form-data; name="id"
487n/a
488n/a1234
489n/a-----------------------------721837373350705526688164684
490n/aContent-Disposition: form-data; name="title"
491n/a
492n/a
493n/a-----------------------------721837373350705526688164684
494n/aContent-Disposition: form-data; name="file"; filename="test.txt"
495n/aContent-Type: text/plain
496n/a
497n/aTesting 123.
498n/a
499n/a-----------------------------721837373350705526688164684
500n/aContent-Disposition: form-data; name="submit"
501n/a
502n/a Add\x20
503n/a-----------------------------721837373350705526688164684--
504n/a"""
505n/a
506n/aPOSTDATA_NON_ASCII = """-----------------------------721837373350705526688164684
507n/aContent-Disposition: form-data; name="id"
508n/a
509n/a\xe7\xf1\x80
510n/a-----------------------------721837373350705526688164684
511n/a"""
512n/a
513n/a# http://www.w3.org/TR/html401/interact/forms.html#h-17.13.4
514n/aBOUNDARY_W3 = "AaB03x"
515n/aPOSTDATA_W3 = """--AaB03x
516n/aContent-Disposition: form-data; name="submit-name"
517n/a
518n/aLarry
519n/a--AaB03x
520n/aContent-Disposition: form-data; name="files"
521n/aContent-Type: multipart/mixed; boundary=BbC04y
522n/a
523n/a--BbC04y
524n/aContent-Disposition: file; filename="file1.txt"
525n/aContent-Type: text/plain
526n/a
527n/a... contents of file1.txt ...
528n/a--BbC04y
529n/aContent-Disposition: file; filename="file2.gif"
530n/aContent-Type: image/gif
531n/aContent-Transfer-Encoding: binary
532n/a
533n/a...contents of file2.gif...
534n/a--BbC04y--
535n/a--AaB03x--
536n/a"""
537n/a
538n/aif __name__ == '__main__':
539n/a unittest.main()