##// END OF EJS Templates
svn: support proxy-prefix properly fixes #5521
marcink -
r3323:1fb993c1 stable
parent child Browse files
Show More
@@ -1,225 +1,226 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2018 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import base64
21 import base64
22 import logging
22 import logging
23 import urllib
23 import urllib
24 import urlparse
24 import urlparse
25
25
26 import requests
26 import requests
27 from pyramid.httpexceptions import HTTPNotAcceptable
27 from pyramid.httpexceptions import HTTPNotAcceptable
28
28
29 from rhodecode.lib import rc_cache
29 from rhodecode.lib import rc_cache
30 from rhodecode.lib.middleware import simplevcs
30 from rhodecode.lib.middleware import simplevcs
31 from rhodecode.lib.utils import is_valid_repo
31 from rhodecode.lib.utils import is_valid_repo
32 from rhodecode.lib.utils2 import str2bool, safe_int
32 from rhodecode.lib.utils2 import str2bool, safe_int
33 from rhodecode.lib.ext_json import json
33 from rhodecode.lib.ext_json import json
34 from rhodecode.lib.hooks_daemon import store_txn_id_data
34 from rhodecode.lib.hooks_daemon import store_txn_id_data
35
35
36
36
37 log = logging.getLogger(__name__)
37 log = logging.getLogger(__name__)
38
38
39
39
40 class SimpleSvnApp(object):
40 class SimpleSvnApp(object):
41 IGNORED_HEADERS = [
41 IGNORED_HEADERS = [
42 'connection', 'keep-alive', 'content-encoding',
42 'connection', 'keep-alive', 'content-encoding',
43 'transfer-encoding', 'content-length']
43 'transfer-encoding', 'content-length']
44 rc_extras = {}
44 rc_extras = {}
45
45
46 def __init__(self, config):
46 def __init__(self, config):
47 self.config = config
47 self.config = config
48
48
49 def __call__(self, environ, start_response):
49 def __call__(self, environ, start_response):
50 request_headers = self._get_request_headers(environ)
50 request_headers = self._get_request_headers(environ)
51 data = environ['wsgi.input']
51 data = environ['wsgi.input']
52 req_method = environ['REQUEST_METHOD']
52 req_method = environ['REQUEST_METHOD']
53 has_content_length = 'CONTENT_LENGTH' in environ
53 has_content_length = 'CONTENT_LENGTH' in environ
54 path_info = self._get_url(environ['PATH_INFO'])
54 path_info = self._get_url(
55 self.config.get('subversion_http_server_url', ''), environ['PATH_INFO'])
55 transfer_encoding = environ.get('HTTP_TRANSFER_ENCODING', '')
56 transfer_encoding = environ.get('HTTP_TRANSFER_ENCODING', '')
56 log.debug('Handling: %s method via `%s`', req_method, path_info)
57 log.debug('Handling: %s method via `%s`', req_method, path_info)
57
58
58 # stream control flag, based on request and content type...
59 # stream control flag, based on request and content type...
59 stream = False
60 stream = False
60
61
61 if req_method in ['MKCOL'] or has_content_length:
62 if req_method in ['MKCOL'] or has_content_length:
62 data_processed = False
63 data_processed = False
63 # read chunk to check if we have txn-with-props
64 # read chunk to check if we have txn-with-props
64 initial_data = data.read(1024)
65 initial_data = data.read(1024)
65 if initial_data.startswith('(create-txn-with-props'):
66 if initial_data.startswith('(create-txn-with-props'):
66 data = initial_data + data.read()
67 data = initial_data + data.read()
67 # store on-the-fly our rc_extra using svn revision properties
68 # store on-the-fly our rc_extra using svn revision properties
68 # those can be read later on in hooks executed so we have a way
69 # those can be read later on in hooks executed so we have a way
69 # to pass in the data into svn hooks
70 # to pass in the data into svn hooks
70 rc_data = base64.urlsafe_b64encode(json.dumps(self.rc_extras))
71 rc_data = base64.urlsafe_b64encode(json.dumps(self.rc_extras))
71 rc_data_len = len(rc_data)
72 rc_data_len = len(rc_data)
72 # header defines data length, and serialized data
73 # header defines data length, and serialized data
73 skel = ' rc-scm-extras {} {}'.format(rc_data_len, rc_data)
74 skel = ' rc-scm-extras {} {}'.format(rc_data_len, rc_data)
74 data = data[:-2] + skel + '))'
75 data = data[:-2] + skel + '))'
75 data_processed = True
76 data_processed = True
76
77
77 if not data_processed:
78 if not data_processed:
78 # NOTE(johbo): Avoid that we end up with sending the request in chunked
79 # NOTE(johbo): Avoid that we end up with sending the request in chunked
79 # transfer encoding (mainly on Gunicorn). If we know the content
80 # transfer encoding (mainly on Gunicorn). If we know the content
80 # length, then we should transfer the payload in one request.
81 # length, then we should transfer the payload in one request.
81 data = initial_data + data.read()
82 data = initial_data + data.read()
82
83
83 if req_method in ['GET', 'PUT'] or transfer_encoding == 'chunked':
84 if req_method in ['GET', 'PUT'] or transfer_encoding == 'chunked':
84 # NOTE(marcink): when getting/uploading files we want to STREAM content
85 # NOTE(marcink): when getting/uploading files we want to STREAM content
85 # back to the client/proxy instead of buffering it here...
86 # back to the client/proxy instead of buffering it here...
86 stream = True
87 stream = True
87
88
88 stream = stream
89 stream = stream
89 log.debug(
90 log.debug(
90 'Calling SVN PROXY: method:%s via `%s`, Stream: %s',
91 'Calling SVN PROXY: method:%s via `%s`, Stream: %s',
91 req_method, path_info, stream)
92 req_method, path_info, stream)
92 response = requests.request(
93 response = requests.request(
93 req_method, path_info,
94 req_method, path_info,
94 data=data, headers=request_headers, stream=stream)
95 data=data, headers=request_headers, stream=stream)
95
96
96 if response.status_code not in [200, 401]:
97 if response.status_code not in [200, 401]:
97 if response.status_code >= 500:
98 if response.status_code >= 500:
98 log.error('Got SVN response:%s with text:\n`%s`',
99 log.error('Got SVN response:%s with text:\n`%s`',
99 response, response.text)
100 response, response.text)
100 else:
101 else:
101 log.debug('Got SVN response:%s with text:\n`%s`',
102 log.debug('Got SVN response:%s with text:\n`%s`',
102 response, response.text)
103 response, response.text)
103 else:
104 else:
104 log.debug('got response code: %s', response.status_code)
105 log.debug('got response code: %s', response.status_code)
105
106
106 response_headers = self._get_response_headers(response.headers)
107 response_headers = self._get_response_headers(response.headers)
107
108
108 if response.headers.get('SVN-Txn-name'):
109 if response.headers.get('SVN-Txn-name'):
109 svn_tx_id = response.headers.get('SVN-Txn-name')
110 svn_tx_id = response.headers.get('SVN-Txn-name')
110 txn_id = rc_cache.utils.compute_key_from_params(
111 txn_id = rc_cache.utils.compute_key_from_params(
111 self.config['repository'], svn_tx_id)
112 self.config['repository'], svn_tx_id)
112 port = safe_int(self.rc_extras['hooks_uri'].split(':')[-1])
113 port = safe_int(self.rc_extras['hooks_uri'].split(':')[-1])
113 store_txn_id_data(txn_id, {'port': port})
114 store_txn_id_data(txn_id, {'port': port})
114
115
115 start_response(
116 start_response(
116 '{} {}'.format(response.status_code, response.reason),
117 '{} {}'.format(response.status_code, response.reason),
117 response_headers)
118 response_headers)
118 return response.iter_content(chunk_size=1024)
119 return response.iter_content(chunk_size=1024)
119
120
120 def _get_url(self, path):
121 def _get_url(self, svn_http_server, path):
121 url_path = urlparse.urljoin(
122 svn_http_server_url = (svn_http_server or '').rstrip('/')
122 self.config.get('subversion_http_server_url', ''), path)
123 url_path = urlparse.urljoin(svn_http_server_url + '/', (path or '').lstrip('/'))
123 url_path = urllib.quote(url_path, safe="/:=~+!$,;'")
124 url_path = urllib.quote(url_path, safe="/:=~+!$,;'")
124 return url_path
125 return url_path
125
126
126 def _get_request_headers(self, environ):
127 def _get_request_headers(self, environ):
127 headers = {}
128 headers = {}
128
129
129 for key in environ:
130 for key in environ:
130 if not key.startswith('HTTP_'):
131 if not key.startswith('HTTP_'):
131 continue
132 continue
132 new_key = key.split('_')
133 new_key = key.split('_')
133 new_key = [k.capitalize() for k in new_key[1:]]
134 new_key = [k.capitalize() for k in new_key[1:]]
134 new_key = '-'.join(new_key)
135 new_key = '-'.join(new_key)
135 headers[new_key] = environ[key]
136 headers[new_key] = environ[key]
136
137
137 if 'CONTENT_TYPE' in environ:
138 if 'CONTENT_TYPE' in environ:
138 headers['Content-Type'] = environ['CONTENT_TYPE']
139 headers['Content-Type'] = environ['CONTENT_TYPE']
139
140
140 if 'CONTENT_LENGTH' in environ:
141 if 'CONTENT_LENGTH' in environ:
141 headers['Content-Length'] = environ['CONTENT_LENGTH']
142 headers['Content-Length'] = environ['CONTENT_LENGTH']
142
143
143 return headers
144 return headers
144
145
145 def _get_response_headers(self, headers):
146 def _get_response_headers(self, headers):
146 headers = [
147 headers = [
147 (h, headers[h])
148 (h, headers[h])
148 for h in headers
149 for h in headers
149 if h.lower() not in self.IGNORED_HEADERS
150 if h.lower() not in self.IGNORED_HEADERS
150 ]
151 ]
151
152
152 return headers
153 return headers
153
154
154
155
155 class DisabledSimpleSvnApp(object):
156 class DisabledSimpleSvnApp(object):
156 def __init__(self, config):
157 def __init__(self, config):
157 self.config = config
158 self.config = config
158
159
159 def __call__(self, environ, start_response):
160 def __call__(self, environ, start_response):
160 reason = 'Cannot handle SVN call because: SVN HTTP Proxy is not enabled'
161 reason = 'Cannot handle SVN call because: SVN HTTP Proxy is not enabled'
161 log.warning(reason)
162 log.warning(reason)
162 return HTTPNotAcceptable(reason)(environ, start_response)
163 return HTTPNotAcceptable(reason)(environ, start_response)
163
164
164
165
165 class SimpleSvn(simplevcs.SimpleVCS):
166 class SimpleSvn(simplevcs.SimpleVCS):
166
167
167 SCM = 'svn'
168 SCM = 'svn'
168 READ_ONLY_COMMANDS = ('OPTIONS', 'PROPFIND', 'GET', 'REPORT')
169 READ_ONLY_COMMANDS = ('OPTIONS', 'PROPFIND', 'GET', 'REPORT')
169 DEFAULT_HTTP_SERVER = 'http://localhost:8090'
170 DEFAULT_HTTP_SERVER = 'http://localhost:8090'
170
171
171 def _get_repository_name(self, environ):
172 def _get_repository_name(self, environ):
172 """
173 """
173 Gets repository name out of PATH_INFO header
174 Gets repository name out of PATH_INFO header
174
175
175 :param environ: environ where PATH_INFO is stored
176 :param environ: environ where PATH_INFO is stored
176 """
177 """
177 path = environ['PATH_INFO'].split('!')
178 path = environ['PATH_INFO'].split('!')
178 repo_name = path[0].strip('/')
179 repo_name = path[0].strip('/')
179
180
180 # SVN includes the whole path in it's requests, including
181 # SVN includes the whole path in it's requests, including
181 # subdirectories inside the repo. Therefore we have to search for
182 # subdirectories inside the repo. Therefore we have to search for
182 # the repo root directory.
183 # the repo root directory.
183 if not is_valid_repo(
184 if not is_valid_repo(
184 repo_name, self.base_path, explicit_scm=self.SCM):
185 repo_name, self.base_path, explicit_scm=self.SCM):
185 current_path = ''
186 current_path = ''
186 for component in repo_name.split('/'):
187 for component in repo_name.split('/'):
187 current_path += component
188 current_path += component
188 if is_valid_repo(
189 if is_valid_repo(
189 current_path, self.base_path, explicit_scm=self.SCM):
190 current_path, self.base_path, explicit_scm=self.SCM):
190 return current_path
191 return current_path
191 current_path += '/'
192 current_path += '/'
192
193
193 return repo_name
194 return repo_name
194
195
195 def _get_action(self, environ):
196 def _get_action(self, environ):
196 return (
197 return (
197 'pull'
198 'pull'
198 if environ['REQUEST_METHOD'] in self.READ_ONLY_COMMANDS
199 if environ['REQUEST_METHOD'] in self.READ_ONLY_COMMANDS
199 else 'push')
200 else 'push')
200
201
201 def _should_use_callback_daemon(self, extras, environ, action):
202 def _should_use_callback_daemon(self, extras, environ, action):
202 # only MERGE command triggers hooks, so we don't want to start
203 # only MERGE command triggers hooks, so we don't want to start
203 # hooks server too many times. POST however starts the svn transaction
204 # hooks server too many times. POST however starts the svn transaction
204 # so we also need to run the init of callback daemon of POST
205 # so we also need to run the init of callback daemon of POST
205 if environ['REQUEST_METHOD'] in ['MERGE', 'POST']:
206 if environ['REQUEST_METHOD'] in ['MERGE', 'POST']:
206 return True
207 return True
207 return False
208 return False
208
209
209 def _create_wsgi_app(self, repo_path, repo_name, config):
210 def _create_wsgi_app(self, repo_path, repo_name, config):
210 if self._is_svn_enabled():
211 if self._is_svn_enabled():
211 return SimpleSvnApp(config)
212 return SimpleSvnApp(config)
212 # we don't have http proxy enabled return dummy request handler
213 # we don't have http proxy enabled return dummy request handler
213 return DisabledSimpleSvnApp(config)
214 return DisabledSimpleSvnApp(config)
214
215
215 def _is_svn_enabled(self):
216 def _is_svn_enabled(self):
216 conf = self.repo_vcs_config
217 conf = self.repo_vcs_config
217 return str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
218 return str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
218
219
219 def _create_config(self, extras, repo_name):
220 def _create_config(self, extras, repo_name):
220 conf = self.repo_vcs_config
221 conf = self.repo_vcs_config
221 server_url = conf.get('vcs_svn_proxy', 'http_server_url')
222 server_url = conf.get('vcs_svn_proxy', 'http_server_url')
222 server_url = server_url or self.DEFAULT_HTTP_SERVER
223 server_url = server_url or self.DEFAULT_HTTP_SERVER
223
224
224 extras['subversion_http_server_url'] = server_url
225 extras['subversion_http_server_url'] = server_url
225 return extras
226 return extras
@@ -1,201 +1,209 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2018 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 from StringIO import StringIO
21 from StringIO import StringIO
22
22
23 import pytest
23 import pytest
24 from mock import patch, Mock
24 from mock import patch, Mock
25
25
26 from rhodecode.lib.middleware.simplesvn import SimpleSvn, SimpleSvnApp
26 from rhodecode.lib.middleware.simplesvn import SimpleSvn, SimpleSvnApp
27 from rhodecode.lib.utils import get_rhodecode_base_path
27 from rhodecode.lib.utils import get_rhodecode_base_path
28
28
29
29
30 class TestSimpleSvn(object):
30 class TestSimpleSvn(object):
31 @pytest.fixture(autouse=True)
31 @pytest.fixture(autouse=True)
32 def simple_svn(self, baseapp, request_stub):
32 def simple_svn(self, baseapp, request_stub):
33 base_path = get_rhodecode_base_path()
33 base_path = get_rhodecode_base_path()
34 self.app = SimpleSvn(
34 self.app = SimpleSvn(
35 config={'auth_ret_code': '', 'base_path': base_path},
35 config={'auth_ret_code': '', 'base_path': base_path},
36 registry=request_stub.registry)
36 registry=request_stub.registry)
37
37
38 def test_get_config(self):
38 def test_get_config(self):
39 extras = {'foo': 'FOO', 'bar': 'BAR'}
39 extras = {'foo': 'FOO', 'bar': 'BAR'}
40 config = self.app._create_config(extras, repo_name='test-repo')
40 config = self.app._create_config(extras, repo_name='test-repo')
41 assert config == extras
41 assert config == extras
42
42
43 @pytest.mark.parametrize(
43 @pytest.mark.parametrize(
44 'method', ['OPTIONS', 'PROPFIND', 'GET', 'REPORT'])
44 'method', ['OPTIONS', 'PROPFIND', 'GET', 'REPORT'])
45 def test_get_action_returns_pull(self, method):
45 def test_get_action_returns_pull(self, method):
46 environment = {'REQUEST_METHOD': method}
46 environment = {'REQUEST_METHOD': method}
47 action = self.app._get_action(environment)
47 action = self.app._get_action(environment)
48 assert action == 'pull'
48 assert action == 'pull'
49
49
50 @pytest.mark.parametrize(
50 @pytest.mark.parametrize(
51 'method', [
51 'method', [
52 'MKACTIVITY', 'PROPPATCH', 'PUT', 'CHECKOUT', 'MKCOL', 'MOVE',
52 'MKACTIVITY', 'PROPPATCH', 'PUT', 'CHECKOUT', 'MKCOL', 'MOVE',
53 'COPY', 'DELETE', 'LOCK', 'UNLOCK', 'MERGE'
53 'COPY', 'DELETE', 'LOCK', 'UNLOCK', 'MERGE'
54 ])
54 ])
55 def test_get_action_returns_push(self, method):
55 def test_get_action_returns_push(self, method):
56 environment = {'REQUEST_METHOD': method}
56 environment = {'REQUEST_METHOD': method}
57 action = self.app._get_action(environment)
57 action = self.app._get_action(environment)
58 assert action == 'push'
58 assert action == 'push'
59
59
60 @pytest.mark.parametrize(
60 @pytest.mark.parametrize(
61 'path, expected_name', [
61 'path, expected_name', [
62 ('/hello-svn', 'hello-svn'),
62 ('/hello-svn', 'hello-svn'),
63 ('/hello-svn/', 'hello-svn'),
63 ('/hello-svn/', 'hello-svn'),
64 ('/group/hello-svn/', 'group/hello-svn'),
64 ('/group/hello-svn/', 'group/hello-svn'),
65 ('/group/hello-svn/!svn/vcc/default', 'group/hello-svn'),
65 ('/group/hello-svn/!svn/vcc/default', 'group/hello-svn'),
66 ])
66 ])
67 def test_get_repository_name(self, path, expected_name):
67 def test_get_repository_name(self, path, expected_name):
68 environment = {'PATH_INFO': path}
68 environment = {'PATH_INFO': path}
69 name = self.app._get_repository_name(environment)
69 name = self.app._get_repository_name(environment)
70 assert name == expected_name
70 assert name == expected_name
71
71
72 def test_get_repository_name_subfolder(self, backend_svn):
72 def test_get_repository_name_subfolder(self, backend_svn):
73 repo = backend_svn.repo
73 repo = backend_svn.repo
74 environment = {
74 environment = {
75 'PATH_INFO': '/{}/path/with/subfolders'.format(repo.repo_name)}
75 'PATH_INFO': '/{}/path/with/subfolders'.format(repo.repo_name)}
76 name = self.app._get_repository_name(environment)
76 name = self.app._get_repository_name(environment)
77 assert name == repo.repo_name
77 assert name == repo.repo_name
78
78
79 def test_create_wsgi_app(self):
79 def test_create_wsgi_app(self):
80 with patch.object(SimpleSvn, '_is_svn_enabled') as mock_method:
80 with patch.object(SimpleSvn, '_is_svn_enabled') as mock_method:
81 mock_method.return_value = False
81 mock_method.return_value = False
82 with patch('rhodecode.lib.middleware.simplesvn.DisabledSimpleSvnApp') as (
82 with patch('rhodecode.lib.middleware.simplesvn.DisabledSimpleSvnApp') as (
83 wsgi_app_mock):
83 wsgi_app_mock):
84 config = Mock()
84 config = Mock()
85 wsgi_app = self.app._create_wsgi_app(
85 wsgi_app = self.app._create_wsgi_app(
86 repo_path='', repo_name='', config=config)
86 repo_path='', repo_name='', config=config)
87
87
88 wsgi_app_mock.assert_called_once_with(config)
88 wsgi_app_mock.assert_called_once_with(config)
89 assert wsgi_app == wsgi_app_mock()
89 assert wsgi_app == wsgi_app_mock()
90
90
91 def test_create_wsgi_app_when_enabled(self):
91 def test_create_wsgi_app_when_enabled(self):
92 with patch.object(SimpleSvn, '_is_svn_enabled') as mock_method:
92 with patch.object(SimpleSvn, '_is_svn_enabled') as mock_method:
93 mock_method.return_value = True
93 mock_method.return_value = True
94 with patch('rhodecode.lib.middleware.simplesvn.SimpleSvnApp') as (
94 with patch('rhodecode.lib.middleware.simplesvn.SimpleSvnApp') as (
95 wsgi_app_mock):
95 wsgi_app_mock):
96 config = Mock()
96 config = Mock()
97 wsgi_app = self.app._create_wsgi_app(
97 wsgi_app = self.app._create_wsgi_app(
98 repo_path='', repo_name='', config=config)
98 repo_path='', repo_name='', config=config)
99
99
100 wsgi_app_mock.assert_called_once_with(config)
100 wsgi_app_mock.assert_called_once_with(config)
101 assert wsgi_app == wsgi_app_mock()
101 assert wsgi_app == wsgi_app_mock()
102
102
103
103
104 class TestSimpleSvnApp(object):
104 class TestSimpleSvnApp(object):
105 data = '<xml></xml>'
105 data = '<xml></xml>'
106 path = '/group/my-repo'
106 path = '/group/my-repo'
107 wsgi_input = StringIO(data)
107 wsgi_input = StringIO(data)
108 environment = {
108 environment = {
109 'HTTP_DAV': (
109 'HTTP_DAV': (
110 'http://subversion.tigris.org/xmlns/dav/svn/depth,'
110 'http://subversion.tigris.org/xmlns/dav/svn/depth,'
111 ' http://subversion.tigris.org/xmlns/dav/svn/mergeinfo'),
111 ' http://subversion.tigris.org/xmlns/dav/svn/mergeinfo'),
112 'HTTP_USER_AGENT': 'SVN/1.8.11 (x86_64-linux) serf/1.3.8',
112 'HTTP_USER_AGENT': 'SVN/1.8.11 (x86_64-linux) serf/1.3.8',
113 'REQUEST_METHOD': 'OPTIONS',
113 'REQUEST_METHOD': 'OPTIONS',
114 'PATH_INFO': path,
114 'PATH_INFO': path,
115 'wsgi.input': wsgi_input,
115 'wsgi.input': wsgi_input,
116 'CONTENT_TYPE': 'text/xml',
116 'CONTENT_TYPE': 'text/xml',
117 'CONTENT_LENGTH': '130'
117 'CONTENT_LENGTH': '130'
118 }
118 }
119
119
120 def setup_method(self, method):
120 def setup_method(self, method):
121 self.host = 'http://localhost/'
121 self.host = 'http://localhost/'
122 base_path = get_rhodecode_base_path()
122 base_path = get_rhodecode_base_path()
123 self.app = SimpleSvnApp(
123 self.app = SimpleSvnApp(
124 config={'subversion_http_server_url': self.host,
124 config={'subversion_http_server_url': self.host,
125 'base_path': base_path})
125 'base_path': base_path})
126
126
127 def test_get_request_headers_with_content_type(self):
127 def test_get_request_headers_with_content_type(self):
128 expected_headers = {
128 expected_headers = {
129 'Dav': self.environment['HTTP_DAV'],
129 'Dav': self.environment['HTTP_DAV'],
130 'User-Agent': self.environment['HTTP_USER_AGENT'],
130 'User-Agent': self.environment['HTTP_USER_AGENT'],
131 'Content-Type': self.environment['CONTENT_TYPE'],
131 'Content-Type': self.environment['CONTENT_TYPE'],
132 'Content-Length': self.environment['CONTENT_LENGTH']
132 'Content-Length': self.environment['CONTENT_LENGTH']
133 }
133 }
134 headers = self.app._get_request_headers(self.environment)
134 headers = self.app._get_request_headers(self.environment)
135 assert headers == expected_headers
135 assert headers == expected_headers
136
136
137 def test_get_request_headers_without_content_type(self):
137 def test_get_request_headers_without_content_type(self):
138 environment = self.environment.copy()
138 environment = self.environment.copy()
139 environment.pop('CONTENT_TYPE')
139 environment.pop('CONTENT_TYPE')
140 expected_headers = {
140 expected_headers = {
141 'Dav': environment['HTTP_DAV'],
141 'Dav': environment['HTTP_DAV'],
142 'Content-Length': self.environment['CONTENT_LENGTH'],
142 'Content-Length': self.environment['CONTENT_LENGTH'],
143 'User-Agent': environment['HTTP_USER_AGENT'],
143 'User-Agent': environment['HTTP_USER_AGENT'],
144 }
144 }
145 request_headers = self.app._get_request_headers(environment)
145 request_headers = self.app._get_request_headers(environment)
146 assert request_headers == expected_headers
146 assert request_headers == expected_headers
147
147
148 def test_get_response_headers(self):
148 def test_get_response_headers(self):
149 headers = {
149 headers = {
150 'Connection': 'keep-alive',
150 'Connection': 'keep-alive',
151 'Keep-Alive': 'timeout=5, max=100',
151 'Keep-Alive': 'timeout=5, max=100',
152 'Transfer-Encoding': 'chunked',
152 'Transfer-Encoding': 'chunked',
153 'Content-Encoding': 'gzip',
153 'Content-Encoding': 'gzip',
154 'MS-Author-Via': 'DAV',
154 'MS-Author-Via': 'DAV',
155 'SVN-Supported-Posts': 'create-txn-with-props'
155 'SVN-Supported-Posts': 'create-txn-with-props'
156 }
156 }
157 expected_headers = [
157 expected_headers = [
158 ('MS-Author-Via', 'DAV'),
158 ('MS-Author-Via', 'DAV'),
159 ('SVN-Supported-Posts', 'create-txn-with-props'),
159 ('SVN-Supported-Posts', 'create-txn-with-props'),
160 ]
160 ]
161 response_headers = self.app._get_response_headers(headers)
161 response_headers = self.app._get_response_headers(headers)
162 assert sorted(response_headers) == sorted(expected_headers)
162 assert sorted(response_headers) == sorted(expected_headers)
163
163
164 def test_get_url(self):
164 @pytest.mark.parametrize('svn_http_url, path_info, expected_url', [
165 url = self.app._get_url(self.path)
165 ('http://localhost:8200', '/repo_name', 'http://localhost:8200/repo_name'),
166 expected_url = '{}{}'.format(self.host.strip('/'), self.path)
166 ('http://localhost:8200///', '/repo_name', 'http://localhost:8200/repo_name'),
167 ('http://localhost:8200', '/group/repo_name', 'http://localhost:8200/group/repo_name'),
168 ('http://localhost:8200/', '/group/repo_name', 'http://localhost:8200/group/repo_name'),
169 ('http://localhost:8200/prefix', '/repo_name', 'http://localhost:8200/prefix/repo_name'),
170 ('http://localhost:8200/prefix', 'repo_name', 'http://localhost:8200/prefix/repo_name'),
171 ('http://localhost:8200/prefix', '/group/repo_name', 'http://localhost:8200/prefix/group/repo_name')
172 ])
173 def test_get_url(self, svn_http_url, path_info, expected_url):
174 url = self.app._get_url(svn_http_url, path_info)
167 assert url == expected_url
175 assert url == expected_url
168
176
169 def test_call(self):
177 def test_call(self):
170 start_response = Mock()
178 start_response = Mock()
171 response_mock = Mock()
179 response_mock = Mock()
172 response_mock.headers = {
180 response_mock.headers = {
173 'Content-Encoding': 'gzip',
181 'Content-Encoding': 'gzip',
174 'MS-Author-Via': 'DAV',
182 'MS-Author-Via': 'DAV',
175 'SVN-Supported-Posts': 'create-txn-with-props'
183 'SVN-Supported-Posts': 'create-txn-with-props'
176 }
184 }
177 response_mock.status_code = 200
185 response_mock.status_code = 200
178 response_mock.reason = 'OK'
186 response_mock.reason = 'OK'
179 with patch('rhodecode.lib.middleware.simplesvn.requests.request') as (
187 with patch('rhodecode.lib.middleware.simplesvn.requests.request') as (
180 request_mock):
188 request_mock):
181 request_mock.return_value = response_mock
189 request_mock.return_value = response_mock
182 self.app(self.environment, start_response)
190 self.app(self.environment, start_response)
183
191
184 expected_url = '{}{}'.format(self.host.strip('/'), self.path)
192 expected_url = '{}{}'.format(self.host.strip('/'), self.path)
185 expected_request_headers = {
193 expected_request_headers = {
186 'Dav': self.environment['HTTP_DAV'],
194 'Dav': self.environment['HTTP_DAV'],
187 'User-Agent': self.environment['HTTP_USER_AGENT'],
195 'User-Agent': self.environment['HTTP_USER_AGENT'],
188 'Content-Type': self.environment['CONTENT_TYPE'],
196 'Content-Type': self.environment['CONTENT_TYPE'],
189 'Content-Length': self.environment['CONTENT_LENGTH']
197 'Content-Length': self.environment['CONTENT_LENGTH']
190 }
198 }
191 expected_response_headers = [
199 expected_response_headers = [
192 ('SVN-Supported-Posts', 'create-txn-with-props'),
200 ('SVN-Supported-Posts', 'create-txn-with-props'),
193 ('MS-Author-Via', 'DAV'),
201 ('MS-Author-Via', 'DAV'),
194 ]
202 ]
195 request_mock.assert_called_once_with(
203 request_mock.assert_called_once_with(
196 self.environment['REQUEST_METHOD'], expected_url,
204 self.environment['REQUEST_METHOD'], expected_url,
197 data=self.data, headers=expected_request_headers, stream=False)
205 data=self.data, headers=expected_request_headers, stream=False)
198 response_mock.iter_content.assert_called_once_with(chunk_size=1024)
206 response_mock.iter_content.assert_called_once_with(chunk_size=1024)
199 args, _ = start_response.call_args
207 args, _ = start_response.call_args
200 assert args[0] == '200 OK'
208 assert args[0] == '200 OK'
201 assert sorted(args[1]) == sorted(expected_response_headers)
209 assert sorted(args[1]) == sorted(expected_response_headers)
General Comments 0
You need to be logged in to leave comments. Login now