~bzr-pqm/bzr/bzr.dev

2052.3.1 by John Arbash Meinel
Add tests to cleanup the copyright of all source files
1
# Copyright (C) 2005, 2006 Canonical Ltd
1540.3.24 by Martin Pool
Add new protocol 'http+pycurl' that always uses PyCurl.
2
#
1540.3.15 by Martin Pool
[merge] large merge to sync with bzr.dev
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.
1540.3.24 by Martin Pool
Add new protocol 'http+pycurl' that always uses PyCurl.
7
#
1540.3.15 by Martin Pool
[merge] large merge to sync with bzr.dev
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.
1540.3.24 by Martin Pool
Add new protocol 'http+pycurl' that always uses PyCurl.
12
#
1540.3.15 by Martin Pool
[merge] large merge to sync with bzr.dev
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
1185.16.68 by Martin Pool
- http url fixes suggested by Robey Pointer, and tests
16
3111.1.30 by Vincent Ladeuil
Update NEWS. Some cosmetic changes.
17
"""Tests for HTTP implementations.
3111.1.10 by Vincent Ladeuil
Finish http parameterization, 24 auth tests failing for pycurl (not
18
3111.1.30 by Vincent Ladeuil
Update NEWS. Some cosmetic changes.
19
This module defines a load_tests() method that parametrize tests classes for
20
transport implementation, http protocol versions and authentication schemes.
3111.1.10 by Vincent Ladeuil
Finish http parameterization, 24 auth tests failing for pycurl (not
21
"""
1540.3.3 by Martin Pool
Review updates of pycurl transport
22
1540.3.22 by Martin Pool
[patch] Add TestCase.assertIsInstance
23
# TODO: Should be renamed to bzrlib.transport.http.tests?
2167.3.5 by v.ladeuil+lp at free
Tests for proxies, covering #74759.
24
# TODO: What about renaming to bzrlib.tests.transport.http ?
1540.3.22 by Martin Pool
[patch] Add TestCase.assertIsInstance
25
2363.4.12 by Vincent Ladeuil
Take jam's review comments into account. Fix typos, give better
26
from cStringIO import StringIO
3111.1.4 by Vincent Ladeuil
Select the server depending on the request handler protocol. Add tests.
27
import httplib
2167.3.5 by v.ladeuil+lp at free
Tests for proxies, covering #74759.
28
import os
2018.2.9 by Andrew Bennetts
(Andrew Bennetts, Robert Collins) Add test_http.RecordingServer, and use it to
29
import select
3111.1.20 by Vincent Ladeuil
Make all the test pass. Looks like we are HTTP/1.1 compliant.
30
import SimpleHTTPServer
2000.2.2 by John Arbash Meinel
Update the urllib.has test.
31
import socket
2420.1.20 by Vincent Ladeuil
Fix test failure on pqm.
32
import sys
2018.2.9 by Andrew Bennetts
(Andrew Bennetts, Robert Collins) Add test_http.RecordingServer, and use it to
33
import threading
2000.2.2 by John Arbash Meinel
Update the urllib.has test.
34
1553.1.2 by James Henstridge
Add a test to make sure the user-agent header is being sent correctly.
35
import bzrlib
2164.2.22 by Vincent Ladeuil
Take Aaron's review comments into account.
36
from bzrlib import (
3606.4.1 by Andrew Bennetts
Fix NotImplementedError when probing for smart protocol via HTTP.
37
    bzrdir,
2900.2.6 by Vincent Ladeuil
Make http aware of authentication config.
38
    config,
2164.2.22 by Vincent Ladeuil
Take Aaron's review comments into account.
39
    errors,
40
    osutils,
3606.4.1 by Andrew Bennetts
Fix NotImplementedError when probing for smart protocol via HTTP.
41
    remote as _mod_remote,
3052.3.2 by Vincent Ladeuil
Add tests and fix trivial bugs and other typos.
42
    tests,
3111.1.10 by Vincent Ladeuil
Finish http parameterization, 24 auth tests failing for pycurl (not
43
    transport,
2363.4.12 by Vincent Ladeuil
Take jam's review comments into account. Fix typos, give better
44
    ui,
2164.2.22 by Vincent Ladeuil
Take Aaron's review comments into account.
45
    urlutils,
46
    )
3102.1.1 by Vincent Ladeuil
Rename bzrlib/test/HTTPTestUtils.py to bzrlib/tests/http_utils.py and fix
47
from bzrlib.tests import (
3111.1.4 by Vincent Ladeuil
Select the server depending on the request handler protocol. Add tests.
48
    http_server,
3111.1.7 by Vincent Ladeuil
Further refactoring.
49
    http_utils,
3102.1.1 by Vincent Ladeuil
Rename bzrlib/test/HTTPTestUtils.py to bzrlib/tests/http_utils.py and fix
50
    )
3111.1.25 by Vincent Ladeuil
Fix the smart server failing test and use it against protocol combinations.
51
from bzrlib.transport import (
52
    http,
53
    remote,
54
    )
2004.3.3 by vila
Better (but still incomplete) design for bogus servers.
55
from bzrlib.transport.http import (
3111.1.19 by Vincent Ladeuil
Merge back test_http_implementations.pc into test_http.py.
56
    _urllib,
2164.2.29 by Vincent Ladeuil
Test the http redirection at the request level even if it's not
57
    _urllib2_wrappers,
2004.3.3 by vila
Better (but still incomplete) design for bogus servers.
58
    )
3111.1.19 by Vincent Ladeuil
Merge back test_http_implementations.pc into test_http.py.
59
60
61
try:
62
    from bzrlib.transport.http._pycurl import PyCurlTransport
63
    pycurl_present = True
64
except errors.DependencyNotPresent:
65
    pycurl_present = False
66
67
68
class TransportAdapter(tests.TestScenarioApplier):
69
    """Generate the same test for each transport implementation."""
70
71
    def __init__(self):
72
        transport_scenarios = [
73
            ('urllib', dict(_transport=_urllib.HttpTransport_urllib,
74
                            _server=http_server.HttpServer_urllib,
75
                            _qualified_prefix='http+urllib',)),
76
            ]
77
        if pycurl_present:
78
            transport_scenarios.append(
79
                ('pycurl', dict(_transport=PyCurlTransport,
80
                                _server=http_server.HttpServer_PyCurl,
81
                                _qualified_prefix='http+pycurl',)))
82
        self.scenarios = transport_scenarios
83
84
85
class TransportProtocolAdapter(TransportAdapter):
86
    """Generate the same test for each protocol implementation.
87
88
    In addition to the transport adaptatation that we inherit from.
89
    """
90
91
    def __init__(self):
92
        super(TransportProtocolAdapter, self).__init__()
93
        protocol_scenarios = [
94
            ('HTTP/1.0',  dict(_protocol_version='HTTP/1.0')),
95
            ('HTTP/1.1',  dict(_protocol_version='HTTP/1.1')),
96
            ]
97
        self.scenarios = tests.multiply_scenarios(self.scenarios,
98
                                                  protocol_scenarios)
99
100
101
class TransportProtocolAuthenticationAdapter(TransportProtocolAdapter):
102
    """Generate the same test for each authentication scheme implementation.
103
104
    In addition to the protocol adaptatation that we inherit from.
105
    """
106
107
    def __init__(self):
108
        super(TransportProtocolAuthenticationAdapter, self).__init__()
109
        auth_scheme_scenarios = [
110
            ('basic', dict(_auth_scheme='basic')),
111
            ('digest', dict(_auth_scheme='digest')),
112
            ]
113
114
        self.scenarios = tests.multiply_scenarios(self.scenarios,
115
                                                  auth_scheme_scenarios)
116
117
def load_tests(standard_tests, module, loader):
118
    """Multiply tests for http clients and protocol versions."""
119
    # one for each transport
120
    t_adapter = TransportAdapter()
121
    t_classes= (TestHttpTransportRegistration,
122
                TestHttpTransportUrls,
123
                )
124
    is_testing_for_transports = tests.condition_isinstance(t_classes)
125
126
    # multiplied by one for each protocol version
127
    tp_adapter = TransportProtocolAdapter()
3111.1.25 by Vincent Ladeuil
Fix the smart server failing test and use it against protocol combinations.
128
    tp_classes= (SmartHTTPTunnellingTest,
129
                 TestDoCatchRedirections,
3111.1.19 by Vincent Ladeuil
Merge back test_http_implementations.pc into test_http.py.
130
                 TestHTTPConnections,
131
                 TestHTTPRedirections,
132
                 TestHTTPSilentRedirections,
133
                 TestLimitedRangeRequestServer,
134
                 TestPost,
135
                 TestProxyHttpServer,
136
                 TestRanges,
137
                 TestSpecificRequestHandler,
138
                 )
139
    is_also_testing_for_protocols = tests.condition_isinstance(tp_classes)
140
141
    # multiplied by one for each authentication scheme
142
    tpa_adapter = TransportProtocolAuthenticationAdapter()
143
    tpa_classes = (TestAuth,
144
                   )
3111.1.30 by Vincent Ladeuil
Update NEWS. Some cosmetic changes.
145
    is_also_testing_for_authentication = tests.condition_isinstance(
146
        tpa_classes)
3111.1.19 by Vincent Ladeuil
Merge back test_http_implementations.pc into test_http.py.
147
148
    result = loader.suiteClass()
3111.1.24 by Vincent Ladeuil
Cleanups.
149
    for test_class in tests.iter_suite_tests(standard_tests):
3111.1.19 by Vincent Ladeuil
Merge back test_http_implementations.pc into test_http.py.
150
        # Each test class is either standalone or testing for some combination
151
        # of transport, protocol version, authentication scheme. Use the right
152
        # adpater (or none) depending on the class.
3111.1.24 by Vincent Ladeuil
Cleanups.
153
        if is_testing_for_transports(test_class):
154
            result.addTests(t_adapter.adapt(test_class))
155
        elif is_also_testing_for_protocols(test_class):
156
            result.addTests(tp_adapter.adapt(test_class))
157
        elif is_also_testing_for_authentication(test_class):
158
            result.addTests(tpa_adapter.adapt(test_class))
3111.1.19 by Vincent Ladeuil
Merge back test_http_implementations.pc into test_http.py.
159
        else:
3111.1.24 by Vincent Ladeuil
Cleanups.
160
            result.addTest(test_class)
3111.1.19 by Vincent Ladeuil
Merge back test_http_implementations.pc into test_http.py.
161
    return result
1185.40.20 by Robey Pointer
allow user:pass@ info in http urls to be used for auth; this should be easily expandable later to use auth config files
162
1786.1.8 by John Arbash Meinel
[merge] Johan Rydberg test updates
163
2018.2.9 by Andrew Bennetts
(Andrew Bennetts, Robert Collins) Add test_http.RecordingServer, and use it to
164
class FakeManager(object):
1786.1.8 by John Arbash Meinel
[merge] Johan Rydberg test updates
165
1185.40.20 by Robey Pointer
allow user:pass@ info in http urls to be used for auth; this should be easily expandable later to use auth config files
166
    def __init__(self):
167
        self.credentials = []
2004.3.1 by vila
Test ConnectionError exceptions.
168
1185.40.20 by Robey Pointer
allow user:pass@ info in http urls to be used for auth; this should be easily expandable later to use auth config files
169
    def add_password(self, realm, host, username, password):
170
        self.credentials.append([realm, host, username, password])
171
1553.1.2 by James Henstridge
Add a test to make sure the user-agent header is being sent correctly.
172
3111.1.29 by Vincent Ladeuil
Cancel RecordingServer move, that was useless.
173
class RecordingServer(object):
174
    """A fake HTTP server.
175
    
176
    It records the bytes sent to it, and replies with a 200.
177
    """
178
179
    def __init__(self, expect_body_tail=None):
180
        """Constructor.
181
182
        :type expect_body_tail: str
183
        :param expect_body_tail: a reply won't be sent until this string is
184
            received.
185
        """
186
        self._expect_body_tail = expect_body_tail
187
        self.host = None
188
        self.port = None
189
        self.received_bytes = ''
190
191
    def setUp(self):
192
        self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
193
        self._sock.bind(('127.0.0.1', 0))
194
        self.host, self.port = self._sock.getsockname()
195
        self._ready = threading.Event()
196
        self._thread = threading.Thread(target=self._accept_read_and_reply)
197
        self._thread.setDaemon(True)
198
        self._thread.start()
199
        self._ready.wait(5)
200
201
    def _accept_read_and_reply(self):
202
        self._sock.listen(1)
203
        self._ready.set()
204
        self._sock.settimeout(5)
205
        try:
206
            conn, address = self._sock.accept()
207
            # On win32, the accepted connection will be non-blocking to start
208
            # with because we're using settimeout.
209
            conn.setblocking(True)
210
            while not self.received_bytes.endswith(self._expect_body_tail):
211
                self.received_bytes += conn.recv(4096)
212
            conn.sendall('HTTP/1.1 200 OK\r\n')
213
        except socket.timeout:
214
            # Make sure the client isn't stuck waiting for us to e.g. accept.
215
            self._sock.close()
216
        except socket.error:
217
            # The client may have already closed the socket.
218
            pass
219
220
    def tearDown(self):
221
        try:
222
            self._sock.close()
223
        except socket.error:
224
            # We might have already closed it.  We don't care.
225
            pass
226
        self.host = None
227
        self.port = None
228
229
3111.1.4 by Vincent Ladeuil
Select the server depending on the request handler protocol. Add tests.
230
class TestHTTPServer(tests.TestCase):
231
    """Test the HTTP servers implementations."""
232
233
    def test_invalid_protocol(self):
234
        class BogusRequestHandler(http_server.TestingHTTPRequestHandler):
235
236
            protocol_version = 'HTTP/0.1'
237
238
        server = http_server.HttpServer(BogusRequestHandler)
239
        try:
240
            self.assertRaises(httplib.UnknownProtocol,server.setUp)
241
        except:
242
            server.tearDown()
243
            self.fail('HTTP Server creation did not raise UnknownProtocol')
244
3111.1.17 by Vincent Ladeuil
Add tests for the protocol version parameter.
245
    def test_force_invalid_protocol(self):
246
        server = http_server.HttpServer(protocol_version='HTTP/0.1')
247
        try:
248
            self.assertRaises(httplib.UnknownProtocol,server.setUp)
249
        except:
250
            server.tearDown()
251
            self.fail('HTTP Server creation did not raise UnknownProtocol')
252
3111.1.4 by Vincent Ladeuil
Select the server depending on the request handler protocol. Add tests.
253
    def test_server_start_and_stop(self):
254
        server = http_server.HttpServer()
255
        server.setUp()
256
        self.assertTrue(server._http_running)
257
        server.tearDown()
258
        self.assertFalse(server._http_running)
259
260
    def test_create_http_server_one_zero(self):
261
        class RequestHandlerOneZero(http_server.TestingHTTPRequestHandler):
262
263
            protocol_version = 'HTTP/1.0'
264
265
        server = http_server.HttpServer(RequestHandlerOneZero)
266
        server.setUp()
3111.1.17 by Vincent Ladeuil
Add tests for the protocol version parameter.
267
        self.addCleanup(server.tearDown)
268
        self.assertIsInstance(server._httpd, http_server.TestingHTTPServer)
3111.1.4 by Vincent Ladeuil
Select the server depending on the request handler protocol. Add tests.
269
270
    def test_create_http_server_one_one(self):
271
        class RequestHandlerOneOne(http_server.TestingHTTPRequestHandler):
272
273
            protocol_version = 'HTTP/1.1'
274
275
        server = http_server.HttpServer(RequestHandlerOneOne)
276
        server.setUp()
3111.1.17 by Vincent Ladeuil
Add tests for the protocol version parameter.
277
        self.addCleanup(server.tearDown)
278
        self.assertIsInstance(server._httpd,
279
                              http_server.TestingThreadingHTTPServer)
280
281
    def test_create_http_server_force_one_one(self):
282
        class RequestHandlerOneZero(http_server.TestingHTTPRequestHandler):
283
284
            protocol_version = 'HTTP/1.0'
285
286
        server = http_server.HttpServer(RequestHandlerOneZero,
287
                                        protocol_version='HTTP/1.1')
288
        server.setUp()
289
        self.addCleanup(server.tearDown)
290
        self.assertIsInstance(server._httpd,
291
                              http_server.TestingThreadingHTTPServer)
292
293
    def test_create_http_server_force_one_zero(self):
294
        class RequestHandlerOneOne(http_server.TestingHTTPRequestHandler):
295
296
            protocol_version = 'HTTP/1.1'
297
298
        server = http_server.HttpServer(RequestHandlerOneOne,
299
                                        protocol_version='HTTP/1.0')
300
        server.setUp()
301
        self.addCleanup(server.tearDown)
302
        self.assertIsInstance(server._httpd,
303
                              http_server.TestingHTTPServer)
3111.1.4 by Vincent Ladeuil
Select the server depending on the request handler protocol. Add tests.
304
305
2294.3.1 by Vincent Ladeuil
Fix #85305 by issuing an exception instead of a traceback.
306
class TestWithTransport_pycurl(object):
307
    """Test case to inherit from if pycurl is present"""
308
309
    def _get_pycurl_maybe(self):
310
        try:
311
            from bzrlib.transport.http._pycurl import PyCurlTransport
312
            return PyCurlTransport
313
        except errors.DependencyNotPresent:
3052.3.2 by Vincent Ladeuil
Add tests and fix trivial bugs and other typos.
314
            raise tests.TestSkipped('pycurl not present')
2294.3.1 by Vincent Ladeuil
Fix #85305 by issuing an exception instead of a traceback.
315
316
    _transport = property(_get_pycurl_maybe)
317
318
3052.3.2 by Vincent Ladeuil
Add tests and fix trivial bugs and other typos.
319
class TestHttpUrls(tests.TestCase):
1786.1.8 by John Arbash Meinel
[merge] Johan Rydberg test updates
320
2294.3.1 by Vincent Ladeuil
Fix #85305 by issuing an exception instead of a traceback.
321
    # TODO: This should be moved to authorization tests once they
322
    # are written.
2004.1.40 by v.ladeuil+lp at free
Fix the race condition again and correct some small typos to be in
323
1185.40.20 by Robey Pointer
allow user:pass@ info in http urls to be used for auth; this should be easily expandable later to use auth config files
324
    def test_url_parsing(self):
325
        f = FakeManager()
3111.1.20 by Vincent Ladeuil
Make all the test pass. Looks like we are HTTP/1.1 compliant.
326
        url = http.extract_auth('http://example.com', f)
1185.40.20 by Robey Pointer
allow user:pass@ info in http urls to be used for auth; this should be easily expandable later to use auth config files
327
        self.assertEquals('http://example.com', url)
328
        self.assertEquals(0, len(f.credentials))
3111.1.30 by Vincent Ladeuil
Update NEWS. Some cosmetic changes.
329
        url = http.extract_auth(
330
            'http://user:pass@www.bazaar-vcs.org/bzr/bzr.dev', f)
1185.50.94 by John Arbash Meinel
Updated web page url to http://bazaar-vcs.org
331
        self.assertEquals('http://www.bazaar-vcs.org/bzr/bzr.dev', url)
1185.40.20 by Robey Pointer
allow user:pass@ info in http urls to be used for auth; this should be easily expandable later to use auth config files
332
        self.assertEquals(1, len(f.credentials))
2004.3.1 by vila
Test ConnectionError exceptions.
333
        self.assertEquals([None, 'www.bazaar-vcs.org', 'user', 'pass'],
334
                          f.credentials[0])
335
2294.3.1 by Vincent Ladeuil
Fix #85305 by issuing an exception instead of a traceback.
336
3111.1.19 by Vincent Ladeuil
Merge back test_http_implementations.pc into test_http.py.
337
class TestHttpTransportUrls(tests.TestCase):
338
    """Test the http urls."""
339
340
    def test_abs_url(self):
341
        """Construction of absolute http URLs"""
342
        t = self._transport('http://bazaar-vcs.org/bzr/bzr.dev/')
343
        eq = self.assertEqualDiff
344
        eq(t.abspath('.'), 'http://bazaar-vcs.org/bzr/bzr.dev')
345
        eq(t.abspath('foo/bar'), 'http://bazaar-vcs.org/bzr/bzr.dev/foo/bar')
346
        eq(t.abspath('.bzr'), 'http://bazaar-vcs.org/bzr/bzr.dev/.bzr')
347
        eq(t.abspath('.bzr/1//2/./3'),
348
           'http://bazaar-vcs.org/bzr/bzr.dev/.bzr/1/2/3')
349
350
    def test_invalid_http_urls(self):
351
        """Trap invalid construction of urls"""
352
        t = self._transport('http://bazaar-vcs.org/bzr/bzr.dev/')
353
        self.assertRaises(errors.InvalidURL,
354
                          self._transport,
355
                          'http://http://bazaar-vcs.org/bzr/bzr.dev/')
356
357
    def test_http_root_urls(self):
358
        """Construction of URLs from server root"""
359
        t = self._transport('http://bzr.ozlabs.org/')
360
        eq = self.assertEqualDiff
361
        eq(t.abspath('.bzr/tree-version'),
362
           'http://bzr.ozlabs.org/.bzr/tree-version')
363
364
    def test_http_impl_urls(self):
365
        """There are servers which ask for particular clients to connect"""
366
        server = self._server()
367
        try:
368
            server.setUp()
369
            url = server.get_url()
370
            self.assertTrue(url.startswith('%s://' % self._qualified_prefix))
371
        finally:
372
            server.tearDown()
373
374
3111.1.9 by Vincent Ladeuil
Most refactoring regarding parameterization for urllib/pycurl and custom
375
class TestHttps_pycurl(TestWithTransport_pycurl, tests.TestCase):
2294.3.1 by Vincent Ladeuil
Fix #85305 by issuing an exception instead of a traceback.
376
377
    # TODO: This should really be moved into another pycurl
378
    # specific test. When https tests will be implemented, take
379
    # this one into account.
380
    def test_pycurl_without_https_support(self):
381
        """Test that pycurl without SSL do not fail with a traceback.
382
383
        For the purpose of the test, we force pycurl to ignore
384
        https by supplying a fake version_info that do not
385
        support it.
386
        """
387
        try:
388
            import pycurl
389
        except ImportError:
3052.3.2 by Vincent Ladeuil
Add tests and fix trivial bugs and other typos.
390
            raise tests.TestSkipped('pycurl not present')
3111.1.14 by Vincent Ladeuil
Fix test leakage.
391
392
        version_info_orig = pycurl.version_info
393
        try:
394
            # Now that we have pycurl imported, we can fake its version_info
395
            # This was taken from a windows pycurl without SSL
396
            # (thanks to bialix)
397
            pycurl.version_info = lambda : (2,
398
                                            '7.13.2',
399
                                            462082,
400
                                            'i386-pc-win32',
401
                                            2576,
402
                                            None,
403
                                            0,
404
                                            None,
405
                                            ('ftp', 'gopher', 'telnet',
406
                                             'dict', 'ldap', 'http', 'file'),
407
                                            None,
408
                                            0,
409
                                            None)
410
            self.assertRaises(errors.DependencyNotPresent, self._transport,
411
                              'https://launchpad.net')
412
        finally:
413
            # Restore the right function
414
            pycurl.version_info = version_info_orig
2294.3.1 by Vincent Ladeuil
Fix #85305 by issuing an exception instead of a traceback.
415
3111.1.19 by Vincent Ladeuil
Merge back test_http_implementations.pc into test_http.py.
416
417
class TestHTTPConnections(http_utils.TestCaseWithWebserver):
418
    """Test the http connections."""
419
420
    def setUp(self):
421
        http_utils.TestCaseWithWebserver.setUp(self)
422
        self.build_tree(['foo/', 'foo/bar'], line_endings='binary',
423
                        transport=self.get_transport())
424
425
    def test_http_has(self):
426
        server = self.get_readonly_server()
427
        t = self._transport(server.get_url())
428
        self.assertEqual(t.has('foo/bar'), True)
429
        self.assertEqual(len(server.logs), 1)
430
        self.assertContainsRe(server.logs[0],
431
            r'"HEAD /foo/bar HTTP/1.." (200|302) - "-" "bzr/')
432
433
    def test_http_has_not_found(self):
434
        server = self.get_readonly_server()
435
        t = self._transport(server.get_url())
436
        self.assertEqual(t.has('not-found'), False)
437
        self.assertContainsRe(server.logs[1],
438
            r'"HEAD /not-found HTTP/1.." 404 - "-" "bzr/')
439
440
    def test_http_get(self):
441
        server = self.get_readonly_server()
442
        t = self._transport(server.get_url())
443
        fp = t.get('foo/bar')
444
        self.assertEqualDiff(
445
            fp.read(),
446
            'contents of foo/bar\n')
447
        self.assertEqual(len(server.logs), 1)
448
        self.assertTrue(server.logs[0].find(
449
            '"GET /foo/bar HTTP/1.1" 200 - "-" "bzr/%s'
450
            % bzrlib.__version__) > -1)
451
452
    def test_get_smart_medium(self):
453
        # For HTTP, get_smart_medium should return the transport object.
454
        server = self.get_readonly_server()
455
        http_transport = self._transport(server.get_url())
456
        medium = http_transport.get_smart_medium()
457
        self.assertIs(medium, http_transport)
458
459
    def test_has_on_bogus_host(self):
460
        # Get a free address and don't 'accept' on it, so that we
461
        # can be sure there is no http handler there, but set a
462
        # reasonable timeout to not slow down tests too much.
463
        default_timeout = socket.getdefaulttimeout()
464
        try:
465
            socket.setdefaulttimeout(2)
466
            s = socket.socket()
467
            s.bind(('localhost', 0))
468
            t = self._transport('http://%s:%s/' % s.getsockname())
469
            self.assertRaises(errors.ConnectionError, t.has, 'foo/bar')
470
        finally:
471
            socket.setdefaulttimeout(default_timeout)
472
473
474
class TestHttpTransportRegistration(tests.TestCase):
475
    """Test registrations of various http implementations"""
476
477
    def test_http_registered(self):
478
        t = transport.get_transport('%s://foo.com/' % self._qualified_prefix)
479
        self.assertIsInstance(t, transport.Transport)
480
        self.assertIsInstance(t, self._transport)
481
482
483
class TestPost(tests.TestCase):
484
485
    def test_post_body_is_received(self):
3111.1.29 by Vincent Ladeuil
Cancel RecordingServer move, that was useless.
486
        server = RecordingServer(expect_body_tail='end-of-body')
3111.1.19 by Vincent Ladeuil
Merge back test_http_implementations.pc into test_http.py.
487
        server.setUp()
488
        self.addCleanup(server.tearDown)
489
        scheme = self._qualified_prefix
490
        url = '%s://%s:%s/' % (scheme, server.host, server.port)
491
        http_transport = self._transport(url)
492
        code, response = http_transport._post('abc def end-of-body')
493
        self.assertTrue(
494
            server.received_bytes.startswith('POST /.bzr/smart HTTP/1.'))
495
        self.assertTrue('content-length: 19\r' in server.received_bytes.lower())
496
        # The transport should not be assuming that the server can accept
497
        # chunked encoding the first time it connects, because HTTP/1.1, so we
498
        # check for the literal string.
499
        self.assertTrue(
500
            server.received_bytes.endswith('\r\n\r\nabc def end-of-body'))
501
502
3052.3.2 by Vincent Ladeuil
Add tests and fix trivial bugs and other typos.
503
class TestRangeHeader(tests.TestCase):
1786.1.28 by John Arbash Meinel
Update and add tests for the HttpTransportBase.range_header
504
    """Test range_header method"""
505
506
    def check_header(self, value, ranges=[], tail=0):
2520.2.1 by Vincent Ladeuil
First step to fix #115209 use _coalesce_offsets like other transports.
507
        offsets = [ (start, end - start + 1) for start, end in ranges]
3111.1.10 by Vincent Ladeuil
Finish http parameterization, 24 auth tests failing for pycurl (not
508
        coalesce = transport.Transport._coalesce_offsets
2520.2.1 by Vincent Ladeuil
First step to fix #115209 use _coalesce_offsets like other transports.
509
        coalesced = list(coalesce(offsets, limit=0, fudge_factor=0))
3111.1.20 by Vincent Ladeuil
Make all the test pass. Looks like we are HTTP/1.1 compliant.
510
        range_header = http.HttpTransportBase._range_header
2520.2.1 by Vincent Ladeuil
First step to fix #115209 use _coalesce_offsets like other transports.
511
        self.assertEqual(value, range_header(coalesced, tail))
1786.1.28 by John Arbash Meinel
Update and add tests for the HttpTransportBase.range_header
512
513
    def test_range_header_single(self):
2520.2.1 by Vincent Ladeuil
First step to fix #115209 use _coalesce_offsets like other transports.
514
        self.check_header('0-9', ranges=[(0,9)])
515
        self.check_header('100-109', ranges=[(100,109)])
1786.1.28 by John Arbash Meinel
Update and add tests for the HttpTransportBase.range_header
516
517
    def test_range_header_tail(self):
1786.1.36 by John Arbash Meinel
pycurl expects us to just set the range of bytes, not including bytes=
518
        self.check_header('-10', tail=10)
519
        self.check_header('-50', tail=50)
1786.1.28 by John Arbash Meinel
Update and add tests for the HttpTransportBase.range_header
520
521
    def test_range_header_multi(self):
1786.1.36 by John Arbash Meinel
pycurl expects us to just set the range of bytes, not including bytes=
522
        self.check_header('0-9,100-200,300-5000',
1786.1.28 by John Arbash Meinel
Update and add tests for the HttpTransportBase.range_header
523
                          ranges=[(0,9), (100, 200), (300,5000)])
524
525
    def test_range_header_mixed(self):
1786.1.36 by John Arbash Meinel
pycurl expects us to just set the range of bytes, not including bytes=
526
        self.check_header('0-9,300-5000,-50',
1786.1.28 by John Arbash Meinel
Update and add tests for the HttpTransportBase.range_header
527
                          ranges=[(0,9), (300,5000)],
528
                          tail=50)
2018.2.9 by Andrew Bennetts
(Andrew Bennetts, Robert Collins) Add test_http.RecordingServer, and use it to
529
2004.1.15 by v.ladeuil+lp at free
Better design for bogus servers. Both urllib and pycurl pass tests.
530
3111.1.19 by Vincent Ladeuil
Merge back test_http_implementations.pc into test_http.py.
531
class TestSpecificRequestHandler(http_utils.TestCaseWithWebserver):
532
    """Tests a specific request handler.
533
3111.1.31 by Vincent Ladeuil
Review feeback.
534
    Daughter classes are expected to override _req_handler_class
3111.1.19 by Vincent Ladeuil
Merge back test_http_implementations.pc into test_http.py.
535
    """
536
537
    # Provide a useful default
538
    _req_handler_class = http_server.TestingHTTPRequestHandler
539
540
    def create_transport_readonly_server(self):
541
        return http_server.HttpServer(self._req_handler_class,
542
                                      protocol_version=self._protocol_version)
543
3111.1.20 by Vincent Ladeuil
Make all the test pass. Looks like we are HTTP/1.1 compliant.
544
    def _testing_pycurl(self):
545
        return pycurl_present and self._transport == PyCurlTransport
546
3111.1.19 by Vincent Ladeuil
Merge back test_http_implementations.pc into test_http.py.
547
548
class WallRequestHandler(http_server.TestingHTTPRequestHandler):
549
    """Whatever request comes in, close the connection"""
550
551
    def handle_one_request(self):
552
        """Handle a single HTTP request, by abruptly closing the connection"""
553
        self.close_connection = 1
554
555
556
class TestWallServer(TestSpecificRequestHandler):
557
    """Tests exceptions during the connection phase"""
558
559
    _req_handler_class = WallRequestHandler
560
561
    def test_http_has(self):
562
        server = self.get_readonly_server()
563
        t = self._transport(server.get_url())
564
        # Unfortunately httplib (see HTTPResponse._read_status
565
        # for details) make no distinction between a closed
566
        # socket and badly formatted status line, so we can't
567
        # just test for ConnectionError, we have to test
568
        # InvalidHttpResponse too.
569
        self.assertRaises((errors.ConnectionError, errors.InvalidHttpResponse),
570
                          t.has, 'foo/bar')
571
572
    def test_http_get(self):
573
        server = self.get_readonly_server()
574
        t = self._transport(server.get_url())
575
        self.assertRaises((errors.ConnectionError, errors.InvalidHttpResponse),
576
                          t.get, 'foo/bar')
577
578
579
class BadStatusRequestHandler(http_server.TestingHTTPRequestHandler):
580
    """Whatever request comes in, returns a bad status"""
581
582
    def parse_request(self):
583
        """Fakes handling a single HTTP request, returns a bad status"""
584
        ignored = http_server.TestingHTTPRequestHandler.parse_request(self)
3111.1.20 by Vincent Ladeuil
Make all the test pass. Looks like we are HTTP/1.1 compliant.
585
        self.send_response(0, "Bad status")
586
        self.close_connection = 1
3111.1.19 by Vincent Ladeuil
Merge back test_http_implementations.pc into test_http.py.
587
        return False
588
589
590
class TestBadStatusServer(TestSpecificRequestHandler):
591
    """Tests bad status from server."""
592
593
    _req_handler_class = BadStatusRequestHandler
594
595
    def test_http_has(self):
596
        server = self.get_readonly_server()
597
        t = self._transport(server.get_url())
598
        self.assertRaises(errors.InvalidHttpResponse, t.has, 'foo/bar')
599
600
    def test_http_get(self):
601
        server = self.get_readonly_server()
602
        t = self._transport(server.get_url())
603
        self.assertRaises(errors.InvalidHttpResponse, t.get, 'foo/bar')
604
605
606
class InvalidStatusRequestHandler(http_server.TestingHTTPRequestHandler):
3111.1.20 by Vincent Ladeuil
Make all the test pass. Looks like we are HTTP/1.1 compliant.
607
    """Whatever request comes in, returns an invalid status"""
3111.1.19 by Vincent Ladeuil
Merge back test_http_implementations.pc into test_http.py.
608
609
    def parse_request(self):
610
        """Fakes handling a single HTTP request, returns a bad status"""
611
        ignored = http_server.TestingHTTPRequestHandler.parse_request(self)
612
        self.wfile.write("Invalid status line\r\n")
613
        return False
614
615
616
class TestInvalidStatusServer(TestBadStatusServer):
617
    """Tests invalid status from server.
618
619
    Both implementations raises the same error as for a bad status.
620
    """
621
622
    _req_handler_class = InvalidStatusRequestHandler
623
3111.1.20 by Vincent Ladeuil
Make all the test pass. Looks like we are HTTP/1.1 compliant.
624
    def test_http_has(self):
625
        if self._testing_pycurl() and self._protocol_version == 'HTTP/1.1':
3111.1.22 by Vincent Ladeuil
Rework TestingHTTPServer classes, fix test bug.
626
            raise tests.KnownFailure(
627
                'pycurl hangs if the server send back garbage')
3111.1.20 by Vincent Ladeuil
Make all the test pass. Looks like we are HTTP/1.1 compliant.
628
        super(TestInvalidStatusServer, self).test_http_has()
629
630
    def test_http_get(self):
631
        if self._testing_pycurl() and self._protocol_version == 'HTTP/1.1':
3111.1.22 by Vincent Ladeuil
Rework TestingHTTPServer classes, fix test bug.
632
            raise tests.KnownFailure(
633
                'pycurl hangs if the server send back garbage')
3111.1.20 by Vincent Ladeuil
Make all the test pass. Looks like we are HTTP/1.1 compliant.
634
        super(TestInvalidStatusServer, self).test_http_get()
635
3111.1.19 by Vincent Ladeuil
Merge back test_http_implementations.pc into test_http.py.
636
637
class BadProtocolRequestHandler(http_server.TestingHTTPRequestHandler):
638
    """Whatever request comes in, returns a bad protocol version"""
639
640
    def parse_request(self):
641
        """Fakes handling a single HTTP request, returns a bad status"""
642
        ignored = http_server.TestingHTTPRequestHandler.parse_request(self)
643
        # Returns an invalid protocol version, but curl just
644
        # ignores it and those cannot be tested.
645
        self.wfile.write("%s %d %s\r\n" % ('HTTP/0.0',
646
                                           404,
647
                                           'Look at my protocol version'))
648
        return False
649
650
651
class TestBadProtocolServer(TestSpecificRequestHandler):
652
    """Tests bad protocol from server."""
653
654
    _req_handler_class = BadProtocolRequestHandler
655
656
    def setUp(self):
657
        if pycurl_present and self._transport == PyCurlTransport:
658
            raise tests.TestNotApplicable(
659
                "pycurl doesn't check the protocol version")
660
        super(TestBadProtocolServer, self).setUp()
661
662
    def test_http_has(self):
663
        server = self.get_readonly_server()
664
        t = self._transport(server.get_url())
665
        self.assertRaises(errors.InvalidHttpResponse, t.has, 'foo/bar')
666
667
    def test_http_get(self):
668
        server = self.get_readonly_server()
669
        t = self._transport(server.get_url())
670
        self.assertRaises(errors.InvalidHttpResponse, t.get, 'foo/bar')
671
672
673
class ForbiddenRequestHandler(http_server.TestingHTTPRequestHandler):
674
    """Whatever request comes in, returns a 403 code"""
675
676
    def parse_request(self):
677
        """Handle a single HTTP request, by replying we cannot handle it"""
678
        ignored = http_server.TestingHTTPRequestHandler.parse_request(self)
679
        self.send_error(403)
680
        return False
681
682
683
class TestForbiddenServer(TestSpecificRequestHandler):
684
    """Tests forbidden server"""
685
686
    _req_handler_class = ForbiddenRequestHandler
687
688
    def test_http_has(self):
689
        server = self.get_readonly_server()
690
        t = self._transport(server.get_url())
691
        self.assertRaises(errors.TransportError, t.has, 'foo/bar')
692
693
    def test_http_get(self):
694
        server = self.get_readonly_server()
695
        t = self._transport(server.get_url())
696
        self.assertRaises(errors.TransportError, t.get, 'foo/bar')
697
698
3052.3.2 by Vincent Ladeuil
Add tests and fix trivial bugs and other typos.
699
class TestRecordingServer(tests.TestCase):
2018.2.9 by Andrew Bennetts
(Andrew Bennetts, Robert Collins) Add test_http.RecordingServer, and use it to
700
701
    def test_create(self):
3111.1.29 by Vincent Ladeuil
Cancel RecordingServer move, that was useless.
702
        server = RecordingServer(expect_body_tail=None)
2018.2.9 by Andrew Bennetts
(Andrew Bennetts, Robert Collins) Add test_http.RecordingServer, and use it to
703
        self.assertEqual('', server.received_bytes)
704
        self.assertEqual(None, server.host)
705
        self.assertEqual(None, server.port)
706
707
    def test_setUp_and_tearDown(self):
3111.1.29 by Vincent Ladeuil
Cancel RecordingServer move, that was useless.
708
        server = RecordingServer(expect_body_tail=None)
2018.2.9 by Andrew Bennetts
(Andrew Bennetts, Robert Collins) Add test_http.RecordingServer, and use it to
709
        server.setUp()
710
        try:
711
            self.assertNotEqual(None, server.host)
712
            self.assertNotEqual(None, server.port)
713
        finally:
714
            server.tearDown()
715
        self.assertEqual(None, server.host)
716
        self.assertEqual(None, server.port)
717
718
    def test_send_receive_bytes(self):
3111.1.29 by Vincent Ladeuil
Cancel RecordingServer move, that was useless.
719
        server = RecordingServer(expect_body_tail='c')
2018.2.9 by Andrew Bennetts
(Andrew Bennetts, Robert Collins) Add test_http.RecordingServer, and use it to
720
        server.setUp()
721
        self.addCleanup(server.tearDown)
722
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
723
        sock.connect((server.host, server.port))
724
        sock.sendall('abc')
725
        self.assertEqual('HTTP/1.1 200 OK\r\n',
2091.1.1 by Martin Pool
Avoid MSG_WAITALL as it doesn't work on Windows
726
                         osutils.recv_all(sock, 4096))
2018.2.9 by Andrew Bennetts
(Andrew Bennetts, Robert Collins) Add test_http.RecordingServer, and use it to
727
        self.assertEqual('abc', server.received_bytes)
2004.1.29 by v.ladeuil+lp at free
New tests for http range requests handling.
728
729
3111.1.19 by Vincent Ladeuil
Merge back test_http_implementations.pc into test_http.py.
730
class TestRangeRequestServer(TestSpecificRequestHandler):
731
    """Tests readv requests against server.
732
733
    We test against default "normal" server.
734
    """
735
736
    def setUp(self):
737
        super(TestRangeRequestServer, self).setUp()
738
        self.build_tree_contents([('a', '0123456789')],)
739
740
    def test_readv(self):
741
        server = self.get_readonly_server()
742
        t = self._transport(server.get_url())
743
        l = list(t.readv('a', ((0, 1), (1, 1), (3, 2), (9, 1))))
744
        self.assertEqual(l[0], (0, '0'))
745
        self.assertEqual(l[1], (1, '1'))
746
        self.assertEqual(l[2], (3, '34'))
747
        self.assertEqual(l[3], (9, '9'))
748
749
    def test_readv_out_of_order(self):
750
        server = self.get_readonly_server()
751
        t = self._transport(server.get_url())
752
        l = list(t.readv('a', ((1, 1), (9, 1), (0, 1), (3, 2))))
753
        self.assertEqual(l[0], (1, '1'))
754
        self.assertEqual(l[1], (9, '9'))
755
        self.assertEqual(l[2], (0, '0'))
756
        self.assertEqual(l[3], (3, '34'))
757
758
    def test_readv_invalid_ranges(self):
759
        server = self.get_readonly_server()
760
        t = self._transport(server.get_url())
761
762
        # This is intentionally reading off the end of the file
763
        # since we are sure that it cannot get there
764
        self.assertListRaises((errors.InvalidRange, errors.ShortReadvError,),
765
                              t.readv, 'a', [(1,1), (8,10)])
766
767
        # This is trying to seek past the end of the file, it should
768
        # also raise a special error
769
        self.assertListRaises((errors.InvalidRange, errors.ShortReadvError,),
770
                              t.readv, 'a', [(12,2)])
771
772
    def test_readv_multiple_get_requests(self):
773
        server = self.get_readonly_server()
774
        t = self._transport(server.get_url())
775
        # force transport to issue multiple requests
776
        t._max_readv_combine = 1
777
        t._max_get_ranges = 1
778
        l = list(t.readv('a', ((0, 1), (1, 1), (3, 2), (9, 1))))
779
        self.assertEqual(l[0], (0, '0'))
780
        self.assertEqual(l[1], (1, '1'))
781
        self.assertEqual(l[2], (3, '34'))
782
        self.assertEqual(l[3], (9, '9'))
783
        # The server should have issued 4 requests
784
        self.assertEqual(4, server.GET_request_nb)
785
786
    def test_readv_get_max_size(self):
787
        server = self.get_readonly_server()
788
        t = self._transport(server.get_url())
789
        # force transport to issue multiple requests by limiting the number of
790
        # bytes by request. Note that this apply to coalesced offsets only, a
3111.1.28 by Vincent Ladeuil
Fix the multi-ranges http server and add tests.
791
        # single range will keep its size even if bigger than the limit.
3111.1.19 by Vincent Ladeuil
Merge back test_http_implementations.pc into test_http.py.
792
        t._get_max_size = 2
793
        l = list(t.readv('a', ((0, 1), (1, 1), (2, 4), (6, 4))))
794
        self.assertEqual(l[0], (0, '0'))
795
        self.assertEqual(l[1], (1, '1'))
796
        self.assertEqual(l[2], (2, '2345'))
797
        self.assertEqual(l[3], (6, '6789'))
798
        # The server should have issued 3 requests
799
        self.assertEqual(3, server.GET_request_nb)
800
3111.1.28 by Vincent Ladeuil
Fix the multi-ranges http server and add tests.
801
    def test_complete_readv_leave_pipe_clean(self):
802
        server = self.get_readonly_server()
803
        t = self._transport(server.get_url())
804
        # force transport to issue multiple requests
805
        t._get_max_size = 2
806
        l = list(t.readv('a', ((0, 1), (1, 1), (2, 4), (6, 4))))
807
        # The server should have issued 3 requests
808
        self.assertEqual(3, server.GET_request_nb)
809
        self.assertEqual('0123456789', t.get_bytes('a'))
810
        self.assertEqual(4, server.GET_request_nb)
811
812
    def test_incomplete_readv_leave_pipe_clean(self):
813
        server = self.get_readonly_server()
814
        t = self._transport(server.get_url())
815
        # force transport to issue multiple requests
816
        t._get_max_size = 2
817
        # Don't collapse readv results into a list so that we leave unread
818
        # bytes on the socket
819
        ireadv = iter(t.readv('a', ((0, 1), (1, 1), (2, 4), (6, 4))))
820
        self.assertEqual((0, '0'), ireadv.next())
821
        # The server should have issued one request so far 
822
        self.assertEqual(1, server.GET_request_nb)
823
        self.assertEqual('0123456789', t.get_bytes('a'))
824
        # get_bytes issued an additional request, the readv pending ones are
825
        # lost
826
        self.assertEqual(2, server.GET_request_nb)
827
3111.1.19 by Vincent Ladeuil
Merge back test_http_implementations.pc into test_http.py.
828
829
class SingleRangeRequestHandler(http_server.TestingHTTPRequestHandler):
830
    """Always reply to range request as if they were single.
831
832
    Don't be explicit about it, just to annoy the clients.
833
    """
834
835
    def get_multiple_ranges(self, file, file_size, ranges):
836
        """Answer as if it was a single range request and ignores the rest"""
837
        (start, end) = ranges[0]
838
        return self.get_single_range(file, file_size, start, end)
839
840
841
class TestSingleRangeRequestServer(TestRangeRequestServer):
842
    """Test readv against a server which accept only single range requests"""
843
844
    _req_handler_class = SingleRangeRequestHandler
845
846
847
class SingleOnlyRangeRequestHandler(http_server.TestingHTTPRequestHandler):
848
    """Only reply to simple range requests, errors out on multiple"""
849
850
    def get_multiple_ranges(self, file, file_size, ranges):
851
        """Refuses the multiple ranges request"""
852
        if len(ranges) > 1:
853
            file.close()
854
            self.send_error(416, "Requested range not satisfiable")
855
            return
856
        (start, end) = ranges[0]
857
        return self.get_single_range(file, file_size, start, end)
858
859
860
class TestSingleOnlyRangeRequestServer(TestRangeRequestServer):
861
    """Test readv against a server which only accept single range requests"""
862
863
    _req_handler_class = SingleOnlyRangeRequestHandler
864
865
866
class NoRangeRequestHandler(http_server.TestingHTTPRequestHandler):
867
    """Ignore range requests without notice"""
868
869
    def do_GET(self):
870
        # Update the statistics
871
        self.server.test_case_server.GET_request_nb += 1
872
        # Just bypass the range handling done by TestingHTTPRequestHandler
873
        return SimpleHTTPServer.SimpleHTTPRequestHandler.do_GET(self)
874
875
876
class TestNoRangeRequestServer(TestRangeRequestServer):
877
    """Test readv against a server which do not accept range requests"""
878
879
    _req_handler_class = NoRangeRequestHandler
880
881
3111.1.28 by Vincent Ladeuil
Fix the multi-ranges http server and add tests.
882
class MultipleRangeWithoutContentLengthRequestHandler(
883
    http_server.TestingHTTPRequestHandler):
884
    """Reply to multiple range requests without content length header."""
885
886
    def get_multiple_ranges(self, file, file_size, ranges):
887
        self.send_response(206)
888
        self.send_header('Accept-Ranges', 'bytes')
889
        boundary = "%d" % random.randint(0,0x7FFFFFFF)
890
        self.send_header("Content-Type",
891
                         "multipart/byteranges; boundary=%s" % boundary)
892
        self.end_headers()
893
        for (start, end) in ranges:
894
            self.wfile.write("--%s\r\n" % boundary)
895
            self.send_header("Content-type", 'application/octet-stream')
896
            self.send_header("Content-Range", "bytes %d-%d/%d" % (start,
897
                                                                  end,
898
                                                                  file_size))
899
            self.end_headers()
900
            self.send_range_content(file, start, end - start + 1)
901
        # Final boundary
902
        self.wfile.write("--%s\r\n" % boundary)
903
904
905
class TestMultipleRangeWithoutContentLengthServer(TestRangeRequestServer):
906
907
    _req_handler_class = MultipleRangeWithoutContentLengthRequestHandler
908
3146.3.2 by Vincent Ladeuil
Fix #179368 by keeping the current range hint on ShortReadvErrors.
909
910
class TruncatedMultipleRangeRequestHandler(
911
    http_server.TestingHTTPRequestHandler):
912
    """Reply to multiple range requests truncating the last ones.
913
914
    This server generates responses whose Content-Length describes all the
915
    ranges, but fail to include the last ones leading to client short reads.
916
    This has been observed randomly with lighttpd (bug #179368).
917
    """
918
919
    _truncated_ranges = 2
920
921
    def get_multiple_ranges(self, file, file_size, ranges):
922
        self.send_response(206)
923
        self.send_header('Accept-Ranges', 'bytes')
924
        boundary = 'tagada'
925
        self.send_header('Content-Type',
926
                         'multipart/byteranges; boundary=%s' % boundary)
927
        boundary_line = '--%s\r\n' % boundary
928
        # Calculate the Content-Length
929
        content_length = 0
930
        for (start, end) in ranges:
931
            content_length += len(boundary_line)
932
            content_length += self._header_line_length(
933
                'Content-type', 'application/octet-stream')
934
            content_length += self._header_line_length(
935
                'Content-Range', 'bytes %d-%d/%d' % (start, end, file_size))
936
            content_length += len('\r\n') # end headers
937
            content_length += end - start # + 1
938
        content_length += len(boundary_line)
939
        self.send_header('Content-length', content_length)
940
        self.end_headers()
941
942
        # Send the multipart body
943
        cur = 0
944
        for (start, end) in ranges:
945
            self.wfile.write(boundary_line)
946
            self.send_header('Content-type', 'application/octet-stream')
947
            self.send_header('Content-Range', 'bytes %d-%d/%d'
948
                             % (start, end, file_size))
949
            self.end_headers()
950
            if cur + self._truncated_ranges >= len(ranges):
951
                # Abruptly ends the response and close the connection
952
                self.close_connection = 1
953
                return
954
            self.send_range_content(file, start, end - start + 1)
955
            cur += 1
956
        # No final boundary
957
        self.wfile.write(boundary_line)
958
959
960
class TestTruncatedMultipleRangeServer(TestSpecificRequestHandler):
961
962
    _req_handler_class = TruncatedMultipleRangeRequestHandler
963
964
    def setUp(self):
965
        super(TestTruncatedMultipleRangeServer, self).setUp()
966
        self.build_tree_contents([('a', '0123456789')],)
967
968
    def test_readv_with_short_reads(self):
969
        server = self.get_readonly_server()
970
        t = self._transport(server.get_url())
971
        # Force separate ranges for each offset
972
        t._bytes_to_read_before_seek = 0
973
        ireadv = iter(t.readv('a', ((0, 1), (2, 1), (4, 2), (9, 1))))
974
        self.assertEqual((0, '0'), ireadv.next())
975
        self.assertEqual((2, '2'), ireadv.next())
976
        if not self._testing_pycurl():
977
            # Only one request have been issued so far (except for pycurl that
978
            # try to read the whole response at once)
979
            self.assertEqual(1, server.GET_request_nb)
980
        self.assertEqual((4, '45'), ireadv.next())
981
        self.assertEqual((9, '9'), ireadv.next())
982
        # Both implementations issue 3 requests but:
983
        # - urllib does two multiple (4 ranges, then 2 ranges) then a single
984
        #   range,
985
        # - pycurl does two multiple (4 ranges, 4 ranges) then a single range
986
        self.assertEqual(3, server.GET_request_nb)
987
        # Finally the client have tried a single range request and stays in
988
        # that mode
989
        self.assertEqual('single', t._range_hint)
990
3111.1.19 by Vincent Ladeuil
Merge back test_http_implementations.pc into test_http.py.
991
class LimitedRangeRequestHandler(http_server.TestingHTTPRequestHandler):
992
    """Errors out when range specifiers exceed the limit"""
993
994
    def get_multiple_ranges(self, file, file_size, ranges):
995
        """Refuses the multiple ranges request"""
996
        tcs = self.server.test_case_server
997
        if tcs.range_limit is not None and len(ranges) > tcs.range_limit:
998
            file.close()
999
            # Emulate apache behavior
1000
            self.send_error(400, "Bad Request")
1001
            return
1002
        return http_server.TestingHTTPRequestHandler.get_multiple_ranges(
1003
            self, file, file_size, ranges)
1004
1005
1006
class LimitedRangeHTTPServer(http_server.HttpServer):
1007
    """An HttpServer erroring out on requests with too much range specifiers"""
1008
1009
    def __init__(self, request_handler=LimitedRangeRequestHandler,
1010
                 protocol_version=None,
1011
                 range_limit=None):
1012
        http_server.HttpServer.__init__(self, request_handler,
1013
                                        protocol_version=protocol_version)
1014
        self.range_limit = range_limit
1015
1016
1017
class TestLimitedRangeRequestServer(http_utils.TestCaseWithWebserver):
1018
    """Tests readv requests against a server erroring out on too much ranges."""
1019
3111.1.22 by Vincent Ladeuil
Rework TestingHTTPServer classes, fix test bug.
1020
    # Requests with more range specifiers will error out
3111.1.19 by Vincent Ladeuil
Merge back test_http_implementations.pc into test_http.py.
1021
    range_limit = 3
1022
1023
    def create_transport_readonly_server(self):
1024
        return LimitedRangeHTTPServer(range_limit=self.range_limit,
1025
                                      protocol_version=self._protocol_version)
1026
1027
    def get_transport(self):
1028
        return self._transport(self.get_readonly_server().get_url())
1029
1030
    def setUp(self):
1031
        http_utils.TestCaseWithWebserver.setUp(self)
1032
        # We need to manipulate ranges that correspond to real chunks in the
1033
        # response, so we build a content appropriately.
1034
        filler = ''.join(['abcdefghij' for x in range(102)])
1035
        content = ''.join(['%04d' % v + filler for v in range(16)])
1036
        self.build_tree_contents([('a', content)],)
1037
1038
    def test_few_ranges(self):
1039
        t = self.get_transport()
1040
        l = list(t.readv('a', ((0, 4), (1024, 4), )))
1041
        self.assertEqual(l[0], (0, '0000'))
1042
        self.assertEqual(l[1], (1024, '0001'))
1043
        self.assertEqual(1, self.get_readonly_server().GET_request_nb)
1044
1045
    def test_more_ranges(self):
1046
        t = self.get_transport()
1047
        l = list(t.readv('a', ((0, 4), (1024, 4), (4096, 4), (8192, 4))))
1048
        self.assertEqual(l[0], (0, '0000'))
1049
        self.assertEqual(l[1], (1024, '0001'))
1050
        self.assertEqual(l[2], (4096, '0004'))
1051
        self.assertEqual(l[3], (8192, '0008'))
1052
        # The server will refuse to serve the first request (too much ranges),
3199.1.2 by Vincent Ladeuil
Fix two more leaked log files.
1053
        # a second request will succeed.
3111.1.19 by Vincent Ladeuil
Merge back test_http_implementations.pc into test_http.py.
1054
        self.assertEqual(2, self.get_readonly_server().GET_request_nb)
1055
1056
3052.3.2 by Vincent Ladeuil
Add tests and fix trivial bugs and other typos.
1057
class TestHttpProxyWhiteBox(tests.TestCase):
2298.7.1 by Vincent Ladeuil
Fix bug #87765: proxy env variables without scheme should cause
1058
    """Whitebox test proxy http authorization.
1059
2420.1.3 by Vincent Ladeuil
Implement http proxy basic authentication.
1060
    Only the urllib implementation is tested here.
2298.7.1 by Vincent Ladeuil
Fix bug #87765: proxy env variables without scheme should cause
1061
    """
2273.2.2 by v.ladeuil+lp at free
Really fix bug #83954, with tests.
1062
1063
    def setUp(self):
3052.3.2 by Vincent Ladeuil
Add tests and fix trivial bugs and other typos.
1064
        tests.TestCase.setUp(self)
2273.2.2 by v.ladeuil+lp at free
Really fix bug #83954, with tests.
1065
        self._old_env = {}
1066
1067
    def tearDown(self):
1068
        self._restore_env()
3199.1.2 by Vincent Ladeuil
Fix two more leaked log files.
1069
        tests.TestCase.tearDown(self)
2273.2.2 by v.ladeuil+lp at free
Really fix bug #83954, with tests.
1070
1071
    def _install_env(self, env):
1072
        for name, value in env.iteritems():
2420.1.2 by Vincent Ladeuil
Define tests for http proxy basic authentication. They fail.
1073
            self._old_env[name] = osutils.set_or_unset_env(name, value)
2273.2.2 by v.ladeuil+lp at free
Really fix bug #83954, with tests.
1074
1075
    def _restore_env(self):
1076
        for name, value in self._old_env.iteritems():
1077
            osutils.set_or_unset_env(name, value)
1078
1079
    def _proxied_request(self):
3111.1.20 by Vincent Ladeuil
Make all the test pass. Looks like we are HTTP/1.1 compliant.
1080
        handler = _urllib2_wrappers.ProxyHandler()
1081
        request = _urllib2_wrappers.Request('GET','http://baz/buzzle')
2273.2.2 by v.ladeuil+lp at free
Really fix bug #83954, with tests.
1082
        handler.set_proxy(request, 'http')
1083
        return request
1084
1085
    def test_empty_user(self):
1086
        self._install_env({'http_proxy': 'http://bar.com'})
1087
        request = self._proxied_request()
1088
        self.assertFalse(request.headers.has_key('Proxy-authorization'))
1089
2298.7.1 by Vincent Ladeuil
Fix bug #87765: proxy env variables without scheme should cause
1090
    def test_invalid_proxy(self):
1091
        """A proxy env variable without scheme"""
1092
        self._install_env({'http_proxy': 'host:1234'})
1093
        self.assertRaises(errors.InvalidURL, self._proxied_request)
2273.2.2 by v.ladeuil+lp at free
Really fix bug #83954, with tests.
1094
1095
3111.1.19 by Vincent Ladeuil
Merge back test_http_implementations.pc into test_http.py.
1096
class TestProxyHttpServer(http_utils.TestCaseWithTwoWebservers):
1097
    """Tests proxy server.
1098
1099
    Be aware that we do not setup a real proxy here. Instead, we
1100
    check that the *connection* goes through the proxy by serving
1101
    different content (the faked proxy server append '-proxied'
1102
    to the file names).
1103
    """
1104
1105
    # FIXME: We don't have an https server available, so we don't
1106
    # test https connections.
1107
1108
    def setUp(self):
1109
        super(TestProxyHttpServer, self).setUp()
1110
        self.build_tree_contents([('foo', 'contents of foo\n'),
1111
                                  ('foo-proxied', 'proxied contents of foo\n')])
1112
        # Let's setup some attributes for tests
1113
        self.server = self.get_readonly_server()
1114
        self.proxy_address = '%s:%d' % (self.server.host, self.server.port)
1115
        if self._testing_pycurl():
1116
            # Oh my ! pycurl does not check for the port as part of
1117
            # no_proxy :-( So we just test the host part
1118
            self.no_proxy_host = 'localhost'
1119
        else:
1120
            self.no_proxy_host = self.proxy_address
1121
        # The secondary server is the proxy
1122
        self.proxy = self.get_secondary_server()
1123
        self.proxy_url = self.proxy.get_url()
1124
        self._old_env = {}
1125
1126
    def _testing_pycurl(self):
1127
        return pycurl_present and self._transport == PyCurlTransport
1128
1129
    def create_transport_secondary_server(self):
1130
        """Creates an http server that will serve files with
1131
        '-proxied' appended to their names.
1132
        """
1133
        return http_utils.ProxyServer(protocol_version=self._protocol_version)
1134
1135
    def _install_env(self, env):
1136
        for name, value in env.iteritems():
1137
            self._old_env[name] = osutils.set_or_unset_env(name, value)
1138
1139
    def _restore_env(self):
1140
        for name, value in self._old_env.iteritems():
1141
            osutils.set_or_unset_env(name, value)
1142
1143
    def proxied_in_env(self, env):
1144
        self._install_env(env)
1145
        url = self.server.get_url()
1146
        t = self._transport(url)
1147
        try:
1148
            self.assertEqual(t.get('foo').read(), 'proxied contents of foo\n')
1149
        finally:
1150
            self._restore_env()
1151
1152
    def not_proxied_in_env(self, env):
1153
        self._install_env(env)
1154
        url = self.server.get_url()
1155
        t = self._transport(url)
1156
        try:
1157
            self.assertEqual(t.get('foo').read(), 'contents of foo\n')
1158
        finally:
1159
            self._restore_env()
1160
1161
    def test_http_proxy(self):
1162
        self.proxied_in_env({'http_proxy': self.proxy_url})
1163
1164
    def test_HTTP_PROXY(self):
1165
        if self._testing_pycurl():
1166
            # pycurl does not check HTTP_PROXY for security reasons
1167
            # (for use in a CGI context that we do not care
1168
            # about. Should we ?)
1169
            raise tests.TestNotApplicable(
1170
                'pycurl does not check HTTP_PROXY for security reasons')
1171
        self.proxied_in_env({'HTTP_PROXY': self.proxy_url})
1172
1173
    def test_all_proxy(self):
1174
        self.proxied_in_env({'all_proxy': self.proxy_url})
1175
1176
    def test_ALL_PROXY(self):
1177
        self.proxied_in_env({'ALL_PROXY': self.proxy_url})
1178
1179
    def test_http_proxy_with_no_proxy(self):
1180
        self.not_proxied_in_env({'http_proxy': self.proxy_url,
1181
                                 'no_proxy': self.no_proxy_host})
1182
1183
    def test_HTTP_PROXY_with_NO_PROXY(self):
1184
        if self._testing_pycurl():
1185
            raise tests.TestNotApplicable(
1186
                'pycurl does not check HTTP_PROXY for security reasons')
1187
        self.not_proxied_in_env({'HTTP_PROXY': self.proxy_url,
1188
                                 'NO_PROXY': self.no_proxy_host})
1189
1190
    def test_all_proxy_with_no_proxy(self):
1191
        self.not_proxied_in_env({'all_proxy': self.proxy_url,
1192
                                 'no_proxy': self.no_proxy_host})
1193
1194
    def test_ALL_PROXY_with_NO_PROXY(self):
1195
        self.not_proxied_in_env({'ALL_PROXY': self.proxy_url,
1196
                                 'NO_PROXY': self.no_proxy_host})
1197
1198
    def test_http_proxy_without_scheme(self):
1199
        if self._testing_pycurl():
1200
            # pycurl *ignores* invalid proxy env variables. If that ever change
1201
            # in the future, this test will fail indicating that pycurl do not
1202
            # ignore anymore such variables.
1203
            self.not_proxied_in_env({'http_proxy': self.proxy_address})
1204
        else:
1205
            self.assertRaises(errors.InvalidURL,
1206
                              self.proxied_in_env,
1207
                              {'http_proxy': self.proxy_address})
1208
1209
1210
class TestRanges(http_utils.TestCaseWithWebserver):
1211
    """Test the Range header in GET methods."""
1212
1213
    def setUp(self):
1214
        http_utils.TestCaseWithWebserver.setUp(self)
1215
        self.build_tree_contents([('a', '0123456789')],)
1216
        server = self.get_readonly_server()
1217
        self.transport = self._transport(server.get_url())
1218
3111.1.22 by Vincent Ladeuil
Rework TestingHTTPServer classes, fix test bug.
1219
    def create_transport_readonly_server(self):
1220
        return http_server.HttpServer(protocol_version=self._protocol_version)
1221
3111.1.19 by Vincent Ladeuil
Merge back test_http_implementations.pc into test_http.py.
1222
    def _file_contents(self, relpath, ranges):
1223
        offsets = [ (start, end - start + 1) for start, end in ranges]
1224
        coalesce = self.transport._coalesce_offsets
1225
        coalesced = list(coalesce(offsets, limit=0, fudge_factor=0))
1226
        code, data = self.transport._get(relpath, coalesced)
1227
        self.assertTrue(code in (200, 206),'_get returns: %d' % code)
1228
        for start, end in ranges:
1229
            data.seek(start)
1230
            yield data.read(end - start + 1)
1231
1232
    def _file_tail(self, relpath, tail_amount):
1233
        code, data = self.transport._get(relpath, [], tail_amount)
1234
        self.assertTrue(code in (200, 206),'_get returns: %d' % code)
1235
        data.seek(-tail_amount, 2)
1236
        return data.read(tail_amount)
1237
1238
    def test_range_header(self):
1239
        # Valid ranges
1240
        map(self.assertEqual,['0', '234'],
1241
            list(self._file_contents('a', [(0,0), (2,4)])),)
1242
1243
    def test_range_header_tail(self):
1244
        self.assertEqual('789', self._file_tail('a', 3))
1245
1246
    def test_syntactically_invalid_range_header(self):
1247
        self.assertListRaises(errors.InvalidHttpRange,
1248
                          self._file_contents, 'a', [(4, 3)])
1249
1250
    def test_semantically_invalid_range_header(self):
1251
        self.assertListRaises(errors.InvalidHttpRange,
1252
                          self._file_contents, 'a', [(42, 128)])
1253
1254
1255
class TestHTTPRedirections(http_utils.TestCaseWithRedirectedWebserver):
1256
    """Test redirection between http servers."""
1257
1258
    def create_transport_secondary_server(self):
1259
        """Create the secondary server redirecting to the primary server"""
1260
        new = self.get_readonly_server()
1261
1262
        redirecting = http_utils.HTTPServerRedirecting(
1263
            protocol_version=self._protocol_version)
1264
        redirecting.redirect_to(new.host, new.port)
1265
        return redirecting
1266
1267
    def setUp(self):
1268
        super(TestHTTPRedirections, self).setUp()
1269
        self.build_tree_contents([('a', '0123456789'),
1270
                                  ('bundle',
1271
                                  '# Bazaar revision bundle v0.9\n#\n')
1272
                                  ],)
1273
1274
        self.old_transport = self._transport(self.old_server.get_url())
1275
1276
    def test_redirected(self):
1277
        self.assertRaises(errors.RedirectRequested, self.old_transport.get, 'a')
1278
        t = self._transport(self.new_server.get_url())
1279
        self.assertEqual('0123456789', t.get('a').read())
1280
1281
    def test_read_redirected_bundle_from_url(self):
1282
        from bzrlib.bundle import read_bundle_from_url
1283
        url = self.old_transport.abspath('bundle')
1284
        bundle = read_bundle_from_url(url)
1285
        # If read_bundle_from_url was successful we get an empty bundle
1286
        self.assertEqual([], bundle.revisions)
1287
1288
1289
class RedirectedRequest(_urllib2_wrappers.Request):
1290
    """Request following redirections. """
1291
1292
    init_orig = _urllib2_wrappers.Request.__init__
1293
1294
    def __init__(self, method, url, *args, **kwargs):
1295
        """Constructor.
1296
1297
        """
1298
        # Since the tests using this class will replace
1299
        # _urllib2_wrappers.Request, we can't just call the base class __init__
1300
        # or we'll loop.
1301
        RedirectedRequest.init_orig(self, method, url, args, kwargs)
1302
        self.follow_redirections = True
1303
1304
1305
class TestHTTPSilentRedirections(http_utils.TestCaseWithRedirectedWebserver):
1306
    """Test redirections.
1307
1308
    http implementations do not redirect silently anymore (they
1309
    do not redirect at all in fact). The mechanism is still in
1310
    place at the _urllib2_wrappers.Request level and these tests
1311
    exercise it.
1312
1313
    For the pycurl implementation
1314
    the redirection have been deleted as we may deprecate pycurl
1315
    and I have no place to keep a working implementation.
1316
    -- vila 20070212
1317
    """
1318
1319
    def setUp(self):
1320
        if pycurl_present and self._transport == PyCurlTransport:
1321
            raise tests.TestNotApplicable(
1322
                "pycurl doesn't redirect silently annymore")
1323
        super(TestHTTPSilentRedirections, self).setUp()
1324
        self.setup_redirected_request()
1325
        self.addCleanup(self.cleanup_redirected_request)
1326
        self.build_tree_contents([('a','a'),
1327
                                  ('1/',),
1328
                                  ('1/a', 'redirected once'),
1329
                                  ('2/',),
1330
                                  ('2/a', 'redirected twice'),
1331
                                  ('3/',),
1332
                                  ('3/a', 'redirected thrice'),
1333
                                  ('4/',),
1334
                                  ('4/a', 'redirected 4 times'),
1335
                                  ('5/',),
1336
                                  ('5/a', 'redirected 5 times'),
1337
                                  ],)
1338
1339
        self.old_transport = self._transport(self.old_server.get_url())
1340
1341
    def setup_redirected_request(self):
1342
        self.original_class = _urllib2_wrappers.Request
1343
        _urllib2_wrappers.Request = RedirectedRequest
1344
1345
    def cleanup_redirected_request(self):
1346
        _urllib2_wrappers.Request = self.original_class
1347
1348
    def create_transport_secondary_server(self):
1349
        """Create the secondary server, redirections are defined in the tests"""
1350
        return http_utils.HTTPServerRedirecting(
1351
            protocol_version=self._protocol_version)
1352
1353
    def test_one_redirection(self):
1354
        t = self.old_transport
1355
1356
        req = RedirectedRequest('GET', t.abspath('a'))
1357
        req.follow_redirections = True
1358
        new_prefix = 'http://%s:%s' % (self.new_server.host,
1359
                                       self.new_server.port)
1360
        self.old_server.redirections = \
1361
            [('(.*)', r'%s/1\1' % (new_prefix), 301),]
1362
        self.assertEquals('redirected once',t._perform(req).read())
1363
1364
    def test_five_redirections(self):
1365
        t = self.old_transport
1366
1367
        req = RedirectedRequest('GET', t.abspath('a'))
1368
        req.follow_redirections = True
1369
        old_prefix = 'http://%s:%s' % (self.old_server.host,
1370
                                       self.old_server.port)
1371
        new_prefix = 'http://%s:%s' % (self.new_server.host,
1372
                                       self.new_server.port)
3111.1.20 by Vincent Ladeuil
Make all the test pass. Looks like we are HTTP/1.1 compliant.
1373
        self.old_server.redirections = [
1374
            ('/1(.*)', r'%s/2\1' % (old_prefix), 302),
1375
            ('/2(.*)', r'%s/3\1' % (old_prefix), 303),
1376
            ('/3(.*)', r'%s/4\1' % (old_prefix), 307),
1377
            ('/4(.*)', r'%s/5\1' % (new_prefix), 301),
1378
            ('(/[^/]+)', r'%s/1\1' % (old_prefix), 301),
1379
            ]
3111.1.19 by Vincent Ladeuil
Merge back test_http_implementations.pc into test_http.py.
1380
        self.assertEquals('redirected 5 times',t._perform(req).read())
1381
1382
1383
class TestDoCatchRedirections(http_utils.TestCaseWithRedirectedWebserver):
1384
    """Test transport.do_catching_redirections."""
1385
1386
    def setUp(self):
1387
        super(TestDoCatchRedirections, self).setUp()
1388
        self.build_tree_contents([('a', '0123456789'),],)
1389
1390
        self.old_transport = self._transport(self.old_server.get_url())
1391
1392
    def get_a(self, transport):
1393
        return transport.get('a')
1394
1395
    def test_no_redirection(self):
1396
        t = self._transport(self.new_server.get_url())
1397
1398
        # We use None for redirected so that we fail if redirected
1399
        self.assertEquals('0123456789',
1400
                          transport.do_catching_redirections(
1401
                self.get_a, t, None).read())
1402
1403
    def test_one_redirection(self):
1404
        self.redirections = 0
1405
1406
        def redirected(transport, exception, redirection_notice):
1407
            self.redirections += 1
1408
            dir, file = urlutils.split(exception.target)
1409
            return self._transport(dir)
1410
1411
        self.assertEquals('0123456789',
1412
                          transport.do_catching_redirections(
1413
                self.get_a, self.old_transport, redirected).read())
1414
        self.assertEquals(1, self.redirections)
1415
1416
    def test_redirection_loop(self):
1417
1418
        def redirected(transport, exception, redirection_notice):
1419
            # By using the redirected url as a base dir for the
1420
            # *old* transport, we create a loop: a => a/a =>
1421
            # a/a/a
1422
            return self.old_transport.clone(exception.target)
1423
1424
        self.assertRaises(errors.TooManyRedirections,
1425
                          transport.do_catching_redirections,
1426
                          self.get_a, self.old_transport, redirected)
1427
1428
1429
class TestAuth(http_utils.TestCaseWithWebserver):
1430
    """Test authentication scheme"""
1431
1432
    _auth_header = 'Authorization'
1433
    _password_prompt_prefix = ''
1434
1435
    def setUp(self):
1436
        super(TestAuth, self).setUp()
1437
        self.server = self.get_readonly_server()
1438
        self.build_tree_contents([('a', 'contents of a\n'),
1439
                                  ('b', 'contents of b\n'),])
1440
1441
    def create_transport_readonly_server(self):
1442
        if self._auth_scheme == 'basic':
1443
            server = http_utils.HTTPBasicAuthServer(
1444
                protocol_version=self._protocol_version)
1445
        else:
1446
            if self._auth_scheme != 'digest':
1447
                raise AssertionError('Unknown auth scheme: %r'
1448
                                     % self._auth_scheme)
1449
            server = http_utils.HTTPDigestAuthServer(
1450
                protocol_version=self._protocol_version)
1451
        return server
1452
1453
    def _testing_pycurl(self):
1454
        return pycurl_present and self._transport == PyCurlTransport
1455
1456
    def get_user_url(self, user=None, password=None):
1457
        """Build an url embedding user and password"""
1458
        url = '%s://' % self.server._url_protocol
1459
        if user is not None:
1460
            url += user
1461
            if password is not None:
1462
                url += ':' + password
1463
            url += '@'
1464
        url += '%s:%s/' % (self.server.host, self.server.port)
1465
        return url
1466
1467
    def get_user_transport(self, user=None, password=None):
1468
        return self._transport(self.get_user_url(user, password))
1469
1470
    def test_no_user(self):
1471
        self.server.add_user('joe', 'foo')
1472
        t = self.get_user_transport()
1473
        self.assertRaises(errors.InvalidHttpResponse, t.get, 'a')
1474
        # Only one 'Authentication Required' error should occur
1475
        self.assertEqual(1, self.server.auth_required_errors)
1476
1477
    def test_empty_pass(self):
1478
        self.server.add_user('joe', '')
1479
        t = self.get_user_transport('joe', '')
1480
        self.assertEqual('contents of a\n', t.get('a').read())
1481
        # Only one 'Authentication Required' error should occur
1482
        self.assertEqual(1, self.server.auth_required_errors)
1483
1484
    def test_user_pass(self):
1485
        self.server.add_user('joe', 'foo')
1486
        t = self.get_user_transport('joe', 'foo')
1487
        self.assertEqual('contents of a\n', t.get('a').read())
1488
        # Only one 'Authentication Required' error should occur
1489
        self.assertEqual(1, self.server.auth_required_errors)
1490
1491
    def test_unknown_user(self):
1492
        self.server.add_user('joe', 'foo')
1493
        t = self.get_user_transport('bill', 'foo')
1494
        self.assertRaises(errors.InvalidHttpResponse, t.get, 'a')
1495
        # Two 'Authentication Required' errors should occur (the
1496
        # initial 'who are you' and 'I don't know you, who are
1497
        # you').
1498
        self.assertEqual(2, self.server.auth_required_errors)
1499
1500
    def test_wrong_pass(self):
1501
        self.server.add_user('joe', 'foo')
1502
        t = self.get_user_transport('joe', 'bar')
1503
        self.assertRaises(errors.InvalidHttpResponse, t.get, 'a')
1504
        # Two 'Authentication Required' errors should occur (the
1505
        # initial 'who are you' and 'this is not you, who are you')
1506
        self.assertEqual(2, self.server.auth_required_errors)
1507
1508
    def test_prompt_for_password(self):
1509
        if self._testing_pycurl():
1510
            raise tests.TestNotApplicable(
1511
                'pycurl cannot prompt, it handles auth by embedding'
1512
                ' user:pass in urls only')
1513
1514
        self.server.add_user('joe', 'foo')
1515
        t = self.get_user_transport('joe', None)
1516
        stdout = tests.StringIOWrapper()
1517
        ui.ui_factory = tests.TestUIFactory(stdin='foo\n', stdout=stdout)
1518
        self.assertEqual('contents of a\n',t.get('a').read())
1519
        # stdin should be empty
1520
        self.assertEqual('', ui.ui_factory.stdin.readline())
1521
        self._check_password_prompt(t._unqualified_scheme, 'joe',
1522
                                    stdout.getvalue())
1523
        # And we shouldn't prompt again for a different request
1524
        # against the same transport.
1525
        self.assertEqual('contents of b\n',t.get('b').read())
1526
        t2 = t.clone()
1527
        # And neither against a clone
1528
        self.assertEqual('contents of b\n',t2.get('b').read())
1529
        # Only one 'Authentication Required' error should occur
1530
        self.assertEqual(1, self.server.auth_required_errors)
1531
1532
    def _check_password_prompt(self, scheme, user, actual_prompt):
1533
        expected_prompt = (self._password_prompt_prefix
1534
                           + ("%s %s@%s:%d, Realm: '%s' password: "
1535
                              % (scheme.upper(),
1536
                                 user, self.server.host, self.server.port,
1537
                                 self.server.auth_realm)))
1538
        self.assertEquals(expected_prompt, actual_prompt)
1539
1540
    def test_no_prompt_for_password_when_using_auth_config(self):
1541
        if self._testing_pycurl():
1542
            raise tests.TestNotApplicable(
1543
                'pycurl does not support authentication.conf'
1544
                ' since it cannot prompt')
1545
1546
        user =' joe'
1547
        password = 'foo'
1548
        stdin_content = 'bar\n'  # Not the right password
1549
        self.server.add_user(user, password)
1550
        t = self.get_user_transport(user, None)
1551
        ui.ui_factory = tests.TestUIFactory(stdin=stdin_content,
1552
                                            stdout=tests.StringIOWrapper())
1553
        # Create a minimal config file with the right password
1554
        conf = config.AuthenticationConfig()
1555
        conf._get_config().update(
1556
            {'httptest': {'scheme': 'http', 'port': self.server.port,
1557
                          'user': user, 'password': password}})
1558
        conf._save()
1559
        # Issue a request to the server to connect
1560
        self.assertEqual('contents of a\n',t.get('a').read())
1561
        # stdin should have  been left untouched
1562
        self.assertEqual(stdin_content, ui.ui_factory.stdin.readline())
1563
        # Only one 'Authentication Required' error should occur
1564
        self.assertEqual(1, self.server.auth_required_errors)
1565
3111.1.26 by Vincent Ladeuil
Re-add a test lost in refactoring.
1566
    def test_changing_nonce(self):
1567
        if self._auth_scheme != 'digest':
1568
            raise tests.TestNotApplicable('HTTP auth digest only test')
1569
        if self._testing_pycurl():
1570
            raise tests.KnownFailure(
1571
                'pycurl does not handle a nonce change')
1572
        self.server.add_user('joe', 'foo')
1573
        t = self.get_user_transport('joe', 'foo')
1574
        self.assertEqual('contents of a\n', t.get('a').read())
1575
        self.assertEqual('contents of b\n', t.get('b').read())
1576
        # Only one 'Authentication Required' error should have
1577
        # occured so far
1578
        self.assertEqual(1, self.server.auth_required_errors)
1579
        # The server invalidates the current nonce
1580
        self.server.auth_nonce = self.server.auth_nonce + '. No, now!'
1581
        self.assertEqual('contents of a\n', t.get('a').read())
1582
        # Two 'Authentication Required' errors should occur (the
1583
        # initial 'who are you' and a second 'who are you' with the new nonce)
1584
        self.assertEqual(2, self.server.auth_required_errors)
1585
3111.1.19 by Vincent Ladeuil
Merge back test_http_implementations.pc into test_http.py.
1586
1587
1588
class TestProxyAuth(TestAuth):
1589
    """Test proxy authentication schemes."""
1590
1591
    _auth_header = 'Proxy-authorization'
1592
    _password_prompt_prefix='Proxy '
1593
1594
    def setUp(self):
1595
        super(TestProxyAuth, self).setUp()
1596
        self._old_env = {}
1597
        self.addCleanup(self._restore_env)
1598
        # Override the contents to avoid false positives
1599
        self.build_tree_contents([('a', 'not proxied contents of a\n'),
1600
                                  ('b', 'not proxied contents of b\n'),
1601
                                  ('a-proxied', 'contents of a\n'),
1602
                                  ('b-proxied', 'contents of b\n'),
1603
                                  ])
1604
1605
    def create_transport_readonly_server(self):
1606
        if self._auth_scheme == 'basic':
1607
            server = http_utils.ProxyBasicAuthServer(
1608
                protocol_version=self._protocol_version)
1609
        else:
1610
            if self._auth_scheme != 'digest':
1611
                raise AssertionError('Unknown auth scheme: %r'
1612
                                     % self._auth_scheme)
1613
            server = http_utils.ProxyDigestAuthServer(
1614
                protocol_version=self._protocol_version)
1615
        return server
1616
1617
    def get_user_transport(self, user=None, password=None):
1618
        self._install_env({'all_proxy': self.get_user_url(user, password)})
1619
        return self._transport(self.server.get_url())
1620
1621
    def _install_env(self, env):
1622
        for name, value in env.iteritems():
1623
            self._old_env[name] = osutils.set_or_unset_env(name, value)
1624
1625
    def _restore_env(self):
1626
        for name, value in self._old_env.iteritems():
1627
            osutils.set_or_unset_env(name, value)
1628
1629
    def test_empty_pass(self):
1630
        if self._testing_pycurl():
1631
            import pycurl
1632
            if pycurl.version_info()[1] < '7.16.0':
1633
                raise tests.KnownFailure(
1634
                    'pycurl < 7.16.0 does not handle empty proxy passwords')
1635
        super(TestProxyAuth, self).test_empty_pass()
1636
3111.1.25 by Vincent Ladeuil
Fix the smart server failing test and use it against protocol combinations.
1637
1638
class SampleSocket(object):
1639
    """A socket-like object for use in testing the HTTP request handler."""
1640
1641
    def __init__(self, socket_read_content):
1642
        """Constructs a sample socket.
1643
1644
        :param socket_read_content: a byte sequence
1645
        """
1646
        # Use plain python StringIO so we can monkey-patch the close method to
1647
        # not discard the contents.
1648
        from StringIO import StringIO
1649
        self.readfile = StringIO(socket_read_content)
1650
        self.writefile = StringIO()
1651
        self.writefile.close = lambda: None
1652
1653
    def makefile(self, mode='r', bufsize=None):
1654
        if 'r' in mode:
1655
            return self.readfile
1656
        else:
1657
            return self.writefile
1658
1659
1660
class SmartHTTPTunnellingTest(tests.TestCaseWithTransport):
1661
1662
    def setUp(self):
1663
        super(SmartHTTPTunnellingTest, self).setUp()
1664
        # We use the VFS layer as part of HTTP tunnelling tests.
1665
        self._captureVar('BZR_NO_SMART_VFS', None)
1666
        self.transport_readonly_server = http_utils.HTTPServerWithSmarts
1667
1668
    def create_transport_readonly_server(self):
1669
        return http_utils.HTTPServerWithSmarts(
1670
            protocol_version=self._protocol_version)
1671
3606.4.1 by Andrew Bennetts
Fix NotImplementedError when probing for smart protocol via HTTP.
1672
    def test_open_bzrdir(self):
1673
        branch = self.make_branch('relpath')
1674
        http_server = self.get_readonly_server()
1675
        url = http_server.get_url() + 'relpath'
1676
        bd = bzrdir.BzrDir.open(url)
1677
        self.assertIsInstance(bd, _mod_remote.RemoteBzrDir)
1678
3111.1.25 by Vincent Ladeuil
Fix the smart server failing test and use it against protocol combinations.
1679
    def test_bulk_data(self):
1680
        # We should be able to send and receive bulk data in a single message.
1681
        # The 'readv' command in the smart protocol both sends and receives
1682
        # bulk data, so we use that.
1683
        self.build_tree(['data-file'])
1684
        http_server = self.get_readonly_server()
1685
        http_transport = self._transport(http_server.get_url())
1686
        medium = http_transport.get_smart_medium()
1687
        # Since we provide the medium, the url below will be mostly ignored
1688
        # during the test, as long as the path is '/'.
1689
        remote_transport = remote.RemoteTransport('bzr://fake_host/',
1690
                                                  medium=medium)
1691
        self.assertEqual(
1692
            [(0, "c")], list(remote_transport.readv("data-file", [(0,1)])))
1693
1694
    def test_http_send_smart_request(self):
1695
1696
        post_body = 'hello\n'
3245.4.59 by Andrew Bennetts
Various tweaks in response to Martin's review.
1697
        expected_reply_body = 'ok\x012\n'
3111.1.25 by Vincent Ladeuil
Fix the smart server failing test and use it against protocol combinations.
1698
1699
        http_server = self.get_readonly_server()
1700
        http_transport = self._transport(http_server.get_url())
1701
        medium = http_transport.get_smart_medium()
1702
        response = medium.send_http_smart_request(post_body)
1703
        reply_body = response.read()
1704
        self.assertEqual(expected_reply_body, reply_body)
1705
1706
    def test_smart_http_server_post_request_handler(self):
1707
        httpd = self.get_readonly_server()._get_httpd()
1708
1709
        socket = SampleSocket(
1710
            'POST /.bzr/smart %s \r\n' % self._protocol_version
1711
            # HTTP/1.1 posts must have a Content-Length (but it doesn't hurt
1712
            # for 1.0)
1713
            + 'Content-Length: 6\r\n'
1714
            '\r\n'
1715
            'hello\n')
1716
        # Beware: the ('localhost', 80) below is the
1717
        # client_address parameter, but we don't have one because
1718
        # we have defined a socket which is not bound to an
1719
        # address. The test framework never uses this client
1720
        # address, so far...
1721
        request_handler = http_utils.SmartRequestHandler(socket,
1722
                                                         ('localhost', 80),
1723
                                                         httpd)
1724
        response = socket.writefile.getvalue()
1725
        self.assertStartsWith(response, '%s 200 ' % self._protocol_version)
1726
        # This includes the end of the HTTP headers, and all the body.
3245.4.59 by Andrew Bennetts
Various tweaks in response to Martin's review.
1727
        expected_end_of_response = '\r\n\r\nok\x012\n'
3111.1.25 by Vincent Ladeuil
Fix the smart server failing test and use it against protocol combinations.
1728
        self.assertEndsWith(response, expected_end_of_response)
1729
1730
3430.3.4 by Vincent Ladeuil
Of course we can write tests !
1731
class ForbiddenRequestHandler(http_server.TestingHTTPRequestHandler):
1732
    """No smart server here request handler."""
1733
1734
    def do_POST(self):
1735
        self.send_error(403, "Forbidden")
1736
1737
1738
class SmartClientAgainstNotSmartServer(TestSpecificRequestHandler):
1739
    """Test smart client behaviour against an http server without smarts."""
1740
1741
    _req_handler_class = ForbiddenRequestHandler
1742
1743
    def test_probe_smart_server(self):
1744
        """Test error handling against server refusing smart requests."""
1745
        server = self.get_readonly_server()
1746
        t = self._transport(server.get_url())
1747
        # No need to build a valid smart request here, the server will not even
1748
        # try to interpret it.
1749
        self.assertRaises(errors.SmartProtocolError,
1750
                          t.send_http_smart_request, 'whatever')
1751