/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar
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(
2018.5.104 by Andrew Bennetts
Completely rework chrooted transports.
74
            app.backing_transport, chroot.ChrootTransport)
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):
2018.5.104 by Andrew Bennetts
Completely rework chrooted transports.
86
        # XXX XXX XXX update comment
2018.4.1 by Andrew Bennetts
Add WSGI smart server.
87
        # 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
88
        # WSGI environ to clone from its backing transport to get a specific
89
        # transport for this request.
2018.4.1 by Andrew Bennetts
Add WSGI smart server.
90
        transport = FakeTransport()
91
        wsgi_app = wsgi.SmartWSGIApp(transport)
2018.5.104 by Andrew Bennetts
Completely rework chrooted transports.
92
        wsgi_app.backing_transport = transport
2018.4.1 by Andrew Bennetts
Add WSGI smart server.
93
        def make_request(transport, write_func):
94
            request = FakeRequest(transport, write_func)
95
            self.request = request
96
            return request
97
        wsgi_app.make_request = make_request
98
        fake_input = StringIO('fake request')
2018.4.5 by Andrew Bennetts
Improvement thanks to John's review.
99
        environ = self.build_environ({
2018.4.1 by Andrew Bennetts
Add WSGI smart server.
100
            'REQUEST_METHOD': 'POST',
101
            'CONTENT_LENGTH': len(fake_input.getvalue()),
102
            'wsgi.input': fake_input,
103
            'bzrlib.relpath': 'foo/bar',
104
        })
105
        iterable = wsgi_app(environ, self.start_response)
106
        response = self.read_response(iterable)
107
        self.assertEqual([('clone', 'foo/bar')] , transport.calls)
108
109
    def test_smart_wsgi_app_request_and_response(self):
110
        # SmartWSGIApp reads the smart request from the 'wsgi.input' file-like
111
        # object in the environ dict, and returns the response via the iterable
112
        # returned to the WSGI handler.
113
        transport = memory.MemoryTransport()
114
        transport.put_bytes('foo', 'some bytes')
115
        wsgi_app = wsgi.SmartWSGIApp(transport)
116
        def make_request(transport, write_func):
117
            request = FakeRequest(transport, write_func)
118
            self.request = request
119
            return request
120
        wsgi_app.make_request = make_request
121
        fake_input = StringIO('fake request')
2018.4.5 by Andrew Bennetts
Improvement thanks to John's review.
122
        environ = self.build_environ({
2018.4.1 by Andrew Bennetts
Add WSGI smart server.
123
            'REQUEST_METHOD': 'POST',
124
            'CONTENT_LENGTH': len(fake_input.getvalue()),
125
            'wsgi.input': fake_input,
126
            'bzrlib.relpath': 'foo',
127
        })
128
        iterable = wsgi_app(environ, self.start_response)
129
        response = self.read_response(iterable)
130
        self.assertEqual('200 OK', self.status)
131
        self.assertEqual('got bytes: fake request', response)
132
133
    def test_relpath_setter(self):
134
        # wsgi.RelpathSetter is WSGI "middleware" to set the 'bzrlib.relpath'
135
        # variable.
136
        calls = []
137
        def fake_app(environ, start_response):
138
            calls.append(environ['bzrlib.relpath'])
139
        wrapped_app = wsgi.RelpathSetter(
140
            fake_app, prefix='/abc/', path_var='FOO')
141
        wrapped_app({'FOO': '/abc/xyz/.bzr/smart'}, None)
142
        self.assertEqual(['xyz'], calls)
143
       
2018.4.5 by Andrew Bennetts
Improvement thanks to John's review.
144
    def test_relpath_setter_bad_path_prefix(self):
2018.4.1 by Andrew Bennetts
Add WSGI smart server.
145
        # wsgi.RelpathSetter will reject paths with that don't match the prefix
2018.4.5 by Andrew Bennetts
Improvement thanks to John's review.
146
        # with a 404.  This is probably a sign of misconfiguration; a server
147
        # shouldn't ever be invoking our WSGI application with bad paths.
2018.4.1 by Andrew Bennetts
Add WSGI smart server.
148
        def fake_app(environ, start_response):
149
            self.fail('The app should never be called when the path is wrong')
150
        wrapped_app = wsgi.RelpathSetter(
151
            fake_app, prefix='/abc/', path_var='FOO')
152
        iterable = wrapped_app(
153
            {'FOO': 'AAA/abc/xyz/.bzr/smart'}, self.start_response)
154
        self.read_response(iterable)
155
        self.assertTrue(self.status.startswith('404'))
156
        
2018.4.5 by Andrew Bennetts
Improvement thanks to John's review.
157
    def test_relpath_setter_bad_path_suffix(self):
158
        # Similar to test_relpath_setter_bad_path_prefix: wsgi.RelpathSetter
159
        # will reject paths with that don't match the suffix '.bzr/smart' with a
160
        # 404 as well.  Again, this shouldn't be seen by our WSGI application if
161
        # the server is configured correctly.
162
        def fake_app(environ, start_response):
163
            self.fail('The app should never be called when the path is wrong')
164
        wrapped_app = wsgi.RelpathSetter(
165
            fake_app, prefix='/abc/', path_var='FOO')
166
        iterable = wrapped_app(
167
            {'FOO': '/abc/xyz/.bzr/AAA'}, self.start_response)
168
        self.read_response(iterable)
169
        self.assertTrue(self.status.startswith('404'))
170
        
2018.4.1 by Andrew Bennetts
Add WSGI smart server.
171
    def test_make_app(self):
172
        # The make_app helper constructs a SmartWSGIApp wrapped in a
173
        # RelpathSetter.
174
        app = wsgi.make_app(
175
            root='a root',
176
            prefix='a prefix',
177
            path_var='a path_var')
178
        self.assertIsInstance(app, wsgi.RelpathSetter)
179
        self.assertIsInstance(app.app, wsgi.SmartWSGIApp)
2018.5.104 by Andrew Bennetts
Completely rework chrooted transports.
180
        self.assertStartsWith(app.app.backing_transport.base, 'chroot-')
181
        backing_transport = app.app.backing_transport
182
        chroot_backing_transport = backing_transport.server.backing_transport
183
        self.assertEndsWith(chroot_backing_transport.base, 'a%20root/')
2018.4.1 by Andrew Bennetts
Add WSGI smart server.
184
        self.assertEqual(app.prefix, 'a prefix')
185
        self.assertEqual(app.path_var, 'a path_var')
186
2018.4.5 by Andrew Bennetts
Improvement thanks to John's review.
187
    def test_incomplete_request(self):
188
        transport = FakeTransport()
189
        wsgi_app = wsgi.SmartWSGIApp(transport)
190
        def make_request(transport, write_func):
191
            request = IncompleteRequest(transport, write_func)
192
            self.request = request
193
            return request
194
        wsgi_app.make_request = make_request
195
196
        fake_input = StringIO('incomplete request')
197
        environ = self.build_environ({
198
            'REQUEST_METHOD': 'POST',
199
            'CONTENT_LENGTH': len(fake_input.getvalue()),
200
            'wsgi.input': fake_input,
201
            'bzrlib.relpath': 'foo/bar',
202
        })
203
        iterable = wsgi_app(environ, self.start_response)
204
        response = self.read_response(iterable)
205
        self.assertEqual('200 OK', self.status)
206
        self.assertEqual('error\x01incomplete request\n', response)
207
2018.4.1 by Andrew Bennetts
Add WSGI smart server.
208
209
class FakeRequest(object):
210
    
211
    def __init__(self, transport, write_func):
212
        self.transport = transport
213
        self.write_func = write_func
214
        self.accepted_bytes = ''
215
216
    def accept_bytes(self, bytes):
217
        self.accepted_bytes = bytes
218
        self.write_func('got bytes: ' + bytes)
219
220
    def next_read_size(self):
221
        return 0
222
223
224
class FakeTransport(object):
225
226
    def __init__(self):
227
        self.calls = []
2018.4.11 by Andrew Bennetts
Use ChrootTransportDecorator so that the WSGI server won't let you access the entire filesystem.
228
        self.base = 'fake:///'
229
230
    def abspath(self, relpath):
231
        return 'fake:///' + relpath
2018.4.1 by Andrew Bennetts
Add WSGI smart server.
232
233
    def clone(self, relpath):
234
        self.calls.append(('clone', relpath))
235
        return self
236
2018.4.5 by Andrew Bennetts
Improvement thanks to John's review.
237
238
class IncompleteRequest(FakeRequest):
239
    """A request-like object that always expects to read more bytes."""
240
241
    def next_read_size(self):
242
        # this request always asks for more
243
        return 1
244