1
# Copyright (c) 2001-2010 Twisted Matrix Laboratories.
2
# See LICENSE for details.
5
Tests for L{twisted.web.client}.
9
from errno import ENOSPC
11
from urlparse import urlparse
13
from twisted.trial import unittest
14
from twisted.web import server, static, client, error, util, resource, http_headers
15
from twisted.internet import reactor, defer, interfaces
16
from twisted.python.filepath import FilePath
17
from twisted.python.log import msg
18
from twisted.protocols.policies import WrappingFactory
19
from twisted.test.proto_helpers import StringTransport
20
from twisted.test.proto_helpers import MemoryReactor
21
from twisted.internet.address import IPv4Address
22
from twisted.internet.task import Clock
23
from twisted.internet.error import ConnectionRefusedError
24
from twisted.internet.protocol import Protocol
25
from twisted.internet.defer import Deferred
26
from twisted.web.client import Request
27
from twisted.web.error import SchemeNotSupported
30
from twisted.internet import ssl
36
class ExtendedRedirect(resource.Resource):
40
The HTTP status code is set according to the C{code} query parameter.
42
@type lastMethod: C{str}
43
@ivar lastMethod: Last handled HTTP request method
49
def __init__(self, url):
50
resource.Resource.__init__(self)
54
def render(self, request):
56
self.lastMethod = request.method
59
self.lastMethod = request.method
60
code = int(request.args['code'][0])
61
return self.redirectTo(self.url, request, code)
64
def getChild(self, name, request):
68
def redirectTo(self, url, request, code):
69
request.setResponseCode(code)
70
request.setHeader("location", url)
75
class ForeverTakingResource(resource.Resource):
77
L{ForeverTakingResource} is a resource which never finishes responding
80
def __init__(self, write=False):
81
resource.Resource.__init__(self)
84
def render(self, request):
86
request.write('some bytes')
87
return server.NOT_DONE_YET
90
class CookieMirrorResource(resource.Resource):
91
def render(self, request):
93
for k,v in request.received_cookies.items():
98
class RawCookieMirrorResource(resource.Resource):
99
def render(self, request):
100
return repr(request.getHeader('cookie'))
102
class ErrorResource(resource.Resource):
104
def render(self, request):
105
request.setResponseCode(401)
106
if request.args.get("showlength"):
107
request.setHeader("content-length", "0")
110
class NoLengthResource(resource.Resource):
112
def render(self, request):
117
class HostHeaderResource(resource.Resource):
119
A testing resource which renders itself as the value of the host header
122
def render(self, request):
123
return request.received_headers['host']
127
class PayloadResource(resource.Resource):
129
A testing resource which renders itself as the contents of the request body
130
as long as the request body is 100 bytes long, otherwise which renders
131
itself as C{"ERROR"}.
133
def render(self, request):
134
data = request.content.read()
135
contentLength = request.received_headers['content-length']
136
if len(data) != 100 or int(contentLength) != 100:
142
class BrokenDownloadResource(resource.Resource):
144
def render(self, request):
145
# only sends 3 bytes even though it claims to send 5
146
request.setHeader("content-length", "5")
150
class CountingRedirect(util.Redirect):
152
A L{util.Redirect} resource that keeps track of the number of times the
153
resource has been accessed.
155
def __init__(self, *a, **kw):
156
util.Redirect.__init__(self, *a, **kw)
159
def render(self, request):
161
return util.Redirect.render(self, request)
165
class ParseUrlTestCase(unittest.TestCase):
167
Test URL parsing facility and defaults values.
170
def test_parse(self):
172
L{client._parse} correctly parses a URL into its various components.
174
# The default port for HTTP is 80.
176
client._parse('http://127.0.0.1/'),
177
('http', '127.0.0.1', 80, '/'))
179
# The default port for HTTPS is 443.
181
client._parse('https://127.0.0.1/'),
182
('https', '127.0.0.1', 443, '/'))
186
client._parse('http://spam:12345/'),
187
('http', 'spam', 12345, '/'))
189
# Weird (but commonly accepted) structure uses default port.
191
client._parse('http://spam:/'),
192
('http', 'spam', 80, '/'))
194
# Spaces in the hostname are trimmed, the default path is /.
196
client._parse('http://foo '),
197
('http', 'foo', 80, '/'))
200
def test_externalUnicodeInterference(self):
202
L{client._parse} should return C{str} for the scheme, host, and path
203
elements of its return tuple, even when passed an URL which has
204
previously been passed to L{urlparse} as a C{unicode} string.
206
badInput = u'http://example.com/path'
207
goodInput = badInput.encode('ascii')
209
scheme, host, port, path = client._parse(goodInput)
210
self.assertTrue(isinstance(scheme, str))
211
self.assertTrue(isinstance(host, str))
212
self.assertTrue(isinstance(path, str))
216
class HTTPPageGetterTests(unittest.TestCase):
218
Tests for L{HTTPPagerGetter}, the HTTP client protocol implementation
219
used to implement L{getPage}.
221
def test_earlyHeaders(self):
223
When a connection is made, L{HTTPPagerGetter} sends the headers from
224
its factory's C{headers} dict. If I{Host} or I{Content-Length} is
225
present in this dict, the values are not sent, since they are sent with
226
special values before the C{headers} dict is processed. If
227
I{User-Agent} is present in the dict, it overrides the value of the
228
C{agent} attribute of the factory. If I{Cookie} is present in the
229
dict, its value is added to the values from the factory's C{cookies}
232
factory = client.HTTPClientFactory(
235
cookies={'baz': 'quux'},
236
postdata="some data",
238
'Host': 'example.net',
239
'User-Agent': 'fooble',
240
'Cookie': 'blah blah',
241
'Content-Length': '12981',
243
transport = StringTransport()
244
protocol = client.HTTPPageGetter()
245
protocol.factory = factory
246
protocol.makeConnection(transport)
249
"GET /bar HTTP/1.0\r\n"
250
"Host: example.net\r\n"
251
"User-Agent: foobar\r\n"
252
"Content-Length: 9\r\n"
254
"connection: close\r\n"
255
"Cookie: blah blah; baz=quux\r\n"
261
class WebClientTestCase(unittest.TestCase):
262
def _listen(self, site):
263
return reactor.listenTCP(0, site, interface="127.0.0.1")
266
self.cleanupServerConnections = 0
269
FilePath(name).child("file").setContent("0123456789")
270
r = static.File(name)
271
r.putChild("redirect", util.Redirect("/file"))
272
self.infiniteRedirectResource = CountingRedirect("/infiniteRedirect")
273
r.putChild("infiniteRedirect", self.infiniteRedirectResource)
274
r.putChild("wait", ForeverTakingResource())
275
r.putChild("write-then-wait", ForeverTakingResource(write=True))
276
r.putChild("error", ErrorResource())
277
r.putChild("nolength", NoLengthResource())
278
r.putChild("host", HostHeaderResource())
279
r.putChild("payload", PayloadResource())
280
r.putChild("broken", BrokenDownloadResource())
281
r.putChild("cookiemirror", CookieMirrorResource())
283
miscasedHead = static.Data("miscased-head GET response content", "major/minor")
284
miscasedHead.render_Head = lambda request: "miscased-head content"
285
r.putChild("miscased-head", miscasedHead)
287
self.extendedRedirect = ExtendedRedirect('/extendedRedirect')
288
r.putChild("extendedRedirect", self.extendedRedirect)
289
self.site = server.Site(r, timeout=None)
290
self.wrapper = WrappingFactory(self.site)
291
self.port = self._listen(self.wrapper)
292
self.portno = self.port.getHost().port
295
# If the test indicated it might leave some server-side connections
296
# around, clean them up.
297
connections = self.wrapper.protocols.keys()
298
# If there are fewer server-side connections than requested,
299
# that's okay. Some might have noticed that the client closed
300
# the connection and cleaned up after themselves.
301
for n in range(min(len(connections), self.cleanupServerConnections)):
302
proto = connections.pop()
303
msg("Closing %r" % (proto,))
304
proto.transport.loseConnection()
306
msg("Some left-over connections; this test is probably buggy.")
307
return self.port.stopListening()
309
def getURL(self, path):
310
return "http://127.0.0.1:%d/%s" % (self.portno, path)
312
def testPayload(self):
313
s = "0123456789" * 10
314
return client.getPage(self.getURL("payload"), postdata=s
315
).addCallback(self.assertEquals, s
319
def test_getPageBrokenDownload(self):
321
If the connection is closed before the number of bytes indicated by
322
I{Content-Length} have been received, the L{Deferred} returned by
323
L{getPage} fails with L{PartialDownloadError}.
325
d = client.getPage(self.getURL("broken"))
326
d = self.assertFailure(d, client.PartialDownloadError)
327
d.addCallback(lambda exc: self.assertEquals(exc.response, "abc"))
331
def test_downloadPageBrokenDownload(self):
333
If the connection is closed before the number of bytes indicated by
334
I{Content-Length} have been received, the L{Deferred} returned by
335
L{downloadPage} fails with L{PartialDownloadError}.
337
# test what happens when download gets disconnected in the middle
338
path = FilePath(self.mktemp())
339
d = client.downloadPage(self.getURL("broken"), path.path)
340
d = self.assertFailure(d, client.PartialDownloadError)
342
def checkResponse(response):
344
The HTTP status code from the server is propagated through the
345
C{PartialDownloadError}.
347
self.assertEquals(response.status, "200")
348
self.assertEquals(response.message, "OK")
350
d.addCallback(checkResponse)
352
def cbFailed(ignored):
353
self.assertEquals(path.getContent(), "abc")
354
d.addCallback(cbFailed)
358
def test_downloadPageLogsFileCloseError(self):
360
If there is an exception closing the file being written to after the
361
connection is prematurely closed, that exception is logged.
364
def write(self, bytes):
368
raise IOError(ENOSPC, "No file left on device")
370
d = client.downloadPage(self.getURL("broken"), BrokenFile())
371
d = self.assertFailure(d, client.PartialDownloadError)
372
def cbFailed(ignored):
373
self.assertEquals(len(self.flushLoggedErrors(IOError)), 1)
374
d.addCallback(cbFailed)
378
def testHostHeader(self):
379
# if we pass Host header explicitly, it should be used, otherwise
380
# it should extract from url
381
return defer.gatherResults([
382
client.getPage(self.getURL("host")).addCallback(self.assertEquals, "127.0.0.1"),
383
client.getPage(self.getURL("host"), headers={"Host": "www.example.com"}).addCallback(self.assertEquals, "www.example.com")])
386
def test_getPage(self):
388
L{client.getPage} returns a L{Deferred} which is called back with
389
the body of the response if the default method B{GET} is used.
391
d = client.getPage(self.getURL("file"))
392
d.addCallback(self.assertEquals, "0123456789")
396
def test_getPageHEAD(self):
398
L{client.getPage} returns a L{Deferred} which is called back with
399
the empty string if the method is I{HEAD} and there is a successful
402
d = client.getPage(self.getURL("file"), method="HEAD")
403
d.addCallback(self.assertEquals, "")
408
def test_getPageNotQuiteHEAD(self):
410
If the request method is a different casing of I{HEAD} (ie, not all
411
capitalized) then it is not a I{HEAD} request and the response body
414
d = client.getPage(self.getURL("miscased-head"), method='Head')
415
d.addCallback(self.assertEquals, "miscased-head content")
419
def test_timeoutNotTriggering(self):
421
When a non-zero timeout is passed to L{getPage} and the page is
422
retrieved before the timeout period elapses, the L{Deferred} is
423
called back with the contents of the page.
425
d = client.getPage(self.getURL("host"), timeout=100)
426
d.addCallback(self.assertEquals, "127.0.0.1")
430
def test_timeoutTriggering(self):
432
When a non-zero timeout is passed to L{getPage} and that many
433
seconds elapse before the server responds to the request. the
434
L{Deferred} is errbacked with a L{error.TimeoutError}.
436
# This will probably leave some connections around.
437
self.cleanupServerConnections = 1
438
return self.assertFailure(
439
client.getPage(self.getURL("wait"), timeout=0.000001),
443
def testDownloadPage(self):
445
downloadData = [("file", self.mktemp(), "0123456789"),
446
("nolength", self.mktemp(), "nolength")]
448
for (url, name, data) in downloadData:
449
d = client.downloadPage(self.getURL(url), name)
450
d.addCallback(self._cbDownloadPageTest, data, name)
452
return defer.gatherResults(downloads)
454
def _cbDownloadPageTest(self, ignored, data, name):
455
bytes = file(name, "rb").read()
456
self.assertEquals(bytes, data)
458
def testDownloadPageError1(self):
460
def write(self, data):
461
raise IOError, "badness happened during write"
465
return self.assertFailure(
466
client.downloadPage(self.getURL("file"), ef),
469
def testDownloadPageError2(self):
471
def write(self, data):
474
raise IOError, "badness happened during close"
476
return self.assertFailure(
477
client.downloadPage(self.getURL("file"), ef),
480
def testDownloadPageError3(self):
481
# make sure failures in open() are caught too. This is tricky.
482
# Might only work on posix.
483
tmpfile = open("unwritable", "wb")
485
os.chmod("unwritable", 0) # make it unwritable (to us)
486
d = self.assertFailure(
487
client.downloadPage(self.getURL("file"), "unwritable"),
489
d.addBoth(self._cleanupDownloadPageError3)
492
def _cleanupDownloadPageError3(self, ignored):
493
os.chmod("unwritable", 0700)
494
os.unlink("unwritable")
497
def _downloadTest(self, method):
499
for (url, code) in [("nosuchfile", "404"), ("error", "401"),
500
("error?showlength=1", "401")]:
502
d = self.assertFailure(d, error.Error)
503
d.addCallback(lambda exc, code=code: self.assertEquals(exc.args[0], code))
505
return defer.DeferredList(dl, fireOnOneErrback=True)
507
def testServerError(self):
508
return self._downloadTest(lambda url: client.getPage(self.getURL(url)))
510
def testDownloadServerError(self):
511
return self._downloadTest(lambda url: client.downloadPage(self.getURL(url), url.split('?')[0]))
513
def testFactoryInfo(self):
514
url = self.getURL('file')
515
scheme, host, port, path = client._parse(url)
516
factory = client.HTTPClientFactory(url)
517
reactor.connectTCP(host, port, factory)
518
return factory.deferred.addCallback(self._cbFactoryInfo, factory)
520
def _cbFactoryInfo(self, ignoredResult, factory):
521
self.assertEquals(factory.status, '200')
522
self.assert_(factory.version.startswith('HTTP/'))
523
self.assertEquals(factory.message, 'OK')
524
self.assertEquals(factory.response_headers['content-length'][0], '10')
527
def testRedirect(self):
528
return client.getPage(self.getURL("redirect")).addCallback(self._cbRedirect)
530
def _cbRedirect(self, pageData):
531
self.assertEquals(pageData, "0123456789")
532
d = self.assertFailure(
533
client.getPage(self.getURL("redirect"), followRedirect=0),
535
d.addCallback(self._cbCheckLocation)
538
def _cbCheckLocation(self, exc):
539
self.assertEquals(exc.location, "/file")
542
def test_infiniteRedirection(self):
544
When more than C{redirectLimit} HTTP redirects are encountered, the
545
page request fails with L{InfiniteRedirection}.
547
def checkRedirectCount(*a):
548
self.assertEquals(f._redirectCount, 13)
549
self.assertEquals(self.infiniteRedirectResource.count, 13)
551
f = client._makeGetterFactory(
552
self.getURL('infiniteRedirect'),
553
client.HTTPClientFactory,
555
d = self.assertFailure(f.deferred, error.InfiniteRedirection)
556
d.addCallback(checkRedirectCount)
560
def test_isolatedFollowRedirect(self):
562
C{client.HTTPPagerGetter} instances each obey the C{followRedirect}
563
value passed to the L{client.getPage} call which created them.
565
d1 = client.getPage(self.getURL('redirect'), followRedirect=True)
566
d2 = client.getPage(self.getURL('redirect'), followRedirect=False)
568
d = self.assertFailure(d2, error.PageRedirect
569
).addCallback(lambda dummy: d1)
573
def test_afterFoundGet(self):
575
Enabling unsafe redirection behaviour overwrites the method of
576
redirected C{POST} requests with C{GET}.
578
url = self.getURL('extendedRedirect?code=302')
579
f = client.HTTPClientFactory(url, followRedirect=True, method="POST")
582
"By default, afterFoundGet must be disabled")
586
self.extendedRedirect.lastMethod,
588
"With afterFoundGet, the HTTP method must change to GET")
591
url, followRedirect=True, afterFoundGet=True, method="POST")
592
d.addCallback(gotPage)
596
def testPartial(self):
602
partialDownload = [(True, "abcd456789"),
603
(True, "abcd456789"),
604
(False, "0123456789")]
606
d = defer.succeed(None)
607
for (partial, expectedData) in partialDownload:
608
d.addCallback(self._cbRunPartial, name, partial)
609
d.addCallback(self._cbPartialTest, expectedData, name)
613
testPartial.skip = "Cannot test until webserver can serve partial data properly"
615
def _cbRunPartial(self, ignored, name, partial):
616
return client.downloadPage(self.getURL("file"), name, supportPartial=partial)
618
def _cbPartialTest(self, ignored, expectedData, filename):
619
bytes = file(filename, "rb").read()
620
self.assertEquals(bytes, expectedData)
623
def test_downloadTimeout(self):
625
If the timeout indicated by the C{timeout} parameter to
626
L{client.HTTPDownloader.__init__} elapses without the complete response
627
being received, the L{defer.Deferred} returned by
628
L{client.downloadPage} fires with a L{Failure} wrapping a
629
L{defer.TimeoutError}.
631
self.cleanupServerConnections = 2
632
# Verify the behavior if no bytes are ever written.
633
first = client.downloadPage(
635
self.mktemp(), timeout=0.01)
637
# Verify the behavior if some bytes are written but then the request
639
second = client.downloadPage(
640
self.getURL("write-then-wait"),
641
self.mktemp(), timeout=0.01)
643
return defer.gatherResults([
644
self.assertFailure(first, defer.TimeoutError),
645
self.assertFailure(second, defer.TimeoutError)])
648
def test_downloadHeaders(self):
650
After L{client.HTTPDownloader.deferred} fires, the
651
L{client.HTTPDownloader} instance's C{status} and C{response_headers}
652
attributes are populated with the values from the response.
654
def checkHeaders(factory):
655
self.assertEquals(factory.status, '200')
656
self.assertEquals(factory.response_headers['content-type'][0], 'text/html')
657
self.assertEquals(factory.response_headers['content-length'][0], '10')
658
os.unlink(factory.fileName)
659
factory = client._makeGetterFactory(
661
client.HTTPDownloader,
662
fileOrName=self.mktemp())
663
return factory.deferred.addCallback(lambda _: checkHeaders(factory))
666
def test_downloadCookies(self):
668
The C{cookies} dict passed to the L{client.HTTPDownloader}
669
initializer is used to populate the I{Cookie} header included in the
670
request sent to the server.
672
output = self.mktemp()
673
factory = client._makeGetterFactory(
674
self.getURL('cookiemirror'),
675
client.HTTPDownloader,
677
cookies={'foo': 'bar'})
678
def cbFinished(ignored):
680
FilePath(output).getContent(),
682
factory.deferred.addCallback(cbFinished)
683
return factory.deferred
686
def test_downloadRedirectLimit(self):
688
When more than C{redirectLimit} HTTP redirects are encountered, the
689
page request fails with L{InfiniteRedirection}.
691
def checkRedirectCount(*a):
692
self.assertEquals(f._redirectCount, 7)
693
self.assertEquals(self.infiniteRedirectResource.count, 7)
695
f = client._makeGetterFactory(
696
self.getURL('infiniteRedirect'),
697
client.HTTPDownloader,
698
fileOrName=self.mktemp(),
700
d = self.assertFailure(f.deferred, error.InfiniteRedirection)
701
d.addCallback(checkRedirectCount)
706
class WebClientSSLTestCase(WebClientTestCase):
707
def _listen(self, site):
708
from twisted import test
709
return reactor.listenSSL(0, site,
710
contextFactory=ssl.DefaultOpenSSLContextFactory(
711
FilePath(test.__file__).sibling('server.pem').path,
712
FilePath(test.__file__).sibling('server.pem').path,
714
interface="127.0.0.1")
716
def getURL(self, path):
717
return "https://127.0.0.1:%d/%s" % (self.portno, path)
719
def testFactoryInfo(self):
720
url = self.getURL('file')
721
scheme, host, port, path = client._parse(url)
722
factory = client.HTTPClientFactory(url)
723
reactor.connectSSL(host, port, factory, ssl.ClientContextFactory())
724
# The base class defines _cbFactoryInfo correctly for this
725
return factory.deferred.addCallback(self._cbFactoryInfo, factory)
727
class WebClientRedirectBetweenSSLandPlainText(unittest.TestCase):
728
def getHTTPS(self, path):
729
return "https://127.0.0.1:%d/%s" % (self.tlsPortno, path)
731
def getHTTP(self, path):
732
return "http://127.0.0.1:%d/%s" % (self.plainPortno, path)
735
plainRoot = static.Data('not me', 'text/plain')
736
tlsRoot = static.Data('me neither', 'text/plain')
738
plainSite = server.Site(plainRoot, timeout=None)
739
tlsSite = server.Site(tlsRoot, timeout=None)
741
from twisted import test
742
self.tlsPort = reactor.listenSSL(0, tlsSite,
743
contextFactory=ssl.DefaultOpenSSLContextFactory(
744
FilePath(test.__file__).sibling('server.pem').path,
745
FilePath(test.__file__).sibling('server.pem').path,
747
interface="127.0.0.1")
748
self.plainPort = reactor.listenTCP(0, plainSite, interface="127.0.0.1")
750
self.plainPortno = self.plainPort.getHost().port
751
self.tlsPortno = self.tlsPort.getHost().port
753
plainRoot.putChild('one', util.Redirect(self.getHTTPS('two')))
754
tlsRoot.putChild('two', util.Redirect(self.getHTTP('three')))
755
plainRoot.putChild('three', util.Redirect(self.getHTTPS('four')))
756
tlsRoot.putChild('four', static.Data('FOUND IT!', 'text/plain'))
759
ds = map(defer.maybeDeferred,
760
[self.plainPort.stopListening, self.tlsPort.stopListening])
761
return defer.gatherResults(ds)
763
def testHoppingAround(self):
764
return client.getPage(self.getHTTP("one")
765
).addCallback(self.assertEquals, "FOUND IT!"
769
disconnecting = False
772
def write(self, stuff):
773
self.data.append(stuff)
775
class CookieTestCase(unittest.TestCase):
776
def _listen(self, site):
777
return reactor.listenTCP(0, site, interface="127.0.0.1")
780
root = static.Data('El toro!', 'text/plain')
781
root.putChild("cookiemirror", CookieMirrorResource())
782
root.putChild("rawcookiemirror", RawCookieMirrorResource())
783
site = server.Site(root, timeout=None)
784
self.port = self._listen(site)
785
self.portno = self.port.getHost().port
788
return self.port.stopListening()
790
def getHTTP(self, path):
791
return "http://127.0.0.1:%d/%s" % (self.portno, path)
793
def testNoCookies(self):
794
return client.getPage(self.getHTTP("cookiemirror")
795
).addCallback(self.assertEquals, "[]"
798
def testSomeCookies(self):
799
cookies = {'foo': 'bar', 'baz': 'quux'}
800
return client.getPage(self.getHTTP("cookiemirror"), cookies=cookies
801
).addCallback(self.assertEquals, "[('baz', 'quux'), ('foo', 'bar')]"
804
def testRawNoCookies(self):
805
return client.getPage(self.getHTTP("rawcookiemirror")
806
).addCallback(self.assertEquals, "None"
809
def testRawSomeCookies(self):
810
cookies = {'foo': 'bar', 'baz': 'quux'}
811
return client.getPage(self.getHTTP("rawcookiemirror"), cookies=cookies
812
).addCallback(self.assertEquals, "'foo=bar; baz=quux'"
815
def testCookieHeaderParsing(self):
816
factory = client.HTTPClientFactory('http://foo.example.com/')
817
proto = factory.buildProtocol('127.42.42.42')
818
proto.transport = FakeTransport()
819
proto.connectionMade()
824
'Set-Cookie: CUSTOMER=WILE_E_COYOTE; path=/; expires=Wednesday, 09-Nov-99 23:12:40 GMT',
825
'Set-Cookie: PART_NUMBER=ROCKET_LAUNCHER_0001; path=/',
826
'Set-Cookie: SHIPPING=FEDEX; path=/foo',
831
proto.dataReceived(line + '\r\n')
832
self.assertEquals(proto.transport.data,
833
['GET / HTTP/1.0\r\n',
834
'Host: foo.example.com\r\n',
835
'User-Agent: Twisted PageGetter\r\n',
837
self.assertEquals(factory.cookies,
839
'CUSTOMER': 'WILE_E_COYOTE',
840
'PART_NUMBER': 'ROCKET_LAUNCHER_0001',
846
class StubHTTPProtocol(Protocol):
848
A protocol like L{HTTP11ClientProtocol} but which does not actually know
849
HTTP/1.1 and only collects requests in a list.
851
@ivar requests: A C{list} of two-tuples. Each time a request is made, a
852
tuple consisting of the request and the L{Deferred} returned from the
853
request method is appended to this list.
859
def request(self, request):
861
Capture the given request for later inspection.
863
@return: A L{Deferred} which this code will never fire.
866
self.requests.append((request, result))
871
class AgentTests(unittest.TestCase):
873
Tests for the new HTTP client API provided by L{Agent}.
877
Create an L{Agent} wrapped around a fake reactor.
879
class Reactor(MemoryReactor, Clock):
881
MemoryReactor.__init__(self)
884
self.reactor = Reactor()
885
self.agent = client.Agent(self.reactor)
888
def completeConnection(self):
890
Do whitebox stuff to finish any outstanding connection attempts the
891
agent may have initiated.
893
This spins the fake reactor clock just enough to get L{ClientCreator},
894
which agent is implemented in terms of, to fire its Deferreds.
896
self.reactor.advance(0)
899
def _verifyAndCompleteConnectionTo(self, host, port):
901
Assert that the destination of the oldest unverified TCP connection
902
attempt is the given host and port. Then pop it, create a protocol,
903
connect it to a L{StringTransport}, and return the protocol.
905
# Grab the connection attempt, make sure it goes to the right place,
906
# and cause it to succeed.
907
host, port, factory = self.reactor.tcpClients.pop()[:3]
908
self.assertEquals(host, host)
909
self.assertEquals(port, port)
911
protocol = factory.buildProtocol(IPv4Address('TCP', '10.0.0.3', 1234))
912
transport = StringTransport()
913
protocol.makeConnection(transport)
914
self.completeConnection()
918
def test_unsupportedScheme(self):
920
L{Agent.request} returns a L{Deferred} which fails with
921
L{SchemeNotSupported} if the scheme of the URI passed to it is not
924
return self.assertFailure(
925
self.agent.request('GET', 'mailto:alice@example.com'),
929
def test_connectionFailed(self):
931
The L{Deferred} returned by L{Agent.request} fires with a L{Failure} if
932
the TCP connection attempt fails.
934
result = self.agent.request('GET', 'http://foo/')
936
# Cause the connection to be refused
937
host, port, factory = self.reactor.tcpClients.pop()[:3]
938
factory.clientConnectionFailed(None, ConnectionRefusedError())
939
self.completeConnection()
941
return self.assertFailure(result, ConnectionRefusedError)
944
def test_request(self):
946
L{Agent.request} establishes a new connection to the host indicated by
947
the host part of the URI passed to it and issues a request using the
948
method, the path portion of the URI, the headers, and the body producer
949
passed to it. It returns a L{Deferred} which fires with a L{Response}
952
self.agent._protocol = StubHTTPProtocol
954
headers = http_headers.Headers({'foo': ['bar']})
955
# Just going to check the body for identity, so it doesn't need to be
959
'GET', 'http://example.com:1234/foo?bar', headers, body)
961
protocol = self._verifyAndCompleteConnectionTo('example.com', 1234)
963
# The request should be issued.
964
self.assertEquals(len(protocol.requests), 1)
965
req, res = protocol.requests.pop()
966
self.assertTrue(isinstance(req, Request))
967
self.assertEquals(req.method, 'GET')
968
self.assertEquals(req.uri, '/foo?bar')
971
http_headers.Headers({'foo': ['bar'],
972
'host': ['example.com:1234']}))
973
self.assertIdentical(req.bodyProducer, body)
976
def test_hostProvided(self):
978
If C{None} is passed to L{Agent.request} for the C{headers}
979
parameter, a L{Headers} instance is created for the request and a
980
I{Host} header added to it.
982
self.agent._protocol = StubHTTPProtocol
984
self.agent.request('GET', 'http://example.com/foo')
986
protocol = self._verifyAndCompleteConnectionTo('example.com', 80)
988
# The request should have been issued with a host header based on
990
self.assertEquals(len(protocol.requests), 1)
991
req, res = protocol.requests.pop()
992
self.assertEquals(req.headers.getRawHeaders('host'), ['example.com'])
995
def test_hostOverride(self):
997
If the headers passed to L{Agent.request} includes a value for the
998
I{Host} header, that value takes precedence over the one which would
999
otherwise be automatically provided.
1001
self.agent._protocol = StubHTTPProtocol
1003
headers = http_headers.Headers({'foo': ['bar'], 'host': ['quux']})
1006
'GET', 'http://example.com/baz', headers, body)
1008
protocol = self._verifyAndCompleteConnectionTo('example.com', 80)
1010
# The request should have been issued with the host header specified
1011
# above, not one based on the request URI.
1012
self.assertEquals(len(protocol.requests), 1)
1013
req, res = protocol.requests.pop()
1014
self.assertEquals(req.headers.getRawHeaders('host'), ['quux'])
1017
def test_headersUnmodified(self):
1019
If a I{Host} header must be added to the request, the L{Headers}
1020
instance passed to L{Agent.request} is not modified.
1022
self.agent._protocol = StubHTTPProtocol
1024
headers = http_headers.Headers()
1027
'GET', 'http://example.com/foo', headers, body)
1029
protocol = self._verifyAndCompleteConnectionTo('example.com', 80)
1031
# The request should have been issued.
1032
self.assertEquals(len(protocol.requests), 1)
1033
# And the headers object passed in should not have changed.
1034
self.assertEquals(headers, http_headers.Headers())
1037
def test_hostValue(self):
1039
L{Agent._computeHostValue} returns just the hostname it is passed if
1040
the port number it is passed is the default for the scheme it is
1041
passed, otherwise it returns a string containing both the host and port
1042
separated by C{":"}.
1045
self.agent._computeHostValue('http', 'example.com', 80),
1049
self.agent._computeHostValue('http', 'example.com', 54321),
1050
'example.com:54321')
1054
if ssl is None or not hasattr(ssl, 'DefaultOpenSSLContextFactory'):
1055
for case in [WebClientSSLTestCase, WebClientRedirectBetweenSSLandPlainText]:
1056
case.skip = "OpenSSL not present"
1058
if not interfaces.IReactorSSL(reactor, None):
1059
for case in [WebClientSSLTestCase, WebClientRedirectBetweenSSLandPlainText]:
1060
case.skip = "Reactor doesn't support SSL"