~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
22
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.
23
from bzrlib.transport import chroot, memory
2018.4.1 by Andrew Bennetts
Add WSGI smart server.
24
2018.4.5 by Andrew Bennetts
Improvement thanks to John's review.
25
2018.4.1 by Andrew Bennetts
Add WSGI smart server.
26
class TestWSGI(tests.TestCase):
27
28
    def setUp(self):
29
        tests.TestCase.setUp(self)
30
        self.status = None
31
        self.headers = None
32
2018.4.5 by Andrew Bennetts
Improvement thanks to John's review.
33
    def build_environ(self, updates=None):
2018.4.1 by Andrew Bennetts
Add WSGI smart server.
34
        """Builds an environ dict with all fields required by PEP 333.
35
        
2018.4.5 by Andrew Bennetts
Improvement thanks to John's review.
36
        :param updates: a dict to that will be incorporated into the returned
37
            dict using dict.update(updates).
2018.4.1 by Andrew Bennetts
Add WSGI smart server.
38
        """
39
        environ = {
40
            # Required CGI variables
41
            'REQUEST_METHOD': 'GET',
42
            'SCRIPT_NAME': '/script/name/',
43
            'PATH_INFO': 'path/info',
44
            'SERVER_NAME': 'test',
45
            'SERVER_PORT': '9999',
46
            'SERVER_PROTOCOL': 'HTTP/1.0',
47
48
            # Required WSGI variables
49
            'wsgi.version': (1,0),
50
            'wsgi.url_scheme': 'http',
51
            'wsgi.input': StringIO(''),
52
            'wsgi.errors': StringIO(),
53
            'wsgi.multithread': False,
54
            'wsgi.multiprocess': False,
55
            'wsgi.run_once': True,
56
        }
2018.4.5 by Andrew Bennetts
Improvement thanks to John's review.
57
        if updates is not None:
58
            environ.update(updates)
2018.4.1 by Andrew Bennetts
Add WSGI smart server.
59
        return environ
60
        
61
    def read_response(self, iterable):
62
        response = ''
63
        for string in iterable:
64
            response += string
65
        return response
66
67
    def start_response(self, status, headers):
68
        self.status = status
69
        self.headers = headers
70
71
    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.
72
        app = wsgi.SmartWSGIApp(FakeTransport())
73
        self.assertIsInstance(
74
            app.backing_transport, chroot.ChrootTransportDecorator)
2018.4.1 by Andrew Bennetts
Add WSGI smart server.
75
76
    def test_http_get_rejected(self):
77
        # 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.
78
        app = wsgi.SmartWSGIApp(FakeTransport())
2018.4.5 by Andrew Bennetts
Improvement thanks to John's review.
79
        environ = self.build_environ({'REQUEST_METHOD': 'GET'})
2018.4.1 by Andrew Bennetts
Add WSGI smart server.
80
        iterable = app(environ, self.start_response)
81
        self.read_response(iterable)
82
        self.assertEqual('405 Method not allowed', self.status)
83
        self.assertTrue(('Allow', 'POST') in self.headers)
84
        
85
    def test_smart_wsgi_app_uses_given_relpath(self):
86
        # The SmartWSGIApp should use the "bzrlib.relpath" field from the
87
        # WSGI environ to construct the transport for this request, by cloning
88
        # its base transport with the given relpath.
89
        transport = FakeTransport()
90
        wsgi_app = wsgi.SmartWSGIApp(transport)
91
        def make_request(transport, write_func):
92
            request = FakeRequest(transport, write_func)
93
            self.request = request
94
            return request
95
        wsgi_app.make_request = make_request
96
        fake_input = StringIO('fake request')
2018.4.5 by Andrew Bennetts
Improvement thanks to John's review.
97
        environ = self.build_environ({
2018.4.1 by Andrew Bennetts
Add WSGI smart server.
98
            'REQUEST_METHOD': 'POST',
99
            'CONTENT_LENGTH': len(fake_input.getvalue()),
100
            'wsgi.input': fake_input,
101
            'bzrlib.relpath': 'foo/bar',
102
        })
103
        iterable = wsgi_app(environ, self.start_response)
104
        response = self.read_response(iterable)
105
        self.assertEqual([('clone', 'foo/bar')] , transport.calls)
106
107
    def test_smart_wsgi_app_request_and_response(self):
108
        # SmartWSGIApp reads the smart request from the 'wsgi.input' file-like
109
        # object in the environ dict, and returns the response via the iterable
110
        # returned to the WSGI handler.
111
        transport = memory.MemoryTransport()
112
        transport.put_bytes('foo', 'some bytes')
113
        wsgi_app = wsgi.SmartWSGIApp(transport)
114
        def make_request(transport, write_func):
115
            request = FakeRequest(transport, write_func)
116
            self.request = request
117
            return request
118
        wsgi_app.make_request = make_request
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)
178
        self.assertEndsWith(app.app.backing_transport.base, 'a%20root/')
179
        self.assertEqual(app.prefix, 'a prefix')
180
        self.assertEqual(app.path_var, 'a path_var')
181
2018.4.5 by Andrew Bennetts
Improvement thanks to John's review.
182
    def test_incomplete_request(self):
183
        transport = FakeTransport()
184
        wsgi_app = wsgi.SmartWSGIApp(transport)
185
        def make_request(transport, write_func):
186
            request = IncompleteRequest(transport, write_func)
187
            self.request = request
188
            return request
189
        wsgi_app.make_request = make_request
190
191
        fake_input = StringIO('incomplete request')
192
        environ = self.build_environ({
193
            'REQUEST_METHOD': 'POST',
194
            'CONTENT_LENGTH': len(fake_input.getvalue()),
195
            'wsgi.input': fake_input,
196
            'bzrlib.relpath': 'foo/bar',
197
        })
198
        iterable = wsgi_app(environ, self.start_response)
199
        response = self.read_response(iterable)
200
        self.assertEqual('200 OK', self.status)
201
        self.assertEqual('error\x01incomplete request\n', response)
202
2018.4.11 by Andrew Bennetts
Use ChrootTransportDecorator so that the WSGI server won't let you access the entire filesystem.
203
    def test_chrooting(self):
204
        # Show that requests that try to access things outside of the base
205
        # really will get intercepted by the ChrootTransportDecorator.
206
        transport = memory.MemoryTransport()
207
        transport.mkdir('foo')
208
        transport.put_bytes('foo/bar', 'this is foo/bar')
209
        wsgi_app = wsgi.SmartWSGIApp(transport.clone('foo'))
210
211
        smart_request = StringIO('mkdir\x01/bad file\x01\n0\ndone\n')
212
        environ = self.build_environ({
213
            'REQUEST_METHOD': 'POST',
214
            'CONTENT_LENGTH': len(smart_request.getvalue()),
215
            'wsgi.input': smart_request,
216
            'bzrlib.relpath': '.',
217
        })
218
        iterable = wsgi_app(environ, self.start_response)
219
        response = self.read_response(iterable)
220
        self.assertEqual('200 OK', self.status)
221
        self.assertEqual(
222
            "error\x01Path '/bad file' is not a child of "
223
            "path 'memory:///foo/'\n",
224
            response)
225
2018.4.1 by Andrew Bennetts
Add WSGI smart server.
226
227
class FakeRequest(object):
228
    
229
    def __init__(self, transport, write_func):
230
        self.transport = transport
231
        self.write_func = write_func
232
        self.accepted_bytes = ''
233
234
    def accept_bytes(self, bytes):
235
        self.accepted_bytes = bytes
236
        self.write_func('got bytes: ' + bytes)
237
238
    def next_read_size(self):
239
        return 0
240
241
242
class FakeTransport(object):
243
244
    def __init__(self):
245
        self.calls = []
2018.4.11 by Andrew Bennetts
Use ChrootTransportDecorator so that the WSGI server won't let you access the entire filesystem.
246
        self.base = 'fake:///'
247
248
    def abspath(self, relpath):
249
        return 'fake:///' + relpath
2018.4.1 by Andrew Bennetts
Add WSGI smart server.
250
251
    def clone(self, relpath):
252
        self.calls.append(('clone', relpath))
253
        return self
254
2018.4.5 by Andrew Bennetts
Improvement thanks to John's review.
255
256
class IncompleteRequest(FakeRequest):
257
    """A request-like object that always expects to read more bytes."""
258
259
    def next_read_size(self):
260
        # this request always asks for more
261
        return 1
262