##// END OF EJS Templates
hooks: expose user agent in the variables submitted to pull/push hooks.
marcink -
r1710:7173cb6f default
parent child Browse files
Show More
@@ -1,589 +1,594 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 """
21 """
22 The base Controller API
22 The base Controller API
23 Provides the BaseController class for subclassing. And usage in different
23 Provides the BaseController class for subclassing. And usage in different
24 controllers
24 controllers
25 """
25 """
26
26
27 import logging
27 import logging
28 import socket
28 import socket
29
29
30 import ipaddress
30 import ipaddress
31 import pyramid.threadlocal
31 import pyramid.threadlocal
32
32
33 from paste.auth.basic import AuthBasicAuthenticator
33 from paste.auth.basic import AuthBasicAuthenticator
34 from paste.httpexceptions import HTTPUnauthorized, HTTPForbidden, get_exception
34 from paste.httpexceptions import HTTPUnauthorized, HTTPForbidden, get_exception
35 from paste.httpheaders import WWW_AUTHENTICATE, AUTHORIZATION
35 from paste.httpheaders import WWW_AUTHENTICATE, AUTHORIZATION
36 from pylons import config, tmpl_context as c, request, session, url
36 from pylons import config, tmpl_context as c, request, session, url
37 from pylons.controllers import WSGIController
37 from pylons.controllers import WSGIController
38 from pylons.controllers.util import redirect
38 from pylons.controllers.util import redirect
39 from pylons.i18n import translation
39 from pylons.i18n import translation
40 # marcink: don't remove this import
40 # marcink: don't remove this import
41 from pylons.templating import render_mako as render # noqa
41 from pylons.templating import render_mako as render # noqa
42 from pylons.i18n.translation import _
42 from pylons.i18n.translation import _
43 from webob.exc import HTTPFound
43 from webob.exc import HTTPFound
44
44
45
45
46 import rhodecode
46 import rhodecode
47 from rhodecode.authentication.base import VCS_TYPE
47 from rhodecode.authentication.base import VCS_TYPE
48 from rhodecode.lib import auth, utils2
48 from rhodecode.lib import auth, utils2
49 from rhodecode.lib import helpers as h
49 from rhodecode.lib import helpers as h
50 from rhodecode.lib.auth import AuthUser, CookieStoreWrapper
50 from rhodecode.lib.auth import AuthUser, CookieStoreWrapper
51 from rhodecode.lib.exceptions import UserCreationError
51 from rhodecode.lib.exceptions import UserCreationError
52 from rhodecode.lib.utils import (
52 from rhodecode.lib.utils import (
53 get_repo_slug, set_rhodecode_config, password_changed,
53 get_repo_slug, set_rhodecode_config, password_changed,
54 get_enabled_hook_classes)
54 get_enabled_hook_classes)
55 from rhodecode.lib.utils2 import (
55 from rhodecode.lib.utils2 import (
56 str2bool, safe_unicode, AttributeDict, safe_int, md5, aslist)
56 str2bool, safe_unicode, AttributeDict, safe_int, md5, aslist)
57 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
57 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
58 from rhodecode.model import meta
58 from rhodecode.model import meta
59 from rhodecode.model.db import Repository, User, ChangesetComment
59 from rhodecode.model.db import Repository, User, ChangesetComment
60 from rhodecode.model.notification import NotificationModel
60 from rhodecode.model.notification import NotificationModel
61 from rhodecode.model.scm import ScmModel
61 from rhodecode.model.scm import ScmModel
62 from rhodecode.model.settings import VcsSettingsModel, SettingsModel
62 from rhodecode.model.settings import VcsSettingsModel, SettingsModel
63
63
64
64
65 log = logging.getLogger(__name__)
65 log = logging.getLogger(__name__)
66
66
67
67
68 def _filter_proxy(ip):
68 def _filter_proxy(ip):
69 """
69 """
70 Passed in IP addresses in HEADERS can be in a special format of multiple
70 Passed in IP addresses in HEADERS can be in a special format of multiple
71 ips. Those comma separated IPs are passed from various proxies in the
71 ips. Those comma separated IPs are passed from various proxies in the
72 chain of request processing. The left-most being the original client.
72 chain of request processing. The left-most being the original client.
73 We only care about the first IP which came from the org. client.
73 We only care about the first IP which came from the org. client.
74
74
75 :param ip: ip string from headers
75 :param ip: ip string from headers
76 """
76 """
77 if ',' in ip:
77 if ',' in ip:
78 _ips = ip.split(',')
78 _ips = ip.split(',')
79 _first_ip = _ips[0].strip()
79 _first_ip = _ips[0].strip()
80 log.debug('Got multiple IPs %s, using %s', ','.join(_ips), _first_ip)
80 log.debug('Got multiple IPs %s, using %s', ','.join(_ips), _first_ip)
81 return _first_ip
81 return _first_ip
82 return ip
82 return ip
83
83
84
84
85 def _filter_port(ip):
85 def _filter_port(ip):
86 """
86 """
87 Removes a port from ip, there are 4 main cases to handle here.
87 Removes a port from ip, there are 4 main cases to handle here.
88 - ipv4 eg. 127.0.0.1
88 - ipv4 eg. 127.0.0.1
89 - ipv6 eg. ::1
89 - ipv6 eg. ::1
90 - ipv4+port eg. 127.0.0.1:8080
90 - ipv4+port eg. 127.0.0.1:8080
91 - ipv6+port eg. [::1]:8080
91 - ipv6+port eg. [::1]:8080
92
92
93 :param ip:
93 :param ip:
94 """
94 """
95 def is_ipv6(ip_addr):
95 def is_ipv6(ip_addr):
96 if hasattr(socket, 'inet_pton'):
96 if hasattr(socket, 'inet_pton'):
97 try:
97 try:
98 socket.inet_pton(socket.AF_INET6, ip_addr)
98 socket.inet_pton(socket.AF_INET6, ip_addr)
99 except socket.error:
99 except socket.error:
100 return False
100 return False
101 else:
101 else:
102 # fallback to ipaddress
102 # fallback to ipaddress
103 try:
103 try:
104 ipaddress.IPv6Address(ip_addr)
104 ipaddress.IPv6Address(ip_addr)
105 except Exception:
105 except Exception:
106 return False
106 return False
107 return True
107 return True
108
108
109 if ':' not in ip: # must be ipv4 pure ip
109 if ':' not in ip: # must be ipv4 pure ip
110 return ip
110 return ip
111
111
112 if '[' in ip and ']' in ip: # ipv6 with port
112 if '[' in ip and ']' in ip: # ipv6 with port
113 return ip.split(']')[0][1:].lower()
113 return ip.split(']')[0][1:].lower()
114
114
115 # must be ipv6 or ipv4 with port
115 # must be ipv6 or ipv4 with port
116 if is_ipv6(ip):
116 if is_ipv6(ip):
117 return ip
117 return ip
118 else:
118 else:
119 ip, _port = ip.split(':')[:2] # means ipv4+port
119 ip, _port = ip.split(':')[:2] # means ipv4+port
120 return ip
120 return ip
121
121
122
122
123 def get_ip_addr(environ):
123 def get_ip_addr(environ):
124 proxy_key = 'HTTP_X_REAL_IP'
124 proxy_key = 'HTTP_X_REAL_IP'
125 proxy_key2 = 'HTTP_X_FORWARDED_FOR'
125 proxy_key2 = 'HTTP_X_FORWARDED_FOR'
126 def_key = 'REMOTE_ADDR'
126 def_key = 'REMOTE_ADDR'
127 _filters = lambda x: _filter_port(_filter_proxy(x))
127 _filters = lambda x: _filter_port(_filter_proxy(x))
128
128
129 ip = environ.get(proxy_key)
129 ip = environ.get(proxy_key)
130 if ip:
130 if ip:
131 return _filters(ip)
131 return _filters(ip)
132
132
133 ip = environ.get(proxy_key2)
133 ip = environ.get(proxy_key2)
134 if ip:
134 if ip:
135 return _filters(ip)
135 return _filters(ip)
136
136
137 ip = environ.get(def_key, '0.0.0.0')
137 ip = environ.get(def_key, '0.0.0.0')
138 return _filters(ip)
138 return _filters(ip)
139
139
140
140
141 def get_server_ip_addr(environ, log_errors=True):
141 def get_server_ip_addr(environ, log_errors=True):
142 hostname = environ.get('SERVER_NAME')
142 hostname = environ.get('SERVER_NAME')
143 try:
143 try:
144 return socket.gethostbyname(hostname)
144 return socket.gethostbyname(hostname)
145 except Exception as e:
145 except Exception as e:
146 if log_errors:
146 if log_errors:
147 # in some cases this lookup is not possible, and we don't want to
147 # in some cases this lookup is not possible, and we don't want to
148 # make it an exception in logs
148 # make it an exception in logs
149 log.exception('Could not retrieve server ip address: %s', e)
149 log.exception('Could not retrieve server ip address: %s', e)
150 return hostname
150 return hostname
151
151
152
152
153 def get_server_port(environ):
153 def get_server_port(environ):
154 return environ.get('SERVER_PORT')
154 return environ.get('SERVER_PORT')
155
155
156
156
157 def get_access_path(environ):
157 def get_access_path(environ):
158 path = environ.get('PATH_INFO')
158 path = environ.get('PATH_INFO')
159 org_req = environ.get('pylons.original_request')
159 org_req = environ.get('pylons.original_request')
160 if org_req:
160 if org_req:
161 path = org_req.environ.get('PATH_INFO')
161 path = org_req.environ.get('PATH_INFO')
162 return path
162 return path
163
163
164
164
165 def get_user_agent(environ):
166 return environ.get('HTTP_USER_AGENT')
167
168
165 def vcs_operation_context(
169 def vcs_operation_context(
166 environ, repo_name, username, action, scm, check_locking=True,
170 environ, repo_name, username, action, scm, check_locking=True,
167 is_shadow_repo=False):
171 is_shadow_repo=False):
168 """
172 """
169 Generate the context for a vcs operation, e.g. push or pull.
173 Generate the context for a vcs operation, e.g. push or pull.
170
174
171 This context is passed over the layers so that hooks triggered by the
175 This context is passed over the layers so that hooks triggered by the
172 vcs operation know details like the user, the user's IP address etc.
176 vcs operation know details like the user, the user's IP address etc.
173
177
174 :param check_locking: Allows to switch of the computation of the locking
178 :param check_locking: Allows to switch of the computation of the locking
175 data. This serves mainly the need of the simplevcs middleware to be
179 data. This serves mainly the need of the simplevcs middleware to be
176 able to disable this for certain operations.
180 able to disable this for certain operations.
177
181
178 """
182 """
179 # Tri-state value: False: unlock, None: nothing, True: lock
183 # Tri-state value: False: unlock, None: nothing, True: lock
180 make_lock = None
184 make_lock = None
181 locked_by = [None, None, None]
185 locked_by = [None, None, None]
182 is_anonymous = username == User.DEFAULT_USER
186 is_anonymous = username == User.DEFAULT_USER
183 if not is_anonymous and check_locking:
187 if not is_anonymous and check_locking:
184 log.debug('Checking locking on repository "%s"', repo_name)
188 log.debug('Checking locking on repository "%s"', repo_name)
185 user = User.get_by_username(username)
189 user = User.get_by_username(username)
186 repo = Repository.get_by_repo_name(repo_name)
190 repo = Repository.get_by_repo_name(repo_name)
187 make_lock, __, locked_by = repo.get_locking_state(
191 make_lock, __, locked_by = repo.get_locking_state(
188 action, user.user_id)
192 action, user.user_id)
189
193
190 settings_model = VcsSettingsModel(repo=repo_name)
194 settings_model = VcsSettingsModel(repo=repo_name)
191 ui_settings = settings_model.get_ui_settings()
195 ui_settings = settings_model.get_ui_settings()
192
196
193 extras = {
197 extras = {
194 'ip': get_ip_addr(environ),
198 'ip': get_ip_addr(environ),
195 'username': username,
199 'username': username,
196 'action': action,
200 'action': action,
197 'repository': repo_name,
201 'repository': repo_name,
198 'scm': scm,
202 'scm': scm,
199 'config': rhodecode.CONFIG['__file__'],
203 'config': rhodecode.CONFIG['__file__'],
200 'make_lock': make_lock,
204 'make_lock': make_lock,
201 'locked_by': locked_by,
205 'locked_by': locked_by,
202 'server_url': utils2.get_server_url(environ),
206 'server_url': utils2.get_server_url(environ),
207 'user_agent': get_user_agent(environ),
203 'hooks': get_enabled_hook_classes(ui_settings),
208 'hooks': get_enabled_hook_classes(ui_settings),
204 'is_shadow_repo': is_shadow_repo,
209 'is_shadow_repo': is_shadow_repo,
205 }
210 }
206 return extras
211 return extras
207
212
208
213
209 class BasicAuth(AuthBasicAuthenticator):
214 class BasicAuth(AuthBasicAuthenticator):
210
215
211 def __init__(self, realm, authfunc, registry, auth_http_code=None,
216 def __init__(self, realm, authfunc, registry, auth_http_code=None,
212 initial_call_detection=False, acl_repo_name=None):
217 initial_call_detection=False, acl_repo_name=None):
213 self.realm = realm
218 self.realm = realm
214 self.initial_call = initial_call_detection
219 self.initial_call = initial_call_detection
215 self.authfunc = authfunc
220 self.authfunc = authfunc
216 self.registry = registry
221 self.registry = registry
217 self.acl_repo_name = acl_repo_name
222 self.acl_repo_name = acl_repo_name
218 self._rc_auth_http_code = auth_http_code
223 self._rc_auth_http_code = auth_http_code
219
224
220 def _get_response_from_code(self, http_code):
225 def _get_response_from_code(self, http_code):
221 try:
226 try:
222 return get_exception(safe_int(http_code))
227 return get_exception(safe_int(http_code))
223 except Exception:
228 except Exception:
224 log.exception('Failed to fetch response for code %s' % http_code)
229 log.exception('Failed to fetch response for code %s' % http_code)
225 return HTTPForbidden
230 return HTTPForbidden
226
231
227 def build_authentication(self):
232 def build_authentication(self):
228 head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
233 head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
229 if self._rc_auth_http_code and not self.initial_call:
234 if self._rc_auth_http_code and not self.initial_call:
230 # return alternative HTTP code if alternative http return code
235 # return alternative HTTP code if alternative http return code
231 # is specified in RhodeCode config, but ONLY if it's not the
236 # is specified in RhodeCode config, but ONLY if it's not the
232 # FIRST call
237 # FIRST call
233 custom_response_klass = self._get_response_from_code(
238 custom_response_klass = self._get_response_from_code(
234 self._rc_auth_http_code)
239 self._rc_auth_http_code)
235 return custom_response_klass(headers=head)
240 return custom_response_klass(headers=head)
236 return HTTPUnauthorized(headers=head)
241 return HTTPUnauthorized(headers=head)
237
242
238 def authenticate(self, environ):
243 def authenticate(self, environ):
239 authorization = AUTHORIZATION(environ)
244 authorization = AUTHORIZATION(environ)
240 if not authorization:
245 if not authorization:
241 return self.build_authentication()
246 return self.build_authentication()
242 (authmeth, auth) = authorization.split(' ', 1)
247 (authmeth, auth) = authorization.split(' ', 1)
243 if 'basic' != authmeth.lower():
248 if 'basic' != authmeth.lower():
244 return self.build_authentication()
249 return self.build_authentication()
245 auth = auth.strip().decode('base64')
250 auth = auth.strip().decode('base64')
246 _parts = auth.split(':', 1)
251 _parts = auth.split(':', 1)
247 if len(_parts) == 2:
252 if len(_parts) == 2:
248 username, password = _parts
253 username, password = _parts
249 if self.authfunc(
254 if self.authfunc(
250 username, password, environ, VCS_TYPE,
255 username, password, environ, VCS_TYPE,
251 registry=self.registry, acl_repo_name=self.acl_repo_name):
256 registry=self.registry, acl_repo_name=self.acl_repo_name):
252 return username
257 return username
253 if username and password:
258 if username and password:
254 # we mark that we actually executed authentication once, at
259 # we mark that we actually executed authentication once, at
255 # that point we can use the alternative auth code
260 # that point we can use the alternative auth code
256 self.initial_call = False
261 self.initial_call = False
257
262
258 return self.build_authentication()
263 return self.build_authentication()
259
264
260 __call__ = authenticate
265 __call__ = authenticate
261
266
262
267
263 def attach_context_attributes(context, request):
268 def attach_context_attributes(context, request):
264 """
269 """
265 Attach variables into template context called `c`, please note that
270 Attach variables into template context called `c`, please note that
266 request could be pylons or pyramid request in here.
271 request could be pylons or pyramid request in here.
267 """
272 """
268 rc_config = SettingsModel().get_all_settings(cache=True)
273 rc_config = SettingsModel().get_all_settings(cache=True)
269
274
270 context.rhodecode_version = rhodecode.__version__
275 context.rhodecode_version = rhodecode.__version__
271 context.rhodecode_edition = config.get('rhodecode.edition')
276 context.rhodecode_edition = config.get('rhodecode.edition')
272 # unique secret + version does not leak the version but keep consistency
277 # unique secret + version does not leak the version but keep consistency
273 context.rhodecode_version_hash = md5(
278 context.rhodecode_version_hash = md5(
274 config.get('beaker.session.secret', '') +
279 config.get('beaker.session.secret', '') +
275 rhodecode.__version__)[:8]
280 rhodecode.__version__)[:8]
276
281
277 # Default language set for the incoming request
282 # Default language set for the incoming request
278 context.language = translation.get_lang()[0]
283 context.language = translation.get_lang()[0]
279
284
280 # Visual options
285 # Visual options
281 context.visual = AttributeDict({})
286 context.visual = AttributeDict({})
282
287
283 # DB stored Visual Items
288 # DB stored Visual Items
284 context.visual.show_public_icon = str2bool(
289 context.visual.show_public_icon = str2bool(
285 rc_config.get('rhodecode_show_public_icon'))
290 rc_config.get('rhodecode_show_public_icon'))
286 context.visual.show_private_icon = str2bool(
291 context.visual.show_private_icon = str2bool(
287 rc_config.get('rhodecode_show_private_icon'))
292 rc_config.get('rhodecode_show_private_icon'))
288 context.visual.stylify_metatags = str2bool(
293 context.visual.stylify_metatags = str2bool(
289 rc_config.get('rhodecode_stylify_metatags'))
294 rc_config.get('rhodecode_stylify_metatags'))
290 context.visual.dashboard_items = safe_int(
295 context.visual.dashboard_items = safe_int(
291 rc_config.get('rhodecode_dashboard_items', 100))
296 rc_config.get('rhodecode_dashboard_items', 100))
292 context.visual.admin_grid_items = safe_int(
297 context.visual.admin_grid_items = safe_int(
293 rc_config.get('rhodecode_admin_grid_items', 100))
298 rc_config.get('rhodecode_admin_grid_items', 100))
294 context.visual.repository_fields = str2bool(
299 context.visual.repository_fields = str2bool(
295 rc_config.get('rhodecode_repository_fields'))
300 rc_config.get('rhodecode_repository_fields'))
296 context.visual.show_version = str2bool(
301 context.visual.show_version = str2bool(
297 rc_config.get('rhodecode_show_version'))
302 rc_config.get('rhodecode_show_version'))
298 context.visual.use_gravatar = str2bool(
303 context.visual.use_gravatar = str2bool(
299 rc_config.get('rhodecode_use_gravatar'))
304 rc_config.get('rhodecode_use_gravatar'))
300 context.visual.gravatar_url = rc_config.get('rhodecode_gravatar_url')
305 context.visual.gravatar_url = rc_config.get('rhodecode_gravatar_url')
301 context.visual.default_renderer = rc_config.get(
306 context.visual.default_renderer = rc_config.get(
302 'rhodecode_markup_renderer', 'rst')
307 'rhodecode_markup_renderer', 'rst')
303 context.visual.comment_types = ChangesetComment.COMMENT_TYPES
308 context.visual.comment_types = ChangesetComment.COMMENT_TYPES
304 context.visual.rhodecode_support_url = \
309 context.visual.rhodecode_support_url = \
305 rc_config.get('rhodecode_support_url') or h.route_url('rhodecode_support')
310 rc_config.get('rhodecode_support_url') or h.route_url('rhodecode_support')
306
311
307 context.pre_code = rc_config.get('rhodecode_pre_code')
312 context.pre_code = rc_config.get('rhodecode_pre_code')
308 context.post_code = rc_config.get('rhodecode_post_code')
313 context.post_code = rc_config.get('rhodecode_post_code')
309 context.rhodecode_name = rc_config.get('rhodecode_title')
314 context.rhodecode_name = rc_config.get('rhodecode_title')
310 context.default_encodings = aslist(config.get('default_encoding'), sep=',')
315 context.default_encodings = aslist(config.get('default_encoding'), sep=',')
311 # if we have specified default_encoding in the request, it has more
316 # if we have specified default_encoding in the request, it has more
312 # priority
317 # priority
313 if request.GET.get('default_encoding'):
318 if request.GET.get('default_encoding'):
314 context.default_encodings.insert(0, request.GET.get('default_encoding'))
319 context.default_encodings.insert(0, request.GET.get('default_encoding'))
315 context.clone_uri_tmpl = rc_config.get('rhodecode_clone_uri_tmpl')
320 context.clone_uri_tmpl = rc_config.get('rhodecode_clone_uri_tmpl')
316
321
317 # INI stored
322 # INI stored
318 context.labs_active = str2bool(
323 context.labs_active = str2bool(
319 config.get('labs_settings_active', 'false'))
324 config.get('labs_settings_active', 'false'))
320 context.visual.allow_repo_location_change = str2bool(
325 context.visual.allow_repo_location_change = str2bool(
321 config.get('allow_repo_location_change', True))
326 config.get('allow_repo_location_change', True))
322 context.visual.allow_custom_hooks_settings = str2bool(
327 context.visual.allow_custom_hooks_settings = str2bool(
323 config.get('allow_custom_hooks_settings', True))
328 config.get('allow_custom_hooks_settings', True))
324 context.debug_style = str2bool(config.get('debug_style', False))
329 context.debug_style = str2bool(config.get('debug_style', False))
325
330
326 context.rhodecode_instanceid = config.get('instance_id')
331 context.rhodecode_instanceid = config.get('instance_id')
327
332
328 # AppEnlight
333 # AppEnlight
329 context.appenlight_enabled = str2bool(config.get('appenlight', 'false'))
334 context.appenlight_enabled = str2bool(config.get('appenlight', 'false'))
330 context.appenlight_api_public_key = config.get(
335 context.appenlight_api_public_key = config.get(
331 'appenlight.api_public_key', '')
336 'appenlight.api_public_key', '')
332 context.appenlight_server_url = config.get('appenlight.server_url', '')
337 context.appenlight_server_url = config.get('appenlight.server_url', '')
333
338
334 # JS template context
339 # JS template context
335 context.template_context = {
340 context.template_context = {
336 'repo_name': None,
341 'repo_name': None,
337 'repo_type': None,
342 'repo_type': None,
338 'repo_landing_commit': None,
343 'repo_landing_commit': None,
339 'rhodecode_user': {
344 'rhodecode_user': {
340 'username': None,
345 'username': None,
341 'email': None,
346 'email': None,
342 'notification_status': False
347 'notification_status': False
343 },
348 },
344 'visual': {
349 'visual': {
345 'default_renderer': None
350 'default_renderer': None
346 },
351 },
347 'commit_data': {
352 'commit_data': {
348 'commit_id': None
353 'commit_id': None
349 },
354 },
350 'pull_request_data': {'pull_request_id': None},
355 'pull_request_data': {'pull_request_id': None},
351 'timeago': {
356 'timeago': {
352 'refresh_time': 120 * 1000,
357 'refresh_time': 120 * 1000,
353 'cutoff_limit': 1000 * 60 * 60 * 24 * 7
358 'cutoff_limit': 1000 * 60 * 60 * 24 * 7
354 },
359 },
355 'pylons_dispatch': {
360 'pylons_dispatch': {
356 # 'controller': request.environ['pylons.routes_dict']['controller'],
361 # 'controller': request.environ['pylons.routes_dict']['controller'],
357 # 'action': request.environ['pylons.routes_dict']['action'],
362 # 'action': request.environ['pylons.routes_dict']['action'],
358 },
363 },
359 'pyramid_dispatch': {
364 'pyramid_dispatch': {
360
365
361 },
366 },
362 'extra': {'plugins': {}}
367 'extra': {'plugins': {}}
363 }
368 }
364 # END CONFIG VARS
369 # END CONFIG VARS
365
370
366 # TODO: This dosn't work when called from pylons compatibility tween.
371 # TODO: This dosn't work when called from pylons compatibility tween.
367 # Fix this and remove it from base controller.
372 # Fix this and remove it from base controller.
368 # context.repo_name = get_repo_slug(request) # can be empty
373 # context.repo_name = get_repo_slug(request) # can be empty
369
374
370 diffmode = 'sideside'
375 diffmode = 'sideside'
371 if request.GET.get('diffmode'):
376 if request.GET.get('diffmode'):
372 if request.GET['diffmode'] == 'unified':
377 if request.GET['diffmode'] == 'unified':
373 diffmode = 'unified'
378 diffmode = 'unified'
374 elif request.session.get('diffmode'):
379 elif request.session.get('diffmode'):
375 diffmode = request.session['diffmode']
380 diffmode = request.session['diffmode']
376
381
377 context.diffmode = diffmode
382 context.diffmode = diffmode
378
383
379 if request.session.get('diffmode') != diffmode:
384 if request.session.get('diffmode') != diffmode:
380 request.session['diffmode'] = diffmode
385 request.session['diffmode'] = diffmode
381
386
382 context.csrf_token = auth.get_csrf_token()
387 context.csrf_token = auth.get_csrf_token()
383 context.backends = rhodecode.BACKENDS.keys()
388 context.backends = rhodecode.BACKENDS.keys()
384 context.backends.sort()
389 context.backends.sort()
385 context.unread_notifications = NotificationModel().get_unread_cnt_for_user(
390 context.unread_notifications = NotificationModel().get_unread_cnt_for_user(
386 context.rhodecode_user.user_id)
391 context.rhodecode_user.user_id)
387
392
388 context.pyramid_request = pyramid.threadlocal.get_current_request()
393 context.pyramid_request = pyramid.threadlocal.get_current_request()
389
394
390
395
391 def get_auth_user(environ):
396 def get_auth_user(environ):
392 ip_addr = get_ip_addr(environ)
397 ip_addr = get_ip_addr(environ)
393 # make sure that we update permissions each time we call controller
398 # make sure that we update permissions each time we call controller
394 _auth_token = (request.GET.get('auth_token', '') or
399 _auth_token = (request.GET.get('auth_token', '') or
395 request.GET.get('api_key', ''))
400 request.GET.get('api_key', ''))
396
401
397 if _auth_token:
402 if _auth_token:
398 # when using API_KEY we assume user exists, and
403 # when using API_KEY we assume user exists, and
399 # doesn't need auth based on cookies.
404 # doesn't need auth based on cookies.
400 auth_user = AuthUser(api_key=_auth_token, ip_addr=ip_addr)
405 auth_user = AuthUser(api_key=_auth_token, ip_addr=ip_addr)
401 authenticated = False
406 authenticated = False
402 else:
407 else:
403 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
408 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
404 try:
409 try:
405 auth_user = AuthUser(user_id=cookie_store.get('user_id', None),
410 auth_user = AuthUser(user_id=cookie_store.get('user_id', None),
406 ip_addr=ip_addr)
411 ip_addr=ip_addr)
407 except UserCreationError as e:
412 except UserCreationError as e:
408 h.flash(e, 'error')
413 h.flash(e, 'error')
409 # container auth or other auth functions that create users
414 # container auth or other auth functions that create users
410 # on the fly can throw this exception signaling that there's
415 # on the fly can throw this exception signaling that there's
411 # issue with user creation, explanation should be provided
416 # issue with user creation, explanation should be provided
412 # in Exception itself. We then create a simple blank
417 # in Exception itself. We then create a simple blank
413 # AuthUser
418 # AuthUser
414 auth_user = AuthUser(ip_addr=ip_addr)
419 auth_user = AuthUser(ip_addr=ip_addr)
415
420
416 if password_changed(auth_user, session):
421 if password_changed(auth_user, session):
417 session.invalidate()
422 session.invalidate()
418 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
423 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
419 auth_user = AuthUser(ip_addr=ip_addr)
424 auth_user = AuthUser(ip_addr=ip_addr)
420
425
421 authenticated = cookie_store.get('is_authenticated')
426 authenticated = cookie_store.get('is_authenticated')
422
427
423 if not auth_user.is_authenticated and auth_user.is_user_object:
428 if not auth_user.is_authenticated and auth_user.is_user_object:
424 # user is not authenticated and not empty
429 # user is not authenticated and not empty
425 auth_user.set_authenticated(authenticated)
430 auth_user.set_authenticated(authenticated)
426
431
427 return auth_user
432 return auth_user
428
433
429
434
430 class BaseController(WSGIController):
435 class BaseController(WSGIController):
431
436
432 def __before__(self):
437 def __before__(self):
433 """
438 """
434 __before__ is called before controller methods and after __call__
439 __before__ is called before controller methods and after __call__
435 """
440 """
436 # on each call propagate settings calls into global settings.
441 # on each call propagate settings calls into global settings.
437 set_rhodecode_config(config)
442 set_rhodecode_config(config)
438 attach_context_attributes(c, request)
443 attach_context_attributes(c, request)
439
444
440 # TODO: Remove this when fixed in attach_context_attributes()
445 # TODO: Remove this when fixed in attach_context_attributes()
441 c.repo_name = get_repo_slug(request) # can be empty
446 c.repo_name = get_repo_slug(request) # can be empty
442
447
443 self.cut_off_limit_diff = safe_int(config.get('cut_off_limit_diff'))
448 self.cut_off_limit_diff = safe_int(config.get('cut_off_limit_diff'))
444 self.cut_off_limit_file = safe_int(config.get('cut_off_limit_file'))
449 self.cut_off_limit_file = safe_int(config.get('cut_off_limit_file'))
445 self.sa = meta.Session
450 self.sa = meta.Session
446 self.scm_model = ScmModel(self.sa)
451 self.scm_model = ScmModel(self.sa)
447
452
448 # set user language
453 # set user language
449 user_lang = getattr(c.pyramid_request, '_LOCALE_', None)
454 user_lang = getattr(c.pyramid_request, '_LOCALE_', None)
450 if user_lang:
455 if user_lang:
451 translation.set_lang(user_lang)
456 translation.set_lang(user_lang)
452 log.debug('set language to %s for user %s',
457 log.debug('set language to %s for user %s',
453 user_lang, self._rhodecode_user)
458 user_lang, self._rhodecode_user)
454
459
455 def _dispatch_redirect(self, with_url, environ, start_response):
460 def _dispatch_redirect(self, with_url, environ, start_response):
456 resp = HTTPFound(with_url)
461 resp = HTTPFound(with_url)
457 environ['SCRIPT_NAME'] = '' # handle prefix middleware
462 environ['SCRIPT_NAME'] = '' # handle prefix middleware
458 environ['PATH_INFO'] = with_url
463 environ['PATH_INFO'] = with_url
459 return resp(environ, start_response)
464 return resp(environ, start_response)
460
465
461 def __call__(self, environ, start_response):
466 def __call__(self, environ, start_response):
462 """Invoke the Controller"""
467 """Invoke the Controller"""
463 # WSGIController.__call__ dispatches to the Controller method
468 # WSGIController.__call__ dispatches to the Controller method
464 # the request is routed to. This routing information is
469 # the request is routed to. This routing information is
465 # available in environ['pylons.routes_dict']
470 # available in environ['pylons.routes_dict']
466 from rhodecode.lib import helpers as h
471 from rhodecode.lib import helpers as h
467
472
468 # Provide the Pylons context to Pyramid's debugtoolbar if it asks
473 # Provide the Pylons context to Pyramid's debugtoolbar if it asks
469 if environ.get('debugtoolbar.wants_pylons_context', False):
474 if environ.get('debugtoolbar.wants_pylons_context', False):
470 environ['debugtoolbar.pylons_context'] = c._current_obj()
475 environ['debugtoolbar.pylons_context'] = c._current_obj()
471
476
472 _route_name = '.'.join([environ['pylons.routes_dict']['controller'],
477 _route_name = '.'.join([environ['pylons.routes_dict']['controller'],
473 environ['pylons.routes_dict']['action']])
478 environ['pylons.routes_dict']['action']])
474
479
475 self.rc_config = SettingsModel().get_all_settings(cache=True)
480 self.rc_config = SettingsModel().get_all_settings(cache=True)
476 self.ip_addr = get_ip_addr(environ)
481 self.ip_addr = get_ip_addr(environ)
477
482
478 # The rhodecode auth user is looked up and passed through the
483 # The rhodecode auth user is looked up and passed through the
479 # environ by the pylons compatibility tween in pyramid.
484 # environ by the pylons compatibility tween in pyramid.
480 # So we can just grab it from there.
485 # So we can just grab it from there.
481 auth_user = environ['rc_auth_user']
486 auth_user = environ['rc_auth_user']
482
487
483 # set globals for auth user
488 # set globals for auth user
484 request.user = auth_user
489 request.user = auth_user
485 c.rhodecode_user = self._rhodecode_user = auth_user
490 c.rhodecode_user = self._rhodecode_user = auth_user
486
491
487 log.info('IP: %s User: %s accessed %s [%s]' % (
492 log.info('IP: %s User: %s accessed %s [%s]' % (
488 self.ip_addr, auth_user, safe_unicode(get_access_path(environ)),
493 self.ip_addr, auth_user, safe_unicode(get_access_path(environ)),
489 _route_name)
494 _route_name)
490 )
495 )
491
496
492 user_obj = auth_user.get_instance()
497 user_obj = auth_user.get_instance()
493 if user_obj and user_obj.user_data.get('force_password_change'):
498 if user_obj and user_obj.user_data.get('force_password_change'):
494 h.flash('You are required to change your password', 'warning',
499 h.flash('You are required to change your password', 'warning',
495 ignore_duplicate=True)
500 ignore_duplicate=True)
496 return self._dispatch_redirect(
501 return self._dispatch_redirect(
497 url('my_account_password'), environ, start_response)
502 url('my_account_password'), environ, start_response)
498
503
499 return WSGIController.__call__(self, environ, start_response)
504 return WSGIController.__call__(self, environ, start_response)
500
505
501
506
502 class BaseRepoController(BaseController):
507 class BaseRepoController(BaseController):
503 """
508 """
504 Base class for controllers responsible for loading all needed data for
509 Base class for controllers responsible for loading all needed data for
505 repository loaded items are
510 repository loaded items are
506
511
507 c.rhodecode_repo: instance of scm repository
512 c.rhodecode_repo: instance of scm repository
508 c.rhodecode_db_repo: instance of db
513 c.rhodecode_db_repo: instance of db
509 c.repository_requirements_missing: shows that repository specific data
514 c.repository_requirements_missing: shows that repository specific data
510 could not be displayed due to the missing requirements
515 could not be displayed due to the missing requirements
511 c.repository_pull_requests: show number of open pull requests
516 c.repository_pull_requests: show number of open pull requests
512 """
517 """
513
518
514 def __before__(self):
519 def __before__(self):
515 super(BaseRepoController, self).__before__()
520 super(BaseRepoController, self).__before__()
516 if c.repo_name: # extracted from routes
521 if c.repo_name: # extracted from routes
517 db_repo = Repository.get_by_repo_name(c.repo_name)
522 db_repo = Repository.get_by_repo_name(c.repo_name)
518 if not db_repo:
523 if not db_repo:
519 return
524 return
520
525
521 log.debug(
526 log.debug(
522 'Found repository in database %s with state `%s`',
527 'Found repository in database %s with state `%s`',
523 safe_unicode(db_repo), safe_unicode(db_repo.repo_state))
528 safe_unicode(db_repo), safe_unicode(db_repo.repo_state))
524 route = getattr(request.environ.get('routes.route'), 'name', '')
529 route = getattr(request.environ.get('routes.route'), 'name', '')
525
530
526 # allow to delete repos that are somehow damages in filesystem
531 # allow to delete repos that are somehow damages in filesystem
527 if route in ['delete_repo']:
532 if route in ['delete_repo']:
528 return
533 return
529
534
530 if db_repo.repo_state in [Repository.STATE_PENDING]:
535 if db_repo.repo_state in [Repository.STATE_PENDING]:
531 if route in ['repo_creating_home']:
536 if route in ['repo_creating_home']:
532 return
537 return
533 check_url = url('repo_creating_home', repo_name=c.repo_name)
538 check_url = url('repo_creating_home', repo_name=c.repo_name)
534 return redirect(check_url)
539 return redirect(check_url)
535
540
536 self.rhodecode_db_repo = db_repo
541 self.rhodecode_db_repo = db_repo
537
542
538 missing_requirements = False
543 missing_requirements = False
539 try:
544 try:
540 self.rhodecode_repo = self.rhodecode_db_repo.scm_instance()
545 self.rhodecode_repo = self.rhodecode_db_repo.scm_instance()
541 except RepositoryRequirementError as e:
546 except RepositoryRequirementError as e:
542 missing_requirements = True
547 missing_requirements = True
543 self._handle_missing_requirements(e)
548 self._handle_missing_requirements(e)
544
549
545 if self.rhodecode_repo is None and not missing_requirements:
550 if self.rhodecode_repo is None and not missing_requirements:
546 log.error('%s this repository is present in database but it '
551 log.error('%s this repository is present in database but it '
547 'cannot be created as an scm instance', c.repo_name)
552 'cannot be created as an scm instance', c.repo_name)
548
553
549 h.flash(_(
554 h.flash(_(
550 "The repository at %(repo_name)s cannot be located.") %
555 "The repository at %(repo_name)s cannot be located.") %
551 {'repo_name': c.repo_name},
556 {'repo_name': c.repo_name},
552 category='error', ignore_duplicate=True)
557 category='error', ignore_duplicate=True)
553 redirect(url('home'))
558 redirect(url('home'))
554
559
555 # update last change according to VCS data
560 # update last change according to VCS data
556 if not missing_requirements:
561 if not missing_requirements:
557 commit = db_repo.get_commit(
562 commit = db_repo.get_commit(
558 pre_load=["author", "date", "message", "parents"])
563 pre_load=["author", "date", "message", "parents"])
559 db_repo.update_commit_cache(commit)
564 db_repo.update_commit_cache(commit)
560
565
561 # Prepare context
566 # Prepare context
562 c.rhodecode_db_repo = db_repo
567 c.rhodecode_db_repo = db_repo
563 c.rhodecode_repo = self.rhodecode_repo
568 c.rhodecode_repo = self.rhodecode_repo
564 c.repository_requirements_missing = missing_requirements
569 c.repository_requirements_missing = missing_requirements
565
570
566 self._update_global_counters(self.scm_model, db_repo)
571 self._update_global_counters(self.scm_model, db_repo)
567
572
568 def _update_global_counters(self, scm_model, db_repo):
573 def _update_global_counters(self, scm_model, db_repo):
569 """
574 """
570 Base variables that are exposed to every page of repository
575 Base variables that are exposed to every page of repository
571 """
576 """
572 c.repository_pull_requests = scm_model.get_pull_requests(db_repo)
577 c.repository_pull_requests = scm_model.get_pull_requests(db_repo)
573
578
574 def _handle_missing_requirements(self, error):
579 def _handle_missing_requirements(self, error):
575 self.rhodecode_repo = None
580 self.rhodecode_repo = None
576 log.error(
581 log.error(
577 'Requirements are missing for repository %s: %s',
582 'Requirements are missing for repository %s: %s',
578 c.repo_name, error.message)
583 c.repo_name, error.message)
579
584
580 summary_url = url('summary_home', repo_name=c.repo_name)
585 summary_url = url('summary_home', repo_name=c.repo_name)
581 statistics_url = url('edit_repo_statistics', repo_name=c.repo_name)
586 statistics_url = url('edit_repo_statistics', repo_name=c.repo_name)
582 settings_update_url = url('repo', repo_name=c.repo_name)
587 settings_update_url = url('repo', repo_name=c.repo_name)
583 path = request.path
588 path = request.path
584 should_redirect = (
589 should_redirect = (
585 path not in (summary_url, settings_update_url)
590 path not in (summary_url, settings_update_url)
586 and '/settings' not in path or path == statistics_url
591 and '/settings' not in path or path == statistics_url
587 )
592 )
588 if should_redirect:
593 if should_redirect:
589 redirect(summary_url)
594 redirect(summary_url)
@@ -1,117 +1,118 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 pytest
21 import pytest
22
22
23 from rhodecode.tests.events.conftest import EventCatcher
23 from rhodecode.tests.events.conftest import EventCatcher
24
24
25 from rhodecode.lib import hooks_base, utils2
25 from rhodecode.lib import hooks_base, utils2
26 from rhodecode.model.repo import RepoModel
26 from rhodecode.model.repo import RepoModel
27 from rhodecode.events.repo import (
27 from rhodecode.events.repo import (
28 RepoPrePullEvent, RepoPullEvent,
28 RepoPrePullEvent, RepoPullEvent,
29 RepoPrePushEvent, RepoPushEvent,
29 RepoPrePushEvent, RepoPushEvent,
30 RepoPreCreateEvent, RepoCreateEvent,
30 RepoPreCreateEvent, RepoCreateEvent,
31 RepoPreDeleteEvent, RepoDeleteEvent,
31 RepoPreDeleteEvent, RepoDeleteEvent,
32 )
32 )
33
33
34
34
35 @pytest.fixture
35 @pytest.fixture
36 def scm_extras(user_regular, repo_stub):
36 def scm_extras(user_regular, repo_stub):
37 extras = utils2.AttributeDict({
37 extras = utils2.AttributeDict({
38 'ip': '127.0.0.1',
38 'ip': '127.0.0.1',
39 'username': user_regular.username,
39 'username': user_regular.username,
40 'action': '',
40 'action': '',
41 'repository': repo_stub.repo_name,
41 'repository': repo_stub.repo_name,
42 'scm': repo_stub.scm_instance().alias,
42 'scm': repo_stub.scm_instance().alias,
43 'config': '',
43 'config': '',
44 'server_url': 'http://example.com',
44 'server_url': 'http://example.com',
45 'make_lock': None,
45 'make_lock': None,
46 'user-agent': 'some-client',
46 'locked_by': [None],
47 'locked_by': [None],
47 'commit_ids': ['a' * 40] * 3,
48 'commit_ids': ['a' * 40] * 3,
48 'is_shadow_repo': False,
49 'is_shadow_repo': False,
49 })
50 })
50 return extras
51 return extras
51
52
52
53
53 # TODO: dan: make the serialization tests complete json comparisons
54 # TODO: dan: make the serialization tests complete json comparisons
54 @pytest.mark.parametrize('EventClass', [
55 @pytest.mark.parametrize('EventClass', [
55 RepoPreCreateEvent, RepoCreateEvent,
56 RepoPreCreateEvent, RepoCreateEvent,
56 RepoPreDeleteEvent, RepoDeleteEvent,
57 RepoPreDeleteEvent, RepoDeleteEvent,
57 ])
58 ])
58 def test_repo_events_serialized(repo_stub, EventClass):
59 def test_repo_events_serialized(repo_stub, EventClass):
59 event = EventClass(repo_stub)
60 event = EventClass(repo_stub)
60 data = event.as_dict()
61 data = event.as_dict()
61 assert data['name'] == EventClass.name
62 assert data['name'] == EventClass.name
62 assert data['repo']['repo_name'] == repo_stub.repo_name
63 assert data['repo']['repo_name'] == repo_stub.repo_name
63 assert data['repo']['url']
64 assert data['repo']['url']
64
65
65
66
66 @pytest.mark.parametrize('EventClass', [
67 @pytest.mark.parametrize('EventClass', [
67 RepoPrePullEvent, RepoPullEvent, RepoPrePushEvent
68 RepoPrePullEvent, RepoPullEvent, RepoPrePushEvent
68 ])
69 ])
69 def test_vcs_repo_events_serialize(repo_stub, scm_extras, EventClass):
70 def test_vcs_repo_events_serialize(repo_stub, scm_extras, EventClass):
70 event = EventClass(repo_name=repo_stub.repo_name, extras=scm_extras)
71 event = EventClass(repo_name=repo_stub.repo_name, extras=scm_extras)
71 data = event.as_dict()
72 data = event.as_dict()
72 assert data['name'] == EventClass.name
73 assert data['name'] == EventClass.name
73 assert data['repo']['repo_name'] == repo_stub.repo_name
74 assert data['repo']['repo_name'] == repo_stub.repo_name
74 assert data['repo']['url']
75 assert data['repo']['url']
75
76
76
77
77
78
78 @pytest.mark.parametrize('EventClass', [RepoPushEvent])
79 @pytest.mark.parametrize('EventClass', [RepoPushEvent])
79 def test_vcs_repo_push_event_serialize(repo_stub, scm_extras, EventClass):
80 def test_vcs_repo_push_event_serialize(repo_stub, scm_extras, EventClass):
80 event = EventClass(repo_name=repo_stub.repo_name,
81 event = EventClass(repo_name=repo_stub.repo_name,
81 pushed_commit_ids=scm_extras['commit_ids'],
82 pushed_commit_ids=scm_extras['commit_ids'],
82 extras=scm_extras)
83 extras=scm_extras)
83 data = event.as_dict()
84 data = event.as_dict()
84 assert data['name'] == EventClass.name
85 assert data['name'] == EventClass.name
85 assert data['repo']['repo_name'] == repo_stub.repo_name
86 assert data['repo']['repo_name'] == repo_stub.repo_name
86 assert data['repo']['url']
87 assert data['repo']['url']
87
88
88
89
89 def test_create_delete_repo_fires_events(backend):
90 def test_create_delete_repo_fires_events(backend):
90 with EventCatcher() as event_catcher:
91 with EventCatcher() as event_catcher:
91 repo = backend.create_repo()
92 repo = backend.create_repo()
92 assert event_catcher.events_types == [RepoPreCreateEvent, RepoCreateEvent]
93 assert event_catcher.events_types == [RepoPreCreateEvent, RepoCreateEvent]
93
94
94 with EventCatcher() as event_catcher:
95 with EventCatcher() as event_catcher:
95 RepoModel().delete(repo)
96 RepoModel().delete(repo)
96 assert event_catcher.events_types == [RepoPreDeleteEvent, RepoDeleteEvent]
97 assert event_catcher.events_types == [RepoPreDeleteEvent, RepoDeleteEvent]
97
98
98
99
99 def test_pull_fires_events(scm_extras):
100 def test_pull_fires_events(scm_extras):
100 with EventCatcher() as event_catcher:
101 with EventCatcher() as event_catcher:
101 hooks_base.pre_push(scm_extras)
102 hooks_base.pre_push(scm_extras)
102 assert event_catcher.events_types == [RepoPrePushEvent]
103 assert event_catcher.events_types == [RepoPrePushEvent]
103
104
104 with EventCatcher() as event_catcher:
105 with EventCatcher() as event_catcher:
105 hooks_base.post_push(scm_extras)
106 hooks_base.post_push(scm_extras)
106 assert event_catcher.events_types == [RepoPushEvent]
107 assert event_catcher.events_types == [RepoPushEvent]
107
108
108
109
109 def test_push_fires_events(scm_extras):
110 def test_push_fires_events(scm_extras):
110 with EventCatcher() as event_catcher:
111 with EventCatcher() as event_catcher:
111 hooks_base.pre_pull(scm_extras)
112 hooks_base.pre_pull(scm_extras)
112 assert event_catcher.events_types == [RepoPrePullEvent]
113 assert event_catcher.events_types == [RepoPrePullEvent]
113
114
114 with EventCatcher() as event_catcher:
115 with EventCatcher() as event_catcher:
115 hooks_base.post_pull(scm_extras)
116 hooks_base.post_pull(scm_extras)
116 assert event_catcher.events_types == [RepoPullEvent]
117 assert event_catcher.events_types == [RepoPullEvent]
117
118
@@ -1,253 +1,255 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 pytest
21 import pytest
22 from mock import Mock, patch
22 from mock import Mock, patch
23 from pylons import url
23 from pylons import url
24
24
25 from rhodecode.lib import base
25 from rhodecode.lib import base
26 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
26 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
27 from rhodecode.model import db
27 from rhodecode.model import db
28
28
29
29
30 @pytest.mark.parametrize('result_key, expected_value', [
30 @pytest.mark.parametrize('result_key, expected_value', [
31 ('username', 'stub_username'),
31 ('username', 'stub_username'),
32 ('action', 'stub_action'),
32 ('action', 'stub_action'),
33 ('repository', 'stub_repo_name'),
33 ('repository', 'stub_repo_name'),
34 ('scm', 'stub_scm'),
34 ('scm', 'stub_scm'),
35 ('hooks', ['stub_hook']),
35 ('hooks', ['stub_hook']),
36 ('config', 'stub_ini_filename'),
36 ('config', 'stub_ini_filename'),
37 ('ip', 'fake_ip'),
37 ('ip', '1.2.3.4'),
38 ('server_url', 'https://example.com'),
38 ('server_url', 'https://example.com'),
39 ('user_agent', 'client-text-v1.1'),
39 # TODO: johbo: Commpare locking parameters with `_get_rc_scm_extras`
40 # TODO: johbo: Commpare locking parameters with `_get_rc_scm_extras`
40 # in hooks_utils.
41 # in hooks_utils.
41 ('make_lock', None),
42 ('make_lock', None),
42 ('locked_by', [None, None, None]),
43 ('locked_by', [None, None, None]),
43 ])
44 ])
44 def test_vcs_operation_context_parameters(result_key, expected_value):
45 def test_vcs_operation_context_parameters(result_key, expected_value):
45 result = call_vcs_operation_context()
46 result = call_vcs_operation_context()
46 assert result[result_key] == expected_value
47 assert result[result_key] == expected_value
47
48
48
49
49 @patch('rhodecode.model.db.User.get_by_username', Mock())
50 @patch('rhodecode.model.db.User.get_by_username', Mock())
50 @patch('rhodecode.model.db.Repository.get_by_repo_name')
51 @patch('rhodecode.model.db.Repository.get_by_repo_name')
51 def test_vcs_operation_context_checks_locking(mock_get_by_repo_name):
52 def test_vcs_operation_context_checks_locking(mock_get_by_repo_name):
52 mock_get_locking_state = mock_get_by_repo_name().get_locking_state
53 mock_get_locking_state = mock_get_by_repo_name().get_locking_state
53 mock_get_locking_state.return_value = (None, None, [None, None, None])
54 mock_get_locking_state.return_value = (None, None, [None, None, None])
54 call_vcs_operation_context(check_locking=True)
55 call_vcs_operation_context(check_locking=True)
55 assert mock_get_locking_state.called
56 assert mock_get_locking_state.called
56
57
57
58
58 @patch('rhodecode.model.db.Repository.get_locking_state')
59 @patch('rhodecode.model.db.Repository.get_locking_state')
59 def test_vcs_operation_context_skips_locking_checks_if_anonymouse(
60 def test_vcs_operation_context_skips_locking_checks_if_anonymouse(
60 mock_get_locking_state):
61 mock_get_locking_state):
61 call_vcs_operation_context(
62 call_vcs_operation_context(
62 username=db.User.DEFAULT_USER, check_locking=True)
63 username=db.User.DEFAULT_USER, check_locking=True)
63 assert not mock_get_locking_state.called
64 assert not mock_get_locking_state.called
64
65
65
66
66 @patch('rhodecode.model.db.Repository.get_locking_state')
67 @patch('rhodecode.model.db.Repository.get_locking_state')
67 def test_vcs_operation_context_can_skip_locking_check(mock_get_locking_state):
68 def test_vcs_operation_context_can_skip_locking_check(mock_get_locking_state):
68 call_vcs_operation_context(check_locking=False)
69 call_vcs_operation_context(check_locking=False)
69 assert not mock_get_locking_state.called
70 assert not mock_get_locking_state.called
70
71
71
72
72 @patch.object(
73 @patch.object(
73 base, 'get_enabled_hook_classes', Mock(return_value=['stub_hook']))
74 base, 'get_enabled_hook_classes', Mock(return_value=['stub_hook']))
74 @patch.object(base, 'get_ip_addr', Mock(return_value="fake_ip"))
75 @patch('rhodecode.lib.utils2.get_server_url',
75 @patch('rhodecode.lib.utils2.get_server_url',
76 Mock(return_value='https://example.com'))
76 Mock(return_value='https://example.com'))
77 def call_vcs_operation_context(**kwargs_override):
77 def call_vcs_operation_context(**kwargs_override):
78 kwargs = {
78 kwargs = {
79 'repo_name': 'stub_repo_name',
79 'repo_name': 'stub_repo_name',
80 'username': 'stub_username',
80 'username': 'stub_username',
81 'action': 'stub_action',
81 'action': 'stub_action',
82 'scm': 'stub_scm',
82 'scm': 'stub_scm',
83 'check_locking': False,
83 'check_locking': False,
84 }
84 }
85 kwargs.update(kwargs_override)
85 kwargs.update(kwargs_override)
86 config_file_patch = patch.dict(
86 config_file_patch = patch.dict(
87 'rhodecode.CONFIG', {'__file__': 'stub_ini_filename'})
87 'rhodecode.CONFIG', {'__file__': 'stub_ini_filename'})
88 settings_patch = patch.object(base, 'VcsSettingsModel')
88 settings_patch = patch.object(base, 'VcsSettingsModel')
89 with config_file_patch, settings_patch as settings_mock:
89 with config_file_patch, settings_patch as settings_mock:
90 result = base.vcs_operation_context(environ={}, **kwargs)
90 result = base.vcs_operation_context(
91 environ={'HTTP_USER_AGENT': 'client-text-v1.1',
92 'REMOTE_ADDR': '1.2.3.4'}, **kwargs)
91 settings_mock.assert_called_once_with(repo='stub_repo_name')
93 settings_mock.assert_called_once_with(repo='stub_repo_name')
92 return result
94 return result
93
95
94
96
95 class TestBaseRepoController(object):
97 class TestBaseRepoController(object):
96 def test_context_is_updated_when_update_global_counters_is_called(self):
98 def test_context_is_updated_when_update_global_counters_is_called(self):
97 followers = 1
99 followers = 1
98 forks = 2
100 forks = 2
99 pull_requests = 3
101 pull_requests = 3
100 is_following = True
102 is_following = True
101 scm_model = Mock(name="scm_model")
103 scm_model = Mock(name="scm_model")
102 db_repo = Mock(name="db_repo")
104 db_repo = Mock(name="db_repo")
103 scm_model.get_followers.return_value = followers
105 scm_model.get_followers.return_value = followers
104 scm_model.get_forks.return_value = forks
106 scm_model.get_forks.return_value = forks
105 scm_model.get_pull_requests.return_value = pull_requests
107 scm_model.get_pull_requests.return_value = pull_requests
106 scm_model.is_following_repo.return_value = is_following
108 scm_model.is_following_repo.return_value = is_following
107
109
108 controller = base.BaseRepoController()
110 controller = base.BaseRepoController()
109 with patch.object(base, 'c') as context_mock:
111 with patch.object(base, 'c') as context_mock:
110 controller._update_global_counters(scm_model, db_repo)
112 controller._update_global_counters(scm_model, db_repo)
111
113
112 scm_model.get_pull_requests.assert_called_once_with(db_repo)
114 scm_model.get_pull_requests.assert_called_once_with(db_repo)
113
115
114 assert context_mock.repository_pull_requests == pull_requests
116 assert context_mock.repository_pull_requests == pull_requests
115
117
116
118
117 class TestBaseRepoControllerHandleMissingRequirements(object):
119 class TestBaseRepoControllerHandleMissingRequirements(object):
118 def test_logs_error_and_sets_repo_to_none(self, app):
120 def test_logs_error_and_sets_repo_to_none(self, app):
119 controller = base.BaseRepoController()
121 controller = base.BaseRepoController()
120 error_message = 'Some message'
122 error_message = 'Some message'
121 error = RepositoryRequirementError(error_message)
123 error = RepositoryRequirementError(error_message)
122 context_patcher = patch.object(base, 'c')
124 context_patcher = patch.object(base, 'c')
123 log_patcher = patch.object(base, 'log')
125 log_patcher = patch.object(base, 'log')
124 request_patcher = patch.object(base, 'request')
126 request_patcher = patch.object(base, 'request')
125 redirect_patcher = patch.object(base, 'redirect')
127 redirect_patcher = patch.object(base, 'redirect')
126 controller.rhodecode_repo = 'something'
128 controller.rhodecode_repo = 'something'
127
129
128 with context_patcher as context_mock, log_patcher as log_mock, \
130 with context_patcher as context_mock, log_patcher as log_mock, \
129 request_patcher, redirect_patcher:
131 request_patcher, redirect_patcher:
130 context_mock.repo_name = 'abcde'
132 context_mock.repo_name = 'abcde'
131 controller._handle_missing_requirements(error)
133 controller._handle_missing_requirements(error)
132
134
133 expected_log_message = (
135 expected_log_message = (
134 'Requirements are missing for repository %s: %s', 'abcde',
136 'Requirements are missing for repository %s: %s', 'abcde',
135 error_message)
137 error_message)
136 log_mock.error.assert_called_once_with(*expected_log_message)
138 log_mock.error.assert_called_once_with(*expected_log_message)
137
139
138 assert controller.rhodecode_repo is None
140 assert controller.rhodecode_repo is None
139
141
140 @pytest.mark.parametrize('path, should_redirect', [
142 @pytest.mark.parametrize('path, should_redirect', [
141 ('/abcde', False),
143 ('/abcde', False),
142 ('/abcde/settings', False),
144 ('/abcde/settings', False),
143 ('/abcde/settings/vcs', False),
145 ('/abcde/settings/vcs', False),
144 ('/_admin/repos/abcde', False), # Settings update
146 ('/_admin/repos/abcde', False), # Settings update
145 ('/abcde/changelog', True),
147 ('/abcde/changelog', True),
146 ('/abcde/files/tip', True),
148 ('/abcde/files/tip', True),
147 ('/abcde/settings/statistics', True),
149 ('/abcde/settings/statistics', True),
148 ])
150 ])
149 def test_redirects_if_not_summary_or_settings_page(
151 def test_redirects_if_not_summary_or_settings_page(
150 self, app, path, should_redirect):
152 self, app, path, should_redirect):
151 repo_name = 'abcde'
153 repo_name = 'abcde'
152 controller = base.BaseRepoController()
154 controller = base.BaseRepoController()
153 error = RepositoryRequirementError('Some message')
155 error = RepositoryRequirementError('Some message')
154 context_patcher = patch.object(base, 'c')
156 context_patcher = patch.object(base, 'c')
155 controller.rhodecode_repo = repo_name
157 controller.rhodecode_repo = repo_name
156 request_patcher = patch.object(base, 'request')
158 request_patcher = patch.object(base, 'request')
157 redirect_patcher = patch.object(base, 'redirect')
159 redirect_patcher = patch.object(base, 'redirect')
158
160
159 with context_patcher as context_mock, \
161 with context_patcher as context_mock, \
160 request_patcher as request_mock, \
162 request_patcher as request_mock, \
161 redirect_patcher as redirect_mock:
163 redirect_patcher as redirect_mock:
162 request_mock.path = path
164 request_mock.path = path
163 context_mock.repo_name = repo_name
165 context_mock.repo_name = repo_name
164 controller._handle_missing_requirements(error)
166 controller._handle_missing_requirements(error)
165
167
166 expected_url = url('summary_home', repo_name=repo_name)
168 expected_url = url('summary_home', repo_name=repo_name)
167 if should_redirect:
169 if should_redirect:
168 redirect_mock.assert_called_once_with(expected_url)
170 redirect_mock.assert_called_once_with(expected_url)
169 else:
171 else:
170 redirect_mock.call_count == 0
172 redirect_mock.call_count == 0
171
173
172
174
173 class TestBaseRepoControllerBefore(object):
175 class TestBaseRepoControllerBefore(object):
174 def test_flag_is_true_when_requirements_are_missing(self, before_mocks):
176 def test_flag_is_true_when_requirements_are_missing(self, before_mocks):
175 controller = self._get_controller()
177 controller = self._get_controller()
176
178
177 handle_patcher = patch.object(
179 handle_patcher = patch.object(
178 controller, '_handle_missing_requirements')
180 controller, '_handle_missing_requirements')
179
181
180 error = RepositoryRequirementError()
182 error = RepositoryRequirementError()
181 before_mocks.repository.scm_instance.side_effect = error
183 before_mocks.repository.scm_instance.side_effect = error
182
184
183 with handle_patcher as handle_mock:
185 with handle_patcher as handle_mock:
184 controller.__before__()
186 controller.__before__()
185
187
186 handle_mock.assert_called_once_with(error)
188 handle_mock.assert_called_once_with(error)
187 assert before_mocks['context'].repository_requirements_missing is True
189 assert before_mocks['context'].repository_requirements_missing is True
188
190
189 def test_flag_is_false_when_no_requirements_are_missing(
191 def test_flag_is_false_when_no_requirements_are_missing(
190 self, before_mocks):
192 self, before_mocks):
191 controller = self._get_controller()
193 controller = self._get_controller()
192
194
193 handle_patcher = patch.object(
195 handle_patcher = patch.object(
194 controller, '_handle_missing_requirements')
196 controller, '_handle_missing_requirements')
195 with handle_patcher as handle_mock:
197 with handle_patcher as handle_mock:
196 controller.__before__()
198 controller.__before__()
197 handle_mock.call_count == 0
199 handle_mock.call_count == 0
198 assert before_mocks['context'].repository_requirements_missing is False
200 assert before_mocks['context'].repository_requirements_missing is False
199
201
200 def test_update_global_counters_is_called(self, before_mocks):
202 def test_update_global_counters_is_called(self, before_mocks):
201 controller = self._get_controller()
203 controller = self._get_controller()
202
204
203 update_counters_patcher = patch.object(
205 update_counters_patcher = patch.object(
204 controller, '_update_global_counters')
206 controller, '_update_global_counters')
205
207
206 with update_counters_patcher as update_counters_mock:
208 with update_counters_patcher as update_counters_mock:
207 controller.__before__()
209 controller.__before__()
208 update_counters_mock.assert_called_once_with(
210 update_counters_mock.assert_called_once_with(
209 controller.scm_model, before_mocks.repository)
211 controller.scm_model, before_mocks.repository)
210
212
211 def _get_controller(self):
213 def _get_controller(self):
212 controller = base.BaseRepoController()
214 controller = base.BaseRepoController()
213 controller.scm_model = Mock()
215 controller.scm_model = Mock()
214 controller.rhodecode_repo = Mock()
216 controller.rhodecode_repo = Mock()
215 return controller
217 return controller
216
218
217
219
218 @pytest.fixture
220 @pytest.fixture
219 def before_mocks(request):
221 def before_mocks(request):
220 patcher = BeforePatcher()
222 patcher = BeforePatcher()
221 patcher.start()
223 patcher.start()
222 request.addfinalizer(patcher.stop)
224 request.addfinalizer(patcher.stop)
223 return patcher
225 return patcher
224
226
225
227
226 class BeforePatcher(object):
228 class BeforePatcher(object):
227 patchers = {}
229 patchers = {}
228 mocks = {}
230 mocks = {}
229 repository = None
231 repository = None
230
232
231 def __init__(self):
233 def __init__(self):
232 self.repository = Mock()
234 self.repository = Mock()
233
235
234 def start(self):
236 def start(self):
235 self.patchers = {
237 self.patchers = {
236 'request': patch.object(base, 'request'),
238 'request': patch.object(base, 'request'),
237 'before': patch.object(base.BaseController, '__before__'),
239 'before': patch.object(base.BaseController, '__before__'),
238 'context': patch.object(base, 'c'),
240 'context': patch.object(base, 'c'),
239 'repo': patch.object(
241 'repo': patch.object(
240 base.Repository, 'get_by_repo_name',
242 base.Repository, 'get_by_repo_name',
241 return_value=self.repository)
243 return_value=self.repository)
242
244
243 }
245 }
244 self.mocks = {
246 self.mocks = {
245 p: self.patchers[p].start() for p in self.patchers
247 p: self.patchers[p].start() for p in self.patchers
246 }
248 }
247
249
248 def stop(self):
250 def stop(self):
249 for patcher in self.patchers.values():
251 for patcher in self.patchers.values():
250 patcher.stop()
252 patcher.stop()
251
253
252 def __getitem__(self, key):
254 def __getitem__(self, key):
253 return self.mocks[key]
255 return self.mocks[key]
@@ -1,142 +1,144 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 mock
21 import mock
22 import pytest
22 import pytest
23
23
24 from rhodecode.lib import hooks_base, utils2
24 from rhodecode.lib import hooks_base, utils2
25
25
26
26
27 @mock.patch.multiple(
27 @mock.patch.multiple(
28 hooks_base,
28 hooks_base,
29 action_logger=mock.Mock(),
29 action_logger=mock.Mock(),
30 post_push_extension=mock.Mock(),
30 post_push_extension=mock.Mock(),
31 Repository=mock.Mock())
31 Repository=mock.Mock())
32 def test_post_push_truncates_commits(user_regular, repo_stub):
32 def test_post_push_truncates_commits(user_regular, repo_stub):
33 extras = {
33 extras = {
34 'ip': '127.0.0.1',
34 'ip': '127.0.0.1',
35 'username': user_regular.username,
35 'username': user_regular.username,
36 'action': 'push_local',
36 'action': 'push_local',
37 'repository': repo_stub.repo_name,
37 'repository': repo_stub.repo_name,
38 'scm': 'git',
38 'scm': 'git',
39 'config': '',
39 'config': '',
40 'server_url': 'http://example.com',
40 'server_url': 'http://example.com',
41 'make_lock': None,
41 'make_lock': None,
42 'user_agent': 'some-client',
42 'locked_by': [None],
43 'locked_by': [None],
43 'commit_ids': ['abcde12345' * 4] * 30000,
44 'commit_ids': ['abcde12345' * 4] * 30000,
44 'is_shadow_repo': False,
45 'is_shadow_repo': False,
45 }
46 }
46 extras = utils2.AttributeDict(extras)
47 extras = utils2.AttributeDict(extras)
47
48
48 hooks_base.post_push(extras)
49 hooks_base.post_push(extras)
49
50
50 # Calculate appropriate action string here
51 # Calculate appropriate action string here
51 expected_action = 'push_local:%s' % ','.join(extras.commit_ids[:29000])
52 expected_action = 'push_local:%s' % ','.join(extras.commit_ids[:29000])
52
53
53 hooks_base.action_logger.assert_called_with(
54 hooks_base.action_logger.assert_called_with(
54 extras.username, expected_action, extras.repository, extras.ip,
55 extras.username, expected_action, extras.repository, extras.ip,
55 commit=True)
56 commit=True)
56
57
57
58
58 def assert_called_with_mock(callable_, expected_mock_name):
59 def assert_called_with_mock(callable_, expected_mock_name):
59 mock_obj = callable_.call_args[0][0]
60 mock_obj = callable_.call_args[0][0]
60 mock_name = mock_obj._mock_new_parent._mock_new_name
61 mock_name = mock_obj._mock_new_parent._mock_new_name
61 assert mock_name == expected_mock_name
62 assert mock_name == expected_mock_name
62
63
63
64
64 @pytest.fixture
65 @pytest.fixture
65 def hook_extras(user_regular, repo_stub):
66 def hook_extras(user_regular, repo_stub):
66 extras = utils2.AttributeDict({
67 extras = utils2.AttributeDict({
67 'ip': '127.0.0.1',
68 'ip': '127.0.0.1',
68 'username': user_regular.username,
69 'username': user_regular.username,
69 'action': 'push',
70 'action': 'push',
70 'repository': repo_stub.repo_name,
71 'repository': repo_stub.repo_name,
71 'scm': '',
72 'scm': '',
72 'config': '',
73 'config': '',
73 'server_url': 'http://example.com',
74 'server_url': 'http://example.com',
74 'make_lock': None,
75 'make_lock': None,
76 'user_agent': 'some-client',
75 'locked_by': [None],
77 'locked_by': [None],
76 'commit_ids': [],
78 'commit_ids': [],
77 'is_shadow_repo': False,
79 'is_shadow_repo': False,
78 })
80 })
79 return extras
81 return extras
80
82
81
83
82 @pytest.mark.parametrize('func, extension, event', [
84 @pytest.mark.parametrize('func, extension, event', [
83 (hooks_base.pre_push, 'pre_push_extension', 'RepoPrePushEvent'),
85 (hooks_base.pre_push, 'pre_push_extension', 'RepoPrePushEvent'),
84 (hooks_base.post_push, 'post_pull_extension', 'RepoPushEvent'),
86 (hooks_base.post_push, 'post_pull_extension', 'RepoPushEvent'),
85 (hooks_base.pre_pull, 'pre_pull_extension', 'RepoPrePullEvent'),
87 (hooks_base.pre_pull, 'pre_pull_extension', 'RepoPrePullEvent'),
86 (hooks_base.post_pull, 'post_push_extension', 'RepoPullEvent'),
88 (hooks_base.post_pull, 'post_push_extension', 'RepoPullEvent'),
87 ])
89 ])
88 def test_hooks_propagate(func, extension, event, hook_extras):
90 def test_hooks_propagate(func, extension, event, hook_extras):
89 """
91 """
90 Tests that our hook code propagates to rhodecode extensions and triggers
92 Tests that our hook code propagates to rhodecode extensions and triggers
91 the appropriate event.
93 the appropriate event.
92 """
94 """
93 extension_mock = mock.Mock()
95 extension_mock = mock.Mock()
94 events_mock = mock.Mock()
96 events_mock = mock.Mock()
95 patches = {
97 patches = {
96 'Repository': mock.Mock(),
98 'Repository': mock.Mock(),
97 'events': events_mock,
99 'events': events_mock,
98 extension: extension_mock,
100 extension: extension_mock,
99 }
101 }
100
102
101 # Clear shadow repo flag.
103 # Clear shadow repo flag.
102 hook_extras.is_shadow_repo = False
104 hook_extras.is_shadow_repo = False
103
105
104 # Execute hook function.
106 # Execute hook function.
105 with mock.patch.multiple(hooks_base, **patches):
107 with mock.patch.multiple(hooks_base, **patches):
106 func(hook_extras)
108 func(hook_extras)
107
109
108 # Assert that extensions are called and event was fired.
110 # Assert that extensions are called and event was fired.
109 extension_mock.called_once()
111 extension_mock.called_once()
110 assert_called_with_mock(events_mock.trigger, event)
112 assert_called_with_mock(events_mock.trigger, event)
111
113
112
114
113 @pytest.mark.parametrize('func, extension, event', [
115 @pytest.mark.parametrize('func, extension, event', [
114 (hooks_base.pre_push, 'pre_push_extension', 'RepoPrePushEvent'),
116 (hooks_base.pre_push, 'pre_push_extension', 'RepoPrePushEvent'),
115 (hooks_base.post_push, 'post_pull_extension', 'RepoPushEvent'),
117 (hooks_base.post_push, 'post_pull_extension', 'RepoPushEvent'),
116 (hooks_base.pre_pull, 'pre_pull_extension', 'RepoPrePullEvent'),
118 (hooks_base.pre_pull, 'pre_pull_extension', 'RepoPrePullEvent'),
117 (hooks_base.post_pull, 'post_push_extension', 'RepoPullEvent'),
119 (hooks_base.post_pull, 'post_push_extension', 'RepoPullEvent'),
118 ])
120 ])
119 def test_hooks_propagates_not_on_shadow(func, extension, event, hook_extras):
121 def test_hooks_propagates_not_on_shadow(func, extension, event, hook_extras):
120 """
122 """
121 If hooks are called by a request to a shadow repo we only want to run our
123 If hooks are called by a request to a shadow repo we only want to run our
122 internal hooks code but not external ones like rhodecode extensions or
124 internal hooks code but not external ones like rhodecode extensions or
123 trigger an event.
125 trigger an event.
124 """
126 """
125 extension_mock = mock.Mock()
127 extension_mock = mock.Mock()
126 events_mock = mock.Mock()
128 events_mock = mock.Mock()
127 patches = {
129 patches = {
128 'Repository': mock.Mock(),
130 'Repository': mock.Mock(),
129 'events': events_mock,
131 'events': events_mock,
130 extension: extension_mock,
132 extension: extension_mock,
131 }
133 }
132
134
133 # Set shadow repo flag.
135 # Set shadow repo flag.
134 hook_extras.is_shadow_repo = True
136 hook_extras.is_shadow_repo = True
135
137
136 # Execute hook function.
138 # Execute hook function.
137 with mock.patch.multiple(hooks_base, **patches):
139 with mock.patch.multiple(hooks_base, **patches):
138 func(hook_extras)
140 func(hook_extras)
139
141
140 # Assert that extensions are *not* called and event was *not* fired.
142 # Assert that extensions are *not* called and event was *not* fired.
141 assert not extension_mock.called
143 assert not extension_mock.called
142 assert not events_mock.trigger.called
144 assert not events_mock.trigger.called
General Comments 0
You need to be logged in to leave comments. Login now