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