##// END OF EJS Templates
svn: explicitly specify tunnel-user to properly map rhodecode username on svn commit via SSH backend...
dan -
r4286:c8bb3cfc default
parent child Browse files
Show More
@@ -1,254 +1,257 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2019 RhodeCode GmbH
3 # Copyright (C) 2016-2019 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 os
21 import os
22 import re
22 import re
23 import sys
23 import sys
24 import logging
24 import logging
25 import signal
25 import signal
26 import tempfile
26 import tempfile
27 from subprocess import Popen, PIPE
27 from subprocess import Popen, PIPE
28 import urlparse
28 import urlparse
29
29
30 from .base import VcsServer
30 from .base import VcsServer
31
31
32 log = logging.getLogger(__name__)
32 log = logging.getLogger(__name__)
33
33
34
34
35 class SubversionTunnelWrapper(object):
35 class SubversionTunnelWrapper(object):
36 process = None
36 process = None
37
37
38 def __init__(self, server):
38 def __init__(self, server):
39 self.server = server
39 self.server = server
40 self.timeout = 30
40 self.timeout = 30
41 self.stdin = sys.stdin
41 self.stdin = sys.stdin
42 self.stdout = sys.stdout
42 self.stdout = sys.stdout
43 self.svn_conf_fd, self.svn_conf_path = tempfile.mkstemp()
43 self.svn_conf_fd, self.svn_conf_path = tempfile.mkstemp()
44 self.hooks_env_fd, self.hooks_env_path = tempfile.mkstemp()
44 self.hooks_env_fd, self.hooks_env_path = tempfile.mkstemp()
45
45
46 self.read_only = True # flag that we set to make the hooks readonly
46 self.read_only = True # flag that we set to make the hooks readonly
47
47
48 def create_svn_config(self):
48 def create_svn_config(self):
49 content = (
49 content = (
50 '[general]\n'
50 '[general]\n'
51 'hooks-env = {}\n').format(self.hooks_env_path)
51 'hooks-env = {}\n').format(self.hooks_env_path)
52 with os.fdopen(self.svn_conf_fd, 'w') as config_file:
52 with os.fdopen(self.svn_conf_fd, 'w') as config_file:
53 config_file.write(content)
53 config_file.write(content)
54
54
55 def create_hooks_env(self):
55 def create_hooks_env(self):
56 content = (
56 content = (
57 '[default]\n'
57 '[default]\n'
58 'LANG = en_US.UTF-8\n')
58 'LANG = en_US.UTF-8\n')
59 if self.read_only:
59 if self.read_only:
60 content += 'SSH_READ_ONLY = 1\n'
60 content += 'SSH_READ_ONLY = 1\n'
61 with os.fdopen(self.hooks_env_fd, 'w') as hooks_env_file:
61 with os.fdopen(self.hooks_env_fd, 'w') as hooks_env_file:
62 hooks_env_file.write(content)
62 hooks_env_file.write(content)
63
63
64 def remove_configs(self):
64 def remove_configs(self):
65 os.remove(self.svn_conf_path)
65 os.remove(self.svn_conf_path)
66 os.remove(self.hooks_env_path)
66 os.remove(self.hooks_env_path)
67
67
68 def command(self):
68 def command(self):
69 root = self.server.get_root_store()
69 root = self.server.get_root_store()
70 username = self.server.user.username
71
70 command = [
72 command = [
71 self.server.svn_path, '-t',
73 self.server.svn_path, '-t',
72 '--config-file', self.svn_conf_path,
74 '--config-file', self.svn_conf_path,
75 '--tunnel-user', username,
73 '-r', root]
76 '-r', root]
74 log.debug("Final CMD: %s", ' '.join(command))
77 log.debug("Final CMD: %s", ' '.join(command))
75 return command
78 return command
76
79
77 def start(self):
80 def start(self):
78 command = self.command()
81 command = self.command()
79 self.process = Popen(' '.join(command), stdin=PIPE, shell=True)
82 self.process = Popen(' '.join(command), stdin=PIPE, shell=True)
80
83
81 def sync(self):
84 def sync(self):
82 while self.process.poll() is None:
85 while self.process.poll() is None:
83 next_byte = self.stdin.read(1)
86 next_byte = self.stdin.read(1)
84 if not next_byte:
87 if not next_byte:
85 break
88 break
86 self.process.stdin.write(next_byte)
89 self.process.stdin.write(next_byte)
87 self.remove_configs()
90 self.remove_configs()
88
91
89 @property
92 @property
90 def return_code(self):
93 def return_code(self):
91 return self.process.returncode
94 return self.process.returncode
92
95
93 def get_first_client_response(self):
96 def get_first_client_response(self):
94 signal.signal(signal.SIGALRM, self.interrupt)
97 signal.signal(signal.SIGALRM, self.interrupt)
95 signal.alarm(self.timeout)
98 signal.alarm(self.timeout)
96 first_response = self._read_first_client_response()
99 first_response = self._read_first_client_response()
97 signal.alarm(0)
100 signal.alarm(0)
98 return (self._parse_first_client_response(first_response)
101 return (self._parse_first_client_response(first_response)
99 if first_response else None)
102 if first_response else None)
100
103
101 def patch_first_client_response(self, response, **kwargs):
104 def patch_first_client_response(self, response, **kwargs):
102 self.create_hooks_env()
105 self.create_hooks_env()
103 data = response.copy()
106 data = response.copy()
104 data.update(kwargs)
107 data.update(kwargs)
105 data['url'] = self._svn_string(data['url'])
108 data['url'] = self._svn_string(data['url'])
106 data['ra_client'] = self._svn_string(data['ra_client'])
109 data['ra_client'] = self._svn_string(data['ra_client'])
107 data['client'] = data['client'] or ''
110 data['client'] = data['client'] or ''
108 buffer_ = (
111 buffer_ = (
109 "( {version} ( {capabilities} ) {url}{ra_client}"
112 "( {version} ( {capabilities} ) {url}{ra_client}"
110 "( {client}) ) ".format(**data))
113 "( {client}) ) ".format(**data))
111 self.process.stdin.write(buffer_)
114 self.process.stdin.write(buffer_)
112
115
113 def fail(self, message):
116 def fail(self, message):
114 print("( failure ( ( 210005 {message} 0: 0 ) ) )".format(
117 print("( failure ( ( 210005 {message} 0: 0 ) ) )".format(
115 message=self._svn_string(message)))
118 message=self._svn_string(message)))
116 self.remove_configs()
119 self.remove_configs()
117 self.process.kill()
120 self.process.kill()
118 return 1
121 return 1
119
122
120 def interrupt(self, signum, frame):
123 def interrupt(self, signum, frame):
121 self.fail("Exited by timeout")
124 self.fail("Exited by timeout")
122
125
123 def _svn_string(self, str_):
126 def _svn_string(self, str_):
124 if not str_:
127 if not str_:
125 return ''
128 return ''
126 return '{length}:{string} '.format(length=len(str_), string=str_)
129 return '{length}:{string} '.format(length=len(str_), string=str_)
127
130
128 def _read_first_client_response(self):
131 def _read_first_client_response(self):
129 buffer_ = ""
132 buffer_ = ""
130 brackets_stack = []
133 brackets_stack = []
131 while True:
134 while True:
132 next_byte = self.stdin.read(1)
135 next_byte = self.stdin.read(1)
133 buffer_ += next_byte
136 buffer_ += next_byte
134 if next_byte == "(":
137 if next_byte == "(":
135 brackets_stack.append(next_byte)
138 brackets_stack.append(next_byte)
136 elif next_byte == ")":
139 elif next_byte == ")":
137 brackets_stack.pop()
140 brackets_stack.pop()
138 elif next_byte == " " and not brackets_stack:
141 elif next_byte == " " and not brackets_stack:
139 break
142 break
140
143
141 return buffer_
144 return buffer_
142
145
143 def _parse_first_client_response(self, buffer_):
146 def _parse_first_client_response(self, buffer_):
144 """
147 """
145 According to the Subversion RA protocol, the first request
148 According to the Subversion RA protocol, the first request
146 should look like:
149 should look like:
147
150
148 ( version:number ( cap:word ... ) url:string ? ra-client:string
151 ( version:number ( cap:word ... ) url:string ? ra-client:string
149 ( ? client:string ) )
152 ( ? client:string ) )
150
153
151 Please check https://svn.apache.org/repos/asf/subversion/trunk/subversion/libsvn_ra_svn/protocol
154 Please check https://svn.apache.org/repos/asf/subversion/trunk/subversion/libsvn_ra_svn/protocol
152 """
155 """
153 version_re = r'(?P<version>\d+)'
156 version_re = r'(?P<version>\d+)'
154 capabilities_re = r'\(\s(?P<capabilities>[\w\d\-\ ]+)\s\)'
157 capabilities_re = r'\(\s(?P<capabilities>[\w\d\-\ ]+)\s\)'
155 url_re = r'\d+\:(?P<url>[\W\w]+)'
158 url_re = r'\d+\:(?P<url>[\W\w]+)'
156 ra_client_re = r'(\d+\:(?P<ra_client>[\W\w]+)\s)'
159 ra_client_re = r'(\d+\:(?P<ra_client>[\W\w]+)\s)'
157 client_re = r'(\d+\:(?P<client>[\W\w]+)\s)*'
160 client_re = r'(\d+\:(?P<client>[\W\w]+)\s)*'
158 regex = re.compile(
161 regex = re.compile(
159 r'^\(\s{version}\s{capabilities}\s{url}\s{ra_client}'
162 r'^\(\s{version}\s{capabilities}\s{url}\s{ra_client}'
160 r'\(\s{client}\)\s\)\s*$'.format(
163 r'\(\s{client}\)\s\)\s*$'.format(
161 version=version_re, capabilities=capabilities_re,
164 version=version_re, capabilities=capabilities_re,
162 url=url_re, ra_client=ra_client_re, client=client_re))
165 url=url_re, ra_client=ra_client_re, client=client_re))
163 matcher = regex.match(buffer_)
166 matcher = regex.match(buffer_)
164
167
165 return matcher.groupdict() if matcher else None
168 return matcher.groupdict() if matcher else None
166
169
167 def _match_repo_name(self, url):
170 def _match_repo_name(self, url):
168 """
171 """
169 Given an server url, try to match it against ALL known repository names.
172 Given an server url, try to match it against ALL known repository names.
170 This handles a tricky SVN case for SSH and subdir commits.
173 This handles a tricky SVN case for SSH and subdir commits.
171 E.g if our repo name is my-svn-repo, a svn commit on file in a subdir would
174 E.g if our repo name is my-svn-repo, a svn commit on file in a subdir would
172 result in the url with this subdir added.
175 result in the url with this subdir added.
173 """
176 """
174 # case 1 direct match, we don't do any "heavy" lookups
177 # case 1 direct match, we don't do any "heavy" lookups
175 if url in self.server.user_permissions:
178 if url in self.server.user_permissions:
176 return url
179 return url
177
180
178 log.debug('Extracting repository name from subdir path %s', url)
181 log.debug('Extracting repository name from subdir path %s', url)
179 # case 2 we check all permissions, and match closes possible case...
182 # case 2 we check all permissions, and match closes possible case...
180 # NOTE(dan): In this case we only know that url has a subdir parts, it's safe
183 # NOTE(dan): In this case we only know that url has a subdir parts, it's safe
181 # to assume that it will have the repo name as prefix, we ensure the prefix
184 # to assume that it will have the repo name as prefix, we ensure the prefix
182 # for similar repositories isn't matched by adding a /
185 # for similar repositories isn't matched by adding a /
183 # e.g subgroup/repo-name/ and subgroup/repo-name-1/ would work correct.
186 # e.g subgroup/repo-name/ and subgroup/repo-name-1/ would work correct.
184 for repo_name in self.server.user_permissions:
187 for repo_name in self.server.user_permissions:
185 repo_name_prefix = repo_name + '/'
188 repo_name_prefix = repo_name + '/'
186 if url.startswith(repo_name_prefix):
189 if url.startswith(repo_name_prefix):
187 log.debug('Found prefix %s match, returning proper repository name',
190 log.debug('Found prefix %s match, returning proper repository name',
188 repo_name_prefix)
191 repo_name_prefix)
189 return repo_name
192 return repo_name
190
193
191 return
194 return
192
195
193 def run(self, extras):
196 def run(self, extras):
194 action = 'pull'
197 action = 'pull'
195 self.create_svn_config()
198 self.create_svn_config()
196 self.start()
199 self.start()
197
200
198 first_response = self.get_first_client_response()
201 first_response = self.get_first_client_response()
199 if not first_response:
202 if not first_response:
200 return self.fail("Repository name cannot be extracted")
203 return self.fail("Repository name cannot be extracted")
201
204
202 url_parts = urlparse.urlparse(first_response['url'])
205 url_parts = urlparse.urlparse(first_response['url'])
203
206
204 self.server.repo_name = self._match_repo_name(url_parts.path.strip('/'))
207 self.server.repo_name = self._match_repo_name(url_parts.path.strip('/'))
205
208
206 exit_code = self.server._check_permissions(action)
209 exit_code = self.server._check_permissions(action)
207 if exit_code:
210 if exit_code:
208 return exit_code
211 return exit_code
209
212
210 # set the readonly flag to False if we have proper permissions
213 # set the readonly flag to False if we have proper permissions
211 if self.server.has_write_perm():
214 if self.server.has_write_perm():
212 self.read_only = False
215 self.read_only = False
213 self.server.update_environment(action=action, extras=extras)
216 self.server.update_environment(action=action, extras=extras)
214
217
215 self.patch_first_client_response(first_response)
218 self.patch_first_client_response(first_response)
216 self.sync()
219 self.sync()
217 return self.return_code
220 return self.return_code
218
221
219
222
220 class SubversionServer(VcsServer):
223 class SubversionServer(VcsServer):
221 backend = 'svn'
224 backend = 'svn'
222
225
223 def __init__(self, store, ini_path, repo_name,
226 def __init__(self, store, ini_path, repo_name,
224 user, user_permissions, config, env):
227 user, user_permissions, config, env):
225 super(SubversionServer, self)\
228 super(SubversionServer, self)\
226 .__init__(user, user_permissions, config, env)
229 .__init__(user, user_permissions, config, env)
227 self.store = store
230 self.store = store
228 self.ini_path = ini_path
231 self.ini_path = ini_path
229 # NOTE(dan): repo_name at this point is empty,
232 # NOTE(dan): repo_name at this point is empty,
230 # this is set later in .run() based from parsed input stream
233 # this is set later in .run() based from parsed input stream
231 self.repo_name = repo_name
234 self.repo_name = repo_name
232 self._path = self.svn_path = config.get('app:main', 'ssh.executable.svn')
235 self._path = self.svn_path = config.get('app:main', 'ssh.executable.svn')
233
236
234 self.tunnel = SubversionTunnelWrapper(server=self)
237 self.tunnel = SubversionTunnelWrapper(server=self)
235
238
236 def _handle_tunnel(self, extras):
239 def _handle_tunnel(self, extras):
237
240
238 # pre-auth
241 # pre-auth
239 action = 'pull'
242 action = 'pull'
240 # Special case for SVN, we extract repo name at later stage
243 # Special case for SVN, we extract repo name at later stage
241 # exit_code = self._check_permissions(action)
244 # exit_code = self._check_permissions(action)
242 # if exit_code:
245 # if exit_code:
243 # return exit_code, False
246 # return exit_code, False
244
247
245 req = self.env['request']
248 req = self.env['request']
246 server_url = req.host_url + req.script_name
249 server_url = req.host_url + req.script_name
247 extras['server_url'] = server_url
250 extras['server_url'] = server_url
248
251
249 log.debug('Using %s binaries from path %s', self.backend, self._path)
252 log.debug('Using %s binaries from path %s', self.backend, self._path)
250 exit_code = self.tunnel.run(extras)
253 exit_code = self.tunnel.run(extras)
251
254
252 return exit_code, action == "push"
255 return exit_code, action == "push"
253
256
254
257
@@ -1,204 +1,206 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2019 RhodeCode GmbH
3 # Copyright (C) 2016-2019 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 mock
21 import mock
22 import pytest
22 import pytest
23
23
24 from rhodecode.apps.ssh_support.lib.backends.svn import SubversionServer
24 from rhodecode.apps.ssh_support.lib.backends.svn import SubversionServer
25 from rhodecode.apps.ssh_support.tests.conftest import plain_dummy_env, plain_dummy_user
25 from rhodecode.apps.ssh_support.tests.conftest import plain_dummy_env, plain_dummy_user
26
26
27
27
28 class SubversionServerCreator(object):
28 class SubversionServerCreator(object):
29 root = '/tmp/repo/path/'
29 root = '/tmp/repo/path/'
30 svn_path = '/usr/local/bin/svnserve'
30 svn_path = '/usr/local/bin/svnserve'
31 config_data = {
31 config_data = {
32 'app:main': {
32 'app:main': {
33 'ssh.executable.svn': svn_path,
33 'ssh.executable.svn': svn_path,
34 'vcs.hooks.protocol': 'http',
34 'vcs.hooks.protocol': 'http',
35 }
35 }
36 }
36 }
37 repo_name = 'test-svn'
37 repo_name = 'test-svn'
38 user = plain_dummy_user()
38 user = plain_dummy_user()
39
39
40 def __init__(self):
40 def __init__(self):
41 def config_get(part, key):
41 def config_get(part, key):
42 return self.config_data.get(part, {}).get(key)
42 return self.config_data.get(part, {}).get(key)
43 self.config_mock = mock.Mock()
43 self.config_mock = mock.Mock()
44 self.config_mock.get = mock.Mock(side_effect=config_get)
44 self.config_mock.get = mock.Mock(side_effect=config_get)
45
45
46 def create(self, **kwargs):
46 def create(self, **kwargs):
47 parameters = {
47 parameters = {
48 'store': self.root,
48 'store': self.root,
49 'repo_name': self.repo_name,
49 'repo_name': self.repo_name,
50 'ini_path': '',
50 'ini_path': '',
51 'user': self.user,
51 'user': self.user,
52 'user_permissions': {
52 'user_permissions': {
53 self.repo_name: 'repository.admin'
53 self.repo_name: 'repository.admin'
54 },
54 },
55 'config': self.config_mock,
55 'config': self.config_mock,
56 'env': plain_dummy_env()
56 'env': plain_dummy_env()
57 }
57 }
58
58
59 parameters.update(kwargs)
59 parameters.update(kwargs)
60 server = SubversionServer(**parameters)
60 server = SubversionServer(**parameters)
61 return server
61 return server
62
62
63
63
64 @pytest.fixture()
64 @pytest.fixture()
65 def svn_server(app):
65 def svn_server(app):
66 return SubversionServerCreator()
66 return SubversionServerCreator()
67
67
68
68
69 class TestSubversionServer(object):
69 class TestSubversionServer(object):
70 def test_command(self, svn_server):
70 def test_command(self, svn_server):
71 server = svn_server.create()
71 server = svn_server.create()
72 expected_command = [
72 expected_command = [
73 svn_server.svn_path, '-t', '--config-file',
73 svn_server.svn_path, '-t',
74 server.tunnel.svn_conf_path, '-r', svn_server.root
74 '--config-file', server.tunnel.svn_conf_path,
75 '--tunnel-user', svn_server.user.username,
76 '-r', svn_server.root
75 ]
77 ]
76
78
77 assert expected_command == server.tunnel.command()
79 assert expected_command == server.tunnel.command()
78
80
79 @pytest.mark.parametrize('permissions, action, code', [
81 @pytest.mark.parametrize('permissions, action, code', [
80 ({}, 'pull', -2),
82 ({}, 'pull', -2),
81 ({'test-svn': 'repository.read'}, 'pull', 0),
83 ({'test-svn': 'repository.read'}, 'pull', 0),
82 ({'test-svn': 'repository.read'}, 'push', -2),
84 ({'test-svn': 'repository.read'}, 'push', -2),
83 ({'test-svn': 'repository.write'}, 'push', 0),
85 ({'test-svn': 'repository.write'}, 'push', 0),
84 ({'test-svn': 'repository.admin'}, 'push', 0),
86 ({'test-svn': 'repository.admin'}, 'push', 0),
85
87
86 ])
88 ])
87 def test_permission_checks(self, svn_server, permissions, action, code):
89 def test_permission_checks(self, svn_server, permissions, action, code):
88 server = svn_server.create(user_permissions=permissions)
90 server = svn_server.create(user_permissions=permissions)
89 result = server._check_permissions(action)
91 result = server._check_permissions(action)
90 assert result is code
92 assert result is code
91
93
92 @pytest.mark.parametrize('permissions, access_paths, expected_match', [
94 @pytest.mark.parametrize('permissions, access_paths, expected_match', [
93 # not matched repository name
95 # not matched repository name
94 ({
96 ({
95 'test-svn': ''
97 'test-svn': ''
96 }, ['test-svn-1', 'test-svn-1/subpath'],
98 }, ['test-svn-1', 'test-svn-1/subpath'],
97 None),
99 None),
98
100
99 # exact match
101 # exact match
100 ({
102 ({
101 'test-svn': ''
103 'test-svn': ''
102 },
104 },
103 ['test-svn'],
105 ['test-svn'],
104 'test-svn'),
106 'test-svn'),
105
107
106 # subdir commits
108 # subdir commits
107 ({
109 ({
108 'test-svn': ''
110 'test-svn': ''
109 },
111 },
110 ['test-svn/foo',
112 ['test-svn/foo',
111 'test-svn/foo/test-svn',
113 'test-svn/foo/test-svn',
112 'test-svn/trunk/development.txt',
114 'test-svn/trunk/development.txt',
113 ],
115 ],
114 'test-svn'),
116 'test-svn'),
115
117
116 # subgroups + similar patterns
118 # subgroups + similar patterns
117 ({
119 ({
118 'test-svn': '',
120 'test-svn': '',
119 'test-svn-1': '',
121 'test-svn-1': '',
120 'test-svn-subgroup/test-svn': '',
122 'test-svn-subgroup/test-svn': '',
121
123
122 },
124 },
123 ['test-svn-1',
125 ['test-svn-1',
124 'test-svn-1/foo/test-svn',
126 'test-svn-1/foo/test-svn',
125 'test-svn-1/test-svn',
127 'test-svn-1/test-svn',
126 ],
128 ],
127 'test-svn-1'),
129 'test-svn-1'),
128
130
129 # subgroups + similar patterns
131 # subgroups + similar patterns
130 ({
132 ({
131 'test-svn-1': '',
133 'test-svn-1': '',
132 'test-svn-10': '',
134 'test-svn-10': '',
133 'test-svn-100': '',
135 'test-svn-100': '',
134 },
136 },
135 ['test-svn-10',
137 ['test-svn-10',
136 'test-svn-10/foo/test-svn',
138 'test-svn-10/foo/test-svn',
137 'test-svn-10/test-svn',
139 'test-svn-10/test-svn',
138 ],
140 ],
139 'test-svn-10'),
141 'test-svn-10'),
140
142
141 # subgroups + similar patterns
143 # subgroups + similar patterns
142 ({
144 ({
143 'name': '',
145 'name': '',
144 'nameContains': '',
146 'nameContains': '',
145 'nameContainsThis': '',
147 'nameContainsThis': '',
146 },
148 },
147 ['nameContains',
149 ['nameContains',
148 'nameContains/This',
150 'nameContains/This',
149 'nameContains/This/test-svn',
151 'nameContains/This/test-svn',
150 ],
152 ],
151 'nameContains'),
153 'nameContains'),
152
154
153 # subgroups + similar patterns
155 # subgroups + similar patterns
154 ({
156 ({
155 'test-svn': '',
157 'test-svn': '',
156 'test-svn-1': '',
158 'test-svn-1': '',
157 'test-svn-subgroup/test-svn': '',
159 'test-svn-subgroup/test-svn': '',
158
160
159 },
161 },
160 ['test-svn-subgroup/test-svn',
162 ['test-svn-subgroup/test-svn',
161 'test-svn-subgroup/test-svn/foo/test-svn',
163 'test-svn-subgroup/test-svn/foo/test-svn',
162 'test-svn-subgroup/test-svn/trunk/example.txt',
164 'test-svn-subgroup/test-svn/trunk/example.txt',
163 ],
165 ],
164 'test-svn-subgroup/test-svn'),
166 'test-svn-subgroup/test-svn'),
165 ])
167 ])
166 def test_repo_extraction_on_subdir(self, svn_server, permissions, access_paths, expected_match):
168 def test_repo_extraction_on_subdir(self, svn_server, permissions, access_paths, expected_match):
167 server = svn_server.create(user_permissions=permissions)
169 server = svn_server.create(user_permissions=permissions)
168 for path in access_paths:
170 for path in access_paths:
169 repo_name = server.tunnel._match_repo_name(path)
171 repo_name = server.tunnel._match_repo_name(path)
170 assert repo_name == expected_match
172 assert repo_name == expected_match
171
173
172 def test_run_returns_executes_command(self, svn_server):
174 def test_run_returns_executes_command(self, svn_server):
173 server = svn_server.create()
175 server = svn_server.create()
174 from rhodecode.apps.ssh_support.lib.backends.svn import SubversionTunnelWrapper
176 from rhodecode.apps.ssh_support.lib.backends.svn import SubversionTunnelWrapper
175 with mock.patch.object(
177 with mock.patch.object(
176 SubversionTunnelWrapper, 'get_first_client_response',
178 SubversionTunnelWrapper, 'get_first_client_response',
177 return_value={'url': 'http://server/test-svn'}):
179 return_value={'url': 'http://server/test-svn'}):
178 with mock.patch.object(
180 with mock.patch.object(
179 SubversionTunnelWrapper, 'patch_first_client_response',
181 SubversionTunnelWrapper, 'patch_first_client_response',
180 return_value=0):
182 return_value=0):
181 with mock.patch.object(
183 with mock.patch.object(
182 SubversionTunnelWrapper, 'sync',
184 SubversionTunnelWrapper, 'sync',
183 return_value=0):
185 return_value=0):
184 with mock.patch.object(
186 with mock.patch.object(
185 SubversionTunnelWrapper, 'command',
187 SubversionTunnelWrapper, 'command',
186 return_value=['date']):
188 return_value=['date']):
187
189
188 exit_code = server.run()
190 exit_code = server.run()
189 # SVN has this differently configured, and we get in our mock env
191 # SVN has this differently configured, and we get in our mock env
190 # None as return code
192 # None as return code
191 assert exit_code == (None, False)
193 assert exit_code == (None, False)
192
194
193 def test_run_returns_executes_command_that_cannot_extract_repo_name(self, svn_server):
195 def test_run_returns_executes_command_that_cannot_extract_repo_name(self, svn_server):
194 server = svn_server.create()
196 server = svn_server.create()
195 from rhodecode.apps.ssh_support.lib.backends.svn import SubversionTunnelWrapper
197 from rhodecode.apps.ssh_support.lib.backends.svn import SubversionTunnelWrapper
196 with mock.patch.object(
198 with mock.patch.object(
197 SubversionTunnelWrapper, 'command',
199 SubversionTunnelWrapper, 'command',
198 return_value=['date']):
200 return_value=['date']):
199 with mock.patch.object(
201 with mock.patch.object(
200 SubversionTunnelWrapper, 'get_first_client_response',
202 SubversionTunnelWrapper, 'get_first_client_response',
201 return_value=None):
203 return_value=None):
202 exit_code = server.run()
204 exit_code = server.run()
203
205
204 assert exit_code == (1, False)
206 assert exit_code == (1, False)
General Comments 0
You need to be logged in to leave comments. Login now