~bzr-pqm/bzr/bzr.dev

2018.4.1 by Andrew Bennetts
Add WSGI smart server.
1
# Copyright (C) 2006 Canonical Ltd
2
#
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.
7
#
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.
12
#
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
16
17
"""Tests for WSGI application"""
18
19
from cStringIO import StringIO
20
21
from bzrlib import tests
2432.2.7 by Andrew Bennetts
Use less confusing version strings, and define REQUEST_VERSION_TWO/RESPONSE_VERSION_TWO constants for them.
22
from bzrlib.smart import protocol
2018.4.1 by Andrew Bennetts
Add WSGI smart server.
23
from bzrlib.transport.http import wsgi
2018.4.11 by Andrew Bennetts
Use ChrootTransportDecorator so that the WSGI server won't let you access the entire filesystem.
24
from bzrlib.transport import chroot, memory
2018.4.1 by Andrew Bennetts
Add WSGI smart server.
25
2018.4.5 by Andrew Bennetts
Improvement thanks to John's review.
26
2018.4.1 by Andrew Bennetts
Add WSGI smart server.
27
class TestWSGI(tests.TestCase):
28
29
    def setUp(self):
30
        tests.TestCase.setUp(self)
31
        self.status = None
32
        self.headers = None
33
2018.4.5 by Andrew Bennetts
Improvement thanks to John's review.
34
    def build_environ(self, updates=None):
2018.4.1 by Andrew Bennetts
Add WSGI smart server.
35
        """Builds an environ dict with all fields required by PEP 333.
36
        
2018.4.5 by Andrew Bennetts
Improvement thanks to John's review.
37
        :param updates: a dict to that will be incorporated into the returned
38
            dict using dict.update(updates).
2018.4.1 by Andrew Bennetts
Add WSGI smart server.
39
        """
40
        environ = {
41
            # Required CGI variables
42
            'REQUEST_METHOD': 'GET',
43
            'SCRIPT_NAME': '/script/name/',
44
            'PATH_INFO': 'path/info',
45
            'SERVER_NAME': 'test',
46
            'SERVER_PORT': '9999',
47
            'SERVER_PROTOCOL': 'HTTP/1.0',
48
49
            # Required WSGI variables
50
            'wsgi.version': (1,0),
51
            'wsgi.url_scheme': 'http',
52
            'wsgi.input': StringIO(''),
53
            'wsgi.errors': StringIO(),
54
            'wsgi.multithread': False,
55
            'wsgi.multiprocess': False,
56
            'wsgi.run_once': True,
57
        }
2018.4.5 by Andrew Bennetts
Improvement thanks to John's review.
58
        if updates is not None:
59
            environ.update(updates)
2018.4.1 by Andrew Bennetts
Add WSGI smart server.
60
        return environ
61
        
62
    def read_response(self, iterable):
63
        response = ''
64
        for string in iterable:
65
            response += string
66
        return response
67
68
    def start_response(self, status, headers):
69
        self.status = status
70
        self.headers = headers
71
72
    def test_construct(self):
2018.4.11 by Andrew Bennetts
Use ChrootTransportDecorator so that the WSGI server won't let you access the entire filesystem.
73
        app = wsgi.SmartWSGIApp(FakeTransport())
74
        self.assertIsInstance(
2018.5.104 by Andrew Bennetts
Completely rework chrooted transports.
75
            app.backing_transport, chroot.ChrootTransport)
2018.4.1 by Andrew Bennetts
Add WSGI smart server.
76
77
    def test_http_get_rejected(self):
78
        # GET requests are rejected.
2018.4.11 by Andrew Bennetts
Use ChrootTransportDecorator so that the WSGI server won't let you access the entire filesystem.
79
        app = wsgi.SmartWSGIApp(FakeTransport())
2018.4.5 by Andrew Bennetts
Improvement thanks to John's review.
80
        environ = self.build_environ({'REQUEST_METHOD': 'GET'})
2018.4.1 by Andrew Bennetts
Add WSGI smart server.
81
        iterable = app(environ, self.start_response)
82
        self.read_response(iterable)
83
        self.assertEqual('405 Method not allowed', self.status)
84
        self.assertTrue(('Allow', 'POST') in self.headers)
85
        
2432.2.2 by Andrew Bennetts
Smart server mediums now detect which protocol version a request is and dispatch accordingly.
86
    def _fake_make_request(self, transport, write_func, bytes):
87
        request = FakeRequest(transport, write_func)
88
        request.accept_bytes(bytes)
89
        self.request = request
90
        return request
91
    
2018.4.1 by Andrew Bennetts
Add WSGI smart server.
92
    def test_smart_wsgi_app_uses_given_relpath(self):
93
        # The SmartWSGIApp should use the "bzrlib.relpath" field from the
2379.2.1 by Robert Collins
Rewritten chroot transport that prevents accidental chroot escapes when
94
        # WSGI environ to clone from its backing transport to get a specific
95
        # transport for this request.
2018.4.1 by Andrew Bennetts
Add WSGI smart server.
96
        transport = FakeTransport()
97
        wsgi_app = wsgi.SmartWSGIApp(transport)
2018.5.104 by Andrew Bennetts
Completely rework chrooted transports.
98
        wsgi_app.backing_transport = transport
2432.2.2 by Andrew Bennetts
Smart server mediums now detect which protocol version a request is and dispatch accordingly.
99
        wsgi_app.make_request = self._fake_make_request
2018.4.1 by Andrew Bennetts
Add WSGI smart server.
100
        fake_input = StringIO('fake request')
2018.4.5 by Andrew Bennetts
Improvement thanks to John's review.
101
        environ = self.build_environ({
2018.4.1 by Andrew Bennetts
Add WSGI smart server.
102
            'REQUEST_METHOD': 'POST',
103
            'CONTENT_LENGTH': len(fake_input.getvalue()),
104
            'wsgi.input': fake_input,
105
            'bzrlib.relpath': 'foo/bar',
106
        })
107
        iterable = wsgi_app(environ, self.start_response)
108
        response = self.read_response(iterable)
109
        self.assertEqual([('clone', 'foo/bar')] , transport.calls)
110
111
    def test_smart_wsgi_app_request_and_response(self):
112
        # SmartWSGIApp reads the smart request from the 'wsgi.input' file-like
113
        # object in the environ dict, and returns the response via the iterable
114
        # returned to the WSGI handler.
115
        transport = memory.MemoryTransport()
116
        transport.put_bytes('foo', 'some bytes')
117
        wsgi_app = wsgi.SmartWSGIApp(transport)
2432.2.2 by Andrew Bennetts
Smart server mediums now detect which protocol version a request is and dispatch accordingly.
118
        wsgi_app.make_request = self._fake_make_request
2018.4.1 by Andrew Bennetts
Add WSGI smart server.
119
        fake_input = StringIO('fake request')
2018.4.5 by Andrew Bennetts
Improvement thanks to John's review.
120
        environ = self.build_environ({
2018.4.1 by Andrew Bennetts
Add WSGI smart server.
121
            'REQUEST_METHOD': 'POST',
122
            'CONTENT_LENGTH': len(fake_input.getvalue()),
123
            'wsgi.input': fake_input,
124
            'bzrlib.relpath': 'foo',
125
        })
126
        iterable = wsgi_app(environ, self.start_response)
127
        response = self.read_response(iterable)
128
        self.assertEqual('200 OK', self.status)
129
        self.assertEqual('got bytes: fake request', response)
130
131
    def test_relpath_setter(self):
132
        # wsgi.RelpathSetter is WSGI "middleware" to set the 'bzrlib.relpath'
133
        # variable.
134
        calls = []
135
        def fake_app(environ, start_response):
136
            calls.append(environ['bzrlib.relpath'])
137
        wrapped_app = wsgi.RelpathSetter(
138
            fake_app, prefix='/abc/', path_var='FOO')
139
        wrapped_app({'FOO': '/abc/xyz/.bzr/smart'}, None)
140
        self.assertEqual(['xyz'], calls)
141
       
2018.4.5 by Andrew Bennetts
Improvement thanks to John's review.
142
    def test_relpath_setter_bad_path_prefix(self):
2018.4.1 by Andrew Bennetts
Add WSGI smart server.
143
        # wsgi.RelpathSetter will reject paths with that don't match the prefix
2018.4.5 by Andrew Bennetts
Improvement thanks to John's review.
144
        # with a 404.  This is probably a sign of misconfiguration; a server
145
        # shouldn't ever be invoking our WSGI application with bad paths.
2018.4.1 by Andrew Bennetts
Add WSGI smart server.
146
        def fake_app(environ, start_response):
147
            self.fail('The app should never be called when the path is wrong')
148
        wrapped_app = wsgi.RelpathSetter(
149
            fake_app, prefix='/abc/', path_var='FOO')
150
        iterable = wrapped_app(
151
            {'FOO': 'AAA/abc/xyz/.bzr/smart'}, self.start_response)
152
        self.read_response(iterable)
153
        self.assertTrue(self.status.startswith('404'))
154
        
2018.4.5 by Andrew Bennetts
Improvement thanks to John's review.
155
    def test_relpath_setter_bad_path_suffix(self):
156
        # Similar to test_relpath_setter_bad_path_prefix: wsgi.RelpathSetter
157
        # will reject paths with that don't match the suffix '.bzr/smart' with a
158
        # 404 as well.  Again, this shouldn't be seen by our WSGI application if
159
        # the server is configured correctly.
160
        def fake_app(environ, start_response):
161
            self.fail('The app should never be called when the path is wrong')
162
        wrapped_app = wsgi.RelpathSetter(
163
            fake_app, prefix='/abc/', path_var='FOO')
164
        iterable = wrapped_app(
165
            {'FOO': '/abc/xyz/.bzr/AAA'}, self.start_response)
166
        self.read_response(iterable)
167
        self.assertTrue(self.status.startswith('404'))
168
        
2018.4.1 by Andrew Bennetts
Add WSGI smart server.
169
    def test_make_app(self):
170
        # The make_app helper constructs a SmartWSGIApp wrapped in a
171
        # RelpathSetter.
172
        app = wsgi.make_app(
173
            root='a root',
174
            prefix='a prefix',
175
            path_var='a path_var')
176
        self.assertIsInstance(app, wsgi.RelpathSetter)
177
        self.assertIsInstance(app.app, wsgi.SmartWSGIApp)
2018.5.104 by Andrew Bennetts
Completely rework chrooted transports.
178
        self.assertStartsWith(app.app.backing_transport.base, 'chroot-')
179
        backing_transport = app.app.backing_transport
180
        chroot_backing_transport = backing_transport.server.backing_transport
181
        self.assertEndsWith(chroot_backing_transport.base, 'a%20root/')
2018.4.1 by Andrew Bennetts
Add WSGI smart server.
182
        self.assertEqual(app.prefix, 'a prefix')
183
        self.assertEqual(app.path_var, 'a path_var')
184
2018.4.5 by Andrew Bennetts
Improvement thanks to John's review.
185
    def test_incomplete_request(self):
186
        transport = FakeTransport()
187
        wsgi_app = wsgi.SmartWSGIApp(transport)
2432.2.2 by Andrew Bennetts
Smart server mediums now detect which protocol version a request is and dispatch accordingly.
188
        def make_request(transport, write_func, bytes):
2018.4.5 by Andrew Bennetts
Improvement thanks to John's review.
189
            request = IncompleteRequest(transport, write_func)
2432.2.2 by Andrew Bennetts
Smart server mediums now detect which protocol version a request is and dispatch accordingly.
190
            request.accept_bytes(bytes)
2018.4.5 by Andrew Bennetts
Improvement thanks to John's review.
191
            self.request = request
192
            return request
193
        wsgi_app.make_request = make_request
194
195
        fake_input = StringIO('incomplete request')
196
        environ = self.build_environ({
197
            'REQUEST_METHOD': 'POST',
198
            'CONTENT_LENGTH': len(fake_input.getvalue()),
199
            'wsgi.input': fake_input,
200
            'bzrlib.relpath': 'foo/bar',
201
        })
202
        iterable = wsgi_app(environ, self.start_response)
203
        response = self.read_response(iterable)
204
        self.assertEqual('200 OK', self.status)
205
        self.assertEqual('error\x01incomplete request\n', response)
206
2432.2.2 by Andrew Bennetts
Smart server mediums now detect which protocol version a request is and dispatch accordingly.
207
    def test_protocol_version_detection_one(self):
2432.2.7 by Andrew Bennetts
Use less confusing version strings, and define REQUEST_VERSION_TWO/RESPONSE_VERSION_TWO constants for them.
208
        # SmartWSGIApp detects requests that don't start with
209
        # REQUEST_VERSION_TWO as version one.
2432.2.2 by Andrew Bennetts
Smart server mediums now detect which protocol version a request is and dispatch accordingly.
210
        transport = memory.MemoryTransport()
211
        wsgi_app = wsgi.SmartWSGIApp(transport)
212
        fake_input = StringIO('hello\n')
213
        environ = self.build_environ({
214
            'REQUEST_METHOD': 'POST',
215
            'CONTENT_LENGTH': len(fake_input.getvalue()),
216
            'wsgi.input': fake_input,
217
            'bzrlib.relpath': 'foo',
218
        })
219
        iterable = wsgi_app(environ, self.start_response)
220
        response = self.read_response(iterable)
221
        self.assertEqual('200 OK', self.status)
222
        # Expect a version 1-encoded response.
223
        self.assertEqual('ok\x012\n', response)
224
225
    def test_protocol_version_detection_two(self):
2432.2.7 by Andrew Bennetts
Use less confusing version strings, and define REQUEST_VERSION_TWO/RESPONSE_VERSION_TWO constants for them.
226
        # SmartWSGIApp detects requests that start with REQUEST_VERSION_TWO
227
        # as version two.
2432.2.2 by Andrew Bennetts
Smart server mediums now detect which protocol version a request is and dispatch accordingly.
228
        transport = memory.MemoryTransport()
229
        wsgi_app = wsgi.SmartWSGIApp(transport)
2432.2.7 by Andrew Bennetts
Use less confusing version strings, and define REQUEST_VERSION_TWO/RESPONSE_VERSION_TWO constants for them.
230
        fake_input = StringIO(protocol.REQUEST_VERSION_TWO + 'hello\n')
2432.2.2 by Andrew Bennetts
Smart server mediums now detect which protocol version a request is and dispatch accordingly.
231
        environ = self.build_environ({
232
            'REQUEST_METHOD': 'POST',
233
            'CONTENT_LENGTH': len(fake_input.getvalue()),
234
            'wsgi.input': fake_input,
235
            'bzrlib.relpath': 'foo',
236
        })
237
        iterable = wsgi_app(environ, self.start_response)
238
        response = self.read_response(iterable)
239
        self.assertEqual('200 OK', self.status)
240
        # Expect a version 2-encoded response.
2432.3.5 by Andrew Bennetts
Merge Robert's status prefix changes to protocol 2.
241
        self.assertEqual(
242
            protocol.RESPONSE_VERSION_TWO + 'success\nok\x012\n', response)
2432.2.2 by Andrew Bennetts
Smart server mediums now detect which protocol version a request is and dispatch accordingly.
243
2018.4.1 by Andrew Bennetts
Add WSGI smart server.
244
245
class FakeRequest(object):
246
    
247
    def __init__(self, transport, write_func):
248
        self.transport = transport
249
        self.write_func = write_func
250
        self.accepted_bytes = ''
251
252
    def accept_bytes(self, bytes):
253
        self.accepted_bytes = bytes
254
        self.write_func('got bytes: ' + bytes)
255
256
    def next_read_size(self):
257
        return 0
258
259
260
class FakeTransport(object):
261
262
    def __init__(self):
263
        self.calls = []
2018.4.11 by Andrew Bennetts
Use ChrootTransportDecorator so that the WSGI server won't let you access the entire filesystem.
264
        self.base = 'fake:///'
265
266
    def abspath(self, relpath):
267
        return 'fake:///' + relpath
2018.4.1 by Andrew Bennetts
Add WSGI smart server.
268
269
    def clone(self, relpath):
270
        self.calls.append(('clone', relpath))
271
        return self
272
2018.4.5 by Andrew Bennetts
Improvement thanks to John's review.
273
274
class IncompleteRequest(FakeRequest):
275
    """A request-like object that always expects to read more bytes."""
276
277
    def next_read_size(self):
278
        # this request always asks for more
279
        return 1
280