##// END OF EJS Templates
vcs: report 404 for shadow repos that are not existing anymore. Before we got 500 exception in this case.
marcink -
r2069:d39ea19b default
parent child Browse files
Show More
@@ -1,531 +1,540 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2014-2017 RhodeCode GmbH
3 # Copyright (C) 2014-2017 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 """
21 """
22 SimpleVCS middleware for handling protocol request (push/clone etc.)
22 SimpleVCS middleware for handling protocol request (push/clone etc.)
23 It's implemented with basic auth function
23 It's implemented with basic auth function
24 """
24 """
25
25
26 import os
26 import os
27 import logging
27 import logging
28 import importlib
28 import importlib
29 import re
29 import re
30 from functools import wraps
30 from functools import wraps
31
31
32 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
32 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
33 from webob.exc import (
33 from webob.exc import (
34 HTTPNotFound, HTTPForbidden, HTTPNotAcceptable, HTTPInternalServerError)
34 HTTPNotFound, HTTPForbidden, HTTPNotAcceptable, HTTPInternalServerError)
35
35
36 import rhodecode
36 import rhodecode
37 from rhodecode.authentication.base import authenticate, VCS_TYPE
37 from rhodecode.authentication.base import authenticate, VCS_TYPE
38 from rhodecode.lib.auth import AuthUser, HasPermissionAnyMiddleware
38 from rhodecode.lib.auth import AuthUser, HasPermissionAnyMiddleware
39 from rhodecode.lib.base import (
39 from rhodecode.lib.base import (
40 BasicAuth, get_ip_addr, get_user_agent, vcs_operation_context)
40 BasicAuth, get_ip_addr, get_user_agent, vcs_operation_context)
41 from rhodecode.lib.exceptions import (
41 from rhodecode.lib.exceptions import (
42 HTTPLockedRC, HTTPRequirementError, UserCreationError,
42 HTTPLockedRC, HTTPRequirementError, UserCreationError,
43 NotAllowedToCreateUserError)
43 NotAllowedToCreateUserError)
44 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
44 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
45 from rhodecode.lib.middleware import appenlight
45 from rhodecode.lib.middleware import appenlight
46 from rhodecode.lib.middleware.utils import scm_app_http
46 from rhodecode.lib.middleware.utils import scm_app_http
47 from rhodecode.lib.utils import (
47 from rhodecode.lib.utils import (
48 is_valid_repo, get_rhodecode_realm, get_rhodecode_base_path, SLUG_RE)
48 is_valid_repo, get_rhodecode_realm, get_rhodecode_base_path, SLUG_RE)
49 from rhodecode.lib.utils2 import safe_str, fix_PATH, str2bool, safe_unicode
49 from rhodecode.lib.utils2 import safe_str, fix_PATH, str2bool, safe_unicode
50 from rhodecode.lib.vcs.conf import settings as vcs_settings
50 from rhodecode.lib.vcs.conf import settings as vcs_settings
51 from rhodecode.lib.vcs.backends import base
51 from rhodecode.lib.vcs.backends import base
52 from rhodecode.model import meta
52 from rhodecode.model import meta
53 from rhodecode.model.db import User, Repository, PullRequest
53 from rhodecode.model.db import User, Repository, PullRequest
54 from rhodecode.model.scm import ScmModel
54 from rhodecode.model.scm import ScmModel
55 from rhodecode.model.pull_request import PullRequestModel
55 from rhodecode.model.pull_request import PullRequestModel
56
56
57
57
58 log = logging.getLogger(__name__)
58 log = logging.getLogger(__name__)
59
59
60
60
61 def initialize_generator(factory):
61 def initialize_generator(factory):
62 """
62 """
63 Initializes the returned generator by draining its first element.
63 Initializes the returned generator by draining its first element.
64
64
65 This can be used to give a generator an initializer, which is the code
65 This can be used to give a generator an initializer, which is the code
66 up to the first yield statement. This decorator enforces that the first
66 up to the first yield statement. This decorator enforces that the first
67 produced element has the value ``"__init__"`` to make its special
67 produced element has the value ``"__init__"`` to make its special
68 purpose very explicit in the using code.
68 purpose very explicit in the using code.
69 """
69 """
70
70
71 @wraps(factory)
71 @wraps(factory)
72 def wrapper(*args, **kwargs):
72 def wrapper(*args, **kwargs):
73 gen = factory(*args, **kwargs)
73 gen = factory(*args, **kwargs)
74 try:
74 try:
75 init = gen.next()
75 init = gen.next()
76 except StopIteration:
76 except StopIteration:
77 raise ValueError('Generator must yield at least one element.')
77 raise ValueError('Generator must yield at least one element.')
78 if init != "__init__":
78 if init != "__init__":
79 raise ValueError('First yielded element must be "__init__".')
79 raise ValueError('First yielded element must be "__init__".')
80 return gen
80 return gen
81 return wrapper
81 return wrapper
82
82
83
83
84 class SimpleVCS(object):
84 class SimpleVCS(object):
85 """Common functionality for SCM HTTP handlers."""
85 """Common functionality for SCM HTTP handlers."""
86
86
87 SCM = 'unknown'
87 SCM = 'unknown'
88
88
89 acl_repo_name = None
89 acl_repo_name = None
90 url_repo_name = None
90 url_repo_name = None
91 vcs_repo_name = None
91 vcs_repo_name = None
92
92
93 # We have to handle requests to shadow repositories different than requests
93 # We have to handle requests to shadow repositories different than requests
94 # to normal repositories. Therefore we have to distinguish them. To do this
94 # to normal repositories. Therefore we have to distinguish them. To do this
95 # we use this regex which will match only on URLs pointing to shadow
95 # we use this regex which will match only on URLs pointing to shadow
96 # repositories.
96 # repositories.
97 shadow_repo_re = re.compile(
97 shadow_repo_re = re.compile(
98 '(?P<groups>(?:{slug_pat}/)*)' # repo groups
98 '(?P<groups>(?:{slug_pat}/)*)' # repo groups
99 '(?P<target>{slug_pat})/' # target repo
99 '(?P<target>{slug_pat})/' # target repo
100 'pull-request/(?P<pr_id>\d+)/' # pull request
100 'pull-request/(?P<pr_id>\d+)/' # pull request
101 'repository$' # shadow repo
101 'repository$' # shadow repo
102 .format(slug_pat=SLUG_RE.pattern))
102 .format(slug_pat=SLUG_RE.pattern))
103
103
104 def __init__(self, application, config, registry):
104 def __init__(self, application, config, registry):
105 self.registry = registry
105 self.registry = registry
106 self.application = application
106 self.application = application
107 self.config = config
107 self.config = config
108 # re-populated by specialized middleware
108 # re-populated by specialized middleware
109 self.repo_vcs_config = base.Config()
109 self.repo_vcs_config = base.Config()
110
110
111 # base path of repo locations
111 # base path of repo locations
112 self.basepath = get_rhodecode_base_path()
112 self.basepath = get_rhodecode_base_path()
113 # authenticate this VCS request using authfunc
113 # authenticate this VCS request using authfunc
114 auth_ret_code_detection = \
114 auth_ret_code_detection = \
115 str2bool(self.config.get('auth_ret_code_detection', False))
115 str2bool(self.config.get('auth_ret_code_detection', False))
116 self.authenticate = BasicAuth(
116 self.authenticate = BasicAuth(
117 '', authenticate, registry, config.get('auth_ret_code'),
117 '', authenticate, registry, config.get('auth_ret_code'),
118 auth_ret_code_detection)
118 auth_ret_code_detection)
119 self.ip_addr = '0.0.0.0'
119 self.ip_addr = '0.0.0.0'
120
120
121 def set_repo_names(self, environ):
121 def set_repo_names(self, environ):
122 """
122 """
123 This will populate the attributes acl_repo_name, url_repo_name,
123 This will populate the attributes acl_repo_name, url_repo_name,
124 vcs_repo_name and is_shadow_repo. In case of requests to normal (non
124 vcs_repo_name and is_shadow_repo. In case of requests to normal (non
125 shadow) repositories all names are equal. In case of requests to a
125 shadow) repositories all names are equal. In case of requests to a
126 shadow repository the acl-name points to the target repo of the pull
126 shadow repository the acl-name points to the target repo of the pull
127 request and the vcs-name points to the shadow repo file system path.
127 request and the vcs-name points to the shadow repo file system path.
128 The url-name is always the URL used by the vcs client program.
128 The url-name is always the URL used by the vcs client program.
129
129
130 Example in case of a shadow repo:
130 Example in case of a shadow repo:
131 acl_repo_name = RepoGroup/MyRepo
131 acl_repo_name = RepoGroup/MyRepo
132 url_repo_name = RepoGroup/MyRepo/pull-request/3/repository
132 url_repo_name = RepoGroup/MyRepo/pull-request/3/repository
133 vcs_repo_name = /repo/base/path/RepoGroup/.__shadow_MyRepo_pr-3'
133 vcs_repo_name = /repo/base/path/RepoGroup/.__shadow_MyRepo_pr-3'
134 """
134 """
135 # First we set the repo name from URL for all attributes. This is the
135 # First we set the repo name from URL for all attributes. This is the
136 # default if handling normal (non shadow) repo requests.
136 # default if handling normal (non shadow) repo requests.
137 self.url_repo_name = self._get_repository_name(environ)
137 self.url_repo_name = self._get_repository_name(environ)
138 self.acl_repo_name = self.vcs_repo_name = self.url_repo_name
138 self.acl_repo_name = self.vcs_repo_name = self.url_repo_name
139 self.is_shadow_repo = False
139 self.is_shadow_repo = False
140
140
141 # Check if this is a request to a shadow repository.
141 # Check if this is a request to a shadow repository.
142 match = self.shadow_repo_re.match(self.url_repo_name)
142 match = self.shadow_repo_re.match(self.url_repo_name)
143 if match:
143 if match:
144 match_dict = match.groupdict()
144 match_dict = match.groupdict()
145
145
146 # Build acl repo name from regex match.
146 # Build acl repo name from regex match.
147 acl_repo_name = safe_unicode('{groups}{target}'.format(
147 acl_repo_name = safe_unicode('{groups}{target}'.format(
148 groups=match_dict['groups'] or '',
148 groups=match_dict['groups'] or '',
149 target=match_dict['target']))
149 target=match_dict['target']))
150
150
151 # Retrieve pull request instance by ID from regex match.
151 # Retrieve pull request instance by ID from regex match.
152 pull_request = PullRequest.get(match_dict['pr_id'])
152 pull_request = PullRequest.get(match_dict['pr_id'])
153
153
154 # Only proceed if we got a pull request and if acl repo name from
154 # Only proceed if we got a pull request and if acl repo name from
155 # URL equals the target repo name of the pull request.
155 # URL equals the target repo name of the pull request.
156 if pull_request and (acl_repo_name ==
156 if pull_request and (acl_repo_name ==
157 pull_request.target_repo.repo_name):
157 pull_request.target_repo.repo_name):
158 # Get file system path to shadow repository.
158 # Get file system path to shadow repository.
159 workspace_id = PullRequestModel()._workspace_id(pull_request)
159 workspace_id = PullRequestModel()._workspace_id(pull_request)
160 target_vcs = pull_request.target_repo.scm_instance()
160 target_vcs = pull_request.target_repo.scm_instance()
161 vcs_repo_name = target_vcs._get_shadow_repository_path(
161 vcs_repo_name = target_vcs._get_shadow_repository_path(
162 workspace_id)
162 workspace_id)
163
163
164 # Store names for later usage.
164 # Store names for later usage.
165 self.vcs_repo_name = vcs_repo_name
165 self.vcs_repo_name = vcs_repo_name
166 self.acl_repo_name = acl_repo_name
166 self.acl_repo_name = acl_repo_name
167 self.is_shadow_repo = True
167 self.is_shadow_repo = True
168
168
169 log.debug('Setting all VCS repository names: %s', {
169 log.debug('Setting all VCS repository names: %s', {
170 'acl_repo_name': self.acl_repo_name,
170 'acl_repo_name': self.acl_repo_name,
171 'url_repo_name': self.url_repo_name,
171 'url_repo_name': self.url_repo_name,
172 'vcs_repo_name': self.vcs_repo_name,
172 'vcs_repo_name': self.vcs_repo_name,
173 })
173 })
174
174
175 @property
175 @property
176 def scm_app(self):
176 def scm_app(self):
177 custom_implementation = self.config['vcs.scm_app_implementation']
177 custom_implementation = self.config['vcs.scm_app_implementation']
178 if custom_implementation == 'http':
178 if custom_implementation == 'http':
179 log.info('Using HTTP implementation of scm app.')
179 log.info('Using HTTP implementation of scm app.')
180 scm_app_impl = scm_app_http
180 scm_app_impl = scm_app_http
181 else:
181 else:
182 log.info('Using custom implementation of scm_app: "{}"'.format(
182 log.info('Using custom implementation of scm_app: "{}"'.format(
183 custom_implementation))
183 custom_implementation))
184 scm_app_impl = importlib.import_module(custom_implementation)
184 scm_app_impl = importlib.import_module(custom_implementation)
185 return scm_app_impl
185 return scm_app_impl
186
186
187 def _get_by_id(self, repo_name):
187 def _get_by_id(self, repo_name):
188 """
188 """
189 Gets a special pattern _<ID> from clone url and tries to replace it
189 Gets a special pattern _<ID> from clone url and tries to replace it
190 with a repository_name for support of _<ID> non changeable urls
190 with a repository_name for support of _<ID> non changeable urls
191 """
191 """
192
192
193 data = repo_name.split('/')
193 data = repo_name.split('/')
194 if len(data) >= 2:
194 if len(data) >= 2:
195 from rhodecode.model.repo import RepoModel
195 from rhodecode.model.repo import RepoModel
196 by_id_match = RepoModel().get_repo_by_id(repo_name)
196 by_id_match = RepoModel().get_repo_by_id(repo_name)
197 if by_id_match:
197 if by_id_match:
198 data[1] = by_id_match.repo_name
198 data[1] = by_id_match.repo_name
199
199
200 return safe_str('/'.join(data))
200 return safe_str('/'.join(data))
201
201
202 def _invalidate_cache(self, repo_name):
202 def _invalidate_cache(self, repo_name):
203 """
203 """
204 Set's cache for this repository for invalidation on next access
204 Set's cache for this repository for invalidation on next access
205
205
206 :param repo_name: full repo name, also a cache key
206 :param repo_name: full repo name, also a cache key
207 """
207 """
208 ScmModel().mark_for_invalidation(repo_name)
208 ScmModel().mark_for_invalidation(repo_name)
209
209
210 def is_valid_and_existing_repo(self, repo_name, base_path, scm_type):
210 def is_valid_and_existing_repo(self, repo_name, base_path, scm_type):
211 db_repo = Repository.get_by_repo_name(repo_name)
211 db_repo = Repository.get_by_repo_name(repo_name)
212 if not db_repo:
212 if not db_repo:
213 log.debug('Repository `%s` not found inside the database.',
213 log.debug('Repository `%s` not found inside the database.',
214 repo_name)
214 repo_name)
215 return False
215 return False
216
216
217 if db_repo.repo_type != scm_type:
217 if db_repo.repo_type != scm_type:
218 log.warning(
218 log.warning(
219 'Repository `%s` have incorrect scm_type, expected %s got %s',
219 'Repository `%s` have incorrect scm_type, expected %s got %s',
220 repo_name, db_repo.repo_type, scm_type)
220 repo_name, db_repo.repo_type, scm_type)
221 return False
221 return False
222
222
223 return is_valid_repo(repo_name, base_path, explicit_scm=scm_type)
223 return is_valid_repo(repo_name, base_path, explicit_scm=scm_type)
224
224
225 def valid_and_active_user(self, user):
225 def valid_and_active_user(self, user):
226 """
226 """
227 Checks if that user is not empty, and if it's actually object it checks
227 Checks if that user is not empty, and if it's actually object it checks
228 if he's active.
228 if he's active.
229
229
230 :param user: user object or None
230 :param user: user object or None
231 :return: boolean
231 :return: boolean
232 """
232 """
233 if user is None:
233 if user is None:
234 return False
234 return False
235
235
236 elif user.active:
236 elif user.active:
237 return True
237 return True
238
238
239 return False
239 return False
240
240
241 @property
242 def is_shadow_repo_dir(self):
243 return os.path.isdir(self.vcs_repo_name)
244
241 def _check_permission(self, action, user, repo_name, ip_addr=None):
245 def _check_permission(self, action, user, repo_name, ip_addr=None):
242 """
246 """
243 Checks permissions using action (push/pull) user and repository
247 Checks permissions using action (push/pull) user and repository
244 name
248 name
245
249
246 :param action: push or pull action
250 :param action: push or pull action
247 :param user: user instance
251 :param user: user instance
248 :param repo_name: repository name
252 :param repo_name: repository name
249 """
253 """
250 # check IP
254 # check IP
251 inherit = user.inherit_default_permissions
255 inherit = user.inherit_default_permissions
252 ip_allowed = AuthUser.check_ip_allowed(user.user_id, ip_addr,
256 ip_allowed = AuthUser.check_ip_allowed(user.user_id, ip_addr,
253 inherit_from_default=inherit)
257 inherit_from_default=inherit)
254 if ip_allowed:
258 if ip_allowed:
255 log.info('Access for IP:%s allowed', ip_addr)
259 log.info('Access for IP:%s allowed', ip_addr)
256 else:
260 else:
257 return False
261 return False
258
262
259 if action == 'push':
263 if action == 'push':
260 if not HasPermissionAnyMiddleware('repository.write',
264 if not HasPermissionAnyMiddleware('repository.write',
261 'repository.admin')(user,
265 'repository.admin')(user,
262 repo_name):
266 repo_name):
263 return False
267 return False
264
268
265 else:
269 else:
266 # any other action need at least read permission
270 # any other action need at least read permission
267 if not HasPermissionAnyMiddleware('repository.read',
271 if not HasPermissionAnyMiddleware('repository.read',
268 'repository.write',
272 'repository.write',
269 'repository.admin')(user,
273 'repository.admin')(user,
270 repo_name):
274 repo_name):
271 return False
275 return False
272
276
273 return True
277 return True
274
278
275 def _check_ssl(self, environ, start_response):
279 def _check_ssl(self, environ, start_response):
276 """
280 """
277 Checks the SSL check flag and returns False if SSL is not present
281 Checks the SSL check flag and returns False if SSL is not present
278 and required True otherwise
282 and required True otherwise
279 """
283 """
280 org_proto = environ['wsgi._org_proto']
284 org_proto = environ['wsgi._org_proto']
281 # check if we have SSL required ! if not it's a bad request !
285 # check if we have SSL required ! if not it's a bad request !
282 require_ssl = str2bool(self.repo_vcs_config.get('web', 'push_ssl'))
286 require_ssl = str2bool(self.repo_vcs_config.get('web', 'push_ssl'))
283 if require_ssl and org_proto == 'http':
287 if require_ssl and org_proto == 'http':
284 log.debug('proto is %s and SSL is required BAD REQUEST !',
288 log.debug('proto is %s and SSL is required BAD REQUEST !',
285 org_proto)
289 org_proto)
286 return False
290 return False
287 return True
291 return True
288
292
289 def __call__(self, environ, start_response):
293 def __call__(self, environ, start_response):
290 try:
294 try:
291 return self._handle_request(environ, start_response)
295 return self._handle_request(environ, start_response)
292 except Exception:
296 except Exception:
293 log.exception("Exception while handling request")
297 log.exception("Exception while handling request")
294 appenlight.track_exception(environ)
298 appenlight.track_exception(environ)
295 return HTTPInternalServerError()(environ, start_response)
299 return HTTPInternalServerError()(environ, start_response)
296 finally:
300 finally:
297 meta.Session.remove()
301 meta.Session.remove()
298
302
299 def _handle_request(self, environ, start_response):
303 def _handle_request(self, environ, start_response):
300
304
301 if not self._check_ssl(environ, start_response):
305 if not self._check_ssl(environ, start_response):
302 reason = ('SSL required, while RhodeCode was unable '
306 reason = ('SSL required, while RhodeCode was unable '
303 'to detect this as SSL request')
307 'to detect this as SSL request')
304 log.debug('User not allowed to proceed, %s', reason)
308 log.debug('User not allowed to proceed, %s', reason)
305 return HTTPNotAcceptable(reason)(environ, start_response)
309 return HTTPNotAcceptable(reason)(environ, start_response)
306
310
307 if not self.url_repo_name:
311 if not self.url_repo_name:
308 log.warning('Repository name is empty: %s', self.url_repo_name)
312 log.warning('Repository name is empty: %s', self.url_repo_name)
309 # failed to get repo name, we fail now
313 # failed to get repo name, we fail now
310 return HTTPNotFound()(environ, start_response)
314 return HTTPNotFound()(environ, start_response)
311 log.debug('Extracted repo name is %s', self.url_repo_name)
315 log.debug('Extracted repo name is %s', self.url_repo_name)
312
316
313 ip_addr = get_ip_addr(environ)
317 ip_addr = get_ip_addr(environ)
314 user_agent = get_user_agent(environ)
318 user_agent = get_user_agent(environ)
315 username = None
319 username = None
316
320
317 # skip passing error to error controller
321 # skip passing error to error controller
318 environ['pylons.status_code_redirect'] = True
322 environ['pylons.status_code_redirect'] = True
319
323
320 # ======================================================================
324 # ======================================================================
321 # GET ACTION PULL or PUSH
325 # GET ACTION PULL or PUSH
322 # ======================================================================
326 # ======================================================================
323 action = self._get_action(environ)
327 action = self._get_action(environ)
324
328
325 # ======================================================================
329 # ======================================================================
326 # Check if this is a request to a shadow repository of a pull request.
330 # Check if this is a request to a shadow repository of a pull request.
327 # In this case only pull action is allowed.
331 # In this case only pull action is allowed.
328 # ======================================================================
332 # ======================================================================
329 if self.is_shadow_repo and action != 'pull':
333 if self.is_shadow_repo and action != 'pull':
330 reason = 'Only pull action is allowed for shadow repositories.'
334 reason = 'Only pull action is allowed for shadow repositories.'
331 log.debug('User not allowed to proceed, %s', reason)
335 log.debug('User not allowed to proceed, %s', reason)
332 return HTTPNotAcceptable(reason)(environ, start_response)
336 return HTTPNotAcceptable(reason)(environ, start_response)
333
337
338 # Check if the shadow repo actually exists, in case someone refers
339 # to it, and it has been deleted because of successful merge.
340 if self.is_shadow_repo and not self.is_shadow_repo_dir:
341 return HTTPNotFound()(environ, start_response)
342
334 # ======================================================================
343 # ======================================================================
335 # CHECK ANONYMOUS PERMISSION
344 # CHECK ANONYMOUS PERMISSION
336 # ======================================================================
345 # ======================================================================
337 if action in ['pull', 'push']:
346 if action in ['pull', 'push']:
338 anonymous_user = User.get_default_user()
347 anonymous_user = User.get_default_user()
339 username = anonymous_user.username
348 username = anonymous_user.username
340 if anonymous_user.active:
349 if anonymous_user.active:
341 # ONLY check permissions if the user is activated
350 # ONLY check permissions if the user is activated
342 anonymous_perm = self._check_permission(
351 anonymous_perm = self._check_permission(
343 action, anonymous_user, self.acl_repo_name, ip_addr)
352 action, anonymous_user, self.acl_repo_name, ip_addr)
344 else:
353 else:
345 anonymous_perm = False
354 anonymous_perm = False
346
355
347 if not anonymous_user.active or not anonymous_perm:
356 if not anonymous_user.active or not anonymous_perm:
348 if not anonymous_user.active:
357 if not anonymous_user.active:
349 log.debug('Anonymous access is disabled, running '
358 log.debug('Anonymous access is disabled, running '
350 'authentication')
359 'authentication')
351
360
352 if not anonymous_perm:
361 if not anonymous_perm:
353 log.debug('Not enough credentials to access this '
362 log.debug('Not enough credentials to access this '
354 'repository as anonymous user')
363 'repository as anonymous user')
355
364
356 username = None
365 username = None
357 # ==============================================================
366 # ==============================================================
358 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
367 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
359 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
368 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
360 # ==============================================================
369 # ==============================================================
361
370
362 # try to auth based on environ, container auth methods
371 # try to auth based on environ, container auth methods
363 log.debug('Running PRE-AUTH for container based authentication')
372 log.debug('Running PRE-AUTH for container based authentication')
364 pre_auth = authenticate(
373 pre_auth = authenticate(
365 '', '', environ, VCS_TYPE, registry=self.registry,
374 '', '', environ, VCS_TYPE, registry=self.registry,
366 acl_repo_name=self.acl_repo_name)
375 acl_repo_name=self.acl_repo_name)
367 if pre_auth and pre_auth.get('username'):
376 if pre_auth and pre_auth.get('username'):
368 username = pre_auth['username']
377 username = pre_auth['username']
369 log.debug('PRE-AUTH got %s as username', username)
378 log.debug('PRE-AUTH got %s as username', username)
370
379
371 # If not authenticated by the container, running basic auth
380 # If not authenticated by the container, running basic auth
372 # before inject the calling repo_name for special scope checks
381 # before inject the calling repo_name for special scope checks
373 self.authenticate.acl_repo_name = self.acl_repo_name
382 self.authenticate.acl_repo_name = self.acl_repo_name
374 if not username:
383 if not username:
375 self.authenticate.realm = get_rhodecode_realm()
384 self.authenticate.realm = get_rhodecode_realm()
376
385
377 try:
386 try:
378 result = self.authenticate(environ)
387 result = self.authenticate(environ)
379 except (UserCreationError, NotAllowedToCreateUserError) as e:
388 except (UserCreationError, NotAllowedToCreateUserError) as e:
380 log.error(e)
389 log.error(e)
381 reason = safe_str(e)
390 reason = safe_str(e)
382 return HTTPNotAcceptable(reason)(environ, start_response)
391 return HTTPNotAcceptable(reason)(environ, start_response)
383
392
384 if isinstance(result, str):
393 if isinstance(result, str):
385 AUTH_TYPE.update(environ, 'basic')
394 AUTH_TYPE.update(environ, 'basic')
386 REMOTE_USER.update(environ, result)
395 REMOTE_USER.update(environ, result)
387 username = result
396 username = result
388 else:
397 else:
389 return result.wsgi_application(environ, start_response)
398 return result.wsgi_application(environ, start_response)
390
399
391 # ==============================================================
400 # ==============================================================
392 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
401 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
393 # ==============================================================
402 # ==============================================================
394 user = User.get_by_username(username)
403 user = User.get_by_username(username)
395 if not self.valid_and_active_user(user):
404 if not self.valid_and_active_user(user):
396 return HTTPForbidden()(environ, start_response)
405 return HTTPForbidden()(environ, start_response)
397 username = user.username
406 username = user.username
398 user.update_lastactivity()
407 user.update_lastactivity()
399 meta.Session().commit()
408 meta.Session().commit()
400
409
401 # check user attributes for password change flag
410 # check user attributes for password change flag
402 user_obj = user
411 user_obj = user
403 if user_obj and user_obj.username != User.DEFAULT_USER and \
412 if user_obj and user_obj.username != User.DEFAULT_USER and \
404 user_obj.user_data.get('force_password_change'):
413 user_obj.user_data.get('force_password_change'):
405 reason = 'password change required'
414 reason = 'password change required'
406 log.debug('User not allowed to authenticate, %s', reason)
415 log.debug('User not allowed to authenticate, %s', reason)
407 return HTTPNotAcceptable(reason)(environ, start_response)
416 return HTTPNotAcceptable(reason)(environ, start_response)
408
417
409 # check permissions for this repository
418 # check permissions for this repository
410 perm = self._check_permission(
419 perm = self._check_permission(
411 action, user, self.acl_repo_name, ip_addr)
420 action, user, self.acl_repo_name, ip_addr)
412 if not perm:
421 if not perm:
413 return HTTPForbidden()(environ, start_response)
422 return HTTPForbidden()(environ, start_response)
414
423
415 # extras are injected into UI object and later available
424 # extras are injected into UI object and later available
416 # in hooks executed by rhodecode
425 # in hooks executed by rhodecode
417 check_locking = _should_check_locking(environ.get('QUERY_STRING'))
426 check_locking = _should_check_locking(environ.get('QUERY_STRING'))
418 extras = vcs_operation_context(
427 extras = vcs_operation_context(
419 environ, repo_name=self.acl_repo_name, username=username,
428 environ, repo_name=self.acl_repo_name, username=username,
420 action=action, scm=self.SCM, check_locking=check_locking,
429 action=action, scm=self.SCM, check_locking=check_locking,
421 is_shadow_repo=self.is_shadow_repo
430 is_shadow_repo=self.is_shadow_repo
422 )
431 )
423
432
424 # ======================================================================
433 # ======================================================================
425 # REQUEST HANDLING
434 # REQUEST HANDLING
426 # ======================================================================
435 # ======================================================================
427 repo_path = os.path.join(
436 repo_path = os.path.join(
428 safe_str(self.basepath), safe_str(self.vcs_repo_name))
437 safe_str(self.basepath), safe_str(self.vcs_repo_name))
429 log.debug('Repository path is %s', repo_path)
438 log.debug('Repository path is %s', repo_path)
430
439
431 fix_PATH()
440 fix_PATH()
432
441
433 log.info(
442 log.info(
434 '%s action on %s repo "%s" by "%s" from %s %s',
443 '%s action on %s repo "%s" by "%s" from %s %s',
435 action, self.SCM, safe_str(self.url_repo_name),
444 action, self.SCM, safe_str(self.url_repo_name),
436 safe_str(username), ip_addr, user_agent)
445 safe_str(username), ip_addr, user_agent)
437
446
438 return self._generate_vcs_response(
447 return self._generate_vcs_response(
439 environ, start_response, repo_path, extras, action)
448 environ, start_response, repo_path, extras, action)
440
449
441 @initialize_generator
450 @initialize_generator
442 def _generate_vcs_response(
451 def _generate_vcs_response(
443 self, environ, start_response, repo_path, extras, action):
452 self, environ, start_response, repo_path, extras, action):
444 """
453 """
445 Returns a generator for the response content.
454 Returns a generator for the response content.
446
455
447 This method is implemented as a generator, so that it can trigger
456 This method is implemented as a generator, so that it can trigger
448 the cache validation after all content sent back to the client. It
457 the cache validation after all content sent back to the client. It
449 also handles the locking exceptions which will be triggered when
458 also handles the locking exceptions which will be triggered when
450 the first chunk is produced by the underlying WSGI application.
459 the first chunk is produced by the underlying WSGI application.
451 """
460 """
452 callback_daemon, extras = self._prepare_callback_daemon(extras)
461 callback_daemon, extras = self._prepare_callback_daemon(extras)
453 config = self._create_config(extras, self.acl_repo_name)
462 config = self._create_config(extras, self.acl_repo_name)
454 log.debug('HOOKS extras is %s', extras)
463 log.debug('HOOKS extras is %s', extras)
455 app = self._create_wsgi_app(repo_path, self.url_repo_name, config)
464 app = self._create_wsgi_app(repo_path, self.url_repo_name, config)
456
465
457 try:
466 try:
458 with callback_daemon:
467 with callback_daemon:
459 try:
468 try:
460 response = app(environ, start_response)
469 response = app(environ, start_response)
461 finally:
470 finally:
462 # This statement works together with the decorator
471 # This statement works together with the decorator
463 # "initialize_generator" above. The decorator ensures that
472 # "initialize_generator" above. The decorator ensures that
464 # we hit the first yield statement before the generator is
473 # we hit the first yield statement before the generator is
465 # returned back to the WSGI server. This is needed to
474 # returned back to the WSGI server. This is needed to
466 # ensure that the call to "app" above triggers the
475 # ensure that the call to "app" above triggers the
467 # needed callback to "start_response" before the
476 # needed callback to "start_response" before the
468 # generator is actually used.
477 # generator is actually used.
469 yield "__init__"
478 yield "__init__"
470
479
471 for chunk in response:
480 for chunk in response:
472 yield chunk
481 yield chunk
473 except Exception as exc:
482 except Exception as exc:
474 # TODO: martinb: Exceptions are only raised in case of the Pyro4
483 # TODO: martinb: Exceptions are only raised in case of the Pyro4
475 # backend. Refactor this except block after dropping Pyro4 support.
484 # backend. Refactor this except block after dropping Pyro4 support.
476 # TODO: johbo: Improve "translating" back the exception.
485 # TODO: johbo: Improve "translating" back the exception.
477 if getattr(exc, '_vcs_kind', None) == 'repo_locked':
486 if getattr(exc, '_vcs_kind', None) == 'repo_locked':
478 exc = HTTPLockedRC(*exc.args)
487 exc = HTTPLockedRC(*exc.args)
479 _code = rhodecode.CONFIG.get('lock_ret_code')
488 _code = rhodecode.CONFIG.get('lock_ret_code')
480 log.debug('Repository LOCKED ret code %s!', (_code,))
489 log.debug('Repository LOCKED ret code %s!', (_code,))
481 elif getattr(exc, '_vcs_kind', None) == 'requirement':
490 elif getattr(exc, '_vcs_kind', None) == 'requirement':
482 log.debug(
491 log.debug(
483 'Repository requires features unknown to this Mercurial')
492 'Repository requires features unknown to this Mercurial')
484 exc = HTTPRequirementError(*exc.args)
493 exc = HTTPRequirementError(*exc.args)
485 else:
494 else:
486 raise
495 raise
487
496
488 for chunk in exc(environ, start_response):
497 for chunk in exc(environ, start_response):
489 yield chunk
498 yield chunk
490 finally:
499 finally:
491 # invalidate cache on push
500 # invalidate cache on push
492 try:
501 try:
493 if action == 'push':
502 if action == 'push':
494 self._invalidate_cache(self.url_repo_name)
503 self._invalidate_cache(self.url_repo_name)
495 finally:
504 finally:
496 meta.Session.remove()
505 meta.Session.remove()
497
506
498 def _get_repository_name(self, environ):
507 def _get_repository_name(self, environ):
499 """Get repository name out of the environmnent
508 """Get repository name out of the environmnent
500
509
501 :param environ: WSGI environment
510 :param environ: WSGI environment
502 """
511 """
503 raise NotImplementedError()
512 raise NotImplementedError()
504
513
505 def _get_action(self, environ):
514 def _get_action(self, environ):
506 """Map request commands into a pull or push command.
515 """Map request commands into a pull or push command.
507
516
508 :param environ: WSGI environment
517 :param environ: WSGI environment
509 """
518 """
510 raise NotImplementedError()
519 raise NotImplementedError()
511
520
512 def _create_wsgi_app(self, repo_path, repo_name, config):
521 def _create_wsgi_app(self, repo_path, repo_name, config):
513 """Return the WSGI app that will finally handle the request."""
522 """Return the WSGI app that will finally handle the request."""
514 raise NotImplementedError()
523 raise NotImplementedError()
515
524
516 def _create_config(self, extras, repo_name):
525 def _create_config(self, extras, repo_name):
517 """Create a safe config representation."""
526 """Create a safe config representation."""
518 raise NotImplementedError()
527 raise NotImplementedError()
519
528
520 def _prepare_callback_daemon(self, extras):
529 def _prepare_callback_daemon(self, extras):
521 return prepare_callback_daemon(
530 return prepare_callback_daemon(
522 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
531 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
523 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
532 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
524
533
525
534
526 def _should_check_locking(query_string):
535 def _should_check_locking(query_string):
527 # this is kind of hacky, but due to how mercurial handles client-server
536 # this is kind of hacky, but due to how mercurial handles client-server
528 # server see all operation on commit; bookmarks, phases and
537 # server see all operation on commit; bookmarks, phases and
529 # obsolescence marker in different transaction, we don't want to check
538 # obsolescence marker in different transaction, we don't want to check
530 # locking on those
539 # locking on those
531 return query_string not in ['cmd=listkeys']
540 return query_string not in ['cmd=listkeys']
@@ -1,455 +1,485 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import base64
21 import base64
22
22
23 import mock
23 import mock
24 import pytest
24 import pytest
25
25
26 from rhodecode.tests.utils import CustomTestApp
26 from rhodecode.tests.utils import CustomTestApp
27
27
28 from rhodecode.lib.caching_query import FromCache
28 from rhodecode.lib.caching_query import FromCache
29 from rhodecode.lib.hooks_daemon import DummyHooksCallbackDaemon
29 from rhodecode.lib.hooks_daemon import DummyHooksCallbackDaemon
30 from rhodecode.lib.middleware import simplevcs
30 from rhodecode.lib.middleware import simplevcs
31 from rhodecode.lib.middleware.https_fixup import HttpsFixup
31 from rhodecode.lib.middleware.https_fixup import HttpsFixup
32 from rhodecode.lib.middleware.utils import scm_app_http
32 from rhodecode.lib.middleware.utils import scm_app_http
33 from rhodecode.model.db import User, _hash_key
33 from rhodecode.model.db import User, _hash_key
34 from rhodecode.model.meta import Session
34 from rhodecode.model.meta import Session
35 from rhodecode.tests import (
35 from rhodecode.tests import (
36 HG_REPO, TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS)
36 HG_REPO, TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS)
37 from rhodecode.tests.lib.middleware import mock_scm_app
37 from rhodecode.tests.lib.middleware import mock_scm_app
38
38
39
39
40 class StubVCSController(simplevcs.SimpleVCS):
40 class StubVCSController(simplevcs.SimpleVCS):
41
41
42 SCM = 'hg'
42 SCM = 'hg'
43 stub_response_body = tuple()
43 stub_response_body = tuple()
44
44
45 def __init__(self, *args, **kwargs):
45 def __init__(self, *args, **kwargs):
46 super(StubVCSController, self).__init__(*args, **kwargs)
46 super(StubVCSController, self).__init__(*args, **kwargs)
47 self._action = 'pull'
47 self._action = 'pull'
48 self._is_shadow_repo_dir = True
48 self._name = HG_REPO
49 self._name = HG_REPO
49 self.set_repo_names(None)
50 self.set_repo_names(None)
50
51
52 @property
53 def is_shadow_repo_dir(self):
54 return self._is_shadow_repo_dir
55
51 def _get_repository_name(self, environ):
56 def _get_repository_name(self, environ):
52 return self._name
57 return self._name
53
58
54 def _get_action(self, environ):
59 def _get_action(self, environ):
55 return self._action
60 return self._action
56
61
57 def _create_wsgi_app(self, repo_path, repo_name, config):
62 def _create_wsgi_app(self, repo_path, repo_name, config):
58 def fake_app(environ, start_response):
63 def fake_app(environ, start_response):
59 headers = [
64 headers = [
60 ('Http-Accept', 'application/mercurial')
65 ('Http-Accept', 'application/mercurial')
61 ]
66 ]
62 start_response('200 OK', headers)
67 start_response('200 OK', headers)
63 return self.stub_response_body
68 return self.stub_response_body
64 return fake_app
69 return fake_app
65
70
66 def _create_config(self, extras, repo_name):
71 def _create_config(self, extras, repo_name):
67 return None
72 return None
68
73
69
74
70 @pytest.fixture
75 @pytest.fixture
71 def vcscontroller(pylonsapp, config_stub):
76 def vcscontroller(pylonsapp, config_stub):
72 config_stub.testing_securitypolicy()
77 config_stub.testing_securitypolicy()
73 config_stub.include('rhodecode.authentication')
78 config_stub.include('rhodecode.authentication')
74
79
75 #set_anonymous_access(True)
80 #set_anonymous_access(True)
76 controller = StubVCSController(pylonsapp, pylonsapp.config, None)
81 controller = StubVCSController(pylonsapp, pylonsapp.config, None)
77 app = HttpsFixup(controller, pylonsapp.config)
82 app = HttpsFixup(controller, pylonsapp.config)
78 app = CustomTestApp(app)
83 app = CustomTestApp(app)
79
84
80 _remove_default_user_from_query_cache()
85 _remove_default_user_from_query_cache()
81
86
82 # Sanity checks that things are set up correctly
87 # Sanity checks that things are set up correctly
83 app.get('/' + HG_REPO, status=200)
88 app.get('/' + HG_REPO, status=200)
84
89
85 app.controller = controller
90 app.controller = controller
86 return app
91 return app
87
92
88
93
89 def _remove_default_user_from_query_cache():
94 def _remove_default_user_from_query_cache():
90 user = User.get_default_user(cache=True)
95 user = User.get_default_user(cache=True)
91 query = Session().query(User).filter(User.username == user.username)
96 query = Session().query(User).filter(User.username == user.username)
92 query = query.options(
97 query = query.options(
93 FromCache("sql_cache_short", "get_user_%s" % _hash_key(user.username)))
98 FromCache("sql_cache_short", "get_user_%s" % _hash_key(user.username)))
94 query.invalidate()
99 query.invalidate()
95 Session().expire(user)
100 Session().expire(user)
96
101
97
102
98 def test_handles_exceptions_during_permissions_checks(
103 def test_handles_exceptions_during_permissions_checks(
99 vcscontroller, disable_anonymous_user):
104 vcscontroller, disable_anonymous_user):
100 user_and_pass = '%s:%s' % (TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS)
105 user_and_pass = '%s:%s' % (TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS)
101 auth_password = base64.encodestring(user_and_pass).strip()
106 auth_password = base64.encodestring(user_and_pass).strip()
102 extra_environ = {
107 extra_environ = {
103 'AUTH_TYPE': 'Basic',
108 'AUTH_TYPE': 'Basic',
104 'HTTP_AUTHORIZATION': 'Basic %s' % auth_password,
109 'HTTP_AUTHORIZATION': 'Basic %s' % auth_password,
105 'REMOTE_USER': TEST_USER_ADMIN_LOGIN,
110 'REMOTE_USER': TEST_USER_ADMIN_LOGIN,
106 }
111 }
107
112
108 # Verify that things are hooked up correctly
113 # Verify that things are hooked up correctly
109 vcscontroller.get('/', status=200, extra_environ=extra_environ)
114 vcscontroller.get('/', status=200, extra_environ=extra_environ)
110
115
111 # Simulate trouble during permission checks
116 # Simulate trouble during permission checks
112 with mock.patch('rhodecode.model.db.User.get_by_username',
117 with mock.patch('rhodecode.model.db.User.get_by_username',
113 side_effect=Exception) as get_user:
118 side_effect=Exception) as get_user:
114 # Verify that a correct 500 is returned and check that the expected
119 # Verify that a correct 500 is returned and check that the expected
115 # code path was hit.
120 # code path was hit.
116 vcscontroller.get('/', status=500, extra_environ=extra_environ)
121 vcscontroller.get('/', status=500, extra_environ=extra_environ)
117 assert get_user.called
122 assert get_user.called
118
123
119
124
120 def test_returns_forbidden_if_no_anonymous_access(
125 def test_returns_forbidden_if_no_anonymous_access(
121 vcscontroller, disable_anonymous_user):
126 vcscontroller, disable_anonymous_user):
122 vcscontroller.get('/', status=401)
127 vcscontroller.get('/', status=401)
123
128
124
129
125 class StubFailVCSController(simplevcs.SimpleVCS):
130 class StubFailVCSController(simplevcs.SimpleVCS):
126 def _handle_request(self, environ, start_response):
131 def _handle_request(self, environ, start_response):
127 raise Exception("BOOM")
132 raise Exception("BOOM")
128
133
129
134
130 @pytest.fixture(scope='module')
135 @pytest.fixture(scope='module')
131 def fail_controller(pylonsapp):
136 def fail_controller(pylonsapp):
132 controller = StubFailVCSController(pylonsapp, pylonsapp.config, None)
137 controller = StubFailVCSController(pylonsapp, pylonsapp.config, None)
133 controller = HttpsFixup(controller, pylonsapp.config)
138 controller = HttpsFixup(controller, pylonsapp.config)
134 controller = CustomTestApp(controller)
139 controller = CustomTestApp(controller)
135 return controller
140 return controller
136
141
137
142
138 def test_handles_exceptions_as_internal_server_error(fail_controller):
143 def test_handles_exceptions_as_internal_server_error(fail_controller):
139 fail_controller.get('/', status=500)
144 fail_controller.get('/', status=500)
140
145
141
146
142 def test_provides_traceback_for_appenlight(fail_controller):
147 def test_provides_traceback_for_appenlight(fail_controller):
143 response = fail_controller.get(
148 response = fail_controller.get(
144 '/', status=500, extra_environ={'appenlight.client': 'fake'})
149 '/', status=500, extra_environ={'appenlight.client': 'fake'})
145 assert 'appenlight.__traceback' in response.request.environ
150 assert 'appenlight.__traceback' in response.request.environ
146
151
147
152
148 def test_provides_utils_scm_app_as_scm_app_by_default(pylonsapp):
153 def test_provides_utils_scm_app_as_scm_app_by_default(pylonsapp):
149 controller = StubVCSController(pylonsapp, pylonsapp.config, None)
154 controller = StubVCSController(pylonsapp, pylonsapp.config, None)
150 assert controller.scm_app is scm_app_http
155 assert controller.scm_app is scm_app_http
151
156
152
157
153 def test_allows_to_override_scm_app_via_config(pylonsapp):
158 def test_allows_to_override_scm_app_via_config(pylonsapp):
154 config = pylonsapp.config.copy()
159 config = pylonsapp.config.copy()
155 config['vcs.scm_app_implementation'] = (
160 config['vcs.scm_app_implementation'] = (
156 'rhodecode.tests.lib.middleware.mock_scm_app')
161 'rhodecode.tests.lib.middleware.mock_scm_app')
157 controller = StubVCSController(pylonsapp, config, None)
162 controller = StubVCSController(pylonsapp, config, None)
158 assert controller.scm_app is mock_scm_app
163 assert controller.scm_app is mock_scm_app
159
164
160
165
161 @pytest.mark.parametrize('query_string, expected', [
166 @pytest.mark.parametrize('query_string, expected', [
162 ('cmd=stub_command', True),
167 ('cmd=stub_command', True),
163 ('cmd=listkeys', False),
168 ('cmd=listkeys', False),
164 ])
169 ])
165 def test_should_check_locking(query_string, expected):
170 def test_should_check_locking(query_string, expected):
166 result = simplevcs._should_check_locking(query_string)
171 result = simplevcs._should_check_locking(query_string)
167 assert result == expected
172 assert result == expected
168
173
169
174
170 class TestShadowRepoRegularExpression(object):
175 class TestShadowRepoRegularExpression(object):
171 pr_segment = 'pull-request'
176 pr_segment = 'pull-request'
172 shadow_segment = 'repository'
177 shadow_segment = 'repository'
173
178
174 @pytest.mark.parametrize('url, expected', [
179 @pytest.mark.parametrize('url, expected', [
175 # repo with/without groups
180 # repo with/without groups
176 ('My-Repo/{pr_segment}/1/{shadow_segment}', True),
181 ('My-Repo/{pr_segment}/1/{shadow_segment}', True),
177 ('Group/My-Repo/{pr_segment}/2/{shadow_segment}', True),
182 ('Group/My-Repo/{pr_segment}/2/{shadow_segment}', True),
178 ('Group/Sub-Group/My-Repo/{pr_segment}/3/{shadow_segment}', True),
183 ('Group/Sub-Group/My-Repo/{pr_segment}/3/{shadow_segment}', True),
179 ('Group/Sub-Group1/Sub-Group2/My-Repo/{pr_segment}/3/{shadow_segment}', True),
184 ('Group/Sub-Group1/Sub-Group2/My-Repo/{pr_segment}/3/{shadow_segment}', True),
180
185
181 # pull request ID
186 # pull request ID
182 ('MyRepo/{pr_segment}/1/{shadow_segment}', True),
187 ('MyRepo/{pr_segment}/1/{shadow_segment}', True),
183 ('MyRepo/{pr_segment}/1234567890/{shadow_segment}', True),
188 ('MyRepo/{pr_segment}/1234567890/{shadow_segment}', True),
184 ('MyRepo/{pr_segment}/-1/{shadow_segment}', False),
189 ('MyRepo/{pr_segment}/-1/{shadow_segment}', False),
185 ('MyRepo/{pr_segment}/invalid/{shadow_segment}', False),
190 ('MyRepo/{pr_segment}/invalid/{shadow_segment}', False),
186
191
187 # unicode
192 # unicode
188 (u'Sp€çîál-Repö/{pr_segment}/1/{shadow_segment}', True),
193 (u'Sp€çîál-Repö/{pr_segment}/1/{shadow_segment}', True),
189 (u'Sp€çîál-Gröüp/Sp€çîál-Repö/{pr_segment}/1/{shadow_segment}', True),
194 (u'Sp€çîál-Gröüp/Sp€çîál-Repö/{pr_segment}/1/{shadow_segment}', True),
190
195
191 # trailing/leading slash
196 # trailing/leading slash
192 ('/My-Repo/{pr_segment}/1/{shadow_segment}', False),
197 ('/My-Repo/{pr_segment}/1/{shadow_segment}', False),
193 ('My-Repo/{pr_segment}/1/{shadow_segment}/', False),
198 ('My-Repo/{pr_segment}/1/{shadow_segment}/', False),
194 ('/My-Repo/{pr_segment}/1/{shadow_segment}/', False),
199 ('/My-Repo/{pr_segment}/1/{shadow_segment}/', False),
195
200
196 # misc
201 # misc
197 ('My-Repo/{pr_segment}/1/{shadow_segment}/extra', False),
202 ('My-Repo/{pr_segment}/1/{shadow_segment}/extra', False),
198 ('My-Repo/{pr_segment}/1/{shadow_segment}extra', False),
203 ('My-Repo/{pr_segment}/1/{shadow_segment}extra', False),
199 ])
204 ])
200 def test_shadow_repo_regular_expression(self, url, expected):
205 def test_shadow_repo_regular_expression(self, url, expected):
201 from rhodecode.lib.middleware.simplevcs import SimpleVCS
206 from rhodecode.lib.middleware.simplevcs import SimpleVCS
202 url = url.format(
207 url = url.format(
203 pr_segment=self.pr_segment,
208 pr_segment=self.pr_segment,
204 shadow_segment=self.shadow_segment)
209 shadow_segment=self.shadow_segment)
205 match_obj = SimpleVCS.shadow_repo_re.match(url)
210 match_obj = SimpleVCS.shadow_repo_re.match(url)
206 assert (match_obj is not None) == expected
211 assert (match_obj is not None) == expected
207
212
208
213
209 @pytest.mark.backends('git', 'hg')
214 @pytest.mark.backends('git', 'hg')
210 class TestShadowRepoExposure(object):
215 class TestShadowRepoExposure(object):
211
216
212 def test_pull_on_shadow_repo_propagates_to_wsgi_app(self, pylonsapp):
217 def test_pull_on_shadow_repo_propagates_to_wsgi_app(self, pylonsapp):
213 """
218 """
214 Check that a pull action to a shadow repo is propagated to the
219 Check that a pull action to a shadow repo is propagated to the
215 underlying wsgi app.
220 underlying wsgi app.
216 """
221 """
217 controller = StubVCSController(pylonsapp, pylonsapp.config, None)
222 controller = StubVCSController(pylonsapp, pylonsapp.config, None)
218 controller._check_ssl = mock.Mock()
223 controller._check_ssl = mock.Mock()
219 controller.is_shadow_repo = True
224 controller.is_shadow_repo = True
220 controller._action = 'pull'
225 controller._action = 'pull'
226 controller._is_shadow_repo_dir = True
221 controller.stub_response_body = 'dummy body value'
227 controller.stub_response_body = 'dummy body value'
222 environ_stub = {
228 environ_stub = {
223 'HTTP_HOST': 'test.example.com',
229 'HTTP_HOST': 'test.example.com',
224 'HTTP_ACCEPT': 'application/mercurial',
230 'HTTP_ACCEPT': 'application/mercurial',
225 'REQUEST_METHOD': 'GET',
231 'REQUEST_METHOD': 'GET',
226 'wsgi.url_scheme': 'http',
232 'wsgi.url_scheme': 'http',
227 }
233 }
228
234
229 response = controller(environ_stub, mock.Mock())
235 response = controller(environ_stub, mock.Mock())
230 response_body = ''.join(response)
236 response_body = ''.join(response)
231
237
232 # Assert that we got the response from the wsgi app.
238 # Assert that we got the response from the wsgi app.
233 assert response_body == controller.stub_response_body
239 assert response_body == controller.stub_response_body
234
240
241 def test_pull_on_shadow_repo_that_is_missing(self, pylonsapp):
242 """
243 Check that a pull action to a shadow repo is propagated to the
244 underlying wsgi app.
245 """
246 controller = StubVCSController(pylonsapp, pylonsapp.config, None)
247 controller._check_ssl = mock.Mock()
248 controller.is_shadow_repo = True
249 controller._action = 'pull'
250 controller._is_shadow_repo_dir = False
251 controller.stub_response_body = 'dummy body value'
252 environ_stub = {
253 'HTTP_HOST': 'test.example.com',
254 'HTTP_ACCEPT': 'application/mercurial',
255 'REQUEST_METHOD': 'GET',
256 'wsgi.url_scheme': 'http',
257 }
258
259 response = controller(environ_stub, mock.Mock())
260 response_body = ''.join(response)
261
262 # Assert that we got the response from the wsgi app.
263 assert '404 Not Found' in response_body
264
235 def test_push_on_shadow_repo_raises(self, pylonsapp):
265 def test_push_on_shadow_repo_raises(self, pylonsapp):
236 """
266 """
237 Check that a push action to a shadow repo is aborted.
267 Check that a push action to a shadow repo is aborted.
238 """
268 """
239 controller = StubVCSController(pylonsapp, pylonsapp.config, None)
269 controller = StubVCSController(pylonsapp, pylonsapp.config, None)
240 controller._check_ssl = mock.Mock()
270 controller._check_ssl = mock.Mock()
241 controller.is_shadow_repo = True
271 controller.is_shadow_repo = True
242 controller._action = 'push'
272 controller._action = 'push'
243 controller.stub_response_body = 'dummy body value'
273 controller.stub_response_body = 'dummy body value'
244 environ_stub = {
274 environ_stub = {
245 'HTTP_HOST': 'test.example.com',
275 'HTTP_HOST': 'test.example.com',
246 'HTTP_ACCEPT': 'application/mercurial',
276 'HTTP_ACCEPT': 'application/mercurial',
247 'REQUEST_METHOD': 'GET',
277 'REQUEST_METHOD': 'GET',
248 'wsgi.url_scheme': 'http',
278 'wsgi.url_scheme': 'http',
249 }
279 }
250
280
251 response = controller(environ_stub, mock.Mock())
281 response = controller(environ_stub, mock.Mock())
252 response_body = ''.join(response)
282 response_body = ''.join(response)
253
283
254 assert response_body != controller.stub_response_body
284 assert response_body != controller.stub_response_body
255 # Assert that a 406 error is returned.
285 # Assert that a 406 error is returned.
256 assert '406 Not Acceptable' in response_body
286 assert '406 Not Acceptable' in response_body
257
287
258 def test_set_repo_names_no_shadow(self, pylonsapp):
288 def test_set_repo_names_no_shadow(self, pylonsapp):
259 """
289 """
260 Check that the set_repo_names method sets all names to the one returned
290 Check that the set_repo_names method sets all names to the one returned
261 by the _get_repository_name method on a request to a non shadow repo.
291 by the _get_repository_name method on a request to a non shadow repo.
262 """
292 """
263 environ_stub = {}
293 environ_stub = {}
264 controller = StubVCSController(pylonsapp, pylonsapp.config, None)
294 controller = StubVCSController(pylonsapp, pylonsapp.config, None)
265 controller._name = 'RepoGroup/MyRepo'
295 controller._name = 'RepoGroup/MyRepo'
266 controller.set_repo_names(environ_stub)
296 controller.set_repo_names(environ_stub)
267 assert not controller.is_shadow_repo
297 assert not controller.is_shadow_repo
268 assert (controller.url_repo_name ==
298 assert (controller.url_repo_name ==
269 controller.acl_repo_name ==
299 controller.acl_repo_name ==
270 controller.vcs_repo_name ==
300 controller.vcs_repo_name ==
271 controller._get_repository_name(environ_stub))
301 controller._get_repository_name(environ_stub))
272
302
273 def test_set_repo_names_with_shadow(self, pylonsapp, pr_util, config_stub):
303 def test_set_repo_names_with_shadow(self, pylonsapp, pr_util, config_stub):
274 """
304 """
275 Check that the set_repo_names method sets correct names on a request
305 Check that the set_repo_names method sets correct names on a request
276 to a shadow repo.
306 to a shadow repo.
277 """
307 """
278 from rhodecode.model.pull_request import PullRequestModel
308 from rhodecode.model.pull_request import PullRequestModel
279
309
280 pull_request = pr_util.create_pull_request()
310 pull_request = pr_util.create_pull_request()
281 shadow_url = '{target}/{pr_segment}/{pr_id}/{shadow_segment}'.format(
311 shadow_url = '{target}/{pr_segment}/{pr_id}/{shadow_segment}'.format(
282 target=pull_request.target_repo.repo_name,
312 target=pull_request.target_repo.repo_name,
283 pr_id=pull_request.pull_request_id,
313 pr_id=pull_request.pull_request_id,
284 pr_segment=TestShadowRepoRegularExpression.pr_segment,
314 pr_segment=TestShadowRepoRegularExpression.pr_segment,
285 shadow_segment=TestShadowRepoRegularExpression.shadow_segment)
315 shadow_segment=TestShadowRepoRegularExpression.shadow_segment)
286 controller = StubVCSController(pylonsapp, pylonsapp.config, None)
316 controller = StubVCSController(pylonsapp, pylonsapp.config, None)
287 controller._name = shadow_url
317 controller._name = shadow_url
288 controller.set_repo_names({})
318 controller.set_repo_names({})
289
319
290 # Get file system path to shadow repo for assertions.
320 # Get file system path to shadow repo for assertions.
291 workspace_id = PullRequestModel()._workspace_id(pull_request)
321 workspace_id = PullRequestModel()._workspace_id(pull_request)
292 target_vcs = pull_request.target_repo.scm_instance()
322 target_vcs = pull_request.target_repo.scm_instance()
293 vcs_repo_name = target_vcs._get_shadow_repository_path(
323 vcs_repo_name = target_vcs._get_shadow_repository_path(
294 workspace_id)
324 workspace_id)
295
325
296 assert controller.vcs_repo_name == vcs_repo_name
326 assert controller.vcs_repo_name == vcs_repo_name
297 assert controller.url_repo_name == shadow_url
327 assert controller.url_repo_name == shadow_url
298 assert controller.acl_repo_name == pull_request.target_repo.repo_name
328 assert controller.acl_repo_name == pull_request.target_repo.repo_name
299 assert controller.is_shadow_repo
329 assert controller.is_shadow_repo
300
330
301 def test_set_repo_names_with_shadow_but_missing_pr(
331 def test_set_repo_names_with_shadow_but_missing_pr(
302 self, pylonsapp, pr_util, config_stub):
332 self, pylonsapp, pr_util, config_stub):
303 """
333 """
304 Checks that the set_repo_names method enforces matching target repos
334 Checks that the set_repo_names method enforces matching target repos
305 and pull request IDs.
335 and pull request IDs.
306 """
336 """
307 pull_request = pr_util.create_pull_request()
337 pull_request = pr_util.create_pull_request()
308 shadow_url = '{target}/{pr_segment}/{pr_id}/{shadow_segment}'.format(
338 shadow_url = '{target}/{pr_segment}/{pr_id}/{shadow_segment}'.format(
309 target=pull_request.target_repo.repo_name,
339 target=pull_request.target_repo.repo_name,
310 pr_id=999999999,
340 pr_id=999999999,
311 pr_segment=TestShadowRepoRegularExpression.pr_segment,
341 pr_segment=TestShadowRepoRegularExpression.pr_segment,
312 shadow_segment=TestShadowRepoRegularExpression.shadow_segment)
342 shadow_segment=TestShadowRepoRegularExpression.shadow_segment)
313 controller = StubVCSController(pylonsapp, pylonsapp.config, None)
343 controller = StubVCSController(pylonsapp, pylonsapp.config, None)
314 controller._name = shadow_url
344 controller._name = shadow_url
315 controller.set_repo_names({})
345 controller.set_repo_names({})
316
346
317 assert not controller.is_shadow_repo
347 assert not controller.is_shadow_repo
318 assert (controller.url_repo_name ==
348 assert (controller.url_repo_name ==
319 controller.acl_repo_name ==
349 controller.acl_repo_name ==
320 controller.vcs_repo_name)
350 controller.vcs_repo_name)
321
351
322
352
323 @pytest.mark.usefixtures('db')
353 @pytest.mark.usefixtures('db')
324 class TestGenerateVcsResponse(object):
354 class TestGenerateVcsResponse(object):
325
355
326 def test_ensures_that_start_response_is_called_early_enough(self):
356 def test_ensures_that_start_response_is_called_early_enough(self):
327 self.call_controller_with_response_body(iter(['a', 'b']))
357 self.call_controller_with_response_body(iter(['a', 'b']))
328 assert self.start_response.called
358 assert self.start_response.called
329
359
330 def test_invalidates_cache_after_body_is_consumed(self):
360 def test_invalidates_cache_after_body_is_consumed(self):
331 result = self.call_controller_with_response_body(iter(['a', 'b']))
361 result = self.call_controller_with_response_body(iter(['a', 'b']))
332 assert not self.was_cache_invalidated()
362 assert not self.was_cache_invalidated()
333 # Consume the result
363 # Consume the result
334 list(result)
364 list(result)
335 assert self.was_cache_invalidated()
365 assert self.was_cache_invalidated()
336
366
337 @mock.patch('rhodecode.lib.middleware.simplevcs.HTTPLockedRC')
367 @mock.patch('rhodecode.lib.middleware.simplevcs.HTTPLockedRC')
338 def test_handles_locking_exception(self, http_locked_rc):
368 def test_handles_locking_exception(self, http_locked_rc):
339 result = self.call_controller_with_response_body(
369 result = self.call_controller_with_response_body(
340 self.raise_result_iter(vcs_kind='repo_locked'))
370 self.raise_result_iter(vcs_kind='repo_locked'))
341 assert not http_locked_rc.called
371 assert not http_locked_rc.called
342 # Consume the result
372 # Consume the result
343 list(result)
373 list(result)
344 assert http_locked_rc.called
374 assert http_locked_rc.called
345
375
346 @mock.patch('rhodecode.lib.middleware.simplevcs.HTTPRequirementError')
376 @mock.patch('rhodecode.lib.middleware.simplevcs.HTTPRequirementError')
347 def test_handles_requirement_exception(self, http_requirement):
377 def test_handles_requirement_exception(self, http_requirement):
348 result = self.call_controller_with_response_body(
378 result = self.call_controller_with_response_body(
349 self.raise_result_iter(vcs_kind='requirement'))
379 self.raise_result_iter(vcs_kind='requirement'))
350 assert not http_requirement.called
380 assert not http_requirement.called
351 # Consume the result
381 # Consume the result
352 list(result)
382 list(result)
353 assert http_requirement.called
383 assert http_requirement.called
354
384
355 @mock.patch('rhodecode.lib.middleware.simplevcs.HTTPLockedRC')
385 @mock.patch('rhodecode.lib.middleware.simplevcs.HTTPLockedRC')
356 def test_handles_locking_exception_in_app_call(self, http_locked_rc):
386 def test_handles_locking_exception_in_app_call(self, http_locked_rc):
357 app_factory_patcher = mock.patch.object(
387 app_factory_patcher = mock.patch.object(
358 StubVCSController, '_create_wsgi_app')
388 StubVCSController, '_create_wsgi_app')
359 with app_factory_patcher as app_factory:
389 with app_factory_patcher as app_factory:
360 app_factory().side_effect = self.vcs_exception()
390 app_factory().side_effect = self.vcs_exception()
361 result = self.call_controller_with_response_body(['a'])
391 result = self.call_controller_with_response_body(['a'])
362 list(result)
392 list(result)
363 assert http_locked_rc.called
393 assert http_locked_rc.called
364
394
365 def test_raises_unknown_exceptions(self):
395 def test_raises_unknown_exceptions(self):
366 result = self.call_controller_with_response_body(
396 result = self.call_controller_with_response_body(
367 self.raise_result_iter(vcs_kind='unknown'))
397 self.raise_result_iter(vcs_kind='unknown'))
368 with pytest.raises(Exception):
398 with pytest.raises(Exception):
369 list(result)
399 list(result)
370
400
371 def test_prepare_callback_daemon_is_called(self):
401 def test_prepare_callback_daemon_is_called(self):
372 def side_effect(extras):
402 def side_effect(extras):
373 return DummyHooksCallbackDaemon(), extras
403 return DummyHooksCallbackDaemon(), extras
374
404
375 prepare_patcher = mock.patch.object(
405 prepare_patcher = mock.patch.object(
376 StubVCSController, '_prepare_callback_daemon')
406 StubVCSController, '_prepare_callback_daemon')
377 with prepare_patcher as prepare_mock:
407 with prepare_patcher as prepare_mock:
378 prepare_mock.side_effect = side_effect
408 prepare_mock.side_effect = side_effect
379 self.call_controller_with_response_body(iter(['a', 'b']))
409 self.call_controller_with_response_body(iter(['a', 'b']))
380 assert prepare_mock.called
410 assert prepare_mock.called
381 assert prepare_mock.call_count == 1
411 assert prepare_mock.call_count == 1
382
412
383 def call_controller_with_response_body(self, response_body):
413 def call_controller_with_response_body(self, response_body):
384 settings = {
414 settings = {
385 'base_path': 'fake_base_path',
415 'base_path': 'fake_base_path',
386 'vcs.hooks.protocol': 'http',
416 'vcs.hooks.protocol': 'http',
387 'vcs.hooks.direct_calls': False,
417 'vcs.hooks.direct_calls': False,
388 }
418 }
389 controller = StubVCSController(None, settings, None)
419 controller = StubVCSController(None, settings, None)
390 controller._invalidate_cache = mock.Mock()
420 controller._invalidate_cache = mock.Mock()
391 controller.stub_response_body = response_body
421 controller.stub_response_body = response_body
392 self.start_response = mock.Mock()
422 self.start_response = mock.Mock()
393 result = controller._generate_vcs_response(
423 result = controller._generate_vcs_response(
394 environ={}, start_response=self.start_response,
424 environ={}, start_response=self.start_response,
395 repo_path='fake_repo_path',
425 repo_path='fake_repo_path',
396 extras={}, action='push')
426 extras={}, action='push')
397 self.controller = controller
427 self.controller = controller
398 return result
428 return result
399
429
400 def raise_result_iter(self, vcs_kind='repo_locked'):
430 def raise_result_iter(self, vcs_kind='repo_locked'):
401 """
431 """
402 Simulates an exception due to a vcs raised exception if kind vcs_kind
432 Simulates an exception due to a vcs raised exception if kind vcs_kind
403 """
433 """
404 raise self.vcs_exception(vcs_kind=vcs_kind)
434 raise self.vcs_exception(vcs_kind=vcs_kind)
405 yield "never_reached"
435 yield "never_reached"
406
436
407 def vcs_exception(self, vcs_kind='repo_locked'):
437 def vcs_exception(self, vcs_kind='repo_locked'):
408 locked_exception = Exception('TEST_MESSAGE')
438 locked_exception = Exception('TEST_MESSAGE')
409 locked_exception._vcs_kind = vcs_kind
439 locked_exception._vcs_kind = vcs_kind
410 return locked_exception
440 return locked_exception
411
441
412 def was_cache_invalidated(self):
442 def was_cache_invalidated(self):
413 return self.controller._invalidate_cache.called
443 return self.controller._invalidate_cache.called
414
444
415
445
416 class TestInitializeGenerator(object):
446 class TestInitializeGenerator(object):
417
447
418 def test_drains_first_element(self):
448 def test_drains_first_element(self):
419 gen = self.factory(['__init__', 1, 2])
449 gen = self.factory(['__init__', 1, 2])
420 result = list(gen)
450 result = list(gen)
421 assert result == [1, 2]
451 assert result == [1, 2]
422
452
423 @pytest.mark.parametrize('values', [
453 @pytest.mark.parametrize('values', [
424 [],
454 [],
425 [1, 2],
455 [1, 2],
426 ])
456 ])
427 def test_raises_value_error(self, values):
457 def test_raises_value_error(self, values):
428 with pytest.raises(ValueError):
458 with pytest.raises(ValueError):
429 self.factory(values)
459 self.factory(values)
430
460
431 @simplevcs.initialize_generator
461 @simplevcs.initialize_generator
432 def factory(self, iterable):
462 def factory(self, iterable):
433 for elem in iterable:
463 for elem in iterable:
434 yield elem
464 yield elem
435
465
436
466
437 class TestPrepareHooksDaemon(object):
467 class TestPrepareHooksDaemon(object):
438 def test_calls_imported_prepare_callback_daemon(self, app_settings):
468 def test_calls_imported_prepare_callback_daemon(self, app_settings):
439 expected_extras = {'extra1': 'value1'}
469 expected_extras = {'extra1': 'value1'}
440 daemon = DummyHooksCallbackDaemon()
470 daemon = DummyHooksCallbackDaemon()
441
471
442 controller = StubVCSController(None, app_settings, None)
472 controller = StubVCSController(None, app_settings, None)
443 prepare_patcher = mock.patch.object(
473 prepare_patcher = mock.patch.object(
444 simplevcs, 'prepare_callback_daemon',
474 simplevcs, 'prepare_callback_daemon',
445 return_value=(daemon, expected_extras))
475 return_value=(daemon, expected_extras))
446 with prepare_patcher as prepare_mock:
476 with prepare_patcher as prepare_mock:
447 callback_daemon, extras = controller._prepare_callback_daemon(
477 callback_daemon, extras = controller._prepare_callback_daemon(
448 expected_extras.copy())
478 expected_extras.copy())
449 prepare_mock.assert_called_once_with(
479 prepare_mock.assert_called_once_with(
450 expected_extras,
480 expected_extras,
451 protocol=app_settings['vcs.hooks.protocol'],
481 protocol=app_settings['vcs.hooks.protocol'],
452 use_direct_calls=app_settings['vcs.hooks.direct_calls'])
482 use_direct_calls=app_settings['vcs.hooks.direct_calls'])
453
483
454 assert callback_daemon == daemon
484 assert callback_daemon == daemon
455 assert extras == extras
485 assert extras == extras
General Comments 0
You need to be logged in to leave comments. Login now