##// 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 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import os
22 22 import re
23 23 import sys
24 24 import logging
25 25 import signal
26 26 import tempfile
27 27 from subprocess import Popen, PIPE
28 28 import urlparse
29 29
30 30 from .base import VcsServer
31 31
32 32 log = logging.getLogger(__name__)
33 33
34 34
35 35 class SubversionTunnelWrapper(object):
36 36 process = None
37 37
38 38 def __init__(self, server):
39 39 self.server = server
40 40 self.timeout = 30
41 41 self.stdin = sys.stdin
42 42 self.stdout = sys.stdout
43 43 self.svn_conf_fd, self.svn_conf_path = tempfile.mkstemp()
44 44 self.hooks_env_fd, self.hooks_env_path = tempfile.mkstemp()
45 45
46 46 self.read_only = True # flag that we set to make the hooks readonly
47 47
48 48 def create_svn_config(self):
49 49 content = (
50 50 '[general]\n'
51 51 'hooks-env = {}\n').format(self.hooks_env_path)
52 52 with os.fdopen(self.svn_conf_fd, 'w') as config_file:
53 53 config_file.write(content)
54 54
55 55 def create_hooks_env(self):
56 56 content = (
57 57 '[default]\n'
58 58 'LANG = en_US.UTF-8\n')
59 59 if self.read_only:
60 60 content += 'SSH_READ_ONLY = 1\n'
61 61 with os.fdopen(self.hooks_env_fd, 'w') as hooks_env_file:
62 62 hooks_env_file.write(content)
63 63
64 64 def remove_configs(self):
65 65 os.remove(self.svn_conf_path)
66 66 os.remove(self.hooks_env_path)
67 67
68 68 def command(self):
69 69 root = self.server.get_root_store()
70 70 command = [
71 71 self.server.svn_path, '-t',
72 72 '--config-file', self.svn_conf_path,
73 73 '-r', root]
74 74 log.debug("Final CMD: %s", ' '.join(command))
75 75 return command
76 76
77 77 def start(self):
78 78 command = self.command()
79 79 self.process = Popen(' '.join(command), stdin=PIPE, shell=True)
80 80
81 81 def sync(self):
82 82 while self.process.poll() is None:
83 83 next_byte = self.stdin.read(1)
84 84 if not next_byte:
85 85 break
86 86 self.process.stdin.write(next_byte)
87 87 self.remove_configs()
88 88
89 89 @property
90 90 def return_code(self):
91 91 return self.process.returncode
92 92
93 93 def get_first_client_response(self):
94 94 signal.signal(signal.SIGALRM, self.interrupt)
95 95 signal.alarm(self.timeout)
96 96 first_response = self._read_first_client_response()
97 97 signal.alarm(0)
98 98 return (
99 99 self._parse_first_client_response(first_response)
100 100 if first_response else None)
101 101
102 102 def patch_first_client_response(self, response, **kwargs):
103 103 self.create_hooks_env()
104 104 data = response.copy()
105 105 data.update(kwargs)
106 106 data['url'] = self._svn_string(data['url'])
107 107 data['ra_client'] = self._svn_string(data['ra_client'])
108 108 data['client'] = data['client'] or ''
109 109 buffer_ = (
110 110 "( {version} ( {capabilities} ) {url}{ra_client}"
111 111 "( {client}) ) ".format(**data))
112 112 self.process.stdin.write(buffer_)
113 113
114 114 def fail(self, message):
115 115 print(
116 116 "( failure ( ( 210005 {message} 0: 0 ) ) )".format(
117 117 message=self._svn_string(message)))
118 118 self.remove_configs()
119 119 self.process.kill()
120 return 1
120 121
121 122 def interrupt(self, signum, frame):
122 123 self.fail("Exited by timeout")
123 124
124 125 def _svn_string(self, str_):
125 126 if not str_:
126 127 return ''
127 128 return '{length}:{string} '.format(length=len(str_), string=str_)
128 129
129 130 def _read_first_client_response(self):
130 131 buffer_ = ""
131 132 brackets_stack = []
132 133 while True:
133 134 next_byte = self.stdin.read(1)
134 135 buffer_ += next_byte
135 136 if next_byte == "(":
136 137 brackets_stack.append(next_byte)
137 138 elif next_byte == ")":
138 139 brackets_stack.pop()
139 140 elif next_byte == " " and not brackets_stack:
140 141 break
141 142 return buffer_
142 143
143 144 def _parse_first_client_response(self, buffer_):
144 145 """
145 146 According to the Subversion RA protocol, the first request
146 147 should look like:
147 148
148 149 ( version:number ( cap:word ... ) url:string ? ra-client:string
149 150 ( ? client:string ) )
150 151
151 152 Please check https://svn.apache.org/repos/asf/subversion/trunk/
152 153 subversion/libsvn_ra_svn/protocol
153 154 """
154 155 version_re = r'(?P<version>\d+)'
155 156 capabilities_re = r'\(\s(?P<capabilities>[\w\d\-\ ]+)\s\)'
156 157 url_re = r'\d+\:(?P<url>[\W\w]+)'
157 158 ra_client_re = r'(\d+\:(?P<ra_client>[\W\w]+)\s)'
158 159 client_re = r'(\d+\:(?P<client>[\W\w]+)\s)*'
159 160 regex = re.compile(
160 161 r'^\(\s{version}\s{capabilities}\s{url}\s{ra_client}'
161 162 r'\(\s{client}\)\s\)\s*$'.format(
162 163 version=version_re, capabilities=capabilities_re,
163 164 url=url_re, ra_client=ra_client_re, client=client_re))
164 165 matcher = regex.match(buffer_)
165 166 return matcher.groupdict() if matcher else None
166 167
167 168 def run(self, extras):
168 169 action = 'pull'
169 170 self.create_svn_config()
170 171 self.start()
171 172
172 173 first_response = self.get_first_client_response()
173 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 177 url_parts = urlparse.urlparse(first_response['url'])
177 178 self.server.repo_name = url_parts.path.strip('/')
178 179
179 180 exit_code = self.server._check_permissions(action)
180 181 if exit_code:
181 182 return exit_code
182 183
183 184 # set the readonly flag to False if we have proper permissions
184 185 if self.server.has_write_perm():
185 186 self.read_only = False
186 187 self.server.update_environment(action=action, extras=extras)
187 188
188 189 self.patch_first_client_response(first_response)
189 190 self.sync()
190 191 return self.return_code
191 192
192 193
193 194 class SubversionServer(VcsServer):
194 195 backend = 'svn'
195 196
196 197 def __init__(self, store, ini_path, repo_name,
197 198 user, user_permissions, config, env):
198 199 super(SubversionServer, self)\
199 200 .__init__(user, user_permissions, config, env)
200 201 self.store = store
201 202 self.ini_path = ini_path
202 203 # this is set in .run() from input stream
203 204 self.repo_name = repo_name
204 205 self._path = self.svn_path = config.get(
205 206 'app:main', 'ssh.executable.svn')
206 207
207 208 self.tunnel = SubversionTunnelWrapper(server=self)
208 209
209 210 def _handle_tunnel(self, extras):
210 211
211 212 # pre-auth
212 213 action = 'pull'
213 214 # Special case for SVN, we extract repo name at later stage
214 215 # exit_code = self._check_permissions(action)
215 216 # if exit_code:
216 217 # return exit_code, False
217 218
218 219 req = self.env['request']
219 220 server_url = req.host_url + req.script_name
220 221 extras['server_url'] = server_url
221 222
222 223 log.debug('Using %s binaries from path %s', self.backend, self._path)
223 224 exit_code = self.tunnel.run(extras)
224 225
225 226 return exit_code, action == "push"
226 227
227 228
@@ -1,124 +1,124 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import mock
22 22 import pytest
23 23
24 24 from rhodecode.apps.ssh_support.lib.backends.svn import SubversionServer
25 25 from rhodecode.apps.ssh_support.tests.conftest import dummy_env, dummy_user
26 26
27 27
28 28 class SubversionServerCreator(object):
29 29 root = '/tmp/repo/path/'
30 30 svn_path = '/usr/local/bin/svnserve'
31 31 config_data = {
32 32 'app:main': {
33 33 'ssh.executable.svn': svn_path,
34 34 'vcs.hooks.protocol': 'http',
35 35 }
36 36 }
37 37 repo_name = 'test-svn'
38 38 user = dummy_user()
39 39
40 40 def __init__(self):
41 41 def config_get(part, key):
42 42 return self.config_data.get(part, {}).get(key)
43 43 self.config_mock = mock.Mock()
44 44 self.config_mock.get = mock.Mock(side_effect=config_get)
45 45
46 46 def create(self, **kwargs):
47 47 parameters = {
48 48 'store': self.root,
49 49 'repo_name': self.repo_name,
50 50 'ini_path': '',
51 51 'user': self.user,
52 52 'user_permissions': {
53 53 self.repo_name: 'repository.admin'
54 54 },
55 55 'config': self.config_mock,
56 56 'env': dummy_env()
57 57 }
58 58
59 59 parameters.update(kwargs)
60 60 server = SubversionServer(**parameters)
61 61 return server
62 62
63 63
64 64 @pytest.fixture
65 65 def svn_server(app):
66 66 return SubversionServerCreator()
67 67
68 68
69 69 class TestSubversionServer(object):
70 70 def test_command(self, svn_server):
71 71 server = svn_server.create()
72 72 expected_command = [
73 73 svn_server.svn_path, '-t', '--config-file',
74 74 server.tunnel.svn_conf_path, '-r', svn_server.root
75 75 ]
76 76
77 77 assert expected_command == server.tunnel.command()
78 78
79 79 @pytest.mark.parametrize('permissions, action, code', [
80 80 ({}, 'pull', -2),
81 81 ({'test-svn': 'repository.read'}, 'pull', 0),
82 82 ({'test-svn': 'repository.read'}, 'push', -2),
83 83 ({'test-svn': 'repository.write'}, 'push', 0),
84 84 ({'test-svn': 'repository.admin'}, 'push', 0),
85 85
86 86 ])
87 87 def test_permission_checks(self, svn_server, permissions, action, code):
88 88 server = svn_server.create(user_permissions=permissions)
89 89 result = server._check_permissions(action)
90 90 assert result is code
91 91
92 92 def test_run_returns_executes_command(self, svn_server):
93 93 server = svn_server.create()
94 94 from rhodecode.apps.ssh_support.lib.backends.svn import SubversionTunnelWrapper
95 95 with mock.patch.object(
96 96 SubversionTunnelWrapper, 'get_first_client_response',
97 97 return_value={'url': 'http://server/test-svn'}):
98 98 with mock.patch.object(
99 99 SubversionTunnelWrapper, 'patch_first_client_response',
100 100 return_value=0):
101 101 with mock.patch.object(
102 102 SubversionTunnelWrapper, 'sync',
103 103 return_value=0):
104 104 with mock.patch.object(
105 105 SubversionTunnelWrapper, 'command',
106 return_value='date'):
106 return_value=['date']):
107 107
108 108 exit_code = server.run()
109 109 # SVN has this differently configured, and we get in our mock env
110 110 # None as return code
111 111 assert exit_code == (None, False)
112 112
113 113 def test_run_returns_executes_command_that_cannot_extract_repo_name(self, svn_server):
114 114 server = svn_server.create()
115 115 from rhodecode.apps.ssh_support.lib.backends.svn import SubversionTunnelWrapper
116 116 with mock.patch.object(
117 117 SubversionTunnelWrapper, 'command',
118 return_value='date'):
118 return_value=['date']):
119 119 with mock.patch.object(
120 120 SubversionTunnelWrapper, 'get_first_client_response',
121 121 return_value=None):
122 122 exit_code = server.run()
123 123
124 124 assert exit_code == (1, False)
General Comments 0
You need to be logged in to leave comments. Login now