1
# Copyright (C) 2005, 2006, 2007 Canonical Ltd
3
# This program is free software; you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License as published by
5
# the Free Software Foundation; either version 2 of the License, or
6
# (at your option) any later version.
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
# GNU General Public License for more details.
13
# You should have received a copy of the GNU General Public License
14
# along with this program; if not, write to the Free Software
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17
"""Tests from HTTP response parsing.
19
The handle_response method read the response body of a GET request an returns
20
the corresponding RangeFile.
22
There are four different kinds of RangeFile:
23
- a whole file whose size is unknown, seen as a simple byte stream,
24
- a whole file whose size is known, we can't read past its end,
25
- a single range file, a part of a file with a start and a size,
26
- a multiple range file, several consecutive parts with known start offset
29
Some properties are common to all kinds:
30
- seek can only be forward (its really a socket underneath),
31
- read can't cross ranges,
32
- successive ranges are taken into account transparently,
34
- the expected pattern of use is either seek(offset)+read(size) or a single
35
read with no size specified. For multiple range files, multiple read() will
36
return the corresponding ranges, trying to read further will raise
40
from cStringIO import StringIO
47
from bzrlib.transport.http import (
53
class ReadSocket(object):
54
"""A socket-like object that can be given a predefined content."""
56
def __init__(self, data):
57
self.readfile = StringIO(data)
59
def makefile(self, mode='r', bufsize=None):
62
class FakeHTTPConnection(_urllib2_wrappers.HTTPConnection):
64
def __init__(self, sock):
65
_urllib2_wrappers.HTTPConnection.__init__(self, 'localhost')
66
# Set the socket to bypass the connection
70
"""Ignores the writes on the socket."""
74
class TestHTTPConnection(tests.TestCase):
76
def test_cleanup_pipe(self):
77
sock = ReadSocket("""HTTP/1.1 200 OK\r
78
Content-Type: text/plain; charset=UTF-8\r
83
conn = FakeHTTPConnection(sock)
84
# Simulate the request sending so that the connection will be able to
86
conn.putrequest('GET', 'http://localhost/fictious')
88
# Now, get the response
89
resp = conn.getresponse()
90
# Read part of the response
91
self.assertEquals('0123456789\n', resp.read(11))
92
# Override the thresold to force the warning emission
93
conn._range_warning_thresold = 6 # There are 7 bytes pending
95
self.assertContainsRe(self._get_log(keep_log_file=True),
96
'Got a 200 response when asking')
99
class TestRangeFileMixin(object):
100
"""Tests for accessing the first range in a RangeFile."""
102
# A simple string used to represent a file part (also called a range), in
103
# which offsets are easy to calculate for test writers. It's used as a
104
# building block with slight variations but basically 'a' is the first char
105
# of the range and 'z' is the last.
106
alpha = 'abcdefghijklmnopqrstuvwxyz'
108
def test_can_read_at_first_access(self):
109
"""Test that the just created file can be read."""
110
self.assertEquals(self.alpha, self._file.read())
112
def test_seek_read(self):
113
"""Test seek/read inside the range."""
115
start = self.first_range_start
116
# Before any use, tell() should be at the range start
117
self.assertEquals(start, f.tell())
118
cur = start # For an overall offset assertion
121
self.assertEquals('def', f.read(3))
125
self.assertEquals('klmn', f.read(4))
127
# read(0) in the middle of a range
128
self.assertEquals('', f.read(0))
132
self.assertEquals(here, f.tell())
133
self.assertEquals(cur, f.tell())
135
def test_read_zero(self):
137
start = self.first_range_start
138
self.assertEquals('', f.read(0))
140
self.assertEquals('', f.read(0))
142
def test_seek_at_range_end(self):
146
def test_read_at_range_end(self):
147
"""Test read behaviour at range end."""
149
self.assertEquals(self.alpha, f.read())
150
self.assertEquals('', f.read(0))
151
self.assertRaises(errors.InvalidRange, f.read, 1)
153
def test_unbounded_read_after_seek(self):
156
# Should not cross ranges
157
self.assertEquals('yz', f.read())
159
def test_seek_backwards(self):
161
start = self.first_range_start
164
self.assertRaises(errors.InvalidRange, f.seek, start + 5)
166
def test_seek_outside_single_range(self):
168
if f._size == -1 or f._boundary is not None:
169
raise tests.TestNotApplicable('Needs a fully defined range')
170
# Will seek past the range and then errors out
171
self.assertRaises(errors.InvalidRange,
172
f.seek, self.first_range_start + 27)
174
def test_read_past_end_of_range(self):
177
raise tests.TestNotApplicable("Can't check an unknown size")
178
start = self.first_range_start
180
self.assertRaises(errors.InvalidRange, f.read, 10)
182
def test_seek_from_end(self):
183
"""Test seeking from the end of the file.
185
The semantic is unclear in case of multiple ranges. Seeking from end
186
exists only for the http transports, cannot be used if the file size is
187
unknown and is not used in bzrlib itself. This test must be (and is)
188
overridden by daughter classes.
190
Reading from end makes sense only when a range has been requested from
191
the end of the file (see HttpTransportBase._get() when using the
192
'tail_amount' parameter). The HTTP response can only be a whole file or
197
self.assertEquals('yz', f.read())
200
class TestRangeFileSizeUnknown(tests.TestCase, TestRangeFileMixin):
201
"""Test a RangeFile for a whole file whose size is not known."""
204
super(TestRangeFileSizeUnknown, self).setUp()
205
self._file = response.RangeFile('Whole_file_size_known',
206
StringIO(self.alpha))
207
# We define no range, relying on RangeFile to provide default values
208
self.first_range_start = 0 # It's the whole file
210
def test_seek_from_end(self):
211
"""See TestRangeFileMixin.test_seek_from_end.
213
The end of the file can't be determined since the size is unknown.
215
self.assertRaises(errors.InvalidRange, self._file.seek, -1, 2)
217
def test_read_at_range_end(self):
218
"""Test read behaviour at range end."""
220
self.assertEquals(self.alpha, f.read())
221
self.assertEquals('', f.read(0))
222
self.assertEquals('', f.read(1))
224
class TestRangeFileSizeKnown(tests.TestCase, TestRangeFileMixin):
225
"""Test a RangeFile for a whole file whose size is known."""
228
super(TestRangeFileSizeKnown, self).setUp()
229
self._file = response.RangeFile('Whole_file_size_known',
230
StringIO(self.alpha))
231
self._file.set_range(0, len(self.alpha))
232
self.first_range_start = 0 # It's the whole file
235
class TestRangeFileSingleRange(tests.TestCase, TestRangeFileMixin):
236
"""Test a RangeFile for a single range."""
239
super(TestRangeFileSingleRange, self).setUp()
240
self._file = response.RangeFile('Single_range_file',
241
StringIO(self.alpha))
242
self.first_range_start = 15
243
self._file.set_range(self.first_range_start, len(self.alpha))
246
def test_read_before_range(self):
247
# This can't occur under normal circumstances, we have to force it
249
f._pos = 0 # Force an invalid pos
250
self.assertRaises(errors.InvalidRange, f.read, 2)
252
class TestRangeFileMultipleRanges(tests.TestCase, TestRangeFileMixin):
253
"""Test a RangeFile for multiple ranges.
255
The RangeFile used for the tests contains three ranges:
257
- at offset 25: alpha
258
- at offset 100: alpha
259
- at offset 126: alpha.upper()
261
The two last ranges are contiguous. This only rarely occurs (should not in
262
fact) in real uses but may lead to hard to track bugs.
266
super(TestRangeFileMultipleRanges, self).setUp()
268
boundary = 'separation'
271
self.first_range_start = 25
272
file_size = 200 # big enough to encompass all ranges
273
for (start, part) in [(self.first_range_start, self.alpha),
274
# Two contiguous ranges
276
(126, self.alpha.upper())]:
277
content += self._multipart_byterange(part, start, boundary,
280
content += self._boundary_line(boundary)
282
self._file = response.RangeFile('Multiple_ranges_file',
284
# Ranges are set by decoding the range headers, the RangeFile user is
285
# supposed to call the following before using seek or read since it
286
# requires knowing the *response* headers (in that case the boundary
287
# which is part of the Content-Type header).
288
self._file.set_boundary(boundary)
290
def _boundary_line(self, boundary):
291
"""Helper to build the formatted boundary line."""
292
return '--' + boundary + '\r\n'
294
def _multipart_byterange(self, data, offset, boundary, file_size='*'):
295
"""Encode a part of a file as a multipart/byterange MIME type.
297
When a range request is issued, the HTTP response body can be
298
decomposed in parts, each one representing a range (start, size) in a
301
:param data: The payload.
302
:param offset: where data starts in the file
303
:param boundary: used to separate the parts
304
:param file_size: the size of the file containing the range (default to
307
:return: a string containing the data encoded as it will appear in the
310
bline = self._boundary_line(boundary)
311
# Each range begins with a boundary line
313
# A range is described by a set of headers, but only 'Content-Range' is
314
# required for our implementation (TestHandleResponse below will
315
# exercise ranges with multiple or missing headers')
316
range += 'Content-Range: bytes %d-%d/%d\r\n' % (offset,
320
# Finally the raw bytes
324
def test_read_all_ranges(self):
326
self.assertEquals(self.alpha, f.read()) # Read first range
327
f.seek(100) # Trigger the second range recognition
328
self.assertEquals(self.alpha, f.read()) # Read second range
329
self.assertEquals(126, f.tell())
330
f.seek(126) # Start of third range which is also the current pos !
331
self.assertEquals('A', f.read(1))
333
self.assertEquals('LMN', f.read(3))
335
def test_seek_from_end(self):
336
"""See TestRangeFileMixin.test_seek_from_end."""
337
# The actual implementation will seek from end for the first range only
338
# and then fail. Since seeking from end is intended to be used for a
339
# single range only anyway, this test just document the actual
343
self.assertEquals('yz', f.read())
344
self.assertRaises(errors.InvalidRange, f.seek, -2, 2)
346
def test_seek_into_void(self):
348
start = self.first_range_start
350
# Seeking to a point between two ranges is possible (only once) but
351
# reading there is forbidden
353
# We crossed a range boundary, so now the file is positioned at the
354
# start of the new range (i.e. trying to seek below 100 will error out)
358
def test_seek_across_ranges(self):
360
start = self.first_range_start
361
f.seek(126) # skip the two first ranges
362
self.assertEquals('AB', f.read(2))
364
def test_checked_read_dont_overflow_buffers(self):
366
start = self.first_range_start
367
# We force a very low value to exercise all code paths in _checked_read
368
f._discarded_buf_size = 8
369
f.seek(126) # skip the two first ranges
370
self.assertEquals('AB', f.read(2))
372
def test_seek_twice_between_ranges(self):
374
start = self.first_range_start
375
f.seek(start + 40) # Past the first range but before the second
376
# Now the file is positioned at the second range start (100)
377
self.assertRaises(errors.InvalidRange, f.seek, start + 41)
379
def test_seek_at_range_end(self):
380
"""Test seek behavior at range end."""
386
def test_read_at_range_end(self):
388
self.assertEquals(self.alpha, f.read())
389
self.assertEquals(self.alpha, f.read())
390
self.assertEquals(self.alpha.upper(), f.read())
391
self.assertRaises(errors.InvalidHttpResponse, f.read, 1)
394
class TestRangeFileVarious(tests.TestCase):
395
"""Tests RangeFile aspects not covered elsewhere."""
397
def test_seek_whence(self):
398
"""Test the seek whence parameter values."""
399
f = response.RangeFile('foo', StringIO('abc'))
404
self.assertRaises(ValueError, f.seek, 0, 14)
406
def test_range_syntax(self):
407
"""Test the Content-Range scanning."""
409
f = response.RangeFile('foo', StringIO())
411
def ok(expected, header_value):
412
f.set_range_from_header(header_value)
413
# Slightly peek under the covers to get the size
414
self.assertEquals(expected, (f.tell(), f._size))
416
ok((1, 10), 'bytes 1-10/11')
417
ok((1, 10), 'bytes 1-10/*')
418
ok((12, 2), '\tbytes 12-13/*')
419
ok((28, 1), ' bytes 28-28/*')
420
ok((2123, 2120), 'bytes 2123-4242/12310')
421
ok((1, 10), 'bytes 1-10/ttt') # We don't check total (ttt)
423
def nok(header_value):
424
self.assertRaises(errors.InvalidHttpRange,
425
f.set_range_from_header, header_value)
429
nok('bytes xx-yyy/zzz')
430
nok('bytes xx-12/zzz')
431
nok('bytes 11-yy/zzz')
435
# Taken from real request responses
436
_full_text_response = (200, """HTTP/1.1 200 OK\r
437
Date: Tue, 11 Jul 2006 04:32:56 GMT\r
438
Server: Apache/2.0.54 (Fedora)\r
439
Last-Modified: Sun, 23 Apr 2006 19:35:20 GMT\r
440
ETag: "56691-23-38e9ae00"\r
441
Accept-Ranges: bytes\r
444
Content-Type: text/plain; charset=UTF-8\r
446
""", """Bazaar-NG meta directory, format 1
450
_single_range_response = (206, """HTTP/1.1 206 Partial Content\r
451
Date: Tue, 11 Jul 2006 04:45:22 GMT\r
452
Server: Apache/2.0.54 (Fedora)\r
453
Last-Modified: Thu, 06 Jul 2006 20:22:05 GMT\r
454
ETag: "238a3c-16ec2-805c5540"\r
455
Accept-Ranges: bytes\r
456
Content-Length: 100\r
457
Content-Range: bytes 100-199/93890\r
459
Content-Type: text/plain; charset=UTF-8\r
461
""", """mbp@sourcefrog.net-20050309040815-13242001617e4a06
462
mbp@sourcefrog.net-20050309040929-eee0eb3e6d1e762""")
465
_single_range_no_content_type = (206, """HTTP/1.1 206 Partial Content\r
466
Date: Tue, 11 Jul 2006 04:45:22 GMT\r
467
Server: Apache/2.0.54 (Fedora)\r
468
Last-Modified: Thu, 06 Jul 2006 20:22:05 GMT\r
469
ETag: "238a3c-16ec2-805c5540"\r
470
Accept-Ranges: bytes\r
471
Content-Length: 100\r
472
Content-Range: bytes 100-199/93890\r
475
""", """mbp@sourcefrog.net-20050309040815-13242001617e4a06
476
mbp@sourcefrog.net-20050309040929-eee0eb3e6d1e762""")
479
_multipart_range_response = (206, """HTTP/1.1 206 Partial Content\r
480
Date: Tue, 11 Jul 2006 04:49:48 GMT\r
481
Server: Apache/2.0.54 (Fedora)\r
482
Last-Modified: Thu, 06 Jul 2006 20:22:05 GMT\r
483
ETag: "238a3c-16ec2-805c5540"\r
484
Accept-Ranges: bytes\r
485
Content-Length: 1534\r
487
Content-Type: multipart/byteranges; boundary=418470f848b63279b\r
489
\r""", """--418470f848b63279b\r
490
Content-type: text/plain; charset=UTF-8\r
491
Content-range: bytes 0-254/93890\r
493
mbp@sourcefrog.net-20050309040815-13242001617e4a06
494
mbp@sourcefrog.net-20050309040929-eee0eb3e6d1e7627
495
mbp@sourcefrog.net-20050309040957-6cad07f466bb0bb8
496
mbp@sourcefrog.net-20050309041501-c840e09071de3b67
497
mbp@sourcefrog.net-20050309044615-c24a3250be83220a
499
--418470f848b63279b\r
500
Content-type: text/plain; charset=UTF-8\r
501
Content-range: bytes 1000-2049/93890\r
504
mbp@sourcefrog.net-20050311063625-07858525021f270b
505
mbp@sourcefrog.net-20050311231934-aa3776aff5200bb9
506
mbp@sourcefrog.net-20050311231953-73aeb3a131c3699a
507
mbp@sourcefrog.net-20050311232353-f5e33da490872c6a
508
mbp@sourcefrog.net-20050312071639-0a8f59a34a024ff0
509
mbp@sourcefrog.net-20050312073432-b2c16a55e0d6e9fb
510
mbp@sourcefrog.net-20050312073831-a47c3335ece1920f
511
mbp@sourcefrog.net-20050312085412-13373aa129ccbad3
512
mbp@sourcefrog.net-20050313052251-2bf004cb96b39933
513
mbp@sourcefrog.net-20050313052856-3edd84094687cb11
514
mbp@sourcefrog.net-20050313053233-e30a4f28aef48f9d
515
mbp@sourcefrog.net-20050313053853-7c64085594ff3072
516
mbp@sourcefrog.net-20050313054757-a86c3f5871069e22
517
mbp@sourcefrog.net-20050313061422-418f1f73b94879b9
518
mbp@sourcefrog.net-20050313120651-497bd231b19df600
519
mbp@sourcefrog.net-20050314024931-eae0170ef25a5d1a
520
mbp@sourcefrog.net-20050314025438-d52099f915fe65fc
521
mbp@sourcefrog.net-20050314025539-637a636692c055cf
522
mbp@sourcefrog.net-20050314025737-55eb441f430ab4ba
523
mbp@sourcefrog.net-20050314025901-d74aa93bb7ee8f62
525
--418470f848b63279b--\r
529
_multipart_squid_range_response = (206, """HTTP/1.0 206 Partial Content\r
530
Date: Thu, 31 Aug 2006 21:16:22 GMT\r
531
Server: Apache/2.2.2 (Unix) DAV/2\r
532
Last-Modified: Thu, 31 Aug 2006 17:57:06 GMT\r
533
Accept-Ranges: bytes\r
534
Content-Type: multipart/byteranges; boundary="squid/2.5.STABLE12:C99323425AD4FE26F726261FA6C24196"\r
535
Content-Length: 598\r
536
X-Cache: MISS from localhost.localdomain\r
537
X-Cache-Lookup: HIT from localhost.localdomain:3128\r
538
Proxy-Connection: keep-alive\r
542
--squid/2.5.STABLE12:C99323425AD4FE26F726261FA6C24196\r
543
Content-Type: text/plain\r
544
Content-Range: bytes 0-99/18672\r
548
scott@netsplit.com-20050708230047-47c7868f276b939f fulltext 0 863 :
550
--squid/2.5.STABLE12:C99323425AD4FE26F726261FA6C24196\r
551
Content-Type: text/plain\r
552
Content-Range: bytes 300-499/18672\r
554
com-20050708231537-2b124b835395399a :
555
scott@netsplit.com-20050820234126-551311dbb7435b51 line-delta 1803 479 .scott@netsplit.com-20050820232911-dc4322a084eadf7e :
556
scott@netsplit.com-20050821213706-c86\r
557
--squid/2.5.STABLE12:C99323425AD4FE26F726261FA6C24196--\r
562
_full_text_response_no_content_type = (200, """HTTP/1.1 200 OK\r
563
Date: Tue, 11 Jul 2006 04:32:56 GMT\r
564
Server: Apache/2.0.54 (Fedora)\r
565
Last-Modified: Sun, 23 Apr 2006 19:35:20 GMT\r
566
ETag: "56691-23-38e9ae00"\r
567
Accept-Ranges: bytes\r
571
""", """Bazaar-NG meta directory, format 1
575
_full_text_response_no_content_length = (200, """HTTP/1.1 200 OK\r
576
Date: Tue, 11 Jul 2006 04:32:56 GMT\r
577
Server: Apache/2.0.54 (Fedora)\r
578
Last-Modified: Sun, 23 Apr 2006 19:35:20 GMT\r
579
ETag: "56691-23-38e9ae00"\r
580
Accept-Ranges: bytes\r
582
Content-Type: text/plain; charset=UTF-8\r
584
""", """Bazaar-NG meta directory, format 1
588
_single_range_no_content_range = (206, """HTTP/1.1 206 Partial Content\r
589
Date: Tue, 11 Jul 2006 04:45:22 GMT\r
590
Server: Apache/2.0.54 (Fedora)\r
591
Last-Modified: Thu, 06 Jul 2006 20:22:05 GMT\r
592
ETag: "238a3c-16ec2-805c5540"\r
593
Accept-Ranges: bytes\r
594
Content-Length: 100\r
597
""", """mbp@sourcefrog.net-20050309040815-13242001617e4a06
598
mbp@sourcefrog.net-20050309040929-eee0eb3e6d1e762""")
601
_single_range_response_truncated = (206, """HTTP/1.1 206 Partial Content\r
602
Date: Tue, 11 Jul 2006 04:45:22 GMT\r
603
Server: Apache/2.0.54 (Fedora)\r
604
Last-Modified: Thu, 06 Jul 2006 20:22:05 GMT\r
605
ETag: "238a3c-16ec2-805c5540"\r
606
Accept-Ranges: bytes\r
607
Content-Length: 100\r
608
Content-Range: bytes 100-199/93890\r
610
Content-Type: text/plain; charset=UTF-8\r
612
""", """mbp@sourcefrog.net-20050309040815-13242001617e4a06""")
615
_invalid_response = (444, """HTTP/1.1 444 Bad Response\r
616
Date: Tue, 11 Jul 2006 04:32:56 GMT\r
618
Content-Type: text/html; charset=iso-8859-1\r
620
""", """<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
622
<title>404 Not Found</title>
625
<p>I don't know what I'm doing</p>
631
_multipart_no_content_range = (206, """HTTP/1.0 206 Partial Content\r
632
Content-Type: multipart/byteranges; boundary=THIS_SEPARATES\r
633
Content-Length: 598\r
638
Content-Type: text/plain\r
645
_multipart_no_boundary = (206, """HTTP/1.0 206 Partial Content\r
646
Content-Type: multipart/byteranges; boundary=THIS_SEPARATES\r
647
Content-Length: 598\r
652
Content-Type: text/plain\r
653
Content-Range: bytes 0-18/18672\r
657
The range ended at the line above, this text is garbage instead of a boundary
662
class TestHandleResponse(tests.TestCase):
664
def _build_HTTPMessage(self, raw_headers):
665
status_and_headers = StringIO(raw_headers)
666
# Get rid of the status line
667
status_and_headers.readline()
668
msg = httplib.HTTPMessage(status_and_headers)
671
def get_response(self, a_response):
672
"""Process a supplied response, and return the result."""
673
code, raw_headers, body = a_response
674
msg = self._build_HTTPMessage(raw_headers)
675
return response.handle_response('http://foo', code, msg,
676
StringIO(a_response[2]))
678
def test_full_text(self):
679
out = self.get_response(_full_text_response)
680
# It is a StringIO from the original data
681
self.assertEqual(_full_text_response[2], out.read())
683
def test_single_range(self):
684
out = self.get_response(_single_range_response)
687
self.assertEqual(_single_range_response[2], out.read(100))
689
def test_single_range_no_content(self):
690
out = self.get_response(_single_range_no_content_type)
693
self.assertEqual(_single_range_no_content_type[2], out.read(100))
695
def test_single_range_truncated(self):
696
out = self.get_response(_single_range_response_truncated)
697
# Content-Range declares 100 but only 51 present
698
self.assertRaises(errors.ShortReadvError, out.seek, out.tell() + 51)
700
def test_multi_range(self):
701
out = self.get_response(_multipart_range_response)
703
# Just make sure we can read the right contents
710
def test_multi_squid_range(self):
711
out = self.get_response(_multipart_squid_range_response)
713
# Just make sure we can read the right contents
720
def test_invalid_response(self):
721
self.assertRaises(errors.InvalidHttpResponse,
722
self.get_response, _invalid_response)
724
def test_full_text_no_content_type(self):
725
# We should not require Content-Type for a full response
726
code, raw_headers, body = _full_text_response_no_content_type
727
msg = self._build_HTTPMessage(raw_headers)
728
out = response.handle_response('http://foo', code, msg, StringIO(body))
729
self.assertEqual(body, out.read())
731
def test_full_text_no_content_length(self):
732
code, raw_headers, body = _full_text_response_no_content_length
733
msg = self._build_HTTPMessage(raw_headers)
734
out = response.handle_response('http://foo', code, msg, StringIO(body))
735
self.assertEqual(body, out.read())
737
def test_missing_content_range(self):
738
code, raw_headers, body = _single_range_no_content_range
739
msg = self._build_HTTPMessage(raw_headers)
740
self.assertRaises(errors.InvalidHttpResponse,
741
response.handle_response,
742
'http://bogus', code, msg, StringIO(body))
744
def test_multipart_no_content_range(self):
745
code, raw_headers, body = _multipart_no_content_range
746
msg = self._build_HTTPMessage(raw_headers)
747
self.assertRaises(errors.InvalidHttpResponse,
748
response.handle_response,
749
'http://bogus', code, msg, StringIO(body))
751
def test_multipart_no_boundary(self):
752
out = self.get_response(_multipart_no_boundary)
753
out.read() # Read the whole range
754
# Fail to find the boundary line
755
self.assertRaises(errors.InvalidHttpResponse, out.seek, 1, 1)