##// END OF EJS Templates
svn: fixed tests for svn backend.
marcink -
r2603:44eba3e8 default
parent child Browse files
Show More
@@ -1,227 +1,228 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-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 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 command = [
70 command = [
71 self.server.svn_path, '-t',
71 self.server.svn_path, '-t',
72 '--config-file', self.svn_conf_path,
72 '--config-file', self.svn_conf_path,
73 '-r', root]
73 '-r', root]
74 log.debug("Final CMD: %s", ' '.join(command))
74 log.debug("Final CMD: %s", ' '.join(command))
75 return command
75 return command
76
76
77 def start(self):
77 def start(self):
78 command = self.command()
78 command = self.command()
79 self.process = Popen(' '.join(command), stdin=PIPE, shell=True)
79 self.process = Popen(' '.join(command), stdin=PIPE, shell=True)
80
80
81 def sync(self):
81 def sync(self):
82 while self.process.poll() is None:
82 while self.process.poll() is None:
83 next_byte = self.stdin.read(1)
83 next_byte = self.stdin.read(1)
84 if not next_byte:
84 if not next_byte:
85 break
85 break
86 self.process.stdin.write(next_byte)
86 self.process.stdin.write(next_byte)
87 self.remove_configs()
87 self.remove_configs()
88
88
89 @property
89 @property
90 def return_code(self):
90 def return_code(self):
91 return self.process.returncode
91 return self.process.returncode
92
92
93 def get_first_client_response(self):
93 def get_first_client_response(self):
94 signal.signal(signal.SIGALRM, self.interrupt)
94 signal.signal(signal.SIGALRM, self.interrupt)
95 signal.alarm(self.timeout)
95 signal.alarm(self.timeout)
96 first_response = self._read_first_client_response()
96 first_response = self._read_first_client_response()
97 signal.alarm(0)
97 signal.alarm(0)
98 return (
98 return (
99 self._parse_first_client_response(first_response)
99 self._parse_first_client_response(first_response)
100 if first_response else None)
100 if first_response else None)
101
101
102 def patch_first_client_response(self, response, **kwargs):
102 def patch_first_client_response(self, response, **kwargs):
103 self.create_hooks_env()
103 self.create_hooks_env()
104 data = response.copy()
104 data = response.copy()
105 data.update(kwargs)
105 data.update(kwargs)
106 data['url'] = self._svn_string(data['url'])
106 data['url'] = self._svn_string(data['url'])
107 data['ra_client'] = self._svn_string(data['ra_client'])
107 data['ra_client'] = self._svn_string(data['ra_client'])
108 data['client'] = data['client'] or ''
108 data['client'] = data['client'] or ''
109 buffer_ = (
109 buffer_ = (
110 "( {version} ( {capabilities} ) {url}{ra_client}"
110 "( {version} ( {capabilities} ) {url}{ra_client}"
111 "( {client}) ) ".format(**data))
111 "( {client}) ) ".format(**data))
112 self.process.stdin.write(buffer_)
112 self.process.stdin.write(buffer_)
113
113
114 def fail(self, message):
114 def fail(self, message):
115 print(
115 print(
116 "( failure ( ( 210005 {message} 0: 0 ) ) )".format(
116 "( failure ( ( 210005 {message} 0: 0 ) ) )".format(
117 message=self._svn_string(message)))
117 message=self._svn_string(message)))
118 self.remove_configs()
118 self.remove_configs()
119 self.process.kill()
119 self.process.kill()
120 return 1
120
121
121 def interrupt(self, signum, frame):
122 def interrupt(self, signum, frame):
122 self.fail("Exited by timeout")
123 self.fail("Exited by timeout")
123
124
124 def _svn_string(self, str_):
125 def _svn_string(self, str_):
125 if not str_:
126 if not str_:
126 return ''
127 return ''
127 return '{length}:{string} '.format(length=len(str_), string=str_)
128 return '{length}:{string} '.format(length=len(str_), string=str_)
128
129
129 def _read_first_client_response(self):
130 def _read_first_client_response(self):
130 buffer_ = ""
131 buffer_ = ""
131 brackets_stack = []
132 brackets_stack = []
132 while True:
133 while True:
133 next_byte = self.stdin.read(1)
134 next_byte = self.stdin.read(1)
134 buffer_ += next_byte
135 buffer_ += next_byte
135 if next_byte == "(":
136 if next_byte == "(":
136 brackets_stack.append(next_byte)
137 brackets_stack.append(next_byte)
137 elif next_byte == ")":
138 elif next_byte == ")":
138 brackets_stack.pop()
139 brackets_stack.pop()
139 elif next_byte == " " and not brackets_stack:
140 elif next_byte == " " and not brackets_stack:
140 break
141 break
141 return buffer_
142 return buffer_
142
143
143 def _parse_first_client_response(self, buffer_):
144 def _parse_first_client_response(self, buffer_):
144 """
145 """
145 According to the Subversion RA protocol, the first request
146 According to the Subversion RA protocol, the first request
146 should look like:
147 should look like:
147
148
148 ( version:number ( cap:word ... ) url:string ? ra-client:string
149 ( version:number ( cap:word ... ) url:string ? ra-client:string
149 ( ? client:string ) )
150 ( ? client:string ) )
150
151
151 Please check https://svn.apache.org/repos/asf/subversion/trunk/
152 Please check https://svn.apache.org/repos/asf/subversion/trunk/
152 subversion/libsvn_ra_svn/protocol
153 subversion/libsvn_ra_svn/protocol
153 """
154 """
154 version_re = r'(?P<version>\d+)'
155 version_re = r'(?P<version>\d+)'
155 capabilities_re = r'\(\s(?P<capabilities>[\w\d\-\ ]+)\s\)'
156 capabilities_re = r'\(\s(?P<capabilities>[\w\d\-\ ]+)\s\)'
156 url_re = r'\d+\:(?P<url>[\W\w]+)'
157 url_re = r'\d+\:(?P<url>[\W\w]+)'
157 ra_client_re = r'(\d+\:(?P<ra_client>[\W\w]+)\s)'
158 ra_client_re = r'(\d+\:(?P<ra_client>[\W\w]+)\s)'
158 client_re = r'(\d+\:(?P<client>[\W\w]+)\s)*'
159 client_re = r'(\d+\:(?P<client>[\W\w]+)\s)*'
159 regex = re.compile(
160 regex = re.compile(
160 r'^\(\s{version}\s{capabilities}\s{url}\s{ra_client}'
161 r'^\(\s{version}\s{capabilities}\s{url}\s{ra_client}'
161 r'\(\s{client}\)\s\)\s*$'.format(
162 r'\(\s{client}\)\s\)\s*$'.format(
162 version=version_re, capabilities=capabilities_re,
163 version=version_re, capabilities=capabilities_re,
163 url=url_re, ra_client=ra_client_re, client=client_re))
164 url=url_re, ra_client=ra_client_re, client=client_re))
164 matcher = regex.match(buffer_)
165 matcher = regex.match(buffer_)
165 return matcher.groupdict() if matcher else None
166 return matcher.groupdict() if matcher else None
166
167
167 def run(self, extras):
168 def run(self, extras):
168 action = 'pull'
169 action = 'pull'
169 self.create_svn_config()
170 self.create_svn_config()
170 self.start()
171 self.start()
171
172
172 first_response = self.get_first_client_response()
173 first_response = self.get_first_client_response()
173 if not first_response:
174 if not first_response:
174 self.fail("Repository name cannot be extracted")
175 return self.fail("Repository name cannot be extracted")
175
176
176 url_parts = urlparse.urlparse(first_response['url'])
177 url_parts = urlparse.urlparse(first_response['url'])
177 self.server.repo_name = url_parts.path.strip('/')
178 self.server.repo_name = url_parts.path.strip('/')
178
179
179 exit_code = self.server._check_permissions(action)
180 exit_code = self.server._check_permissions(action)
180 if exit_code:
181 if exit_code:
181 return exit_code
182 return exit_code
182
183
183 # set the readonly flag to False if we have proper permissions
184 # set the readonly flag to False if we have proper permissions
184 if self.server.has_write_perm():
185 if self.server.has_write_perm():
185 self.read_only = False
186 self.read_only = False
186 self.server.update_environment(action=action, extras=extras)
187 self.server.update_environment(action=action, extras=extras)
187
188
188 self.patch_first_client_response(first_response)
189 self.patch_first_client_response(first_response)
189 self.sync()
190 self.sync()
190 return self.return_code
191 return self.return_code
191
192
192
193
193 class SubversionServer(VcsServer):
194 class SubversionServer(VcsServer):
194 backend = 'svn'
195 backend = 'svn'
195
196
196 def __init__(self, store, ini_path, repo_name,
197 def __init__(self, store, ini_path, repo_name,
197 user, user_permissions, config, env):
198 user, user_permissions, config, env):
198 super(SubversionServer, self)\
199 super(SubversionServer, self)\
199 .__init__(user, user_permissions, config, env)
200 .__init__(user, user_permissions, config, env)
200 self.store = store
201 self.store = store
201 self.ini_path = ini_path
202 self.ini_path = ini_path
202 # this is set in .run() from input stream
203 # this is set in .run() from input stream
203 self.repo_name = repo_name
204 self.repo_name = repo_name
204 self._path = self.svn_path = config.get(
205 self._path = self.svn_path = config.get(
205 'app:main', 'ssh.executable.svn')
206 'app:main', 'ssh.executable.svn')
206
207
207 self.tunnel = SubversionTunnelWrapper(server=self)
208 self.tunnel = SubversionTunnelWrapper(server=self)
208
209
209 def _handle_tunnel(self, extras):
210 def _handle_tunnel(self, extras):
210
211
211 # pre-auth
212 # pre-auth
212 action = 'pull'
213 action = 'pull'
213 # Special case for SVN, we extract repo name at later stage
214 # Special case for SVN, we extract repo name at later stage
214 # exit_code = self._check_permissions(action)
215 # exit_code = self._check_permissions(action)
215 # if exit_code:
216 # if exit_code:
216 # return exit_code, False
217 # return exit_code, False
217
218
218 req = self.env['request']
219 req = self.env['request']
219 server_url = req.host_url + req.script_name
220 server_url = req.host_url + req.script_name
220 extras['server_url'] = server_url
221 extras['server_url'] = server_url
221
222
222 log.debug('Using %s binaries from path %s', self.backend, self._path)
223 log.debug('Using %s binaries from path %s', self.backend, self._path)
223 exit_code = self.tunnel.run(extras)
224 exit_code = self.tunnel.run(extras)
224
225
225 return exit_code, action == "push"
226 return exit_code, action == "push"
226
227
227
228
@@ -1,124 +1,124 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-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 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 dummy_env, dummy_user
25 from rhodecode.apps.ssh_support.tests.conftest import dummy_env, 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 = dummy_user()
38 user = 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': dummy_env()
56 'env': 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', '--config-file',
74 server.tunnel.svn_conf_path, '-r', svn_server.root
74 server.tunnel.svn_conf_path, '-r', svn_server.root
75 ]
75 ]
76
76
77 assert expected_command == server.tunnel.command()
77 assert expected_command == server.tunnel.command()
78
78
79 @pytest.mark.parametrize('permissions, action, code', [
79 @pytest.mark.parametrize('permissions, action, code', [
80 ({}, 'pull', -2),
80 ({}, 'pull', -2),
81 ({'test-svn': 'repository.read'}, 'pull', 0),
81 ({'test-svn': 'repository.read'}, 'pull', 0),
82 ({'test-svn': 'repository.read'}, 'push', -2),
82 ({'test-svn': 'repository.read'}, 'push', -2),
83 ({'test-svn': 'repository.write'}, 'push', 0),
83 ({'test-svn': 'repository.write'}, 'push', 0),
84 ({'test-svn': 'repository.admin'}, 'push', 0),
84 ({'test-svn': 'repository.admin'}, 'push', 0),
85
85
86 ])
86 ])
87 def test_permission_checks(self, svn_server, permissions, action, code):
87 def test_permission_checks(self, svn_server, permissions, action, code):
88 server = svn_server.create(user_permissions=permissions)
88 server = svn_server.create(user_permissions=permissions)
89 result = server._check_permissions(action)
89 result = server._check_permissions(action)
90 assert result is code
90 assert result is code
91
91
92 def test_run_returns_executes_command(self, svn_server):
92 def test_run_returns_executes_command(self, svn_server):
93 server = svn_server.create()
93 server = svn_server.create()
94 from rhodecode.apps.ssh_support.lib.backends.svn import SubversionTunnelWrapper
94 from rhodecode.apps.ssh_support.lib.backends.svn import SubversionTunnelWrapper
95 with mock.patch.object(
95 with mock.patch.object(
96 SubversionTunnelWrapper, 'get_first_client_response',
96 SubversionTunnelWrapper, 'get_first_client_response',
97 return_value={'url': 'http://server/test-svn'}):
97 return_value={'url': 'http://server/test-svn'}):
98 with mock.patch.object(
98 with mock.patch.object(
99 SubversionTunnelWrapper, 'patch_first_client_response',
99 SubversionTunnelWrapper, 'patch_first_client_response',
100 return_value=0):
100 return_value=0):
101 with mock.patch.object(
101 with mock.patch.object(
102 SubversionTunnelWrapper, 'sync',
102 SubversionTunnelWrapper, 'sync',
103 return_value=0):
103 return_value=0):
104 with mock.patch.object(
104 with mock.patch.object(
105 SubversionTunnelWrapper, 'command',
105 SubversionTunnelWrapper, 'command',
106 return_value='date'):
106 return_value=['date']):
107
107
108 exit_code = server.run()
108 exit_code = server.run()
109 # SVN has this differently configured, and we get in our mock env
109 # SVN has this differently configured, and we get in our mock env
110 # None as return code
110 # None as return code
111 assert exit_code == (None, False)
111 assert exit_code == (None, False)
112
112
113 def test_run_returns_executes_command_that_cannot_extract_repo_name(self, svn_server):
113 def test_run_returns_executes_command_that_cannot_extract_repo_name(self, svn_server):
114 server = svn_server.create()
114 server = svn_server.create()
115 from rhodecode.apps.ssh_support.lib.backends.svn import SubversionTunnelWrapper
115 from rhodecode.apps.ssh_support.lib.backends.svn import SubversionTunnelWrapper
116 with mock.patch.object(
116 with mock.patch.object(
117 SubversionTunnelWrapper, 'command',
117 SubversionTunnelWrapper, 'command',
118 return_value='date'):
118 return_value=['date']):
119 with mock.patch.object(
119 with mock.patch.object(
120 SubversionTunnelWrapper, 'get_first_client_response',
120 SubversionTunnelWrapper, 'get_first_client_response',
121 return_value=None):
121 return_value=None):
122 exit_code = server.run()
122 exit_code = server.run()
123
123
124 assert exit_code == (1, False)
124 assert exit_code == (1, False)
General Comments 0
You need to be logged in to leave comments. Login now