##// END OF EJS Templates
tests: refactor code to use a single test url generator
super-admin -
r5173:95a4b30f default
parent child Browse files
Show More

The requested changes are too big and content was truncated. Show full diff

1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
@@ -1,574 +1,573 b''
1 # Copyright (C) 2011-2023 RhodeCode GmbH
1 # Copyright (C) 2011-2023 RhodeCode GmbH
2 #
2 #
3 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
14 #
15 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19 import itertools
19 import itertools
20 import logging
20 import logging
21 import sys
21 import sys
22 import fnmatch
22 import fnmatch
23
23
24 import decorator
24 import decorator
25 import typing
25 import typing
26 import venusian
26 import venusian
27 from collections import OrderedDict
27 from collections import OrderedDict
28
28
29 from pyramid.exceptions import ConfigurationError
29 from pyramid.exceptions import ConfigurationError
30 from pyramid.renderers import render
30 from pyramid.renderers import render
31 from pyramid.response import Response
31 from pyramid.response import Response
32 from pyramid.httpexceptions import HTTPNotFound
32 from pyramid.httpexceptions import HTTPNotFound
33
33
34 from rhodecode.api.exc import (
34 from rhodecode.api.exc import (
35 JSONRPCBaseError, JSONRPCError, JSONRPCForbidden, JSONRPCValidationError)
35 JSONRPCBaseError, JSONRPCError, JSONRPCForbidden, JSONRPCValidationError)
36 from rhodecode.apps._base import TemplateArgs
36 from rhodecode.apps._base import TemplateArgs
37 from rhodecode.lib.auth import AuthUser
37 from rhodecode.lib.auth import AuthUser
38 from rhodecode.lib.base import get_ip_addr, attach_context_attributes
38 from rhodecode.lib.base import get_ip_addr, attach_context_attributes
39 from rhodecode.lib.exc_tracking import store_exception
39 from rhodecode.lib.exc_tracking import store_exception
40 from rhodecode.lib import ext_json
40 from rhodecode.lib import ext_json
41 from rhodecode.lib.utils2 import safe_str
41 from rhodecode.lib.utils2 import safe_str
42 from rhodecode.lib.plugins.utils import get_plugin_settings
42 from rhodecode.lib.plugins.utils import get_plugin_settings
43 from rhodecode.model.db import User, UserApiKeys
43 from rhodecode.model.db import User, UserApiKeys
44
44
45 log = logging.getLogger(__name__)
45 log = logging.getLogger(__name__)
46
46
47 DEFAULT_RENDERER = 'jsonrpc_renderer'
47 DEFAULT_RENDERER = 'jsonrpc_renderer'
48 DEFAULT_URL = '/_admin/apiv2'
48 DEFAULT_URL = '/_admin/apiv2'
49
49
50
50
51 def find_methods(jsonrpc_methods, pattern):
51 def find_methods(jsonrpc_methods, pattern):
52 matches = OrderedDict()
52 matches = OrderedDict()
53 if not isinstance(pattern, (list, tuple)):
53 if not isinstance(pattern, (list, tuple)):
54 pattern = [pattern]
54 pattern = [pattern]
55
55
56 for single_pattern in pattern:
56 for single_pattern in pattern:
57 for method_name, method in jsonrpc_methods.items():
57 for method_name, method in jsonrpc_methods.items():
58 if fnmatch.fnmatch(method_name, single_pattern):
58 if fnmatch.fnmatch(method_name, single_pattern):
59 matches[method_name] = method
59 matches[method_name] = method
60 return matches
60 return matches
61
61
62
62
63 class ExtJsonRenderer(object):
63 class ExtJsonRenderer(object):
64 """
64 """
65 Custom renderer that makes use of our ext_json lib
65 Custom renderer that makes use of our ext_json lib
66
66
67 """
67 """
68
68
69 def __init__(self):
69 def __init__(self):
70 self.serializer = ext_json.formatted_json
70 self.serializer = ext_json.formatted_json
71
71
72 def __call__(self, info):
72 def __call__(self, info):
73 """ Returns a plain JSON-encoded string with content-type
73 """ Returns a plain JSON-encoded string with content-type
74 ``application/json``. The content-type may be overridden by
74 ``application/json``. The content-type may be overridden by
75 setting ``request.response.content_type``."""
75 setting ``request.response.content_type``."""
76
76
77 def _render(value, system):
77 def _render(value, system):
78 request = system.get('request')
78 request = system.get('request')
79 if request is not None:
79 if request is not None:
80 response = request.response
80 response = request.response
81 ct = response.content_type
81 ct = response.content_type
82 if ct == response.default_content_type:
82 if ct == response.default_content_type:
83 response.content_type = 'application/json'
83 response.content_type = 'application/json'
84
84
85 return self.serializer(value)
85 return self.serializer(value)
86
86
87 return _render
87 return _render
88
88
89
89
90 def jsonrpc_response(request, result):
90 def jsonrpc_response(request, result):
91 rpc_id = getattr(request, 'rpc_id', None)
91 rpc_id = getattr(request, 'rpc_id', None)
92
92
93 ret_value = ''
93 ret_value = ''
94 if rpc_id:
94 if rpc_id:
95 ret_value = {'id': rpc_id, 'result': result, 'error': None}
95 ret_value = {'id': rpc_id, 'result': result, 'error': None}
96
96
97 # fetch deprecation warnings, and store it inside results
97 # fetch deprecation warnings, and store it inside results
98 deprecation = getattr(request, 'rpc_deprecation', None)
98 deprecation = getattr(request, 'rpc_deprecation', None)
99 if deprecation:
99 if deprecation:
100 ret_value['DEPRECATION_WARNING'] = deprecation
100 ret_value['DEPRECATION_WARNING'] = deprecation
101
101
102 raw_body = render(DEFAULT_RENDERER, ret_value, request=request)
102 raw_body = render(DEFAULT_RENDERER, ret_value, request=request)
103 content_type = 'application/json'
103 content_type = 'application/json'
104 content_type_header = 'Content-Type'
104 content_type_header = 'Content-Type'
105 headers = {
105 headers = {
106 content_type_header: content_type
106 content_type_header: content_type
107 }
107 }
108 return Response(
108 return Response(
109 body=raw_body,
109 body=raw_body,
110 content_type=content_type,
110 content_type=content_type,
111 headerlist=[(k, v) for k, v in headers.items()]
111 headerlist=[(k, v) for k, v in headers.items()]
112 )
112 )
113
113
114
114
115 def jsonrpc_error(request, message, retid=None, code: int | None = None, headers: dict | None = None):
115 def jsonrpc_error(request, message, retid=None, code: int | None = None, headers: dict | None = None):
116 """
116 """
117 Generate a Response object with a JSON-RPC error body
117 Generate a Response object with a JSON-RPC error body
118 """
118 """
119 headers = headers or {}
119 headers = headers or {}
120 content_type = 'application/json'
120 content_type = 'application/json'
121 content_type_header = 'Content-Type'
121 content_type_header = 'Content-Type'
122 if content_type_header not in headers:
122 if content_type_header not in headers:
123 headers[content_type_header] = content_type
123 headers[content_type_header] = content_type
124
124
125 err_dict = {'id': retid, 'result': None, 'error': message}
125 err_dict = {'id': retid, 'result': None, 'error': message}
126 raw_body = render(DEFAULT_RENDERER, err_dict, request=request)
126 raw_body = render(DEFAULT_RENDERER, err_dict, request=request)
127
127
128 return Response(
128 return Response(
129 body=raw_body,
129 body=raw_body,
130 status=code,
130 status=code,
131 content_type=content_type,
131 content_type=content_type,
132 headerlist=[(k, v) for k, v in headers.items()]
132 headerlist=[(k, v) for k, v in headers.items()]
133 )
133 )
134
134
135
135
136 def exception_view(exc, request):
136 def exception_view(exc, request):
137 rpc_id = getattr(request, 'rpc_id', None)
137 rpc_id = getattr(request, 'rpc_id', None)
138
138
139 if isinstance(exc, JSONRPCError):
139 if isinstance(exc, JSONRPCError):
140 fault_message = safe_str(exc)
140 fault_message = safe_str(exc)
141 log.debug('json-rpc error rpc_id:%s "%s"', rpc_id, fault_message)
141 log.debug('json-rpc error rpc_id:%s "%s"', rpc_id, fault_message)
142 elif isinstance(exc, JSONRPCValidationError):
142 elif isinstance(exc, JSONRPCValidationError):
143 colander_exc = exc.colander_exception
143 colander_exc = exc.colander_exception
144 # TODO(marcink): think maybe of nicer way to serialize errors ?
144 # TODO(marcink): think maybe of nicer way to serialize errors ?
145 fault_message = colander_exc.asdict()
145 fault_message = colander_exc.asdict()
146 log.debug('json-rpc colander error rpc_id:%s "%s"', rpc_id, fault_message)
146 log.debug('json-rpc colander error rpc_id:%s "%s"', rpc_id, fault_message)
147 elif isinstance(exc, JSONRPCForbidden):
147 elif isinstance(exc, JSONRPCForbidden):
148 fault_message = 'Access was denied to this resource.'
148 fault_message = 'Access was denied to this resource.'
149 log.warning('json-rpc forbidden call rpc_id:%s "%s"', rpc_id, fault_message)
149 log.warning('json-rpc forbidden call rpc_id:%s "%s"', rpc_id, fault_message)
150 elif isinstance(exc, HTTPNotFound):
150 elif isinstance(exc, HTTPNotFound):
151 method = request.rpc_method
151 method = request.rpc_method
152 log.debug('json-rpc method `%s` not found in list of '
152 log.debug('json-rpc method `%s` not found in list of '
153 'api calls: %s, rpc_id:%s',
153 'api calls: %s, rpc_id:%s',
154 method, list(request.registry.jsonrpc_methods.keys()), rpc_id)
154 method, list(request.registry.jsonrpc_methods.keys()), rpc_id)
155
155
156 similar = 'none'
156 similar = 'none'
157 try:
157 try:
158 similar_paterns = [f'*{x}*' for x in method.split('_')]
158 similar_paterns = [f'*{x}*' for x in method.split('_')]
159 similar_found = find_methods(
159 similar_found = find_methods(
160 request.registry.jsonrpc_methods, similar_paterns)
160 request.registry.jsonrpc_methods, similar_paterns)
161 similar = ', '.join(similar_found.keys()) or similar
161 similar = ', '.join(similar_found.keys()) or similar
162 except Exception:
162 except Exception:
163 # make the whole above block safe
163 # make the whole above block safe
164 pass
164 pass
165
165
166 fault_message = "No such method: {}. Similar methods: {}".format(
166 fault_message = f"No such method: {method}. Similar methods: {similar}"
167 method, similar)
168 else:
167 else:
169 fault_message = 'undefined error'
168 fault_message = 'undefined error'
170 exc_info = exc.exc_info()
169 exc_info = exc.exc_info()
171 store_exception(id(exc_info), exc_info, prefix='rhodecode-api')
170 store_exception(id(exc_info), exc_info, prefix='rhodecode-api')
172
171
173 statsd = request.registry.statsd
172 statsd = request.registry.statsd
174 if statsd:
173 if statsd:
175 exc_type = f"{exc.__class__.__module__}.{exc.__class__.__name__}"
174 exc_type = f"{exc.__class__.__module__}.{exc.__class__.__name__}"
176 statsd.incr('rhodecode_exception_total',
175 statsd.incr('rhodecode_exception_total',
177 tags=["exc_source:api", f"type:{exc_type}"])
176 tags=["exc_source:api", f"type:{exc_type}"])
178
177
179 return jsonrpc_error(request, fault_message, rpc_id)
178 return jsonrpc_error(request, fault_message, rpc_id)
180
179
181
180
182 def request_view(request):
181 def request_view(request):
183 """
182 """
184 Main request handling method. It handles all logic to call a specific
183 Main request handling method. It handles all logic to call a specific
185 exposed method
184 exposed method
186 """
185 """
187 # cython compatible inspect
186 # cython compatible inspect
188 from rhodecode.config.patches import inspect_getargspec
187 from rhodecode.config.patches import inspect_getargspec
189 inspect = inspect_getargspec()
188 inspect = inspect_getargspec()
190
189
191 # check if we can find this session using api_key, get_by_auth_token
190 # check if we can find this session using api_key, get_by_auth_token
192 # search not expired tokens only
191 # search not expired tokens only
193 try:
192 try:
194 api_user = User.get_by_auth_token(request.rpc_api_key)
193 api_user = User.get_by_auth_token(request.rpc_api_key)
195
194
196 if api_user is None:
195 if api_user is None:
197 return jsonrpc_error(
196 return jsonrpc_error(
198 request, retid=request.rpc_id, message='Invalid API KEY')
197 request, retid=request.rpc_id, message='Invalid API KEY')
199
198
200 if not api_user.active:
199 if not api_user.active:
201 return jsonrpc_error(
200 return jsonrpc_error(
202 request, retid=request.rpc_id,
201 request, retid=request.rpc_id,
203 message='Request from this user not allowed')
202 message='Request from this user not allowed')
204
203
205 # check if we are allowed to use this IP
204 # check if we are allowed to use this IP
206 auth_u = AuthUser(
205 auth_u = AuthUser(
207 api_user.user_id, request.rpc_api_key, ip_addr=request.rpc_ip_addr)
206 api_user.user_id, request.rpc_api_key, ip_addr=request.rpc_ip_addr)
208 if not auth_u.ip_allowed:
207 if not auth_u.ip_allowed:
209 return jsonrpc_error(
208 return jsonrpc_error(
210 request, retid=request.rpc_id,
209 request, retid=request.rpc_id,
211 message='Request from IP:{} not allowed'.format(
210 message='Request from IP:{} not allowed'.format(
212 request.rpc_ip_addr))
211 request.rpc_ip_addr))
213 else:
212 else:
214 log.info('Access for IP:%s allowed', request.rpc_ip_addr)
213 log.info('Access for IP:%s allowed', request.rpc_ip_addr)
215
214
216 # register our auth-user
215 # register our auth-user
217 request.rpc_user = auth_u
216 request.rpc_user = auth_u
218 request.environ['rc_auth_user_id'] = str(auth_u.user_id)
217 request.environ['rc_auth_user_id'] = str(auth_u.user_id)
219
218
220 # now check if token is valid for API
219 # now check if token is valid for API
221 auth_token = request.rpc_api_key
220 auth_token = request.rpc_api_key
222 token_match = api_user.authenticate_by_token(
221 token_match = api_user.authenticate_by_token(
223 auth_token, roles=[UserApiKeys.ROLE_API])
222 auth_token, roles=[UserApiKeys.ROLE_API])
224 invalid_token = not token_match
223 invalid_token = not token_match
225
224
226 log.debug('Checking if API KEY is valid with proper role')
225 log.debug('Checking if API KEY is valid with proper role')
227 if invalid_token:
226 if invalid_token:
228 return jsonrpc_error(
227 return jsonrpc_error(
229 request, retid=request.rpc_id,
228 request, retid=request.rpc_id,
230 message='API KEY invalid or, has bad role for an API call')
229 message='API KEY invalid or, has bad role for an API call')
231
230
232 except Exception:
231 except Exception:
233 log.exception('Error on API AUTH')
232 log.exception('Error on API AUTH')
234 return jsonrpc_error(
233 return jsonrpc_error(
235 request, retid=request.rpc_id, message='Invalid API KEY')
234 request, retid=request.rpc_id, message='Invalid API KEY')
236
235
237 method = request.rpc_method
236 method = request.rpc_method
238 func = request.registry.jsonrpc_methods[method]
237 func = request.registry.jsonrpc_methods[method]
239
238
240 # now that we have a method, add request._req_params to
239 # now that we have a method, add request._req_params to
241 # self.kargs and dispatch control to WGIController
240 # self.kargs and dispatch control to WGIController
242
241
243 argspec = inspect.getargspec(func)
242 argspec = inspect.getargspec(func)
244 arglist = argspec[0]
243 arglist = argspec[0]
245 defs = argspec[3] or []
244 defs = argspec[3] or []
246 defaults = [type(a) for a in defs]
245 defaults = [type(a) for a in defs]
247 default_empty = type(NotImplemented)
246 default_empty = type(NotImplemented)
248
247
249 # kw arguments required by this method
248 # kw arguments required by this method
250 func_kwargs = dict(itertools.zip_longest(
249 func_kwargs = dict(itertools.zip_longest(
251 reversed(arglist), reversed(defaults), fillvalue=default_empty))
250 reversed(arglist), reversed(defaults), fillvalue=default_empty))
252
251
253 # This attribute will need to be first param of a method that uses
252 # This attribute will need to be first param of a method that uses
254 # api_key, which is translated to instance of user at that name
253 # api_key, which is translated to instance of user at that name
255 user_var = 'apiuser'
254 user_var = 'apiuser'
256 request_var = 'request'
255 request_var = 'request'
257
256
258 for arg in [user_var, request_var]:
257 for arg in [user_var, request_var]:
259 if arg not in arglist:
258 if arg not in arglist:
260 return jsonrpc_error(
259 return jsonrpc_error(
261 request,
260 request,
262 retid=request.rpc_id,
261 retid=request.rpc_id,
263 message='This method [%s] does not support '
262 message='This method [%s] does not support '
264 'required parameter `%s`' % (func.__name__, arg))
263 'required parameter `%s`' % (func.__name__, arg))
265
264
266 # get our arglist and check if we provided them as args
265 # get our arglist and check if we provided them as args
267 for arg, default in func_kwargs.items():
266 for arg, default in func_kwargs.items():
268 if arg in [user_var, request_var]:
267 if arg in [user_var, request_var]:
269 # user_var and request_var are pre-hardcoded parameters and we
268 # user_var and request_var are pre-hardcoded parameters and we
270 # don't need to do any translation
269 # don't need to do any translation
271 continue
270 continue
272
271
273 # skip the required param check if it's default value is
272 # skip the required param check if it's default value is
274 # NotImplementedType (default_empty)
273 # NotImplementedType (default_empty)
275 if default == default_empty and arg not in request.rpc_params:
274 if default == default_empty and arg not in request.rpc_params:
276 return jsonrpc_error(
275 return jsonrpc_error(
277 request,
276 request,
278 retid=request.rpc_id,
277 retid=request.rpc_id,
279 message=('Missing non optional `%s` arg in JSON DATA' % arg)
278 message=('Missing non optional `%s` arg in JSON DATA' % arg)
280 )
279 )
281
280
282 # sanitize extra passed arguments
281 # sanitize extra passed arguments
283 for k in list(request.rpc_params.keys()):
282 for k in list(request.rpc_params.keys()):
284 if k not in func_kwargs:
283 if k not in func_kwargs:
285 del request.rpc_params[k]
284 del request.rpc_params[k]
286
285
287 call_params = request.rpc_params
286 call_params = request.rpc_params
288 call_params.update({
287 call_params.update({
289 'request': request,
288 'request': request,
290 'apiuser': auth_u
289 'apiuser': auth_u
291 })
290 })
292
291
293 # register some common functions for usage
292 # register some common functions for usage
294 attach_context_attributes(TemplateArgs(), request, request.rpc_user.user_id)
293 attach_context_attributes(TemplateArgs(), request, request.rpc_user.user_id)
295
294
296 statsd = request.registry.statsd
295 statsd = request.registry.statsd
297
296
298 try:
297 try:
299 ret_value = func(**call_params)
298 ret_value = func(**call_params)
300 resp = jsonrpc_response(request, ret_value)
299 resp = jsonrpc_response(request, ret_value)
301 if statsd:
300 if statsd:
302 statsd.incr('rhodecode_api_call_success_total')
301 statsd.incr('rhodecode_api_call_success_total')
303 return resp
302 return resp
304 except JSONRPCBaseError:
303 except JSONRPCBaseError:
305 raise
304 raise
306 except Exception:
305 except Exception:
307 log.exception('Unhandled exception occurred on api call: %s', func)
306 log.exception('Unhandled exception occurred on api call: %s', func)
308 exc_info = sys.exc_info()
307 exc_info = sys.exc_info()
309 exc_id, exc_type_name = store_exception(
308 exc_id, exc_type_name = store_exception(
310 id(exc_info), exc_info, prefix='rhodecode-api')
309 id(exc_info), exc_info, prefix='rhodecode-api')
311 error_headers = {
310 error_headers = {
312 'RhodeCode-Exception-Id': str(exc_id),
311 'RhodeCode-Exception-Id': str(exc_id),
313 'RhodeCode-Exception-Type': str(exc_type_name)
312 'RhodeCode-Exception-Type': str(exc_type_name)
314 }
313 }
315 err_resp = jsonrpc_error(
314 err_resp = jsonrpc_error(
316 request, retid=request.rpc_id, message='Internal server error',
315 request, retid=request.rpc_id, message='Internal server error',
317 headers=error_headers)
316 headers=error_headers)
318 if statsd:
317 if statsd:
319 statsd.incr('rhodecode_api_call_fail_total')
318 statsd.incr('rhodecode_api_call_fail_total')
320 return err_resp
319 return err_resp
321
320
322
321
323 def setup_request(request):
322 def setup_request(request):
324 """
323 """
325 Parse a JSON-RPC request body. It's used inside the predicates method
324 Parse a JSON-RPC request body. It's used inside the predicates method
326 to validate and bootstrap requests for usage in rpc calls.
325 to validate and bootstrap requests for usage in rpc calls.
327
326
328 We need to raise JSONRPCError here if we want to return some errors back to
327 We need to raise JSONRPCError here if we want to return some errors back to
329 user.
328 user.
330 """
329 """
331
330
332 log.debug('Executing setup request: %r', request)
331 log.debug('Executing setup request: %r', request)
333 request.rpc_ip_addr = get_ip_addr(request.environ)
332 request.rpc_ip_addr = get_ip_addr(request.environ)
334 # TODO(marcink): deprecate GET at some point
333 # TODO(marcink): deprecate GET at some point
335 if request.method not in ['POST', 'GET']:
334 if request.method not in ['POST', 'GET']:
336 log.debug('unsupported request method "%s"', request.method)
335 log.debug('unsupported request method "%s"', request.method)
337 raise JSONRPCError(
336 raise JSONRPCError(
338 'unsupported request method "%s". Please use POST' % request.method)
337 'unsupported request method "%s". Please use POST' % request.method)
339
338
340 if 'CONTENT_LENGTH' not in request.environ:
339 if 'CONTENT_LENGTH' not in request.environ:
341 log.debug("No Content-Length")
340 log.debug("No Content-Length")
342 raise JSONRPCError("Empty body, No Content-Length in request")
341 raise JSONRPCError("Empty body, No Content-Length in request")
343
342
344 else:
343 else:
345 length = request.environ['CONTENT_LENGTH']
344 length = request.environ['CONTENT_LENGTH']
346 log.debug('Content-Length: %s', length)
345 log.debug('Content-Length: %s', length)
347
346
348 if length == 0:
347 if length == 0:
349 log.debug("Content-Length is 0")
348 log.debug("Content-Length is 0")
350 raise JSONRPCError("Content-Length is 0")
349 raise JSONRPCError("Content-Length is 0")
351
350
352 raw_body = request.body
351 raw_body = request.body
353 log.debug("Loading JSON body now")
352 log.debug("Loading JSON body now")
354 try:
353 try:
355 json_body = ext_json.json.loads(raw_body)
354 json_body = ext_json.json.loads(raw_body)
356 except ValueError as e:
355 except ValueError as e:
357 # catch JSON errors Here
356 # catch JSON errors Here
358 raise JSONRPCError(f"JSON parse error ERR:{e} RAW:{raw_body!r}")
357 raise JSONRPCError(f"JSON parse error ERR:{e} RAW:{raw_body!r}")
359
358
360 request.rpc_id = json_body.get('id')
359 request.rpc_id = json_body.get('id')
361 request.rpc_method = json_body.get('method')
360 request.rpc_method = json_body.get('method')
362
361
363 # check required base parameters
362 # check required base parameters
364 try:
363 try:
365 api_key = json_body.get('api_key')
364 api_key = json_body.get('api_key')
366 if not api_key:
365 if not api_key:
367 api_key = json_body.get('auth_token')
366 api_key = json_body.get('auth_token')
368
367
369 if not api_key:
368 if not api_key:
370 raise KeyError('api_key or auth_token')
369 raise KeyError('api_key or auth_token')
371
370
372 # TODO(marcink): support passing in token in request header
371 # TODO(marcink): support passing in token in request header
373
372
374 request.rpc_api_key = api_key
373 request.rpc_api_key = api_key
375 request.rpc_id = json_body['id']
374 request.rpc_id = json_body['id']
376 request.rpc_method = json_body['method']
375 request.rpc_method = json_body['method']
377 request.rpc_params = json_body['args'] \
376 request.rpc_params = json_body['args'] \
378 if isinstance(json_body['args'], dict) else {}
377 if isinstance(json_body['args'], dict) else {}
379
378
380 log.debug('method: %s, params: %.10240r', request.rpc_method, request.rpc_params)
379 log.debug('method: %s, params: %.10240r', request.rpc_method, request.rpc_params)
381 except KeyError as e:
380 except KeyError as e:
382 raise JSONRPCError(f'Incorrect JSON data. Missing {e}')
381 raise JSONRPCError(f'Incorrect JSON data. Missing {e}')
383
382
384 log.debug('setup complete, now handling method:%s rpcid:%s',
383 log.debug('setup complete, now handling method:%s rpcid:%s',
385 request.rpc_method, request.rpc_id, )
384 request.rpc_method, request.rpc_id, )
386
385
387
386
388 class RoutePredicate(object):
387 class RoutePredicate(object):
389 def __init__(self, val, config):
388 def __init__(self, val, config):
390 self.val = val
389 self.val = val
391
390
392 def text(self):
391 def text(self):
393 return f'jsonrpc route = {self.val}'
392 return f'jsonrpc route = {self.val}'
394
393
395 phash = text
394 phash = text
396
395
397 def __call__(self, info, request):
396 def __call__(self, info, request):
398 if self.val:
397 if self.val:
399 # potentially setup and bootstrap our call
398 # potentially setup and bootstrap our call
400 setup_request(request)
399 setup_request(request)
401
400
402 # Always return True so that even if it isn't a valid RPC it
401 # Always return True so that even if it isn't a valid RPC it
403 # will fall through to the underlaying handlers like notfound_view
402 # will fall through to the underlaying handlers like notfound_view
404 return True
403 return True
405
404
406
405
407 class NotFoundPredicate(object):
406 class NotFoundPredicate(object):
408 def __init__(self, val, config):
407 def __init__(self, val, config):
409 self.val = val
408 self.val = val
410 self.methods = config.registry.jsonrpc_methods
409 self.methods = config.registry.jsonrpc_methods
411
410
412 def text(self):
411 def text(self):
413 return f'jsonrpc method not found = {self.val}'
412 return f'jsonrpc method not found = {self.val}'
414
413
415 phash = text
414 phash = text
416
415
417 def __call__(self, info, request):
416 def __call__(self, info, request):
418 return hasattr(request, 'rpc_method')
417 return hasattr(request, 'rpc_method')
419
418
420
419
421 class MethodPredicate(object):
420 class MethodPredicate(object):
422 def __init__(self, val, config):
421 def __init__(self, val, config):
423 self.method = val
422 self.method = val
424
423
425 def text(self):
424 def text(self):
426 return f'jsonrpc method = {self.method}'
425 return f'jsonrpc method = {self.method}'
427
426
428 phash = text
427 phash = text
429
428
430 def __call__(self, context, request):
429 def __call__(self, context, request):
431 # we need to explicitly return False here, so pyramid doesn't try to
430 # we need to explicitly return False here, so pyramid doesn't try to
432 # execute our view directly. We need our main handler to execute things
431 # execute our view directly. We need our main handler to execute things
433 return getattr(request, 'rpc_method') == self.method
432 return getattr(request, 'rpc_method') == self.method
434
433
435
434
436 def add_jsonrpc_method(config, view, **kwargs):
435 def add_jsonrpc_method(config, view, **kwargs):
437 # pop the method name
436 # pop the method name
438 method = kwargs.pop('method', None)
437 method = kwargs.pop('method', None)
439
438
440 if method is None:
439 if method is None:
441 raise ConfigurationError(
440 raise ConfigurationError(
442 'Cannot register a JSON-RPC method without specifying the "method"')
441 'Cannot register a JSON-RPC method without specifying the "method"')
443
442
444 # we define custom predicate, to enable to detect conflicting methods,
443 # we define custom predicate, to enable to detect conflicting methods,
445 # those predicates are kind of "translation" from the decorator variables
444 # those predicates are kind of "translation" from the decorator variables
446 # to internal predicates names
445 # to internal predicates names
447
446
448 kwargs['jsonrpc_method'] = method
447 kwargs['jsonrpc_method'] = method
449
448
450 # register our view into global view store for validation
449 # register our view into global view store for validation
451 config.registry.jsonrpc_methods[method] = view
450 config.registry.jsonrpc_methods[method] = view
452
451
453 # we're using our main request_view handler, here, so each method
452 # we're using our main request_view handler, here, so each method
454 # has a unified handler for itself
453 # has a unified handler for itself
455 config.add_view(request_view, route_name='apiv2', **kwargs)
454 config.add_view(request_view, route_name='apiv2', **kwargs)
456
455
457
456
458 class jsonrpc_method(object):
457 class jsonrpc_method(object):
459 """
458 """
460 decorator that works similar to @add_view_config decorator,
459 decorator that works similar to @add_view_config decorator,
461 but tailored for our JSON RPC
460 but tailored for our JSON RPC
462 """
461 """
463
462
464 venusian = venusian # for testing injection
463 venusian = venusian # for testing injection
465
464
466 def __init__(self, method=None, **kwargs):
465 def __init__(self, method=None, **kwargs):
467 self.method = method
466 self.method = method
468 self.kwargs = kwargs
467 self.kwargs = kwargs
469
468
470 def __call__(self, wrapped):
469 def __call__(self, wrapped):
471 kwargs = self.kwargs.copy()
470 kwargs = self.kwargs.copy()
472 kwargs['method'] = self.method or wrapped.__name__
471 kwargs['method'] = self.method or wrapped.__name__
473 depth = kwargs.pop('_depth', 0)
472 depth = kwargs.pop('_depth', 0)
474
473
475 def callback(context, name, ob):
474 def callback(context, name, ob):
476 config = context.config.with_package(info.module)
475 config = context.config.with_package(info.module)
477 config.add_jsonrpc_method(view=ob, **kwargs)
476 config.add_jsonrpc_method(view=ob, **kwargs)
478
477
479 info = venusian.attach(wrapped, callback, category='pyramid',
478 info = venusian.attach(wrapped, callback, category='pyramid',
480 depth=depth + 1)
479 depth=depth + 1)
481 if info.scope == 'class':
480 if info.scope == 'class':
482 # ensure that attr is set if decorating a class method
481 # ensure that attr is set if decorating a class method
483 kwargs.setdefault('attr', wrapped.__name__)
482 kwargs.setdefault('attr', wrapped.__name__)
484
483
485 kwargs['_info'] = info.codeinfo # fbo action_method
484 kwargs['_info'] = info.codeinfo # fbo action_method
486 return wrapped
485 return wrapped
487
486
488
487
489 class jsonrpc_deprecated_method(object):
488 class jsonrpc_deprecated_method(object):
490 """
489 """
491 Marks method as deprecated, adds log.warning, and inject special key to
490 Marks method as deprecated, adds log.warning, and inject special key to
492 the request variable to mark method as deprecated.
491 the request variable to mark method as deprecated.
493 Also injects special docstring that extract_docs will catch to mark
492 Also injects special docstring that extract_docs will catch to mark
494 method as deprecated.
493 method as deprecated.
495
494
496 :param use_method: specify which method should be used instead of
495 :param use_method: specify which method should be used instead of
497 the decorated one
496 the decorated one
498
497
499 Use like::
498 Use like::
500
499
501 @jsonrpc_method()
500 @jsonrpc_method()
502 @jsonrpc_deprecated_method(use_method='new_func', deprecated_at_version='3.0.0')
501 @jsonrpc_deprecated_method(use_method='new_func', deprecated_at_version='3.0.0')
503 def old_func(request, apiuser, arg1, arg2):
502 def old_func(request, apiuser, arg1, arg2):
504 ...
503 ...
505 """
504 """
506
505
507 def __init__(self, use_method, deprecated_at_version):
506 def __init__(self, use_method, deprecated_at_version):
508 self.use_method = use_method
507 self.use_method = use_method
509 self.deprecated_at_version = deprecated_at_version
508 self.deprecated_at_version = deprecated_at_version
510 self.deprecated_msg = ''
509 self.deprecated_msg = ''
511
510
512 def __call__(self, func):
511 def __call__(self, func):
513 self.deprecated_msg = 'Please use method `{method}` instead.'.format(
512 self.deprecated_msg = 'Please use method `{method}` instead.'.format(
514 method=self.use_method)
513 method=self.use_method)
515
514
516 docstring = """\n
515 docstring = """\n
517 .. deprecated:: {version}
516 .. deprecated:: {version}
518
517
519 {deprecation_message}
518 {deprecation_message}
520
519
521 {original_docstring}
520 {original_docstring}
522 """
521 """
523 func.__doc__ = docstring.format(
522 func.__doc__ = docstring.format(
524 version=self.deprecated_at_version,
523 version=self.deprecated_at_version,
525 deprecation_message=self.deprecated_msg,
524 deprecation_message=self.deprecated_msg,
526 original_docstring=func.__doc__)
525 original_docstring=func.__doc__)
527 return decorator.decorator(self.__wrapper, func)
526 return decorator.decorator(self.__wrapper, func)
528
527
529 def __wrapper(self, func, *fargs, **fkwargs):
528 def __wrapper(self, func, *fargs, **fkwargs):
530 log.warning('DEPRECATED API CALL on function %s, please '
529 log.warning('DEPRECATED API CALL on function %s, please '
531 'use `%s` instead', func, self.use_method)
530 'use `%s` instead', func, self.use_method)
532 # alter function docstring to mark as deprecated, this is picked up
531 # alter function docstring to mark as deprecated, this is picked up
533 # via fabric file that generates API DOC.
532 # via fabric file that generates API DOC.
534 result = func(*fargs, **fkwargs)
533 result = func(*fargs, **fkwargs)
535
534
536 request = fargs[0]
535 request = fargs[0]
537 request.rpc_deprecation = 'DEPRECATED METHOD ' + self.deprecated_msg
536 request.rpc_deprecation = 'DEPRECATED METHOD ' + self.deprecated_msg
538 return result
537 return result
539
538
540
539
541 def add_api_methods(config):
540 def add_api_methods(config):
542 from rhodecode.api.views import (
541 from rhodecode.api.views import (
543 deprecated_api, gist_api, pull_request_api, repo_api, repo_group_api,
542 deprecated_api, gist_api, pull_request_api, repo_api, repo_group_api,
544 server_api, search_api, testing_api, user_api, user_group_api)
543 server_api, search_api, testing_api, user_api, user_group_api)
545
544
546 config.scan('rhodecode.api.views')
545 config.scan('rhodecode.api.views')
547
546
548
547
549 def includeme(config):
548 def includeme(config):
550 plugin_module = 'rhodecode.api'
549 plugin_module = 'rhodecode.api'
551 plugin_settings = get_plugin_settings(
550 plugin_settings = get_plugin_settings(
552 plugin_module, config.registry.settings)
551 plugin_module, config.registry.settings)
553
552
554 if not hasattr(config.registry, 'jsonrpc_methods'):
553 if not hasattr(config.registry, 'jsonrpc_methods'):
555 config.registry.jsonrpc_methods = OrderedDict()
554 config.registry.jsonrpc_methods = OrderedDict()
556
555
557 # match filter by given method only
556 # match filter by given method only
558 config.add_view_predicate('jsonrpc_method', MethodPredicate)
557 config.add_view_predicate('jsonrpc_method', MethodPredicate)
559 config.add_view_predicate('jsonrpc_method_not_found', NotFoundPredicate)
558 config.add_view_predicate('jsonrpc_method_not_found', NotFoundPredicate)
560
559
561 config.add_renderer(DEFAULT_RENDERER, ExtJsonRenderer())
560 config.add_renderer(DEFAULT_RENDERER, ExtJsonRenderer())
562 config.add_directive('add_jsonrpc_method', add_jsonrpc_method)
561 config.add_directive('add_jsonrpc_method', add_jsonrpc_method)
563
562
564 config.add_route_predicate(
563 config.add_route_predicate(
565 'jsonrpc_call', RoutePredicate)
564 'jsonrpc_call', RoutePredicate)
566
565
567 config.add_route(
566 config.add_route(
568 'apiv2', plugin_settings.get('url', DEFAULT_URL), jsonrpc_call=True)
567 'apiv2', plugin_settings.get('url', DEFAULT_URL), jsonrpc_call=True)
569
568
570 # register some exception handling view
569 # register some exception handling view
571 config.add_view(exception_view, context=JSONRPCBaseError)
570 config.add_view(exception_view, context=JSONRPCBaseError)
572 config.add_notfound_view(exception_view, jsonrpc_method_not_found=True)
571 config.add_notfound_view(exception_view, jsonrpc_method_not_found=True)
573
572
574 add_api_methods(config)
573 add_api_methods(config)
@@ -1,172 +1,156 b''
1
1
2 # Copyright (C) 2010-2023 RhodeCode GmbH
2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 #
3 #
4 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
7 #
7 #
8 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
11 # GNU General Public License for more details.
12 #
12 #
13 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
15 #
16 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19
19
20 import os
20 import os
21 import csv
21 import csv
22 import datetime
22 import datetime
23
23
24 import pytest
24 import pytest
25
25
26 from rhodecode.lib.str_utils import safe_str
26 from rhodecode.lib.str_utils import safe_str
27 from rhodecode.tests import *
27 from rhodecode.tests import *
28 from rhodecode.tests.routes import route_path
28 from rhodecode.tests.fixture import FIXTURES
29 from rhodecode.tests.fixture import FIXTURES
29 from rhodecode.model.db import UserLog
30 from rhodecode.model.db import UserLog
30 from rhodecode.model.meta import Session
31 from rhodecode.model.meta import Session
31
32
32
33
33 def route_path(name, params=None, **kwargs):
34 import urllib.request
35 import urllib.parse
36 import urllib.error
37 from rhodecode.apps._base import ADMIN_PREFIX
38
39 base_url = {
40 'admin_home': ADMIN_PREFIX,
41 'admin_audit_logs': ADMIN_PREFIX + '/audit_logs',
42
43 }[name].format(**kwargs)
44
45 if params:
46 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
47 return base_url
48
49
50 @pytest.mark.usefixtures('app')
34 @pytest.mark.usefixtures('app')
51 class TestAdminController(object):
35 class TestAdminController(object):
52
36
53 @pytest.fixture(scope='class', autouse=True)
37 @pytest.fixture(scope='class', autouse=True)
54 def prepare(self, request, baseapp):
38 def prepare(self, request, baseapp):
55 UserLog.query().delete()
39 UserLog.query().delete()
56 Session().commit()
40 Session().commit()
57
41
58 def strptime(val):
42 def strptime(val):
59 fmt = '%Y-%m-%d %H:%M:%S'
43 fmt = '%Y-%m-%d %H:%M:%S'
60 if '.' not in val:
44 if '.' not in val:
61 return datetime.datetime.strptime(val, fmt)
45 return datetime.datetime.strptime(val, fmt)
62
46
63 nofrag, frag = val.split(".")
47 nofrag, frag = val.split(".")
64 date = datetime.datetime.strptime(nofrag, fmt)
48 date = datetime.datetime.strptime(nofrag, fmt)
65
49
66 frag = frag[:6] # truncate to microseconds
50 frag = frag[:6] # truncate to microseconds
67 frag += (6 - len(frag)) * '0' # add 0s
51 frag += (6 - len(frag)) * '0' # add 0s
68 return date.replace(microsecond=int(frag))
52 return date.replace(microsecond=int(frag))
69
53
70 with open(os.path.join(FIXTURES, 'journal_dump.csv')) as f:
54 with open(os.path.join(FIXTURES, 'journal_dump.csv')) as f:
71 for row in csv.DictReader(f):
55 for row in csv.DictReader(f):
72 ul = UserLog()
56 ul = UserLog()
73 for k, v in row.items():
57 for k, v in row.items():
74 v = safe_str(v)
58 v = safe_str(v)
75 if k == 'action_date':
59 if k == 'action_date':
76 v = strptime(v)
60 v = strptime(v)
77 if k in ['user_id', 'repository_id']:
61 if k in ['user_id', 'repository_id']:
78 # nullable due to FK problems
62 # nullable due to FK problems
79 v = None
63 v = None
80 setattr(ul, k, v)
64 setattr(ul, k, v)
81 Session().add(ul)
65 Session().add(ul)
82 Session().commit()
66 Session().commit()
83
67
84 @request.addfinalizer
68 @request.addfinalizer
85 def cleanup():
69 def cleanup():
86 UserLog.query().delete()
70 UserLog.query().delete()
87 Session().commit()
71 Session().commit()
88
72
89 def test_index(self, autologin_user):
73 def test_index(self, autologin_user):
90 response = self.app.get(route_path('admin_audit_logs'))
74 response = self.app.get(route_path('admin_audit_logs'))
91 response.mustcontain('Admin audit logs')
75 response.mustcontain('Admin audit logs')
92
76
93 def test_filter_all_entries(self, autologin_user):
77 def test_filter_all_entries(self, autologin_user):
94 response = self.app.get(route_path('admin_audit_logs'))
78 response = self.app.get(route_path('admin_audit_logs'))
95 all_count = UserLog.query().count()
79 all_count = UserLog.query().count()
96 response.mustcontain('%s entries' % all_count)
80 response.mustcontain('%s entries' % all_count)
97
81
98 def test_filter_journal_filter_exact_match_on_repository(self, autologin_user):
82 def test_filter_journal_filter_exact_match_on_repository(self, autologin_user):
99 response = self.app.get(route_path('admin_audit_logs',
83 response = self.app.get(route_path('admin_audit_logs',
100 params=dict(filter='repository:rhodecode')))
84 params=dict(filter='repository:rhodecode')))
101 response.mustcontain('3 entries')
85 response.mustcontain('3 entries')
102
86
103 def test_filter_journal_filter_exact_match_on_repository_CamelCase(self, autologin_user):
87 def test_filter_journal_filter_exact_match_on_repository_CamelCase(self, autologin_user):
104 response = self.app.get(route_path('admin_audit_logs',
88 response = self.app.get(route_path('admin_audit_logs',
105 params=dict(filter='repository:RhodeCode')))
89 params=dict(filter='repository:RhodeCode')))
106 response.mustcontain('3 entries')
90 response.mustcontain('3 entries')
107
91
108 def test_filter_journal_filter_wildcard_on_repository(self, autologin_user):
92 def test_filter_journal_filter_wildcard_on_repository(self, autologin_user):
109 response = self.app.get(route_path('admin_audit_logs',
93 response = self.app.get(route_path('admin_audit_logs',
110 params=dict(filter='repository:*test*')))
94 params=dict(filter='repository:*test*')))
111 response.mustcontain('862 entries')
95 response.mustcontain('862 entries')
112
96
113 def test_filter_journal_filter_prefix_on_repository(self, autologin_user):
97 def test_filter_journal_filter_prefix_on_repository(self, autologin_user):
114 response = self.app.get(route_path('admin_audit_logs',
98 response = self.app.get(route_path('admin_audit_logs',
115 params=dict(filter='repository:test*')))
99 params=dict(filter='repository:test*')))
116 response.mustcontain('257 entries')
100 response.mustcontain('257 entries')
117
101
118 def test_filter_journal_filter_prefix_on_repository_CamelCase(self, autologin_user):
102 def test_filter_journal_filter_prefix_on_repository_CamelCase(self, autologin_user):
119 response = self.app.get(route_path('admin_audit_logs',
103 response = self.app.get(route_path('admin_audit_logs',
120 params=dict(filter='repository:Test*')))
104 params=dict(filter='repository:Test*')))
121 response.mustcontain('257 entries')
105 response.mustcontain('257 entries')
122
106
123 def test_filter_journal_filter_prefix_on_repository_and_user(self, autologin_user):
107 def test_filter_journal_filter_prefix_on_repository_and_user(self, autologin_user):
124 response = self.app.get(route_path('admin_audit_logs',
108 response = self.app.get(route_path('admin_audit_logs',
125 params=dict(filter='repository:test* AND username:demo')))
109 params=dict(filter='repository:test* AND username:demo')))
126 response.mustcontain('130 entries')
110 response.mustcontain('130 entries')
127
111
128 def test_filter_journal_filter_prefix_on_repository_or_target_repo(self, autologin_user):
112 def test_filter_journal_filter_prefix_on_repository_or_target_repo(self, autologin_user):
129 response = self.app.get(route_path('admin_audit_logs',
113 response = self.app.get(route_path('admin_audit_logs',
130 params=dict(filter='repository:test* OR repository:rhodecode')))
114 params=dict(filter='repository:test* OR repository:rhodecode')))
131 response.mustcontain('260 entries') # 257 + 3
115 response.mustcontain('260 entries') # 257 + 3
132
116
133 def test_filter_journal_filter_exact_match_on_username(self, autologin_user):
117 def test_filter_journal_filter_exact_match_on_username(self, autologin_user):
134 response = self.app.get(route_path('admin_audit_logs',
118 response = self.app.get(route_path('admin_audit_logs',
135 params=dict(filter='username:demo')))
119 params=dict(filter='username:demo')))
136 response.mustcontain('1087 entries')
120 response.mustcontain('1087 entries')
137
121
138 def test_filter_journal_filter_exact_match_on_username_camelCase(self, autologin_user):
122 def test_filter_journal_filter_exact_match_on_username_camelCase(self, autologin_user):
139 response = self.app.get(route_path('admin_audit_logs',
123 response = self.app.get(route_path('admin_audit_logs',
140 params=dict(filter='username:DemO')))
124 params=dict(filter='username:DemO')))
141 response.mustcontain('1087 entries')
125 response.mustcontain('1087 entries')
142
126
143 def test_filter_journal_filter_wildcard_on_username(self, autologin_user):
127 def test_filter_journal_filter_wildcard_on_username(self, autologin_user):
144 response = self.app.get(route_path('admin_audit_logs',
128 response = self.app.get(route_path('admin_audit_logs',
145 params=dict(filter='username:*test*')))
129 params=dict(filter='username:*test*')))
146 entries_count = UserLog.query().filter(UserLog.username.ilike('%test%')).count()
130 entries_count = UserLog.query().filter(UserLog.username.ilike('%test%')).count()
147 response.mustcontain('{} entries'.format(entries_count))
131 response.mustcontain('{} entries'.format(entries_count))
148
132
149 def test_filter_journal_filter_prefix_on_username(self, autologin_user):
133 def test_filter_journal_filter_prefix_on_username(self, autologin_user):
150 response = self.app.get(route_path('admin_audit_logs',
134 response = self.app.get(route_path('admin_audit_logs',
151 params=dict(filter='username:demo*')))
135 params=dict(filter='username:demo*')))
152 response.mustcontain('1101 entries')
136 response.mustcontain('1101 entries')
153
137
154 def test_filter_journal_filter_prefix_on_user_or_other_user(self, autologin_user):
138 def test_filter_journal_filter_prefix_on_user_or_other_user(self, autologin_user):
155 response = self.app.get(route_path('admin_audit_logs',
139 response = self.app.get(route_path('admin_audit_logs',
156 params=dict(filter='username:demo OR username:volcan')))
140 params=dict(filter='username:demo OR username:volcan')))
157 response.mustcontain('1095 entries') # 1087 + 8
141 response.mustcontain('1095 entries') # 1087 + 8
158
142
159 def test_filter_journal_filter_wildcard_on_action(self, autologin_user):
143 def test_filter_journal_filter_wildcard_on_action(self, autologin_user):
160 response = self.app.get(route_path('admin_audit_logs',
144 response = self.app.get(route_path('admin_audit_logs',
161 params=dict(filter='action:*pull_request*')))
145 params=dict(filter='action:*pull_request*')))
162 response.mustcontain('187 entries')
146 response.mustcontain('187 entries')
163
147
164 def test_filter_journal_filter_on_date(self, autologin_user):
148 def test_filter_journal_filter_on_date(self, autologin_user):
165 response = self.app.get(route_path('admin_audit_logs',
149 response = self.app.get(route_path('admin_audit_logs',
166 params=dict(filter='date:20121010')))
150 params=dict(filter='date:20121010')))
167 response.mustcontain('47 entries')
151 response.mustcontain('47 entries')
168
152
169 def test_filter_journal_filter_on_date_2(self, autologin_user):
153 def test_filter_journal_filter_on_date_2(self, autologin_user):
170 response = self.app.get(route_path('admin_audit_logs',
154 response = self.app.get(route_path('admin_audit_logs',
171 params=dict(filter='date:20121020')))
155 params=dict(filter='date:20121020')))
172 response.mustcontain('17 entries')
156 response.mustcontain('17 entries')
@@ -1,86 +1,69 b''
1
1
2 # Copyright (C) 2010-2023 RhodeCode GmbH
2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 #
3 #
4 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
7 #
7 #
8 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
11 # GNU General Public License for more details.
12 #
12 #
13 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
15 #
16 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19
19
20 import pytest
20 import pytest
21
21
22 from rhodecode.tests import assert_session_flash
22 from rhodecode.tests import assert_session_flash
23 from rhodecode.tests.routes import route_path
23 from rhodecode.model.settings import SettingsModel
24 from rhodecode.model.settings import SettingsModel
24
25
25
26
26 def route_path(name, params=None, **kwargs):
27 import urllib.request
28 import urllib.parse
29 import urllib.error
30 from rhodecode.apps._base import ADMIN_PREFIX
31
32 base_url = {
33 'admin_defaults_repositories':
34 ADMIN_PREFIX + '/defaults/repositories',
35 'admin_defaults_repositories_update':
36 ADMIN_PREFIX + '/defaults/repositories/update',
37 }[name].format(**kwargs)
38
39 if params:
40 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
41 return base_url
42
43
44 @pytest.mark.usefixtures("app")
27 @pytest.mark.usefixtures("app")
45 class TestDefaultsView(object):
28 class TestDefaultsView(object):
46
29
47 def test_index(self, autologin_user):
30 def test_index(self, autologin_user):
48 response = self.app.get(route_path('admin_defaults_repositories'))
31 response = self.app.get(route_path('admin_defaults_repositories'))
49 response.mustcontain('default_repo_private')
32 response.mustcontain('default_repo_private')
50 response.mustcontain('default_repo_enable_statistics')
33 response.mustcontain('default_repo_enable_statistics')
51 response.mustcontain('default_repo_enable_downloads')
34 response.mustcontain('default_repo_enable_downloads')
52 response.mustcontain('default_repo_enable_locking')
35 response.mustcontain('default_repo_enable_locking')
53
36
54 def test_update_params_true_hg(self, autologin_user, csrf_token):
37 def test_update_params_true_hg(self, autologin_user, csrf_token):
55 params = {
38 params = {
56 'default_repo_enable_locking': True,
39 'default_repo_enable_locking': True,
57 'default_repo_enable_downloads': True,
40 'default_repo_enable_downloads': True,
58 'default_repo_enable_statistics': True,
41 'default_repo_enable_statistics': True,
59 'default_repo_private': True,
42 'default_repo_private': True,
60 'default_repo_type': 'hg',
43 'default_repo_type': 'hg',
61 'csrf_token': csrf_token,
44 'csrf_token': csrf_token,
62 }
45 }
63 response = self.app.post(
46 response = self.app.post(
64 route_path('admin_defaults_repositories_update'), params=params)
47 route_path('admin_defaults_repositories_update'), params=params)
65 assert_session_flash(response, 'Default settings updated successfully')
48 assert_session_flash(response, 'Default settings updated successfully')
66
49
67 defs = SettingsModel().get_default_repo_settings()
50 defs = SettingsModel().get_default_repo_settings()
68 del params['csrf_token']
51 del params['csrf_token']
69 assert params == defs
52 assert params == defs
70
53
71 def test_update_params_false_git(self, autologin_user, csrf_token):
54 def test_update_params_false_git(self, autologin_user, csrf_token):
72 params = {
55 params = {
73 'default_repo_enable_locking': False,
56 'default_repo_enable_locking': False,
74 'default_repo_enable_downloads': False,
57 'default_repo_enable_downloads': False,
75 'default_repo_enable_statistics': False,
58 'default_repo_enable_statistics': False,
76 'default_repo_private': False,
59 'default_repo_private': False,
77 'default_repo_type': 'git',
60 'default_repo_type': 'git',
78 'csrf_token': csrf_token,
61 'csrf_token': csrf_token,
79 }
62 }
80 response = self.app.post(
63 response = self.app.post(
81 route_path('admin_defaults_repositories_update'), params=params)
64 route_path('admin_defaults_repositories_update'), params=params)
82 assert_session_flash(response, 'Default settings updated successfully')
65 assert_session_flash(response, 'Default settings updated successfully')
83
66
84 defs = SettingsModel().get_default_repo_settings()
67 defs = SettingsModel().get_default_repo_settings()
85 del params['csrf_token']
68 del params['csrf_token']
86 assert params == defs
69 assert params == defs
@@ -1,86 +1,67 b''
1
1
2 # Copyright (C) 2010-2023 RhodeCode GmbH
2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 #
3 #
4 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
7 #
7 #
8 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
11 # GNU General Public License for more details.
12 #
12 #
13 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
15 #
16 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19
19
20 import pytest
20 import pytest
21
21
22 from rhodecode.tests import TestController
22 from rhodecode.tests import TestController
23 from rhodecode.tests.fixture import Fixture
23 from rhodecode.tests.fixture import Fixture
24 from rhodecode.tests.routes import route_path
24
25
25 fixture = Fixture()
26 fixture = Fixture()
26
27
27
28
28 def route_path(name, params=None, **kwargs):
29 import urllib.request
30 import urllib.parse
31 import urllib.error
32 from rhodecode.apps._base import ADMIN_PREFIX
33
34 base_url = {
35 'admin_home': ADMIN_PREFIX,
36 'pullrequest_show': '/{repo_name}/pull-request/{pull_request_id}',
37 'pull_requests_global': ADMIN_PREFIX + '/pull-request/{pull_request_id}',
38 'pull_requests_global_0': ADMIN_PREFIX + '/pull_requests/{pull_request_id}',
39 'pull_requests_global_1': ADMIN_PREFIX + '/pull-requests/{pull_request_id}',
40
41 }[name].format(**kwargs)
42
43 if params:
44 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
45 return base_url
46
47
48 class TestAdminMainView(TestController):
29 class TestAdminMainView(TestController):
49
30
50 def test_access_admin_home(self):
31 def test_access_admin_home(self):
51 self.log_user()
32 self.log_user()
52 response = self.app.get(route_path('admin_home'), status=200)
33 response = self.app.get(route_path('admin_home'), status=200)
53 response.mustcontain("Administration area")
34 response.mustcontain("Administration area")
54
35
55 @pytest.mark.parametrize('view', [
36 @pytest.mark.parametrize('view', [
56 'pull_requests_global',
37 'pull_requests_global',
57 ])
38 ])
58 def test_redirect_pull_request_view_global(self, view):
39 def test_redirect_pull_request_view_global(self, view):
59 self.log_user()
40 self.log_user()
60 self.app.get(
41 self.app.get(
61 route_path(view, pull_request_id='xxxx'),
42 route_path(view, pull_request_id='xxxx'),
62 status=404)
43 status=404)
63
44
64 @pytest.mark.backends("git", "hg")
45 @pytest.mark.backends("git", "hg")
65 @pytest.mark.parametrize('view', [
46 @pytest.mark.parametrize('view', [
66 'pull_requests_global',
47 'pull_requests_global',
67 'pull_requests_global_0',
48 'pull_requests_global_0',
68 'pull_requests_global_1',
49 'pull_requests_global_1',
69 ])
50 ])
70 def test_redirect_pull_request_view(self, view, pr_util):
51 def test_redirect_pull_request_view(self, view, pr_util):
71 self.log_user()
52 self.log_user()
72 pull_request = pr_util.create_pull_request()
53 pull_request = pr_util.create_pull_request()
73 pull_request_id = pull_request.pull_request_id
54 pull_request_id = pull_request.pull_request_id
74 repo_name = pull_request.target_repo.repo_name
55 repo_name = pull_request.target_repo.repo_name
75
56
76 response = self.app.get(
57 response = self.app.get(
77 route_path(view, pull_request_id=pull_request_id),
58 route_path(view, pull_request_id=pull_request_id),
78 status=302)
59 status=302)
79 assert response.location.endswith(
60 assert response.location.endswith(
80 'pull-request/{}'.format(pull_request_id))
61 'pull-request/{}'.format(pull_request_id))
81
62
82 redirect_url = route_path(
63 redirect_url = route_path(
83 'pullrequest_show', repo_name=repo_name,
64 'pullrequest_show', repo_name=repo_name,
84 pull_request_id=pull_request_id)
65 pull_request_id=pull_request_id)
85
66
86 assert redirect_url in response.location
67 assert redirect_url in response.location
@@ -1,300 +1,253 b''
1
1
2 # Copyright (C) 2010-2023 RhodeCode GmbH
2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 #
3 #
4 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
7 #
7 #
8 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
11 # GNU General Public License for more details.
12 #
12 #
13 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
15 #
16 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19
19
20 import mock
20 import mock
21 import pytest
21 import pytest
22 from rhodecode.model.db import User, UserIpMap
22 from rhodecode.model.db import User, UserIpMap
23 from rhodecode.model.meta import Session
23 from rhodecode.model.meta import Session
24 from rhodecode.model.permission import PermissionModel
24 from rhodecode.model.permission import PermissionModel
25 from rhodecode.model.ssh_key import SshKeyModel
25 from rhodecode.model.ssh_key import SshKeyModel
26 from rhodecode.tests import (
26 from rhodecode.tests import (
27 TestController, clear_cache_regions, assert_session_flash)
27 TestController, clear_cache_regions, assert_session_flash)
28
28 from rhodecode.tests.routes import route_path
29
30 def route_path(name, params=None, **kwargs):
31 import urllib.request
32 import urllib.parse
33 import urllib.error
34 from rhodecode.apps._base import ADMIN_PREFIX
35
36 base_url = {
37 'edit_user_ips':
38 ADMIN_PREFIX + '/users/{user_id}/edit/ips',
39 'edit_user_ips_add':
40 ADMIN_PREFIX + '/users/{user_id}/edit/ips/new',
41 'edit_user_ips_delete':
42 ADMIN_PREFIX + '/users/{user_id}/edit/ips/delete',
43
44 'admin_permissions_application':
45 ADMIN_PREFIX + '/permissions/application',
46 'admin_permissions_application_update':
47 ADMIN_PREFIX + '/permissions/application/update',
48
49 'admin_permissions_global':
50 ADMIN_PREFIX + '/permissions/global',
51 'admin_permissions_global_update':
52 ADMIN_PREFIX + '/permissions/global/update',
53
54 'admin_permissions_object':
55 ADMIN_PREFIX + '/permissions/object',
56 'admin_permissions_object_update':
57 ADMIN_PREFIX + '/permissions/object/update',
58
59 'admin_permissions_ips':
60 ADMIN_PREFIX + '/permissions/ips',
61 'admin_permissions_overview':
62 ADMIN_PREFIX + '/permissions/overview',
63
64 'admin_permissions_ssh_keys':
65 ADMIN_PREFIX + '/permissions/ssh_keys',
66 'admin_permissions_ssh_keys_data':
67 ADMIN_PREFIX + '/permissions/ssh_keys/data',
68 'admin_permissions_ssh_keys_update':
69 ADMIN_PREFIX + '/permissions/ssh_keys/update'
70
71 }[name].format(**kwargs)
72
73 if params:
74 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
75 return base_url
76
29
77
30
78 class TestAdminPermissionsController(TestController):
31 class TestAdminPermissionsController(TestController):
79
32
80 @pytest.fixture(scope='class', autouse=True)
33 @pytest.fixture(scope='class', autouse=True)
81 def prepare(self, request):
34 def prepare(self, request):
82 # cleanup and reset to default permissions after
35 # cleanup and reset to default permissions after
83 @request.addfinalizer
36 @request.addfinalizer
84 def cleanup():
37 def cleanup():
85 PermissionModel().create_default_user_permissions(
38 PermissionModel().create_default_user_permissions(
86 User.get_default_user(), force=True)
39 User.get_default_user(), force=True)
87
40
88 def test_index_application(self):
41 def test_index_application(self):
89 self.log_user()
42 self.log_user()
90 self.app.get(route_path('admin_permissions_application'))
43 self.app.get(route_path('admin_permissions_application'))
91
44
92 @pytest.mark.parametrize(
45 @pytest.mark.parametrize(
93 'anonymous, default_register, default_register_message, default_password_reset,'
46 'anonymous, default_register, default_register_message, default_password_reset,'
94 'default_extern_activate, expect_error, expect_form_error', [
47 'default_extern_activate, expect_error, expect_form_error', [
95 (True, 'hg.register.none', '', 'hg.password_reset.enabled', 'hg.extern_activate.manual',
48 (True, 'hg.register.none', '', 'hg.password_reset.enabled', 'hg.extern_activate.manual',
96 False, False),
49 False, False),
97 (True, 'hg.register.manual_activate', '', 'hg.password_reset.enabled', 'hg.extern_activate.auto',
50 (True, 'hg.register.manual_activate', '', 'hg.password_reset.enabled', 'hg.extern_activate.auto',
98 False, False),
51 False, False),
99 (True, 'hg.register.auto_activate', '', 'hg.password_reset.enabled', 'hg.extern_activate.manual',
52 (True, 'hg.register.auto_activate', '', 'hg.password_reset.enabled', 'hg.extern_activate.manual',
100 False, False),
53 False, False),
101 (True, 'hg.register.auto_activate', '', 'hg.password_reset.enabled', 'hg.extern_activate.manual',
54 (True, 'hg.register.auto_activate', '', 'hg.password_reset.enabled', 'hg.extern_activate.manual',
102 False, False),
55 False, False),
103 (True, 'hg.register.XXX', '', 'hg.password_reset.enabled', 'hg.extern_activate.manual',
56 (True, 'hg.register.XXX', '', 'hg.password_reset.enabled', 'hg.extern_activate.manual',
104 False, True),
57 False, True),
105 (True, '', '', 'hg.password_reset.enabled', '', True, False),
58 (True, '', '', 'hg.password_reset.enabled', '', True, False),
106 ])
59 ])
107 def test_update_application_permissions(
60 def test_update_application_permissions(
108 self, anonymous, default_register, default_register_message, default_password_reset,
61 self, anonymous, default_register, default_register_message, default_password_reset,
109 default_extern_activate, expect_error, expect_form_error):
62 default_extern_activate, expect_error, expect_form_error):
110
63
111 self.log_user()
64 self.log_user()
112
65
113 # TODO: anonymous access set here to False, breaks some other tests
66 # TODO: anonymous access set here to False, breaks some other tests
114 params = {
67 params = {
115 'csrf_token': self.csrf_token,
68 'csrf_token': self.csrf_token,
116 'anonymous': anonymous,
69 'anonymous': anonymous,
117 'default_register': default_register,
70 'default_register': default_register,
118 'default_register_message': default_register_message,
71 'default_register_message': default_register_message,
119 'default_password_reset': default_password_reset,
72 'default_password_reset': default_password_reset,
120 'default_extern_activate': default_extern_activate,
73 'default_extern_activate': default_extern_activate,
121 }
74 }
122 response = self.app.post(route_path('admin_permissions_application_update'),
75 response = self.app.post(route_path('admin_permissions_application_update'),
123 params=params)
76 params=params)
124 if expect_form_error:
77 if expect_form_error:
125 assert response.status_int == 200
78 assert response.status_int == 200
126 response.mustcontain('Value must be one of')
79 response.mustcontain('Value must be one of')
127 else:
80 else:
128 if expect_error:
81 if expect_error:
129 msg = 'Error occurred during update of permissions'
82 msg = 'Error occurred during update of permissions'
130 else:
83 else:
131 msg = 'Application permissions updated successfully'
84 msg = 'Application permissions updated successfully'
132 assert_session_flash(response, msg)
85 assert_session_flash(response, msg)
133
86
134 def test_index_object(self):
87 def test_index_object(self):
135 self.log_user()
88 self.log_user()
136 self.app.get(route_path('admin_permissions_object'))
89 self.app.get(route_path('admin_permissions_object'))
137
90
138 @pytest.mark.parametrize(
91 @pytest.mark.parametrize(
139 'repo, repo_group, user_group, expect_error, expect_form_error', [
92 'repo, repo_group, user_group, expect_error, expect_form_error', [
140 ('repository.none', 'group.none', 'usergroup.none', False, False),
93 ('repository.none', 'group.none', 'usergroup.none', False, False),
141 ('repository.read', 'group.read', 'usergroup.read', False, False),
94 ('repository.read', 'group.read', 'usergroup.read', False, False),
142 ('repository.write', 'group.write', 'usergroup.write',
95 ('repository.write', 'group.write', 'usergroup.write',
143 False, False),
96 False, False),
144 ('repository.admin', 'group.admin', 'usergroup.admin',
97 ('repository.admin', 'group.admin', 'usergroup.admin',
145 False, False),
98 False, False),
146 ('repository.XXX', 'group.admin', 'usergroup.admin', False, True),
99 ('repository.XXX', 'group.admin', 'usergroup.admin', False, True),
147 ('', '', '', True, False),
100 ('', '', '', True, False),
148 ])
101 ])
149 def test_update_object_permissions(self, repo, repo_group, user_group,
102 def test_update_object_permissions(self, repo, repo_group, user_group,
150 expect_error, expect_form_error):
103 expect_error, expect_form_error):
151 self.log_user()
104 self.log_user()
152
105
153 params = {
106 params = {
154 'csrf_token': self.csrf_token,
107 'csrf_token': self.csrf_token,
155 'default_repo_perm': repo,
108 'default_repo_perm': repo,
156 'overwrite_default_repo': False,
109 'overwrite_default_repo': False,
157 'default_group_perm': repo_group,
110 'default_group_perm': repo_group,
158 'overwrite_default_group': False,
111 'overwrite_default_group': False,
159 'default_user_group_perm': user_group,
112 'default_user_group_perm': user_group,
160 'overwrite_default_user_group': False,
113 'overwrite_default_user_group': False,
161 }
114 }
162 response = self.app.post(route_path('admin_permissions_object_update'),
115 response = self.app.post(route_path('admin_permissions_object_update'),
163 params=params)
116 params=params)
164 if expect_form_error:
117 if expect_form_error:
165 assert response.status_int == 200
118 assert response.status_int == 200
166 response.mustcontain('Value must be one of')
119 response.mustcontain('Value must be one of')
167 else:
120 else:
168 if expect_error:
121 if expect_error:
169 msg = 'Error occurred during update of permissions'
122 msg = 'Error occurred during update of permissions'
170 else:
123 else:
171 msg = 'Object permissions updated successfully'
124 msg = 'Object permissions updated successfully'
172 assert_session_flash(response, msg)
125 assert_session_flash(response, msg)
173
126
174 def test_index_global(self):
127 def test_index_global(self):
175 self.log_user()
128 self.log_user()
176 self.app.get(route_path('admin_permissions_global'))
129 self.app.get(route_path('admin_permissions_global'))
177
130
178 @pytest.mark.parametrize(
131 @pytest.mark.parametrize(
179 'repo_create, repo_create_write, user_group_create, repo_group_create,'
132 'repo_create, repo_create_write, user_group_create, repo_group_create,'
180 'fork_create, inherit_default_permissions, expect_error,'
133 'fork_create, inherit_default_permissions, expect_error,'
181 'expect_form_error', [
134 'expect_form_error', [
182 ('hg.create.none', 'hg.create.write_on_repogroup.false',
135 ('hg.create.none', 'hg.create.write_on_repogroup.false',
183 'hg.usergroup.create.false', 'hg.repogroup.create.false',
136 'hg.usergroup.create.false', 'hg.repogroup.create.false',
184 'hg.fork.none', 'hg.inherit_default_perms.false', False, False),
137 'hg.fork.none', 'hg.inherit_default_perms.false', False, False),
185 ('hg.create.repository', 'hg.create.write_on_repogroup.true',
138 ('hg.create.repository', 'hg.create.write_on_repogroup.true',
186 'hg.usergroup.create.true', 'hg.repogroup.create.true',
139 'hg.usergroup.create.true', 'hg.repogroup.create.true',
187 'hg.fork.repository', 'hg.inherit_default_perms.false',
140 'hg.fork.repository', 'hg.inherit_default_perms.false',
188 False, False),
141 False, False),
189 ('hg.create.XXX', 'hg.create.write_on_repogroup.true',
142 ('hg.create.XXX', 'hg.create.write_on_repogroup.true',
190 'hg.usergroup.create.true', 'hg.repogroup.create.true',
143 'hg.usergroup.create.true', 'hg.repogroup.create.true',
191 'hg.fork.repository', 'hg.inherit_default_perms.false',
144 'hg.fork.repository', 'hg.inherit_default_perms.false',
192 False, True),
145 False, True),
193 ('', '', '', '', '', '', True, False),
146 ('', '', '', '', '', '', True, False),
194 ])
147 ])
195 def test_update_global_permissions(
148 def test_update_global_permissions(
196 self, repo_create, repo_create_write, user_group_create,
149 self, repo_create, repo_create_write, user_group_create,
197 repo_group_create, fork_create, inherit_default_permissions,
150 repo_group_create, fork_create, inherit_default_permissions,
198 expect_error, expect_form_error):
151 expect_error, expect_form_error):
199 self.log_user()
152 self.log_user()
200
153
201 params = {
154 params = {
202 'csrf_token': self.csrf_token,
155 'csrf_token': self.csrf_token,
203 'default_repo_create': repo_create,
156 'default_repo_create': repo_create,
204 'default_repo_create_on_write': repo_create_write,
157 'default_repo_create_on_write': repo_create_write,
205 'default_user_group_create': user_group_create,
158 'default_user_group_create': user_group_create,
206 'default_repo_group_create': repo_group_create,
159 'default_repo_group_create': repo_group_create,
207 'default_fork_create': fork_create,
160 'default_fork_create': fork_create,
208 'default_inherit_default_permissions': inherit_default_permissions
161 'default_inherit_default_permissions': inherit_default_permissions
209 }
162 }
210 response = self.app.post(route_path('admin_permissions_global_update'),
163 response = self.app.post(route_path('admin_permissions_global_update'),
211 params=params)
164 params=params)
212 if expect_form_error:
165 if expect_form_error:
213 assert response.status_int == 200
166 assert response.status_int == 200
214 response.mustcontain('Value must be one of')
167 response.mustcontain('Value must be one of')
215 else:
168 else:
216 if expect_error:
169 if expect_error:
217 msg = 'Error occurred during update of permissions'
170 msg = 'Error occurred during update of permissions'
218 else:
171 else:
219 msg = 'Global permissions updated successfully'
172 msg = 'Global permissions updated successfully'
220 assert_session_flash(response, msg)
173 assert_session_flash(response, msg)
221
174
222 def test_index_ips(self):
175 def test_index_ips(self):
223 self.log_user()
176 self.log_user()
224 response = self.app.get(route_path('admin_permissions_ips'))
177 response = self.app.get(route_path('admin_permissions_ips'))
225 response.mustcontain('All IP addresses are allowed')
178 response.mustcontain('All IP addresses are allowed')
226
179
227 def test_add_delete_ips(self):
180 def test_add_delete_ips(self):
228 clear_cache_regions(['sql_cache_short'])
181 clear_cache_regions(['sql_cache_short'])
229 self.log_user()
182 self.log_user()
230
183
231 # ADD
184 # ADD
232 default_user_id = User.get_default_user_id()
185 default_user_id = User.get_default_user_id()
233 self.app.post(
186 self.app.post(
234 route_path('edit_user_ips_add', user_id=default_user_id),
187 route_path('edit_user_ips_add', user_id=default_user_id),
235 params={'new_ip': '0.0.0.0/24', 'csrf_token': self.csrf_token})
188 params={'new_ip': '0.0.0.0/24', 'csrf_token': self.csrf_token})
236
189
237 response = self.app.get(route_path('admin_permissions_ips'))
190 response = self.app.get(route_path('admin_permissions_ips'))
238 response.mustcontain('0.0.0.0/24')
191 response.mustcontain('0.0.0.0/24')
239 response.mustcontain('0.0.0.0 - 0.0.0.255')
192 response.mustcontain('0.0.0.0 - 0.0.0.255')
240
193
241 # DELETE
194 # DELETE
242 default_user_id = User.get_default_user_id()
195 default_user_id = User.get_default_user_id()
243 del_ip_id = UserIpMap.query().filter(UserIpMap.user_id ==
196 del_ip_id = UserIpMap.query().filter(UserIpMap.user_id ==
244 default_user_id).first().ip_id
197 default_user_id).first().ip_id
245
198
246 response = self.app.post(
199 response = self.app.post(
247 route_path('edit_user_ips_delete', user_id=default_user_id),
200 route_path('edit_user_ips_delete', user_id=default_user_id),
248 params={'del_ip_id': del_ip_id, 'csrf_token': self.csrf_token})
201 params={'del_ip_id': del_ip_id, 'csrf_token': self.csrf_token})
249
202
250 assert_session_flash(response, 'Removed ip address from user whitelist')
203 assert_session_flash(response, 'Removed ip address from user whitelist')
251
204
252 clear_cache_regions(['sql_cache_short'])
205 clear_cache_regions(['sql_cache_short'])
253 response = self.app.get(route_path('admin_permissions_ips'))
206 response = self.app.get(route_path('admin_permissions_ips'))
254 response.mustcontain('All IP addresses are allowed')
207 response.mustcontain('All IP addresses are allowed')
255 response.mustcontain(no=['0.0.0.0/24'])
208 response.mustcontain(no=['0.0.0.0/24'])
256 response.mustcontain(no=['0.0.0.0 - 0.0.0.255'])
209 response.mustcontain(no=['0.0.0.0 - 0.0.0.255'])
257
210
258 def test_index_overview(self):
211 def test_index_overview(self):
259 self.log_user()
212 self.log_user()
260 self.app.get(route_path('admin_permissions_overview'))
213 self.app.get(route_path('admin_permissions_overview'))
261
214
262 def test_ssh_keys(self):
215 def test_ssh_keys(self):
263 self.log_user()
216 self.log_user()
264 self.app.get(route_path('admin_permissions_ssh_keys'), status=200)
217 self.app.get(route_path('admin_permissions_ssh_keys'), status=200)
265
218
266 def test_ssh_keys_data(self, user_util, xhr_header):
219 def test_ssh_keys_data(self, user_util, xhr_header):
267 self.log_user()
220 self.log_user()
268 response = self.app.get(route_path('admin_permissions_ssh_keys_data'),
221 response = self.app.get(route_path('admin_permissions_ssh_keys_data'),
269 extra_environ=xhr_header)
222 extra_environ=xhr_header)
270 assert response.json == {u'data': [], u'draw': None,
223 assert response.json == {u'data': [], u'draw': None,
271 u'recordsFiltered': 0, u'recordsTotal': 0}
224 u'recordsFiltered': 0, u'recordsTotal': 0}
272
225
273 dummy_user = user_util.create_user()
226 dummy_user = user_util.create_user()
274 SshKeyModel().create(dummy_user, 'ab:cd:ef', 'KEYKEY', 'test_key')
227 SshKeyModel().create(dummy_user, 'ab:cd:ef', 'KEYKEY', 'test_key')
275 Session().commit()
228 Session().commit()
276 response = self.app.get(route_path('admin_permissions_ssh_keys_data'),
229 response = self.app.get(route_path('admin_permissions_ssh_keys_data'),
277 extra_environ=xhr_header)
230 extra_environ=xhr_header)
278 assert response.json['data'][0]['fingerprint'] == 'ab:cd:ef'
231 assert response.json['data'][0]['fingerprint'] == 'ab:cd:ef'
279
232
280 def test_ssh_keys_update(self):
233 def test_ssh_keys_update(self):
281 self.log_user()
234 self.log_user()
282 response = self.app.post(
235 response = self.app.post(
283 route_path('admin_permissions_ssh_keys_update'),
236 route_path('admin_permissions_ssh_keys_update'),
284 dict(csrf_token=self.csrf_token), status=302)
237 dict(csrf_token=self.csrf_token), status=302)
285
238
286 assert_session_flash(
239 assert_session_flash(
287 response, 'Updated SSH keys file')
240 response, 'Updated SSH keys file')
288
241
289 def test_ssh_keys_update_disabled(self):
242 def test_ssh_keys_update_disabled(self):
290 self.log_user()
243 self.log_user()
291
244
292 from rhodecode.apps.admin.views.permissions import AdminPermissionsView
245 from rhodecode.apps.admin.views.permissions import AdminPermissionsView
293 with mock.patch.object(AdminPermissionsView, 'ssh_enabled',
246 with mock.patch.object(AdminPermissionsView, 'ssh_enabled',
294 return_value=False):
247 return_value=False):
295 response = self.app.post(
248 response = self.app.post(
296 route_path('admin_permissions_ssh_keys_update'),
249 route_path('admin_permissions_ssh_keys_update'),
297 dict(csrf_token=self.csrf_token), status=302)
250 dict(csrf_token=self.csrf_token), status=302)
298
251
299 assert_session_flash(
252 assert_session_flash(
300 response, 'SSH key support is disabled in .ini file') No newline at end of file
253 response, 'SSH key support is disabled in .ini file')
@@ -1,515 +1,497 b''
1
1
2 # Copyright (C) 2010-2023 RhodeCode GmbH
2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 #
3 #
4 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
7 #
7 #
8 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
11 # GNU General Public License for more details.
12 #
12 #
13 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
15 #
16 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19
19
20 import urllib.request
20 import urllib.request
21 import urllib.parse
21 import urllib.parse
22 import urllib.error
22 import urllib.error
23
23
24 import mock
24 import mock
25 import pytest
25 import pytest
26
26
27 from rhodecode.apps._base import ADMIN_PREFIX
27 from rhodecode.apps._base import ADMIN_PREFIX
28 from rhodecode.lib import auth
28 from rhodecode.lib import auth
29 from rhodecode.lib.utils2 import safe_str
29 from rhodecode.lib.utils2 import safe_str
30 from rhodecode.lib import helpers as h
30 from rhodecode.lib import helpers as h
31 from rhodecode.model.db import (
31 from rhodecode.model.db import (
32 Repository, RepoGroup, UserRepoToPerm, User, Permission)
32 Repository, RepoGroup, UserRepoToPerm, User, Permission)
33 from rhodecode.model.meta import Session
33 from rhodecode.model.meta import Session
34 from rhodecode.model.repo import RepoModel
34 from rhodecode.model.repo import RepoModel
35 from rhodecode.model.repo_group import RepoGroupModel
35 from rhodecode.model.repo_group import RepoGroupModel
36 from rhodecode.model.user import UserModel
36 from rhodecode.model.user import UserModel
37 from rhodecode.tests import (
37 from rhodecode.tests import (
38 login_user_session, assert_session_flash, TEST_USER_ADMIN_LOGIN,
38 login_user_session, assert_session_flash, TEST_USER_ADMIN_LOGIN,
39 TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
39 TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
40 from rhodecode.tests.fixture import Fixture, error_function
40 from rhodecode.tests.fixture import Fixture, error_function
41 from rhodecode.tests.utils import AssertResponse, repo_on_filesystem
41 from rhodecode.tests.utils import repo_on_filesystem
42 from rhodecode.tests.routes import route_path
42
43
43 fixture = Fixture()
44 fixture = Fixture()
44
45
45
46
46 def route_path(name, params=None, **kwargs):
47 import urllib.request
48 import urllib.parse
49 import urllib.error
50
51 base_url = {
52 'repos': ADMIN_PREFIX + '/repos',
53 'repos_data': ADMIN_PREFIX + '/repos_data',
54 'repo_new': ADMIN_PREFIX + '/repos/new',
55 'repo_create': ADMIN_PREFIX + '/repos/create',
56
57 'repo_creating_check': '/{repo_name}/repo_creating_check',
58 }[name].format(**kwargs)
59
60 if params:
61 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
62 return base_url
63
64
65 def _get_permission_for_user(user, repo):
47 def _get_permission_for_user(user, repo):
66 perm = UserRepoToPerm.query()\
48 perm = UserRepoToPerm.query()\
67 .filter(UserRepoToPerm.repository ==
49 .filter(UserRepoToPerm.repository ==
68 Repository.get_by_repo_name(repo))\
50 Repository.get_by_repo_name(repo))\
69 .filter(UserRepoToPerm.user == User.get_by_username(user))\
51 .filter(UserRepoToPerm.user == User.get_by_username(user))\
70 .all()
52 .all()
71 return perm
53 return perm
72
54
73
55
74 @pytest.mark.usefixtures("app")
56 @pytest.mark.usefixtures("app")
75 class TestAdminRepos(object):
57 class TestAdminRepos(object):
76
58
77 def test_repo_list(self, autologin_user, user_util, xhr_header):
59 def test_repo_list(self, autologin_user, user_util, xhr_header):
78 repo = user_util.create_repo()
60 repo = user_util.create_repo()
79 repo_name = repo.repo_name
61 repo_name = repo.repo_name
80 response = self.app.get(
62 response = self.app.get(
81 route_path('repos_data'), status=200,
63 route_path('repos_data'), status=200,
82 extra_environ=xhr_header)
64 extra_environ=xhr_header)
83
65
84 response.mustcontain(repo_name)
66 response.mustcontain(repo_name)
85
67
86 def test_create_page_restricted_to_single_backend(self, autologin_user, backend):
68 def test_create_page_restricted_to_single_backend(self, autologin_user, backend):
87 with mock.patch('rhodecode.BACKENDS', {'git': 'git'}):
69 with mock.patch('rhodecode.BACKENDS', {'git': 'git'}):
88 response = self.app.get(route_path('repo_new'), status=200)
70 response = self.app.get(route_path('repo_new'), status=200)
89 assert_response = response.assert_response()
71 assert_response = response.assert_response()
90 element = assert_response.get_element('[name=repo_type]')
72 element = assert_response.get_element('[name=repo_type]')
91 assert element.get('value') == 'git'
73 assert element.get('value') == 'git'
92
74
93 def test_create_page_non_restricted_backends(self, autologin_user, backend):
75 def test_create_page_non_restricted_backends(self, autologin_user, backend):
94 response = self.app.get(route_path('repo_new'), status=200)
76 response = self.app.get(route_path('repo_new'), status=200)
95 assert_response = response.assert_response()
77 assert_response = response.assert_response()
96 assert ['hg', 'git', 'svn'] == [x.get('value') for x in assert_response.get_elements('[name=repo_type]')]
78 assert ['hg', 'git', 'svn'] == [x.get('value') for x in assert_response.get_elements('[name=repo_type]')]
97
79
98 @pytest.mark.parametrize(
80 @pytest.mark.parametrize(
99 "suffix", ['', 'xxa'], ids=['', 'non-ascii'])
81 "suffix", ['', 'xxa'], ids=['', 'non-ascii'])
100 def test_create(self, autologin_user, backend, suffix, csrf_token):
82 def test_create(self, autologin_user, backend, suffix, csrf_token):
101 repo_name_unicode = backend.new_repo_name(suffix=suffix)
83 repo_name_unicode = backend.new_repo_name(suffix=suffix)
102 repo_name = repo_name_unicode
84 repo_name = repo_name_unicode
103
85
104 description_unicode = 'description for newly created repo' + suffix
86 description_unicode = 'description for newly created repo' + suffix
105 description = description_unicode
87 description = description_unicode
106
88
107 response = self.app.post(
89 response = self.app.post(
108 route_path('repo_create'),
90 route_path('repo_create'),
109 fixture._get_repo_create_params(
91 fixture._get_repo_create_params(
110 repo_private=False,
92 repo_private=False,
111 repo_name=repo_name,
93 repo_name=repo_name,
112 repo_type=backend.alias,
94 repo_type=backend.alias,
113 repo_description=description,
95 repo_description=description,
114 csrf_token=csrf_token),
96 csrf_token=csrf_token),
115 status=302)
97 status=302)
116
98
117 self.assert_repository_is_created_correctly(
99 self.assert_repository_is_created_correctly(
118 repo_name, description, backend)
100 repo_name, description, backend)
119
101
120 def test_create_numeric_name(self, autologin_user, backend, csrf_token):
102 def test_create_numeric_name(self, autologin_user, backend, csrf_token):
121 numeric_repo = '1234'
103 numeric_repo = '1234'
122 repo_name = numeric_repo
104 repo_name = numeric_repo
123 description = 'description for newly created repo' + numeric_repo
105 description = 'description for newly created repo' + numeric_repo
124 self.app.post(
106 self.app.post(
125 route_path('repo_create'),
107 route_path('repo_create'),
126 fixture._get_repo_create_params(
108 fixture._get_repo_create_params(
127 repo_private=False,
109 repo_private=False,
128 repo_name=repo_name,
110 repo_name=repo_name,
129 repo_type=backend.alias,
111 repo_type=backend.alias,
130 repo_description=description,
112 repo_description=description,
131 csrf_token=csrf_token))
113 csrf_token=csrf_token))
132
114
133 self.assert_repository_is_created_correctly(
115 self.assert_repository_is_created_correctly(
134 repo_name, description, backend)
116 repo_name, description, backend)
135
117
136 @pytest.mark.parametrize("suffix", ['', '_ąćę'], ids=['', 'non-ascii'])
118 @pytest.mark.parametrize("suffix", ['', '_ąćę'], ids=['', 'non-ascii'])
137 def test_create_in_group(
119 def test_create_in_group(
138 self, autologin_user, backend, suffix, csrf_token):
120 self, autologin_user, backend, suffix, csrf_token):
139 # create GROUP
121 # create GROUP
140 group_name = f'sometest_{backend.alias}'
122 group_name = f'sometest_{backend.alias}'
141 gr = RepoGroupModel().create(group_name=group_name,
123 gr = RepoGroupModel().create(group_name=group_name,
142 group_description='test',
124 group_description='test',
143 owner=TEST_USER_ADMIN_LOGIN)
125 owner=TEST_USER_ADMIN_LOGIN)
144 Session().commit()
126 Session().commit()
145
127
146 repo_name = f'ingroup{suffix}'
128 repo_name = f'ingroup{suffix}'
147 repo_name_full = RepoGroup.url_sep().join([group_name, repo_name])
129 repo_name_full = RepoGroup.url_sep().join([group_name, repo_name])
148 description = 'description for newly created repo'
130 description = 'description for newly created repo'
149
131
150 self.app.post(
132 self.app.post(
151 route_path('repo_create'),
133 route_path('repo_create'),
152 fixture._get_repo_create_params(
134 fixture._get_repo_create_params(
153 repo_private=False,
135 repo_private=False,
154 repo_name=safe_str(repo_name),
136 repo_name=safe_str(repo_name),
155 repo_type=backend.alias,
137 repo_type=backend.alias,
156 repo_description=description,
138 repo_description=description,
157 repo_group=gr.group_id,
139 repo_group=gr.group_id,
158 csrf_token=csrf_token))
140 csrf_token=csrf_token))
159
141
160 # TODO: johbo: Cleanup work to fixture
142 # TODO: johbo: Cleanup work to fixture
161 try:
143 try:
162 self.assert_repository_is_created_correctly(
144 self.assert_repository_is_created_correctly(
163 repo_name_full, description, backend)
145 repo_name_full, description, backend)
164
146
165 new_repo = RepoModel().get_by_repo_name(repo_name_full)
147 new_repo = RepoModel().get_by_repo_name(repo_name_full)
166 inherited_perms = UserRepoToPerm.query().filter(
148 inherited_perms = UserRepoToPerm.query().filter(
167 UserRepoToPerm.repository_id == new_repo.repo_id).all()
149 UserRepoToPerm.repository_id == new_repo.repo_id).all()
168 assert len(inherited_perms) == 1
150 assert len(inherited_perms) == 1
169 finally:
151 finally:
170 RepoModel().delete(repo_name_full)
152 RepoModel().delete(repo_name_full)
171 RepoGroupModel().delete(group_name)
153 RepoGroupModel().delete(group_name)
172 Session().commit()
154 Session().commit()
173
155
174 def test_create_in_group_numeric_name(
156 def test_create_in_group_numeric_name(
175 self, autologin_user, backend, csrf_token):
157 self, autologin_user, backend, csrf_token):
176 # create GROUP
158 # create GROUP
177 group_name = 'sometest_%s' % backend.alias
159 group_name = 'sometest_%s' % backend.alias
178 gr = RepoGroupModel().create(group_name=group_name,
160 gr = RepoGroupModel().create(group_name=group_name,
179 group_description='test',
161 group_description='test',
180 owner=TEST_USER_ADMIN_LOGIN)
162 owner=TEST_USER_ADMIN_LOGIN)
181 Session().commit()
163 Session().commit()
182
164
183 repo_name = '12345'
165 repo_name = '12345'
184 repo_name_full = RepoGroup.url_sep().join([group_name, repo_name])
166 repo_name_full = RepoGroup.url_sep().join([group_name, repo_name])
185 description = 'description for newly created repo'
167 description = 'description for newly created repo'
186 self.app.post(
168 self.app.post(
187 route_path('repo_create'),
169 route_path('repo_create'),
188 fixture._get_repo_create_params(
170 fixture._get_repo_create_params(
189 repo_private=False,
171 repo_private=False,
190 repo_name=repo_name,
172 repo_name=repo_name,
191 repo_type=backend.alias,
173 repo_type=backend.alias,
192 repo_description=description,
174 repo_description=description,
193 repo_group=gr.group_id,
175 repo_group=gr.group_id,
194 csrf_token=csrf_token))
176 csrf_token=csrf_token))
195
177
196 # TODO: johbo: Cleanup work to fixture
178 # TODO: johbo: Cleanup work to fixture
197 try:
179 try:
198 self.assert_repository_is_created_correctly(
180 self.assert_repository_is_created_correctly(
199 repo_name_full, description, backend)
181 repo_name_full, description, backend)
200
182
201 new_repo = RepoModel().get_by_repo_name(repo_name_full)
183 new_repo = RepoModel().get_by_repo_name(repo_name_full)
202 inherited_perms = UserRepoToPerm.query()\
184 inherited_perms = UserRepoToPerm.query()\
203 .filter(UserRepoToPerm.repository_id == new_repo.repo_id).all()
185 .filter(UserRepoToPerm.repository_id == new_repo.repo_id).all()
204 assert len(inherited_perms) == 1
186 assert len(inherited_perms) == 1
205 finally:
187 finally:
206 RepoModel().delete(repo_name_full)
188 RepoModel().delete(repo_name_full)
207 RepoGroupModel().delete(group_name)
189 RepoGroupModel().delete(group_name)
208 Session().commit()
190 Session().commit()
209
191
210 def test_create_in_group_without_needed_permissions(self, backend):
192 def test_create_in_group_without_needed_permissions(self, backend):
211 session = login_user_session(
193 session = login_user_session(
212 self.app, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
194 self.app, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
213 csrf_token = auth.get_csrf_token(session)
195 csrf_token = auth.get_csrf_token(session)
214 # revoke
196 # revoke
215 user_model = UserModel()
197 user_model = UserModel()
216 # disable fork and create on default user
198 # disable fork and create on default user
217 user_model.revoke_perm(User.DEFAULT_USER, 'hg.create.repository')
199 user_model.revoke_perm(User.DEFAULT_USER, 'hg.create.repository')
218 user_model.grant_perm(User.DEFAULT_USER, 'hg.create.none')
200 user_model.grant_perm(User.DEFAULT_USER, 'hg.create.none')
219 user_model.revoke_perm(User.DEFAULT_USER, 'hg.fork.repository')
201 user_model.revoke_perm(User.DEFAULT_USER, 'hg.fork.repository')
220 user_model.grant_perm(User.DEFAULT_USER, 'hg.fork.none')
202 user_model.grant_perm(User.DEFAULT_USER, 'hg.fork.none')
221
203
222 # disable on regular user
204 # disable on regular user
223 user_model.revoke_perm(TEST_USER_REGULAR_LOGIN, 'hg.create.repository')
205 user_model.revoke_perm(TEST_USER_REGULAR_LOGIN, 'hg.create.repository')
224 user_model.grant_perm(TEST_USER_REGULAR_LOGIN, 'hg.create.none')
206 user_model.grant_perm(TEST_USER_REGULAR_LOGIN, 'hg.create.none')
225 user_model.revoke_perm(TEST_USER_REGULAR_LOGIN, 'hg.fork.repository')
207 user_model.revoke_perm(TEST_USER_REGULAR_LOGIN, 'hg.fork.repository')
226 user_model.grant_perm(TEST_USER_REGULAR_LOGIN, 'hg.fork.none')
208 user_model.grant_perm(TEST_USER_REGULAR_LOGIN, 'hg.fork.none')
227 Session().commit()
209 Session().commit()
228
210
229 # create GROUP
211 # create GROUP
230 group_name = 'reg_sometest_%s' % backend.alias
212 group_name = 'reg_sometest_%s' % backend.alias
231 gr = RepoGroupModel().create(group_name=group_name,
213 gr = RepoGroupModel().create(group_name=group_name,
232 group_description='test',
214 group_description='test',
233 owner=TEST_USER_ADMIN_LOGIN)
215 owner=TEST_USER_ADMIN_LOGIN)
234 Session().commit()
216 Session().commit()
235 repo_group_id = gr.group_id
217 repo_group_id = gr.group_id
236
218
237 group_name_allowed = 'reg_sometest_allowed_%s' % backend.alias
219 group_name_allowed = 'reg_sometest_allowed_%s' % backend.alias
238 gr_allowed = RepoGroupModel().create(
220 gr_allowed = RepoGroupModel().create(
239 group_name=group_name_allowed,
221 group_name=group_name_allowed,
240 group_description='test',
222 group_description='test',
241 owner=TEST_USER_REGULAR_LOGIN)
223 owner=TEST_USER_REGULAR_LOGIN)
242 allowed_repo_group_id = gr_allowed.group_id
224 allowed_repo_group_id = gr_allowed.group_id
243 Session().commit()
225 Session().commit()
244
226
245 repo_name = 'ingroup'
227 repo_name = 'ingroup'
246 description = 'description for newly created repo'
228 description = 'description for newly created repo'
247 response = self.app.post(
229 response = self.app.post(
248 route_path('repo_create'),
230 route_path('repo_create'),
249 fixture._get_repo_create_params(
231 fixture._get_repo_create_params(
250 repo_private=False,
232 repo_private=False,
251 repo_name=repo_name,
233 repo_name=repo_name,
252 repo_type=backend.alias,
234 repo_type=backend.alias,
253 repo_description=description,
235 repo_description=description,
254 repo_group=repo_group_id,
236 repo_group=repo_group_id,
255 csrf_token=csrf_token))
237 csrf_token=csrf_token))
256
238
257 response.mustcontain('Invalid value')
239 response.mustcontain('Invalid value')
258
240
259 # user is allowed to create in this group
241 # user is allowed to create in this group
260 repo_name = 'ingroup'
242 repo_name = 'ingroup'
261 repo_name_full = RepoGroup.url_sep().join(
243 repo_name_full = RepoGroup.url_sep().join(
262 [group_name_allowed, repo_name])
244 [group_name_allowed, repo_name])
263 description = 'description for newly created repo'
245 description = 'description for newly created repo'
264 response = self.app.post(
246 response = self.app.post(
265 route_path('repo_create'),
247 route_path('repo_create'),
266 fixture._get_repo_create_params(
248 fixture._get_repo_create_params(
267 repo_private=False,
249 repo_private=False,
268 repo_name=repo_name,
250 repo_name=repo_name,
269 repo_type=backend.alias,
251 repo_type=backend.alias,
270 repo_description=description,
252 repo_description=description,
271 repo_group=allowed_repo_group_id,
253 repo_group=allowed_repo_group_id,
272 csrf_token=csrf_token))
254 csrf_token=csrf_token))
273
255
274 # TODO: johbo: Cleanup in pytest fixture
256 # TODO: johbo: Cleanup in pytest fixture
275 try:
257 try:
276 self.assert_repository_is_created_correctly(
258 self.assert_repository_is_created_correctly(
277 repo_name_full, description, backend)
259 repo_name_full, description, backend)
278
260
279 new_repo = RepoModel().get_by_repo_name(repo_name_full)
261 new_repo = RepoModel().get_by_repo_name(repo_name_full)
280 inherited_perms = UserRepoToPerm.query().filter(
262 inherited_perms = UserRepoToPerm.query().filter(
281 UserRepoToPerm.repository_id == new_repo.repo_id).all()
263 UserRepoToPerm.repository_id == new_repo.repo_id).all()
282 assert len(inherited_perms) == 1
264 assert len(inherited_perms) == 1
283
265
284 assert repo_on_filesystem(repo_name_full)
266 assert repo_on_filesystem(repo_name_full)
285 finally:
267 finally:
286 RepoModel().delete(repo_name_full)
268 RepoModel().delete(repo_name_full)
287 RepoGroupModel().delete(group_name)
269 RepoGroupModel().delete(group_name)
288 RepoGroupModel().delete(group_name_allowed)
270 RepoGroupModel().delete(group_name_allowed)
289 Session().commit()
271 Session().commit()
290
272
291 def test_create_in_group_inherit_permissions(self, autologin_user, backend,
273 def test_create_in_group_inherit_permissions(self, autologin_user, backend,
292 csrf_token):
274 csrf_token):
293 # create GROUP
275 # create GROUP
294 group_name = 'sometest_%s' % backend.alias
276 group_name = 'sometest_%s' % backend.alias
295 gr = RepoGroupModel().create(group_name=group_name,
277 gr = RepoGroupModel().create(group_name=group_name,
296 group_description='test',
278 group_description='test',
297 owner=TEST_USER_ADMIN_LOGIN)
279 owner=TEST_USER_ADMIN_LOGIN)
298 perm = Permission.get_by_key('repository.write')
280 perm = Permission.get_by_key('repository.write')
299 RepoGroupModel().grant_user_permission(
281 RepoGroupModel().grant_user_permission(
300 gr, TEST_USER_REGULAR_LOGIN, perm)
282 gr, TEST_USER_REGULAR_LOGIN, perm)
301
283
302 # add repo permissions
284 # add repo permissions
303 Session().commit()
285 Session().commit()
304 repo_group_id = gr.group_id
286 repo_group_id = gr.group_id
305 repo_name = 'ingroup_inherited_%s' % backend.alias
287 repo_name = 'ingroup_inherited_%s' % backend.alias
306 repo_name_full = RepoGroup.url_sep().join([group_name, repo_name])
288 repo_name_full = RepoGroup.url_sep().join([group_name, repo_name])
307 description = 'description for newly created repo'
289 description = 'description for newly created repo'
308 self.app.post(
290 self.app.post(
309 route_path('repo_create'),
291 route_path('repo_create'),
310 fixture._get_repo_create_params(
292 fixture._get_repo_create_params(
311 repo_private=False,
293 repo_private=False,
312 repo_name=repo_name,
294 repo_name=repo_name,
313 repo_type=backend.alias,
295 repo_type=backend.alias,
314 repo_description=description,
296 repo_description=description,
315 repo_group=repo_group_id,
297 repo_group=repo_group_id,
316 repo_copy_permissions=True,
298 repo_copy_permissions=True,
317 csrf_token=csrf_token))
299 csrf_token=csrf_token))
318
300
319 # TODO: johbo: Cleanup to pytest fixture
301 # TODO: johbo: Cleanup to pytest fixture
320 try:
302 try:
321 self.assert_repository_is_created_correctly(
303 self.assert_repository_is_created_correctly(
322 repo_name_full, description, backend)
304 repo_name_full, description, backend)
323 except Exception:
305 except Exception:
324 RepoGroupModel().delete(group_name)
306 RepoGroupModel().delete(group_name)
325 Session().commit()
307 Session().commit()
326 raise
308 raise
327
309
328 # check if inherited permissions are applied
310 # check if inherited permissions are applied
329 new_repo = RepoModel().get_by_repo_name(repo_name_full)
311 new_repo = RepoModel().get_by_repo_name(repo_name_full)
330 inherited_perms = UserRepoToPerm.query().filter(
312 inherited_perms = UserRepoToPerm.query().filter(
331 UserRepoToPerm.repository_id == new_repo.repo_id).all()
313 UserRepoToPerm.repository_id == new_repo.repo_id).all()
332 assert len(inherited_perms) == 2
314 assert len(inherited_perms) == 2
333
315
334 assert TEST_USER_REGULAR_LOGIN in [
316 assert TEST_USER_REGULAR_LOGIN in [
335 x.user.username for x in inherited_perms]
317 x.user.username for x in inherited_perms]
336 assert 'repository.write' in [
318 assert 'repository.write' in [
337 x.permission.permission_name for x in inherited_perms]
319 x.permission.permission_name for x in inherited_perms]
338
320
339 RepoModel().delete(repo_name_full)
321 RepoModel().delete(repo_name_full)
340 RepoGroupModel().delete(group_name)
322 RepoGroupModel().delete(group_name)
341 Session().commit()
323 Session().commit()
342
324
343 @pytest.mark.xfail_backends(
325 @pytest.mark.xfail_backends(
344 "git", "hg", reason="Missing reposerver support")
326 "git", "hg", reason="Missing reposerver support")
345 def test_create_with_clone_uri(self, autologin_user, backend, reposerver,
327 def test_create_with_clone_uri(self, autologin_user, backend, reposerver,
346 csrf_token):
328 csrf_token):
347 source_repo = backend.create_repo(number_of_commits=2)
329 source_repo = backend.create_repo(number_of_commits=2)
348 source_repo_name = source_repo.repo_name
330 source_repo_name = source_repo.repo_name
349 reposerver.serve(source_repo.scm_instance())
331 reposerver.serve(source_repo.scm_instance())
350
332
351 repo_name = backend.new_repo_name()
333 repo_name = backend.new_repo_name()
352 response = self.app.post(
334 response = self.app.post(
353 route_path('repo_create'),
335 route_path('repo_create'),
354 fixture._get_repo_create_params(
336 fixture._get_repo_create_params(
355 repo_private=False,
337 repo_private=False,
356 repo_name=repo_name,
338 repo_name=repo_name,
357 repo_type=backend.alias,
339 repo_type=backend.alias,
358 repo_description='',
340 repo_description='',
359 clone_uri=reposerver.url,
341 clone_uri=reposerver.url,
360 csrf_token=csrf_token),
342 csrf_token=csrf_token),
361 status=302)
343 status=302)
362
344
363 # Should be redirected to the creating page
345 # Should be redirected to the creating page
364 response.mustcontain('repo_creating')
346 response.mustcontain('repo_creating')
365
347
366 # Expecting that both repositories have same history
348 # Expecting that both repositories have same history
367 source_repo = RepoModel().get_by_repo_name(source_repo_name)
349 source_repo = RepoModel().get_by_repo_name(source_repo_name)
368 source_vcs = source_repo.scm_instance()
350 source_vcs = source_repo.scm_instance()
369 repo = RepoModel().get_by_repo_name(repo_name)
351 repo = RepoModel().get_by_repo_name(repo_name)
370 repo_vcs = repo.scm_instance()
352 repo_vcs = repo.scm_instance()
371 assert source_vcs[0].message == repo_vcs[0].message
353 assert source_vcs[0].message == repo_vcs[0].message
372 assert source_vcs.count() == repo_vcs.count()
354 assert source_vcs.count() == repo_vcs.count()
373 assert source_vcs.commit_ids == repo_vcs.commit_ids
355 assert source_vcs.commit_ids == repo_vcs.commit_ids
374
356
375 @pytest.mark.xfail_backends("svn", reason="Depends on import support")
357 @pytest.mark.xfail_backends("svn", reason="Depends on import support")
376 def test_create_remote_repo_wrong_clone_uri(self, autologin_user, backend,
358 def test_create_remote_repo_wrong_clone_uri(self, autologin_user, backend,
377 csrf_token):
359 csrf_token):
378 repo_name = backend.new_repo_name()
360 repo_name = backend.new_repo_name()
379 description = 'description for newly created repo'
361 description = 'description for newly created repo'
380 response = self.app.post(
362 response = self.app.post(
381 route_path('repo_create'),
363 route_path('repo_create'),
382 fixture._get_repo_create_params(
364 fixture._get_repo_create_params(
383 repo_private=False,
365 repo_private=False,
384 repo_name=repo_name,
366 repo_name=repo_name,
385 repo_type=backend.alias,
367 repo_type=backend.alias,
386 repo_description=description,
368 repo_description=description,
387 clone_uri='http://repo.invalid/repo',
369 clone_uri='http://repo.invalid/repo',
388 csrf_token=csrf_token))
370 csrf_token=csrf_token))
389 response.mustcontain('invalid clone url')
371 response.mustcontain('invalid clone url')
390
372
391 @pytest.mark.xfail_backends("svn", reason="Depends on import support")
373 @pytest.mark.xfail_backends("svn", reason="Depends on import support")
392 def test_create_remote_repo_wrong_clone_uri_hg_svn(
374 def test_create_remote_repo_wrong_clone_uri_hg_svn(
393 self, autologin_user, backend, csrf_token):
375 self, autologin_user, backend, csrf_token):
394 repo_name = backend.new_repo_name()
376 repo_name = backend.new_repo_name()
395 description = 'description for newly created repo'
377 description = 'description for newly created repo'
396 response = self.app.post(
378 response = self.app.post(
397 route_path('repo_create'),
379 route_path('repo_create'),
398 fixture._get_repo_create_params(
380 fixture._get_repo_create_params(
399 repo_private=False,
381 repo_private=False,
400 repo_name=repo_name,
382 repo_name=repo_name,
401 repo_type=backend.alias,
383 repo_type=backend.alias,
402 repo_description=description,
384 repo_description=description,
403 clone_uri='svn+http://svn.invalid/repo',
385 clone_uri='svn+http://svn.invalid/repo',
404 csrf_token=csrf_token))
386 csrf_token=csrf_token))
405 response.mustcontain('invalid clone url')
387 response.mustcontain('invalid clone url')
406
388
407 def test_create_with_git_suffix(
389 def test_create_with_git_suffix(
408 self, autologin_user, backend, csrf_token):
390 self, autologin_user, backend, csrf_token):
409 repo_name = backend.new_repo_name() + ".git"
391 repo_name = backend.new_repo_name() + ".git"
410 description = 'description for newly created repo'
392 description = 'description for newly created repo'
411 response = self.app.post(
393 response = self.app.post(
412 route_path('repo_create'),
394 route_path('repo_create'),
413 fixture._get_repo_create_params(
395 fixture._get_repo_create_params(
414 repo_private=False,
396 repo_private=False,
415 repo_name=repo_name,
397 repo_name=repo_name,
416 repo_type=backend.alias,
398 repo_type=backend.alias,
417 repo_description=description,
399 repo_description=description,
418 csrf_token=csrf_token))
400 csrf_token=csrf_token))
419 response.mustcontain('Repository name cannot end with .git')
401 response.mustcontain('Repository name cannot end with .git')
420
402
421 def test_default_user_cannot_access_private_repo_in_a_group(
403 def test_default_user_cannot_access_private_repo_in_a_group(
422 self, autologin_user, user_util, backend):
404 self, autologin_user, user_util, backend):
423
405
424 group = user_util.create_repo_group()
406 group = user_util.create_repo_group()
425
407
426 repo = backend.create_repo(
408 repo = backend.create_repo(
427 repo_private=True, repo_group=group, repo_copy_permissions=True)
409 repo_private=True, repo_group=group, repo_copy_permissions=True)
428
410
429 permissions = _get_permission_for_user(
411 permissions = _get_permission_for_user(
430 user='default', repo=repo.repo_name)
412 user='default', repo=repo.repo_name)
431 assert len(permissions) == 1
413 assert len(permissions) == 1
432 assert permissions[0].permission.permission_name == 'repository.none'
414 assert permissions[0].permission.permission_name == 'repository.none'
433 assert permissions[0].repository.private is True
415 assert permissions[0].repository.private is True
434
416
435 def test_create_on_top_level_without_permissions(self, backend):
417 def test_create_on_top_level_without_permissions(self, backend):
436 session = login_user_session(
418 session = login_user_session(
437 self.app, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
419 self.app, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
438 csrf_token = auth.get_csrf_token(session)
420 csrf_token = auth.get_csrf_token(session)
439
421
440 # revoke
422 # revoke
441 user_model = UserModel()
423 user_model = UserModel()
442 # disable fork and create on default user
424 # disable fork and create on default user
443 user_model.revoke_perm(User.DEFAULT_USER, 'hg.create.repository')
425 user_model.revoke_perm(User.DEFAULT_USER, 'hg.create.repository')
444 user_model.grant_perm(User.DEFAULT_USER, 'hg.create.none')
426 user_model.grant_perm(User.DEFAULT_USER, 'hg.create.none')
445 user_model.revoke_perm(User.DEFAULT_USER, 'hg.fork.repository')
427 user_model.revoke_perm(User.DEFAULT_USER, 'hg.fork.repository')
446 user_model.grant_perm(User.DEFAULT_USER, 'hg.fork.none')
428 user_model.grant_perm(User.DEFAULT_USER, 'hg.fork.none')
447
429
448 # disable on regular user
430 # disable on regular user
449 user_model.revoke_perm(TEST_USER_REGULAR_LOGIN, 'hg.create.repository')
431 user_model.revoke_perm(TEST_USER_REGULAR_LOGIN, 'hg.create.repository')
450 user_model.grant_perm(TEST_USER_REGULAR_LOGIN, 'hg.create.none')
432 user_model.grant_perm(TEST_USER_REGULAR_LOGIN, 'hg.create.none')
451 user_model.revoke_perm(TEST_USER_REGULAR_LOGIN, 'hg.fork.repository')
433 user_model.revoke_perm(TEST_USER_REGULAR_LOGIN, 'hg.fork.repository')
452 user_model.grant_perm(TEST_USER_REGULAR_LOGIN, 'hg.fork.none')
434 user_model.grant_perm(TEST_USER_REGULAR_LOGIN, 'hg.fork.none')
453 Session().commit()
435 Session().commit()
454
436
455 repo_name = backend.new_repo_name()
437 repo_name = backend.new_repo_name()
456 description = 'description for newly created repo'
438 description = 'description for newly created repo'
457 response = self.app.post(
439 response = self.app.post(
458 route_path('repo_create'),
440 route_path('repo_create'),
459 fixture._get_repo_create_params(
441 fixture._get_repo_create_params(
460 repo_private=False,
442 repo_private=False,
461 repo_name=repo_name,
443 repo_name=repo_name,
462 repo_type=backend.alias,
444 repo_type=backend.alias,
463 repo_description=description,
445 repo_description=description,
464 csrf_token=csrf_token))
446 csrf_token=csrf_token))
465
447
466 response.mustcontain(
448 response.mustcontain(
467 u"You do not have the permission to store repositories in "
449 u"You do not have the permission to store repositories in "
468 u"the root location.")
450 u"the root location.")
469
451
470 @mock.patch.object(RepoModel, '_create_filesystem_repo', error_function)
452 @mock.patch.object(RepoModel, '_create_filesystem_repo', error_function)
471 def test_create_repo_when_filesystem_op_fails(
453 def test_create_repo_when_filesystem_op_fails(
472 self, autologin_user, backend, csrf_token):
454 self, autologin_user, backend, csrf_token):
473 repo_name = backend.new_repo_name()
455 repo_name = backend.new_repo_name()
474 description = 'description for newly created repo'
456 description = 'description for newly created repo'
475
457
476 response = self.app.post(
458 response = self.app.post(
477 route_path('repo_create'),
459 route_path('repo_create'),
478 fixture._get_repo_create_params(
460 fixture._get_repo_create_params(
479 repo_private=False,
461 repo_private=False,
480 repo_name=repo_name,
462 repo_name=repo_name,
481 repo_type=backend.alias,
463 repo_type=backend.alias,
482 repo_description=description,
464 repo_description=description,
483 csrf_token=csrf_token))
465 csrf_token=csrf_token))
484
466
485 assert_session_flash(
467 assert_session_flash(
486 response, 'Error creating repository %s' % repo_name)
468 response, 'Error creating repository %s' % repo_name)
487 # repo must not be in db
469 # repo must not be in db
488 assert backend.repo is None
470 assert backend.repo is None
489 # repo must not be in filesystem !
471 # repo must not be in filesystem !
490 assert not repo_on_filesystem(repo_name)
472 assert not repo_on_filesystem(repo_name)
491
473
492 def assert_repository_is_created_correctly(self, repo_name, description, backend):
474 def assert_repository_is_created_correctly(self, repo_name, description, backend):
493 url_quoted_repo_name = urllib.parse.quote(repo_name)
475 url_quoted_repo_name = urllib.parse.quote(repo_name)
494
476
495 # run the check page that triggers the flash message
477 # run the check page that triggers the flash message
496 response = self.app.get(
478 response = self.app.get(
497 route_path('repo_creating_check', repo_name=repo_name))
479 route_path('repo_creating_check', repo_name=repo_name))
498 assert response.json == {'result': True}
480 assert response.json == {'result': True}
499
481
500 flash_msg = 'Created repository <a href="/{}">{}</a>'.format(url_quoted_repo_name, repo_name)
482 flash_msg = 'Created repository <a href="/{}">{}</a>'.format(url_quoted_repo_name, repo_name)
501 assert_session_flash(response, flash_msg)
483 assert_session_flash(response, flash_msg)
502
484
503 # test if the repo was created in the database
485 # test if the repo was created in the database
504 new_repo = RepoModel().get_by_repo_name(repo_name)
486 new_repo = RepoModel().get_by_repo_name(repo_name)
505
487
506 assert new_repo.repo_name == repo_name
488 assert new_repo.repo_name == repo_name
507 assert new_repo.description == description
489 assert new_repo.description == description
508
490
509 # test if the repository is visible in the list ?
491 # test if the repository is visible in the list ?
510 response = self.app.get(
492 response = self.app.get(
511 h.route_path('repo_summary', repo_name=repo_name))
493 h.route_path('repo_summary', repo_name=repo_name))
512 response.mustcontain(repo_name)
494 response.mustcontain(repo_name)
513 response.mustcontain(backend.alias)
495 response.mustcontain(backend.alias)
514
496
515 assert repo_on_filesystem(repo_name)
497 assert repo_on_filesystem(repo_name)
@@ -1,195 +1,179 b''
1
1
2 # Copyright (C) 2010-2023 RhodeCode GmbH
2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 #
3 #
4 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
7 #
7 #
8 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
11 # GNU General Public License for more details.
12 #
12 #
13 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
15 #
16 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19
19
20 import os
20 import os
21 import pytest
21 import pytest
22
22
23 from rhodecode.apps._base import ADMIN_PREFIX
23 from rhodecode.apps._base import ADMIN_PREFIX
24 from rhodecode.lib import helpers as h
24 from rhodecode.lib import helpers as h
25 from rhodecode.model.db import Repository, UserRepoToPerm, User, RepoGroup
25 from rhodecode.model.db import Repository, UserRepoToPerm, User, RepoGroup
26 from rhodecode.model.meta import Session
26 from rhodecode.model.meta import Session
27 from rhodecode.model.repo_group import RepoGroupModel
27 from rhodecode.model.repo_group import RepoGroupModel
28 from rhodecode.tests import (
28 from rhodecode.tests import (
29 assert_session_flash, TEST_USER_REGULAR_LOGIN, TESTS_TMP_PATH)
29 assert_session_flash, TEST_USER_REGULAR_LOGIN, TESTS_TMP_PATH)
30 from rhodecode.tests.fixture import Fixture
30 from rhodecode.tests.fixture import Fixture
31
31 from rhodecode.tests.routes import route_path
32 fixture = Fixture()
33
32
34
33
35 def route_path(name, params=None, **kwargs):
34 fixture = Fixture()
36 import urllib.request
37 import urllib.parse
38 import urllib.error
39
40 base_url = {
41 'repo_groups': ADMIN_PREFIX + '/repo_groups',
42 'repo_groups_data': ADMIN_PREFIX + '/repo_groups_data',
43 'repo_group_new': ADMIN_PREFIX + '/repo_group/new',
44 'repo_group_create': ADMIN_PREFIX + '/repo_group/create',
45
46 }[name].format(**kwargs)
47
48 if params:
49 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
50 return base_url
51
35
52
36
53 def _get_permission_for_user(user, repo):
37 def _get_permission_for_user(user, repo):
54 perm = UserRepoToPerm.query()\
38 perm = UserRepoToPerm.query()\
55 .filter(UserRepoToPerm.repository ==
39 .filter(UserRepoToPerm.repository ==
56 Repository.get_by_repo_name(repo))\
40 Repository.get_by_repo_name(repo))\
57 .filter(UserRepoToPerm.user == User.get_by_username(user))\
41 .filter(UserRepoToPerm.user == User.get_by_username(user))\
58 .all()
42 .all()
59 return perm
43 return perm
60
44
61
45
62 @pytest.mark.usefixtures("app")
46 @pytest.mark.usefixtures("app")
63 class TestAdminRepositoryGroups(object):
47 class TestAdminRepositoryGroups(object):
64
48
65 def test_show_repo_groups(self, autologin_user):
49 def test_show_repo_groups(self, autologin_user):
66 self.app.get(route_path('repo_groups'))
50 self.app.get(route_path('repo_groups'))
67
51
68 def test_show_repo_groups_data(self, autologin_user, xhr_header):
52 def test_show_repo_groups_data(self, autologin_user, xhr_header):
69 response = self.app.get(route_path(
53 response = self.app.get(route_path(
70 'repo_groups_data'), extra_environ=xhr_header)
54 'repo_groups_data'), extra_environ=xhr_header)
71
55
72 all_repo_groups = RepoGroup.query().count()
56 all_repo_groups = RepoGroup.query().count()
73 assert response.json['recordsTotal'] == all_repo_groups
57 assert response.json['recordsTotal'] == all_repo_groups
74
58
75 def test_show_repo_groups_data_filtered(self, autologin_user, xhr_header):
59 def test_show_repo_groups_data_filtered(self, autologin_user, xhr_header):
76 response = self.app.get(route_path(
60 response = self.app.get(route_path(
77 'repo_groups_data', params={'search[value]': 'empty_search'}),
61 'repo_groups_data', params={'search[value]': 'empty_search'}),
78 extra_environ=xhr_header)
62 extra_environ=xhr_header)
79
63
80 all_repo_groups = RepoGroup.query().count()
64 all_repo_groups = RepoGroup.query().count()
81 assert response.json['recordsTotal'] == all_repo_groups
65 assert response.json['recordsTotal'] == all_repo_groups
82 assert response.json['recordsFiltered'] == 0
66 assert response.json['recordsFiltered'] == 0
83
67
84 def test_show_repo_groups_after_creating_group(self, autologin_user, xhr_header):
68 def test_show_repo_groups_after_creating_group(self, autologin_user, xhr_header):
85 fixture.create_repo_group('test_repo_group')
69 fixture.create_repo_group('test_repo_group')
86 response = self.app.get(route_path(
70 response = self.app.get(route_path(
87 'repo_groups_data'), extra_environ=xhr_header)
71 'repo_groups_data'), extra_environ=xhr_header)
88 response.mustcontain('<a href=\\"/{}/_edit\\" title=\\"Edit\\">Edit</a>'.format('test_repo_group'))
72 response.mustcontain('<a href=\\"/{}/_edit\\" title=\\"Edit\\">Edit</a>'.format('test_repo_group'))
89 fixture.destroy_repo_group('test_repo_group')
73 fixture.destroy_repo_group('test_repo_group')
90
74
91 def test_new(self, autologin_user):
75 def test_new(self, autologin_user):
92 self.app.get(route_path('repo_group_new'))
76 self.app.get(route_path('repo_group_new'))
93
77
94 def test_new_with_parent_group(self, autologin_user, user_util):
78 def test_new_with_parent_group(self, autologin_user, user_util):
95 gr = user_util.create_repo_group()
79 gr = user_util.create_repo_group()
96
80
97 self.app.get(route_path('repo_group_new'),
81 self.app.get(route_path('repo_group_new'),
98 params=dict(parent_group=gr.group_name))
82 params=dict(parent_group=gr.group_name))
99
83
100 def test_new_by_regular_user_no_permission(self, autologin_regular_user):
84 def test_new_by_regular_user_no_permission(self, autologin_regular_user):
101 self.app.get(route_path('repo_group_new'), status=403)
85 self.app.get(route_path('repo_group_new'), status=403)
102
86
103 @pytest.mark.parametrize('repo_group_name', [
87 @pytest.mark.parametrize('repo_group_name', [
104 'git_repo',
88 'git_repo',
105 'git_repo_ąć',
89 'git_repo_ąć',
106 'hg_repo',
90 'hg_repo',
107 '12345',
91 '12345',
108 'hg_repo_ąć',
92 'hg_repo_ąć',
109 ])
93 ])
110 def test_create(self, autologin_user, repo_group_name, csrf_token):
94 def test_create(self, autologin_user, repo_group_name, csrf_token):
111 repo_group_name_non_ascii = repo_group_name
95 repo_group_name_non_ascii = repo_group_name
112 description = 'description for newly created repo group'
96 description = 'description for newly created repo group'
113
97
114 response = self.app.post(
98 response = self.app.post(
115 route_path('repo_group_create'),
99 route_path('repo_group_create'),
116 fixture._get_group_create_params(
100 fixture._get_group_create_params(
117 group_name=repo_group_name,
101 group_name=repo_group_name,
118 group_description=description,
102 group_description=description,
119 csrf_token=csrf_token))
103 csrf_token=csrf_token))
120
104
121 # run the check page that triggers the flash message
105 # run the check page that triggers the flash message
122 repo_gr_url = h.route_path(
106 repo_gr_url = h.route_path(
123 'repo_group_home', repo_group_name=repo_group_name)
107 'repo_group_home', repo_group_name=repo_group_name)
124
108
125 assert_session_flash(
109 assert_session_flash(
126 response,
110 response,
127 'Created repository group <a href="%s">%s</a>' % (
111 'Created repository group <a href="%s">%s</a>' % (
128 repo_gr_url, repo_group_name_non_ascii))
112 repo_gr_url, repo_group_name_non_ascii))
129
113
130 # # test if the repo group was created in the database
114 # # test if the repo group was created in the database
131 new_repo_group = RepoGroupModel()._get_repo_group(
115 new_repo_group = RepoGroupModel()._get_repo_group(
132 repo_group_name_non_ascii)
116 repo_group_name_non_ascii)
133 assert new_repo_group is not None
117 assert new_repo_group is not None
134
118
135 assert new_repo_group.group_name == repo_group_name_non_ascii
119 assert new_repo_group.group_name == repo_group_name_non_ascii
136 assert new_repo_group.group_description == description
120 assert new_repo_group.group_description == description
137
121
138 # test if the repository is visible in the list ?
122 # test if the repository is visible in the list ?
139 response = self.app.get(repo_gr_url)
123 response = self.app.get(repo_gr_url)
140 response.mustcontain(repo_group_name)
124 response.mustcontain(repo_group_name)
141
125
142 # test if the repository group was created on filesystem
126 # test if the repository group was created on filesystem
143 is_on_filesystem = os.path.isdir(
127 is_on_filesystem = os.path.isdir(
144 os.path.join(TESTS_TMP_PATH, repo_group_name))
128 os.path.join(TESTS_TMP_PATH, repo_group_name))
145 if not is_on_filesystem:
129 if not is_on_filesystem:
146 self.fail('no repo group %s in filesystem' % repo_group_name)
130 self.fail('no repo group %s in filesystem' % repo_group_name)
147
131
148 RepoGroupModel().delete(repo_group_name_non_ascii)
132 RepoGroupModel().delete(repo_group_name_non_ascii)
149 Session().commit()
133 Session().commit()
150
134
151 @pytest.mark.parametrize('repo_group_name', [
135 @pytest.mark.parametrize('repo_group_name', [
152 'git_repo',
136 'git_repo',
153 'git_repo_ąć',
137 'git_repo_ąć',
154 'hg_repo',
138 'hg_repo',
155 '12345',
139 '12345',
156 'hg_repo_ąć',
140 'hg_repo_ąć',
157 ])
141 ])
158 def test_create_subgroup(self, autologin_user, user_util, repo_group_name, csrf_token):
142 def test_create_subgroup(self, autologin_user, user_util, repo_group_name, csrf_token):
159 parent_group = user_util.create_repo_group()
143 parent_group = user_util.create_repo_group()
160 parent_group_name = parent_group.group_name
144 parent_group_name = parent_group.group_name
161
145
162 expected_group_name = '{}/{}'.format(
146 expected_group_name = '{}/{}'.format(
163 parent_group_name, repo_group_name)
147 parent_group_name, repo_group_name)
164 expected_group_name_non_ascii = expected_group_name
148 expected_group_name_non_ascii = expected_group_name
165
149
166 try:
150 try:
167 response = self.app.post(
151 response = self.app.post(
168 route_path('repo_group_create'),
152 route_path('repo_group_create'),
169 fixture._get_group_create_params(
153 fixture._get_group_create_params(
170 group_name=repo_group_name,
154 group_name=repo_group_name,
171 group_parent_id=parent_group.group_id,
155 group_parent_id=parent_group.group_id,
172 group_description='Test desciption',
156 group_description='Test desciption',
173 csrf_token=csrf_token))
157 csrf_token=csrf_token))
174
158
175 assert_session_flash(
159 assert_session_flash(
176 response,
160 response,
177 u'Created repository group <a href="%s">%s</a>' % (
161 u'Created repository group <a href="%s">%s</a>' % (
178 h.route_path('repo_group_home',
162 h.route_path('repo_group_home',
179 repo_group_name=expected_group_name),
163 repo_group_name=expected_group_name),
180 expected_group_name_non_ascii))
164 expected_group_name_non_ascii))
181 finally:
165 finally:
182 RepoGroupModel().delete(expected_group_name_non_ascii)
166 RepoGroupModel().delete(expected_group_name_non_ascii)
183 Session().commit()
167 Session().commit()
184
168
185 def test_user_with_creation_permissions_cannot_create_subgroups(
169 def test_user_with_creation_permissions_cannot_create_subgroups(
186 self, autologin_regular_user, user_util):
170 self, autologin_regular_user, user_util):
187
171
188 user_util.grant_user_permission(
172 user_util.grant_user_permission(
189 TEST_USER_REGULAR_LOGIN, 'hg.repogroup.create.true')
173 TEST_USER_REGULAR_LOGIN, 'hg.repogroup.create.true')
190 parent_group = user_util.create_repo_group()
174 parent_group = user_util.create_repo_group()
191 parent_group_id = parent_group.group_id
175 parent_group_id = parent_group.group_id
192 self.app.get(
176 self.app.get(
193 route_path('repo_group_new',
177 route_path('repo_group_new',
194 params=dict(parent_group=parent_group_id), ),
178 params=dict(parent_group=parent_group_id), ),
195 status=403)
179 status=403)
@@ -1,768 +1,695 b''
1
1
2 # Copyright (C) 2010-2023 RhodeCode GmbH
2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 #
3 #
4 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
7 #
7 #
8 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
11 # GNU General Public License for more details.
12 #
12 #
13 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
15 #
16 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19
19
20 import mock
20 import mock
21 import pytest
21 import pytest
22
22
23 import rhodecode
23 import rhodecode
24 from rhodecode.apps._base import ADMIN_PREFIX
24 from rhodecode.apps._base import ADMIN_PREFIX
25 from rhodecode.lib.hash_utils import md5_safe
25 from rhodecode.lib.hash_utils import md5_safe
26 from rhodecode.model.db import RhodeCodeUi
26 from rhodecode.model.db import RhodeCodeUi
27 from rhodecode.model.meta import Session
27 from rhodecode.model.meta import Session
28 from rhodecode.model.settings import SettingsModel, IssueTrackerSettingsModel
28 from rhodecode.model.settings import SettingsModel, IssueTrackerSettingsModel
29 from rhodecode.tests import assert_session_flash
29 from rhodecode.tests import assert_session_flash
30 from rhodecode.tests.routes import route_path
30
31
31
32
32 UPDATE_DATA_QUALNAME = 'rhodecode.model.update.UpdateModel.get_update_data'
33 UPDATE_DATA_QUALNAME = 'rhodecode.model.update.UpdateModel.get_update_data'
33
34
34
35
35 def route_path(name, params=None, **kwargs):
36 import urllib.request
37 import urllib.parse
38 import urllib.error
39 from rhodecode.apps._base import ADMIN_PREFIX
40
41 base_url = {
42
43 'admin_settings':
44 ADMIN_PREFIX +'/settings',
45 'admin_settings_update':
46 ADMIN_PREFIX + '/settings/update',
47 'admin_settings_global':
48 ADMIN_PREFIX + '/settings/global',
49 'admin_settings_global_update':
50 ADMIN_PREFIX + '/settings/global/update',
51 'admin_settings_vcs':
52 ADMIN_PREFIX + '/settings/vcs',
53 'admin_settings_vcs_update':
54 ADMIN_PREFIX + '/settings/vcs/update',
55 'admin_settings_vcs_svn_pattern_delete':
56 ADMIN_PREFIX + '/settings/vcs/svn_pattern_delete',
57 'admin_settings_mapping':
58 ADMIN_PREFIX + '/settings/mapping',
59 'admin_settings_mapping_update':
60 ADMIN_PREFIX + '/settings/mapping/update',
61 'admin_settings_visual':
62 ADMIN_PREFIX + '/settings/visual',
63 'admin_settings_visual_update':
64 ADMIN_PREFIX + '/settings/visual/update',
65 'admin_settings_issuetracker':
66 ADMIN_PREFIX + '/settings/issue-tracker',
67 'admin_settings_issuetracker_update':
68 ADMIN_PREFIX + '/settings/issue-tracker/update',
69 'admin_settings_issuetracker_test':
70 ADMIN_PREFIX + '/settings/issue-tracker/test',
71 'admin_settings_issuetracker_delete':
72 ADMIN_PREFIX + '/settings/issue-tracker/delete',
73 'admin_settings_email':
74 ADMIN_PREFIX + '/settings/email',
75 'admin_settings_email_update':
76 ADMIN_PREFIX + '/settings/email/update',
77 'admin_settings_hooks':
78 ADMIN_PREFIX + '/settings/hooks',
79 'admin_settings_hooks_update':
80 ADMIN_PREFIX + '/settings/hooks/update',
81 'admin_settings_hooks_delete':
82 ADMIN_PREFIX + '/settings/hooks/delete',
83 'admin_settings_search':
84 ADMIN_PREFIX + '/settings/search',
85 'admin_settings_labs':
86 ADMIN_PREFIX + '/settings/labs',
87 'admin_settings_labs_update':
88 ADMIN_PREFIX + '/settings/labs/update',
89
90 'admin_settings_sessions':
91 ADMIN_PREFIX + '/settings/sessions',
92 'admin_settings_sessions_cleanup':
93 ADMIN_PREFIX + '/settings/sessions/cleanup',
94 'admin_settings_system':
95 ADMIN_PREFIX + '/settings/system',
96 'admin_settings_system_update':
97 ADMIN_PREFIX + '/settings/system/updates',
98 'admin_settings_open_source':
99 ADMIN_PREFIX + '/settings/open_source',
100
101
102 }[name].format(**kwargs)
103
104 if params:
105 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
106 return base_url
107
108
109 @pytest.mark.usefixtures('autologin_user', 'app')
36 @pytest.mark.usefixtures('autologin_user', 'app')
110 class TestAdminSettingsController(object):
37 class TestAdminSettingsController(object):
111
38
112 @pytest.mark.parametrize('urlname', [
39 @pytest.mark.parametrize('urlname', [
113 'admin_settings_vcs',
40 'admin_settings_vcs',
114 'admin_settings_mapping',
41 'admin_settings_mapping',
115 'admin_settings_global',
42 'admin_settings_global',
116 'admin_settings_visual',
43 'admin_settings_visual',
117 'admin_settings_email',
44 'admin_settings_email',
118 'admin_settings_hooks',
45 'admin_settings_hooks',
119 'admin_settings_search',
46 'admin_settings_search',
120 ])
47 ])
121 def test_simple_get(self, urlname):
48 def test_simple_get(self, urlname):
122 self.app.get(route_path(urlname))
49 self.app.get(route_path(urlname))
123
50
124 def test_create_custom_hook(self, csrf_token):
51 def test_create_custom_hook(self, csrf_token):
125 response = self.app.post(
52 response = self.app.post(
126 route_path('admin_settings_hooks_update'),
53 route_path('admin_settings_hooks_update'),
127 params={
54 params={
128 'new_hook_ui_key': 'test_hooks_1',
55 'new_hook_ui_key': 'test_hooks_1',
129 'new_hook_ui_value': 'cd /tmp',
56 'new_hook_ui_value': 'cd /tmp',
130 'csrf_token': csrf_token})
57 'csrf_token': csrf_token})
131
58
132 response = response.follow()
59 response = response.follow()
133 response.mustcontain('test_hooks_1')
60 response.mustcontain('test_hooks_1')
134 response.mustcontain('cd /tmp')
61 response.mustcontain('cd /tmp')
135
62
136 def test_create_custom_hook_delete(self, csrf_token):
63 def test_create_custom_hook_delete(self, csrf_token):
137 response = self.app.post(
64 response = self.app.post(
138 route_path('admin_settings_hooks_update'),
65 route_path('admin_settings_hooks_update'),
139 params={
66 params={
140 'new_hook_ui_key': 'test_hooks_2',
67 'new_hook_ui_key': 'test_hooks_2',
141 'new_hook_ui_value': 'cd /tmp2',
68 'new_hook_ui_value': 'cd /tmp2',
142 'csrf_token': csrf_token})
69 'csrf_token': csrf_token})
143
70
144 response = response.follow()
71 response = response.follow()
145 response.mustcontain('test_hooks_2')
72 response.mustcontain('test_hooks_2')
146 response.mustcontain('cd /tmp2')
73 response.mustcontain('cd /tmp2')
147
74
148 hook_id = SettingsModel().get_ui_by_key('test_hooks_2').ui_id
75 hook_id = SettingsModel().get_ui_by_key('test_hooks_2').ui_id
149
76
150 # delete
77 # delete
151 self.app.post(
78 self.app.post(
152 route_path('admin_settings_hooks_delete'),
79 route_path('admin_settings_hooks_delete'),
153 params={'hook_id': hook_id, 'csrf_token': csrf_token})
80 params={'hook_id': hook_id, 'csrf_token': csrf_token})
154 response = self.app.get(route_path('admin_settings_hooks'))
81 response = self.app.get(route_path('admin_settings_hooks'))
155 response.mustcontain(no=['test_hooks_2'])
82 response.mustcontain(no=['test_hooks_2'])
156 response.mustcontain(no=['cd /tmp2'])
83 response.mustcontain(no=['cd /tmp2'])
157
84
158
85
159 @pytest.mark.usefixtures('autologin_user', 'app')
86 @pytest.mark.usefixtures('autologin_user', 'app')
160 class TestAdminSettingsGlobal(object):
87 class TestAdminSettingsGlobal(object):
161
88
162 def test_pre_post_code_code_active(self, csrf_token):
89 def test_pre_post_code_code_active(self, csrf_token):
163 pre_code = 'rc-pre-code-187652122'
90 pre_code = 'rc-pre-code-187652122'
164 post_code = 'rc-postcode-98165231'
91 post_code = 'rc-postcode-98165231'
165
92
166 response = self.post_and_verify_settings({
93 response = self.post_and_verify_settings({
167 'rhodecode_pre_code': pre_code,
94 'rhodecode_pre_code': pre_code,
168 'rhodecode_post_code': post_code,
95 'rhodecode_post_code': post_code,
169 'csrf_token': csrf_token,
96 'csrf_token': csrf_token,
170 })
97 })
171
98
172 response = response.follow()
99 response = response.follow()
173 response.mustcontain(pre_code, post_code)
100 response.mustcontain(pre_code, post_code)
174
101
175 def test_pre_post_code_code_inactive(self, csrf_token):
102 def test_pre_post_code_code_inactive(self, csrf_token):
176 pre_code = 'rc-pre-code-187652122'
103 pre_code = 'rc-pre-code-187652122'
177 post_code = 'rc-postcode-98165231'
104 post_code = 'rc-postcode-98165231'
178 response = self.post_and_verify_settings({
105 response = self.post_and_verify_settings({
179 'rhodecode_pre_code': '',
106 'rhodecode_pre_code': '',
180 'rhodecode_post_code': '',
107 'rhodecode_post_code': '',
181 'csrf_token': csrf_token,
108 'csrf_token': csrf_token,
182 })
109 })
183
110
184 response = response.follow()
111 response = response.follow()
185 response.mustcontain(no=[pre_code, post_code])
112 response.mustcontain(no=[pre_code, post_code])
186
113
187 def test_captcha_activate(self, csrf_token):
114 def test_captcha_activate(self, csrf_token):
188 self.post_and_verify_settings({
115 self.post_and_verify_settings({
189 'rhodecode_captcha_private_key': '1234567890',
116 'rhodecode_captcha_private_key': '1234567890',
190 'rhodecode_captcha_public_key': '1234567890',
117 'rhodecode_captcha_public_key': '1234567890',
191 'csrf_token': csrf_token,
118 'csrf_token': csrf_token,
192 })
119 })
193
120
194 response = self.app.get(ADMIN_PREFIX + '/register')
121 response = self.app.get(ADMIN_PREFIX + '/register')
195 response.mustcontain('captcha')
122 response.mustcontain('captcha')
196
123
197 def test_captcha_deactivate(self, csrf_token):
124 def test_captcha_deactivate(self, csrf_token):
198 self.post_and_verify_settings({
125 self.post_and_verify_settings({
199 'rhodecode_captcha_private_key': '',
126 'rhodecode_captcha_private_key': '',
200 'rhodecode_captcha_public_key': '1234567890',
127 'rhodecode_captcha_public_key': '1234567890',
201 'csrf_token': csrf_token,
128 'csrf_token': csrf_token,
202 })
129 })
203
130
204 response = self.app.get(ADMIN_PREFIX + '/register')
131 response = self.app.get(ADMIN_PREFIX + '/register')
205 response.mustcontain(no=['captcha'])
132 response.mustcontain(no=['captcha'])
206
133
207 def test_title_change(self, csrf_token):
134 def test_title_change(self, csrf_token):
208 old_title = 'RhodeCode'
135 old_title = 'RhodeCode'
209
136
210 for new_title in ['Changed', 'Żółwik', old_title]:
137 for new_title in ['Changed', 'Żółwik', old_title]:
211 response = self.post_and_verify_settings({
138 response = self.post_and_verify_settings({
212 'rhodecode_title': new_title,
139 'rhodecode_title': new_title,
213 'csrf_token': csrf_token,
140 'csrf_token': csrf_token,
214 })
141 })
215
142
216 response = response.follow()
143 response = response.follow()
217 response.mustcontain(new_title)
144 response.mustcontain(new_title)
218
145
219 def post_and_verify_settings(self, settings):
146 def post_and_verify_settings(self, settings):
220 old_title = 'RhodeCode'
147 old_title = 'RhodeCode'
221 old_realm = 'RhodeCode authentication'
148 old_realm = 'RhodeCode authentication'
222 params = {
149 params = {
223 'rhodecode_title': old_title,
150 'rhodecode_title': old_title,
224 'rhodecode_realm': old_realm,
151 'rhodecode_realm': old_realm,
225 'rhodecode_pre_code': '',
152 'rhodecode_pre_code': '',
226 'rhodecode_post_code': '',
153 'rhodecode_post_code': '',
227 'rhodecode_captcha_private_key': '',
154 'rhodecode_captcha_private_key': '',
228 'rhodecode_captcha_public_key': '',
155 'rhodecode_captcha_public_key': '',
229 'rhodecode_create_personal_repo_group': False,
156 'rhodecode_create_personal_repo_group': False,
230 'rhodecode_personal_repo_group_pattern': '${username}',
157 'rhodecode_personal_repo_group_pattern': '${username}',
231 }
158 }
232 params.update(settings)
159 params.update(settings)
233 response = self.app.post(
160 response = self.app.post(
234 route_path('admin_settings_global_update'), params=params)
161 route_path('admin_settings_global_update'), params=params)
235
162
236 assert_session_flash(response, 'Updated application settings')
163 assert_session_flash(response, 'Updated application settings')
237
164
238 app_settings = SettingsModel().get_all_settings()
165 app_settings = SettingsModel().get_all_settings()
239 del settings['csrf_token']
166 del settings['csrf_token']
240 for key, value in settings.items():
167 for key, value in settings.items():
241 assert app_settings[key] == value
168 assert app_settings[key] == value
242
169
243 return response
170 return response
244
171
245
172
246 @pytest.mark.usefixtures('autologin_user', 'app')
173 @pytest.mark.usefixtures('autologin_user', 'app')
247 class TestAdminSettingsVcs(object):
174 class TestAdminSettingsVcs(object):
248
175
249 def test_contains_svn_default_patterns(self):
176 def test_contains_svn_default_patterns(self):
250 response = self.app.get(route_path('admin_settings_vcs'))
177 response = self.app.get(route_path('admin_settings_vcs'))
251 expected_patterns = [
178 expected_patterns = [
252 '/trunk',
179 '/trunk',
253 '/branches/*',
180 '/branches/*',
254 '/tags/*',
181 '/tags/*',
255 ]
182 ]
256 for pattern in expected_patterns:
183 for pattern in expected_patterns:
257 response.mustcontain(pattern)
184 response.mustcontain(pattern)
258
185
259 def test_add_new_svn_branch_and_tag_pattern(
186 def test_add_new_svn_branch_and_tag_pattern(
260 self, backend_svn, form_defaults, disable_sql_cache,
187 self, backend_svn, form_defaults, disable_sql_cache,
261 csrf_token):
188 csrf_token):
262 form_defaults.update({
189 form_defaults.update({
263 'new_svn_branch': '/exp/branches/*',
190 'new_svn_branch': '/exp/branches/*',
264 'new_svn_tag': '/important_tags/*',
191 'new_svn_tag': '/important_tags/*',
265 'csrf_token': csrf_token,
192 'csrf_token': csrf_token,
266 })
193 })
267
194
268 response = self.app.post(
195 response = self.app.post(
269 route_path('admin_settings_vcs_update'),
196 route_path('admin_settings_vcs_update'),
270 params=form_defaults, status=302)
197 params=form_defaults, status=302)
271 response = response.follow()
198 response = response.follow()
272
199
273 # Expect to find the new values on the page
200 # Expect to find the new values on the page
274 response.mustcontain('/exp/branches/*')
201 response.mustcontain('/exp/branches/*')
275 response.mustcontain('/important_tags/*')
202 response.mustcontain('/important_tags/*')
276
203
277 # Expect that those patterns are used to match branches and tags now
204 # Expect that those patterns are used to match branches and tags now
278 repo = backend_svn['svn-simple-layout'].scm_instance()
205 repo = backend_svn['svn-simple-layout'].scm_instance()
279 assert 'exp/branches/exp-sphinx-docs' in repo.branches
206 assert 'exp/branches/exp-sphinx-docs' in repo.branches
280 assert 'important_tags/v0.5' in repo.tags
207 assert 'important_tags/v0.5' in repo.tags
281
208
282 def test_add_same_svn_value_twice_shows_an_error_message(
209 def test_add_same_svn_value_twice_shows_an_error_message(
283 self, form_defaults, csrf_token, settings_util):
210 self, form_defaults, csrf_token, settings_util):
284 settings_util.create_rhodecode_ui('vcs_svn_branch', '/test')
211 settings_util.create_rhodecode_ui('vcs_svn_branch', '/test')
285 settings_util.create_rhodecode_ui('vcs_svn_tag', '/test')
212 settings_util.create_rhodecode_ui('vcs_svn_tag', '/test')
286
213
287 response = self.app.post(
214 response = self.app.post(
288 route_path('admin_settings_vcs_update'),
215 route_path('admin_settings_vcs_update'),
289 params={
216 params={
290 'paths_root_path': form_defaults['paths_root_path'],
217 'paths_root_path': form_defaults['paths_root_path'],
291 'new_svn_branch': '/test',
218 'new_svn_branch': '/test',
292 'new_svn_tag': '/test',
219 'new_svn_tag': '/test',
293 'csrf_token': csrf_token,
220 'csrf_token': csrf_token,
294 },
221 },
295 status=200)
222 status=200)
296
223
297 response.mustcontain("Pattern already exists")
224 response.mustcontain("Pattern already exists")
298 response.mustcontain("Some form inputs contain invalid data.")
225 response.mustcontain("Some form inputs contain invalid data.")
299
226
300 @pytest.mark.parametrize('section', [
227 @pytest.mark.parametrize('section', [
301 'vcs_svn_branch',
228 'vcs_svn_branch',
302 'vcs_svn_tag',
229 'vcs_svn_tag',
303 ])
230 ])
304 def test_delete_svn_patterns(
231 def test_delete_svn_patterns(
305 self, section, csrf_token, settings_util):
232 self, section, csrf_token, settings_util):
306 setting = settings_util.create_rhodecode_ui(
233 setting = settings_util.create_rhodecode_ui(
307 section, '/test_delete', cleanup=False)
234 section, '/test_delete', cleanup=False)
308
235
309 self.app.post(
236 self.app.post(
310 route_path('admin_settings_vcs_svn_pattern_delete'),
237 route_path('admin_settings_vcs_svn_pattern_delete'),
311 params={
238 params={
312 'delete_svn_pattern': setting.ui_id,
239 'delete_svn_pattern': setting.ui_id,
313 'csrf_token': csrf_token},
240 'csrf_token': csrf_token},
314 headers={'X-REQUESTED-WITH': 'XMLHttpRequest'})
241 headers={'X-REQUESTED-WITH': 'XMLHttpRequest'})
315
242
316 @pytest.mark.parametrize('section', [
243 @pytest.mark.parametrize('section', [
317 'vcs_svn_branch',
244 'vcs_svn_branch',
318 'vcs_svn_tag',
245 'vcs_svn_tag',
319 ])
246 ])
320 def test_delete_svn_patterns_raises_404_when_no_xhr(
247 def test_delete_svn_patterns_raises_404_when_no_xhr(
321 self, section, csrf_token, settings_util):
248 self, section, csrf_token, settings_util):
322 setting = settings_util.create_rhodecode_ui(section, '/test_delete')
249 setting = settings_util.create_rhodecode_ui(section, '/test_delete')
323
250
324 self.app.post(
251 self.app.post(
325 route_path('admin_settings_vcs_svn_pattern_delete'),
252 route_path('admin_settings_vcs_svn_pattern_delete'),
326 params={
253 params={
327 'delete_svn_pattern': setting.ui_id,
254 'delete_svn_pattern': setting.ui_id,
328 'csrf_token': csrf_token},
255 'csrf_token': csrf_token},
329 status=404)
256 status=404)
330
257
331 def test_extensions_hgsubversion(self, form_defaults, csrf_token):
258 def test_extensions_hgsubversion(self, form_defaults, csrf_token):
332 form_defaults.update({
259 form_defaults.update({
333 'csrf_token': csrf_token,
260 'csrf_token': csrf_token,
334 'extensions_hgsubversion': 'True',
261 'extensions_hgsubversion': 'True',
335 })
262 })
336 response = self.app.post(
263 response = self.app.post(
337 route_path('admin_settings_vcs_update'),
264 route_path('admin_settings_vcs_update'),
338 params=form_defaults,
265 params=form_defaults,
339 status=302)
266 status=302)
340
267
341 response = response.follow()
268 response = response.follow()
342 extensions_input = (
269 extensions_input = (
343 '<input id="extensions_hgsubversion" '
270 '<input id="extensions_hgsubversion" '
344 'name="extensions_hgsubversion" type="checkbox" '
271 'name="extensions_hgsubversion" type="checkbox" '
345 'value="True" checked="checked" />')
272 'value="True" checked="checked" />')
346 response.mustcontain(extensions_input)
273 response.mustcontain(extensions_input)
347
274
348 def test_extensions_hgevolve(self, form_defaults, csrf_token):
275 def test_extensions_hgevolve(self, form_defaults, csrf_token):
349 form_defaults.update({
276 form_defaults.update({
350 'csrf_token': csrf_token,
277 'csrf_token': csrf_token,
351 'extensions_evolve': 'True',
278 'extensions_evolve': 'True',
352 })
279 })
353 response = self.app.post(
280 response = self.app.post(
354 route_path('admin_settings_vcs_update'),
281 route_path('admin_settings_vcs_update'),
355 params=form_defaults,
282 params=form_defaults,
356 status=302)
283 status=302)
357
284
358 response = response.follow()
285 response = response.follow()
359 extensions_input = (
286 extensions_input = (
360 '<input id="extensions_evolve" '
287 '<input id="extensions_evolve" '
361 'name="extensions_evolve" type="checkbox" '
288 'name="extensions_evolve" type="checkbox" '
362 'value="True" checked="checked" />')
289 'value="True" checked="checked" />')
363 response.mustcontain(extensions_input)
290 response.mustcontain(extensions_input)
364
291
365 def test_has_a_section_for_pull_request_settings(self):
292 def test_has_a_section_for_pull_request_settings(self):
366 response = self.app.get(route_path('admin_settings_vcs'))
293 response = self.app.get(route_path('admin_settings_vcs'))
367 response.mustcontain('Pull Request Settings')
294 response.mustcontain('Pull Request Settings')
368
295
369 def test_has_an_input_for_invalidation_of_inline_comments(self):
296 def test_has_an_input_for_invalidation_of_inline_comments(self):
370 response = self.app.get(route_path('admin_settings_vcs'))
297 response = self.app.get(route_path('admin_settings_vcs'))
371 assert_response = response.assert_response()
298 assert_response = response.assert_response()
372 assert_response.one_element_exists(
299 assert_response.one_element_exists(
373 '[name=rhodecode_use_outdated_comments]')
300 '[name=rhodecode_use_outdated_comments]')
374
301
375 @pytest.mark.parametrize('new_value', [True, False])
302 @pytest.mark.parametrize('new_value', [True, False])
376 def test_allows_to_change_invalidation_of_inline_comments(
303 def test_allows_to_change_invalidation_of_inline_comments(
377 self, form_defaults, csrf_token, new_value):
304 self, form_defaults, csrf_token, new_value):
378 setting_key = 'use_outdated_comments'
305 setting_key = 'use_outdated_comments'
379 setting = SettingsModel().create_or_update_setting(
306 setting = SettingsModel().create_or_update_setting(
380 setting_key, not new_value, 'bool')
307 setting_key, not new_value, 'bool')
381 Session().add(setting)
308 Session().add(setting)
382 Session().commit()
309 Session().commit()
383
310
384 form_defaults.update({
311 form_defaults.update({
385 'csrf_token': csrf_token,
312 'csrf_token': csrf_token,
386 'rhodecode_use_outdated_comments': str(new_value),
313 'rhodecode_use_outdated_comments': str(new_value),
387 })
314 })
388 response = self.app.post(
315 response = self.app.post(
389 route_path('admin_settings_vcs_update'),
316 route_path('admin_settings_vcs_update'),
390 params=form_defaults,
317 params=form_defaults,
391 status=302)
318 status=302)
392 response = response.follow()
319 response = response.follow()
393 setting = SettingsModel().get_setting_by_name(setting_key)
320 setting = SettingsModel().get_setting_by_name(setting_key)
394 assert setting.app_settings_value is new_value
321 assert setting.app_settings_value is new_value
395
322
396 @pytest.mark.parametrize('new_value', [True, False])
323 @pytest.mark.parametrize('new_value', [True, False])
397 def test_allows_to_change_hg_rebase_merge_strategy(
324 def test_allows_to_change_hg_rebase_merge_strategy(
398 self, form_defaults, csrf_token, new_value):
325 self, form_defaults, csrf_token, new_value):
399 setting_key = 'hg_use_rebase_for_merging'
326 setting_key = 'hg_use_rebase_for_merging'
400
327
401 form_defaults.update({
328 form_defaults.update({
402 'csrf_token': csrf_token,
329 'csrf_token': csrf_token,
403 'rhodecode_' + setting_key: str(new_value),
330 'rhodecode_' + setting_key: str(new_value),
404 })
331 })
405
332
406 with mock.patch.dict(
333 with mock.patch.dict(
407 rhodecode.CONFIG, {'labs_settings_active': 'true'}):
334 rhodecode.CONFIG, {'labs_settings_active': 'true'}):
408 self.app.post(
335 self.app.post(
409 route_path('admin_settings_vcs_update'),
336 route_path('admin_settings_vcs_update'),
410 params=form_defaults,
337 params=form_defaults,
411 status=302)
338 status=302)
412
339
413 setting = SettingsModel().get_setting_by_name(setting_key)
340 setting = SettingsModel().get_setting_by_name(setting_key)
414 assert setting.app_settings_value is new_value
341 assert setting.app_settings_value is new_value
415
342
416 @pytest.fixture()
343 @pytest.fixture()
417 def disable_sql_cache(self, request):
344 def disable_sql_cache(self, request):
418 # patch _do_orm_execute so it returns None similar like if we don't use a cached query
345 # patch _do_orm_execute so it returns None similar like if we don't use a cached query
419 patcher = mock.patch(
346 patcher = mock.patch(
420 'rhodecode.lib.caching_query.ORMCache._do_orm_execute', return_value=None)
347 'rhodecode.lib.caching_query.ORMCache._do_orm_execute', return_value=None)
421 request.addfinalizer(patcher.stop)
348 request.addfinalizer(patcher.stop)
422 patcher.start()
349 patcher.start()
423
350
424 @pytest.fixture()
351 @pytest.fixture()
425 def form_defaults(self):
352 def form_defaults(self):
426 from rhodecode.apps.admin.views.settings import AdminSettingsView
353 from rhodecode.apps.admin.views.settings import AdminSettingsView
427 return AdminSettingsView._form_defaults()
354 return AdminSettingsView._form_defaults()
428
355
429 # TODO: johbo: What we really want is to checkpoint before a test run and
356 # TODO: johbo: What we really want is to checkpoint before a test run and
430 # reset the session afterwards.
357 # reset the session afterwards.
431 @pytest.fixture(scope='class', autouse=True)
358 @pytest.fixture(scope='class', autouse=True)
432 def cleanup_settings(self, request, baseapp):
359 def cleanup_settings(self, request, baseapp):
433 ui_id = RhodeCodeUi.ui_id
360 ui_id = RhodeCodeUi.ui_id
434 original_ids = [r.ui_id for r in RhodeCodeUi.query().with_entities(ui_id)]
361 original_ids = [r.ui_id for r in RhodeCodeUi.query().with_entities(ui_id)]
435
362
436 @request.addfinalizer
363 @request.addfinalizer
437 def cleanup():
364 def cleanup():
438 RhodeCodeUi.query().filter(
365 RhodeCodeUi.query().filter(
439 ui_id.notin_(original_ids)).delete(False)
366 ui_id.notin_(original_ids)).delete(False)
440
367
441
368
442 @pytest.mark.usefixtures('autologin_user', 'app')
369 @pytest.mark.usefixtures('autologin_user', 'app')
443 class TestLabsSettings(object):
370 class TestLabsSettings(object):
444 def test_get_settings_page_disabled(self):
371 def test_get_settings_page_disabled(self):
445 with mock.patch.dict(
372 with mock.patch.dict(
446 rhodecode.CONFIG, {'labs_settings_active': 'false'}):
373 rhodecode.CONFIG, {'labs_settings_active': 'false'}):
447
374
448 response = self.app.get(
375 response = self.app.get(
449 route_path('admin_settings_labs'), status=302)
376 route_path('admin_settings_labs'), status=302)
450
377
451 assert response.location.endswith(route_path('admin_settings'))
378 assert response.location.endswith(route_path('admin_settings'))
452
379
453 def test_get_settings_page_enabled(self):
380 def test_get_settings_page_enabled(self):
454 from rhodecode.apps.admin.views import settings
381 from rhodecode.apps.admin.views import settings
455 lab_settings = [
382 lab_settings = [
456 settings.LabSetting(
383 settings.LabSetting(
457 key='rhodecode_bool',
384 key='rhodecode_bool',
458 type='bool',
385 type='bool',
459 group='bool group',
386 group='bool group',
460 label='bool label',
387 label='bool label',
461 help='bool help'
388 help='bool help'
462 ),
389 ),
463 settings.LabSetting(
390 settings.LabSetting(
464 key='rhodecode_text',
391 key='rhodecode_text',
465 type='unicode',
392 type='unicode',
466 group='text group',
393 group='text group',
467 label='text label',
394 label='text label',
468 help='text help'
395 help='text help'
469 ),
396 ),
470 ]
397 ]
471 with mock.patch.dict(rhodecode.CONFIG,
398 with mock.patch.dict(rhodecode.CONFIG,
472 {'labs_settings_active': 'true'}):
399 {'labs_settings_active': 'true'}):
473 with mock.patch.object(settings, '_LAB_SETTINGS', lab_settings):
400 with mock.patch.object(settings, '_LAB_SETTINGS', lab_settings):
474 response = self.app.get(route_path('admin_settings_labs'))
401 response = self.app.get(route_path('admin_settings_labs'))
475
402
476 assert '<label>bool group:</label>' in response
403 assert '<label>bool group:</label>' in response
477 assert '<label for="rhodecode_bool">bool label</label>' in response
404 assert '<label for="rhodecode_bool">bool label</label>' in response
478 assert '<p class="help-block">bool help</p>' in response
405 assert '<p class="help-block">bool help</p>' in response
479 assert 'name="rhodecode_bool" type="checkbox"' in response
406 assert 'name="rhodecode_bool" type="checkbox"' in response
480
407
481 assert '<label>text group:</label>' in response
408 assert '<label>text group:</label>' in response
482 assert '<label for="rhodecode_text">text label</label>' in response
409 assert '<label for="rhodecode_text">text label</label>' in response
483 assert '<p class="help-block">text help</p>' in response
410 assert '<p class="help-block">text help</p>' in response
484 assert 'name="rhodecode_text" size="60" type="text"' in response
411 assert 'name="rhodecode_text" size="60" type="text"' in response
485
412
486
413
487 @pytest.mark.usefixtures('app')
414 @pytest.mark.usefixtures('app')
488 class TestOpenSourceLicenses(object):
415 class TestOpenSourceLicenses(object):
489
416
490 def test_records_are_displayed(self, autologin_user):
417 def test_records_are_displayed(self, autologin_user):
491 sample_licenses = [
418 sample_licenses = [
492 {
419 {
493 "license": [
420 "license": [
494 {
421 {
495 "fullName": "BSD 4-clause \"Original\" or \"Old\" License",
422 "fullName": "BSD 4-clause \"Original\" or \"Old\" License",
496 "shortName": "bsdOriginal",
423 "shortName": "bsdOriginal",
497 "spdxId": "BSD-4-Clause",
424 "spdxId": "BSD-4-Clause",
498 "url": "http://spdx.org/licenses/BSD-4-Clause.html"
425 "url": "http://spdx.org/licenses/BSD-4-Clause.html"
499 }
426 }
500 ],
427 ],
501 "name": "python2.7-coverage-3.7.1"
428 "name": "python2.7-coverage-3.7.1"
502 },
429 },
503 {
430 {
504 "license": [
431 "license": [
505 {
432 {
506 "fullName": "MIT License",
433 "fullName": "MIT License",
507 "shortName": "mit",
434 "shortName": "mit",
508 "spdxId": "MIT",
435 "spdxId": "MIT",
509 "url": "http://spdx.org/licenses/MIT.html"
436 "url": "http://spdx.org/licenses/MIT.html"
510 }
437 }
511 ],
438 ],
512 "name": "python2.7-bootstrapped-pip-9.0.1"
439 "name": "python2.7-bootstrapped-pip-9.0.1"
513 },
440 },
514 ]
441 ]
515 read_licenses_patch = mock.patch(
442 read_licenses_patch = mock.patch(
516 'rhodecode.apps.admin.views.open_source_licenses.read_opensource_licenses',
443 'rhodecode.apps.admin.views.open_source_licenses.read_opensource_licenses',
517 return_value=sample_licenses)
444 return_value=sample_licenses)
518 with read_licenses_patch:
445 with read_licenses_patch:
519 response = self.app.get(
446 response = self.app.get(
520 route_path('admin_settings_open_source'), status=200)
447 route_path('admin_settings_open_source'), status=200)
521
448
522 assert_response = response.assert_response()
449 assert_response = response.assert_response()
523 assert_response.element_contains(
450 assert_response.element_contains(
524 '.panel-heading', 'Licenses of Third Party Packages')
451 '.panel-heading', 'Licenses of Third Party Packages')
525 for license_data in sample_licenses:
452 for license_data in sample_licenses:
526 response.mustcontain(license_data["license"][0]["spdxId"])
453 response.mustcontain(license_data["license"][0]["spdxId"])
527 assert_response.element_contains('.panel-body', license_data["name"])
454 assert_response.element_contains('.panel-body', license_data["name"])
528
455
529 def test_records_can_be_read(self, autologin_user):
456 def test_records_can_be_read(self, autologin_user):
530 response = self.app.get(
457 response = self.app.get(
531 route_path('admin_settings_open_source'), status=200)
458 route_path('admin_settings_open_source'), status=200)
532 assert_response = response.assert_response()
459 assert_response = response.assert_response()
533 assert_response.element_contains(
460 assert_response.element_contains(
534 '.panel-heading', 'Licenses of Third Party Packages')
461 '.panel-heading', 'Licenses of Third Party Packages')
535
462
536 def test_forbidden_when_normal_user(self, autologin_regular_user):
463 def test_forbidden_when_normal_user(self, autologin_regular_user):
537 self.app.get(
464 self.app.get(
538 route_path('admin_settings_open_source'), status=404)
465 route_path('admin_settings_open_source'), status=404)
539
466
540
467
541 @pytest.mark.usefixtures('app')
468 @pytest.mark.usefixtures('app')
542 class TestUserSessions(object):
469 class TestUserSessions(object):
543
470
544 def test_forbidden_when_normal_user(self, autologin_regular_user):
471 def test_forbidden_when_normal_user(self, autologin_regular_user):
545 self.app.get(route_path('admin_settings_sessions'), status=404)
472 self.app.get(route_path('admin_settings_sessions'), status=404)
546
473
547 def test_show_sessions_page(self, autologin_user):
474 def test_show_sessions_page(self, autologin_user):
548 response = self.app.get(route_path('admin_settings_sessions'), status=200)
475 response = self.app.get(route_path('admin_settings_sessions'), status=200)
549 response.mustcontain('file')
476 response.mustcontain('file')
550
477
551 def test_cleanup_old_sessions(self, autologin_user, csrf_token):
478 def test_cleanup_old_sessions(self, autologin_user, csrf_token):
552
479
553 post_data = {
480 post_data = {
554 'csrf_token': csrf_token,
481 'csrf_token': csrf_token,
555 'expire_days': '60'
482 'expire_days': '60'
556 }
483 }
557 response = self.app.post(
484 response = self.app.post(
558 route_path('admin_settings_sessions_cleanup'), params=post_data,
485 route_path('admin_settings_sessions_cleanup'), params=post_data,
559 status=302)
486 status=302)
560 assert_session_flash(response, 'Cleaned up old sessions')
487 assert_session_flash(response, 'Cleaned up old sessions')
561
488
562
489
563 @pytest.mark.usefixtures('app')
490 @pytest.mark.usefixtures('app')
564 class TestAdminSystemInfo(object):
491 class TestAdminSystemInfo(object):
565
492
566 def test_forbidden_when_normal_user(self, autologin_regular_user):
493 def test_forbidden_when_normal_user(self, autologin_regular_user):
567 self.app.get(route_path('admin_settings_system'), status=404)
494 self.app.get(route_path('admin_settings_system'), status=404)
568
495
569 def test_system_info_page(self, autologin_user):
496 def test_system_info_page(self, autologin_user):
570 response = self.app.get(route_path('admin_settings_system'))
497 response = self.app.get(route_path('admin_settings_system'))
571 response.mustcontain('RhodeCode Community Edition, version {}'.format(
498 response.mustcontain('RhodeCode Community Edition, version {}'.format(
572 rhodecode.__version__))
499 rhodecode.__version__))
573
500
574 def test_system_update_new_version(self, autologin_user):
501 def test_system_update_new_version(self, autologin_user):
575 update_data = {
502 update_data = {
576 'versions': [
503 'versions': [
577 {
504 {
578 'version': '100.3.1415926535',
505 'version': '100.3.1415926535',
579 'general': 'The latest version we are ever going to ship'
506 'general': 'The latest version we are ever going to ship'
580 },
507 },
581 {
508 {
582 'version': '0.0.0',
509 'version': '0.0.0',
583 'general': 'The first version we ever shipped'
510 'general': 'The first version we ever shipped'
584 }
511 }
585 ]
512 ]
586 }
513 }
587 with mock.patch(UPDATE_DATA_QUALNAME, return_value=update_data):
514 with mock.patch(UPDATE_DATA_QUALNAME, return_value=update_data):
588 response = self.app.get(route_path('admin_settings_system_update'))
515 response = self.app.get(route_path('admin_settings_system_update'))
589 response.mustcontain('A <b>new version</b> is available')
516 response.mustcontain('A <b>new version</b> is available')
590
517
591 def test_system_update_nothing_new(self, autologin_user):
518 def test_system_update_nothing_new(self, autologin_user):
592 update_data = {
519 update_data = {
593 'versions': [
520 'versions': [
594 {
521 {
595 'version': '0.0.0',
522 'version': '0.0.0',
596 'general': 'The first version we ever shipped'
523 'general': 'The first version we ever shipped'
597 }
524 }
598 ]
525 ]
599 }
526 }
600 with mock.patch(UPDATE_DATA_QUALNAME, return_value=update_data):
527 with mock.patch(UPDATE_DATA_QUALNAME, return_value=update_data):
601 response = self.app.get(route_path('admin_settings_system_update'))
528 response = self.app.get(route_path('admin_settings_system_update'))
602 response.mustcontain(
529 response.mustcontain(
603 'This instance is already running the <b>latest</b> stable version')
530 'This instance is already running the <b>latest</b> stable version')
604
531
605 def test_system_update_bad_response(self, autologin_user):
532 def test_system_update_bad_response(self, autologin_user):
606 with mock.patch(UPDATE_DATA_QUALNAME, side_effect=ValueError('foo')):
533 with mock.patch(UPDATE_DATA_QUALNAME, side_effect=ValueError('foo')):
607 response = self.app.get(route_path('admin_settings_system_update'))
534 response = self.app.get(route_path('admin_settings_system_update'))
608 response.mustcontain(
535 response.mustcontain(
609 'Bad data sent from update server')
536 'Bad data sent from update server')
610
537
611
538
612 @pytest.mark.usefixtures("app")
539 @pytest.mark.usefixtures("app")
613 class TestAdminSettingsIssueTracker(object):
540 class TestAdminSettingsIssueTracker(object):
614 RC_PREFIX = 'rhodecode_'
541 RC_PREFIX = 'rhodecode_'
615 SHORT_PATTERN_KEY = 'issuetracker_pat_'
542 SHORT_PATTERN_KEY = 'issuetracker_pat_'
616 PATTERN_KEY = RC_PREFIX + SHORT_PATTERN_KEY
543 PATTERN_KEY = RC_PREFIX + SHORT_PATTERN_KEY
617 DESC_KEY = RC_PREFIX + 'issuetracker_desc_'
544 DESC_KEY = RC_PREFIX + 'issuetracker_desc_'
618
545
619 def test_issuetracker_index(self, autologin_user):
546 def test_issuetracker_index(self, autologin_user):
620 response = self.app.get(route_path('admin_settings_issuetracker'))
547 response = self.app.get(route_path('admin_settings_issuetracker'))
621 assert response.status_code == 200
548 assert response.status_code == 200
622
549
623 def test_add_empty_issuetracker_pattern(
550 def test_add_empty_issuetracker_pattern(
624 self, request, autologin_user, csrf_token):
551 self, request, autologin_user, csrf_token):
625 post_url = route_path('admin_settings_issuetracker_update')
552 post_url = route_path('admin_settings_issuetracker_update')
626 post_data = {
553 post_data = {
627 'csrf_token': csrf_token
554 'csrf_token': csrf_token
628 }
555 }
629 self.app.post(post_url, post_data, status=302)
556 self.app.post(post_url, post_data, status=302)
630
557
631 def test_add_issuetracker_pattern(
558 def test_add_issuetracker_pattern(
632 self, request, autologin_user, csrf_token):
559 self, request, autologin_user, csrf_token):
633 pattern = 'issuetracker_pat'
560 pattern = 'issuetracker_pat'
634 another_pattern = pattern+'1'
561 another_pattern = pattern+'1'
635 post_url = route_path('admin_settings_issuetracker_update')
562 post_url = route_path('admin_settings_issuetracker_update')
636 post_data = {
563 post_data = {
637 'new_pattern_pattern_0': pattern,
564 'new_pattern_pattern_0': pattern,
638 'new_pattern_url_0': 'http://url',
565 'new_pattern_url_0': 'http://url',
639 'new_pattern_prefix_0': 'prefix',
566 'new_pattern_prefix_0': 'prefix',
640 'new_pattern_description_0': 'description',
567 'new_pattern_description_0': 'description',
641 'new_pattern_pattern_1': another_pattern,
568 'new_pattern_pattern_1': another_pattern,
642 'new_pattern_url_1': 'https://url1',
569 'new_pattern_url_1': 'https://url1',
643 'new_pattern_prefix_1': 'prefix1',
570 'new_pattern_prefix_1': 'prefix1',
644 'new_pattern_description_1': 'description1',
571 'new_pattern_description_1': 'description1',
645 'csrf_token': csrf_token
572 'csrf_token': csrf_token
646 }
573 }
647 self.app.post(post_url, post_data, status=302)
574 self.app.post(post_url, post_data, status=302)
648 settings = SettingsModel().get_all_settings()
575 settings = SettingsModel().get_all_settings()
649 self.uid = md5_safe(pattern)
576 self.uid = md5_safe(pattern)
650 assert settings[self.PATTERN_KEY+self.uid] == pattern
577 assert settings[self.PATTERN_KEY+self.uid] == pattern
651 self.another_uid = md5_safe(another_pattern)
578 self.another_uid = md5_safe(another_pattern)
652 assert settings[self.PATTERN_KEY+self.another_uid] == another_pattern
579 assert settings[self.PATTERN_KEY+self.another_uid] == another_pattern
653
580
654 @request.addfinalizer
581 @request.addfinalizer
655 def cleanup():
582 def cleanup():
656 defaults = SettingsModel().get_all_settings()
583 defaults = SettingsModel().get_all_settings()
657
584
658 entries = [name for name in defaults if (
585 entries = [name for name in defaults if (
659 (self.uid in name) or (self.another_uid in name))]
586 (self.uid in name) or (self.another_uid in name))]
660 start = len(self.RC_PREFIX)
587 start = len(self.RC_PREFIX)
661 for del_key in entries:
588 for del_key in entries:
662 # TODO: anderson: get_by_name needs name without prefix
589 # TODO: anderson: get_by_name needs name without prefix
663 entry = SettingsModel().get_setting_by_name(del_key[start:])
590 entry = SettingsModel().get_setting_by_name(del_key[start:])
664 Session().delete(entry)
591 Session().delete(entry)
665
592
666 Session().commit()
593 Session().commit()
667
594
668 def test_edit_issuetracker_pattern(
595 def test_edit_issuetracker_pattern(
669 self, autologin_user, backend, csrf_token, request):
596 self, autologin_user, backend, csrf_token, request):
670
597
671 old_pattern = 'issuetracker_pat1'
598 old_pattern = 'issuetracker_pat1'
672 old_uid = md5_safe(old_pattern)
599 old_uid = md5_safe(old_pattern)
673
600
674 post_url = route_path('admin_settings_issuetracker_update')
601 post_url = route_path('admin_settings_issuetracker_update')
675 post_data = {
602 post_data = {
676 'new_pattern_pattern_0': old_pattern,
603 'new_pattern_pattern_0': old_pattern,
677 'new_pattern_url_0': 'http://url',
604 'new_pattern_url_0': 'http://url',
678 'new_pattern_prefix_0': 'prefix',
605 'new_pattern_prefix_0': 'prefix',
679 'new_pattern_description_0': 'description',
606 'new_pattern_description_0': 'description',
680
607
681 'csrf_token': csrf_token
608 'csrf_token': csrf_token
682 }
609 }
683 self.app.post(post_url, post_data, status=302)
610 self.app.post(post_url, post_data, status=302)
684
611
685 new_pattern = 'issuetracker_pat1_edited'
612 new_pattern = 'issuetracker_pat1_edited'
686 self.new_uid = md5_safe(new_pattern)
613 self.new_uid = md5_safe(new_pattern)
687
614
688 post_url = route_path('admin_settings_issuetracker_update')
615 post_url = route_path('admin_settings_issuetracker_update')
689 post_data = {
616 post_data = {
690 'new_pattern_pattern_{}'.format(old_uid): new_pattern,
617 'new_pattern_pattern_{}'.format(old_uid): new_pattern,
691 'new_pattern_url_{}'.format(old_uid): 'https://url_edited',
618 'new_pattern_url_{}'.format(old_uid): 'https://url_edited',
692 'new_pattern_prefix_{}'.format(old_uid): 'prefix_edited',
619 'new_pattern_prefix_{}'.format(old_uid): 'prefix_edited',
693 'new_pattern_description_{}'.format(old_uid): 'description_edited',
620 'new_pattern_description_{}'.format(old_uid): 'description_edited',
694 'uid': old_uid,
621 'uid': old_uid,
695 'csrf_token': csrf_token
622 'csrf_token': csrf_token
696 }
623 }
697 self.app.post(post_url, post_data, status=302)
624 self.app.post(post_url, post_data, status=302)
698
625
699 settings = SettingsModel().get_all_settings()
626 settings = SettingsModel().get_all_settings()
700 assert settings[self.PATTERN_KEY+self.new_uid] == new_pattern
627 assert settings[self.PATTERN_KEY+self.new_uid] == new_pattern
701 assert settings[self.DESC_KEY + self.new_uid] == 'description_edited'
628 assert settings[self.DESC_KEY + self.new_uid] == 'description_edited'
702 assert self.PATTERN_KEY+old_uid not in settings
629 assert self.PATTERN_KEY+old_uid not in settings
703
630
704 @request.addfinalizer
631 @request.addfinalizer
705 def cleanup():
632 def cleanup():
706 IssueTrackerSettingsModel().delete_entries(old_uid)
633 IssueTrackerSettingsModel().delete_entries(old_uid)
707 IssueTrackerSettingsModel().delete_entries(self.new_uid)
634 IssueTrackerSettingsModel().delete_entries(self.new_uid)
708
635
709 def test_replace_issuetracker_pattern_description(
636 def test_replace_issuetracker_pattern_description(
710 self, autologin_user, csrf_token, request, settings_util):
637 self, autologin_user, csrf_token, request, settings_util):
711 prefix = 'issuetracker'
638 prefix = 'issuetracker'
712 pattern = 'issuetracker_pat'
639 pattern = 'issuetracker_pat'
713 self.uid = md5_safe(pattern)
640 self.uid = md5_safe(pattern)
714 pattern_key = '_'.join([prefix, 'pat', self.uid])
641 pattern_key = '_'.join([prefix, 'pat', self.uid])
715 rc_pattern_key = '_'.join(['rhodecode', pattern_key])
642 rc_pattern_key = '_'.join(['rhodecode', pattern_key])
716 desc_key = '_'.join([prefix, 'desc', self.uid])
643 desc_key = '_'.join([prefix, 'desc', self.uid])
717 rc_desc_key = '_'.join(['rhodecode', desc_key])
644 rc_desc_key = '_'.join(['rhodecode', desc_key])
718 new_description = 'new_description'
645 new_description = 'new_description'
719
646
720 settings_util.create_rhodecode_setting(
647 settings_util.create_rhodecode_setting(
721 pattern_key, pattern, 'unicode', cleanup=False)
648 pattern_key, pattern, 'unicode', cleanup=False)
722 settings_util.create_rhodecode_setting(
649 settings_util.create_rhodecode_setting(
723 desc_key, 'old description', 'unicode', cleanup=False)
650 desc_key, 'old description', 'unicode', cleanup=False)
724
651
725 post_url = route_path('admin_settings_issuetracker_update')
652 post_url = route_path('admin_settings_issuetracker_update')
726 post_data = {
653 post_data = {
727 'new_pattern_pattern_0': pattern,
654 'new_pattern_pattern_0': pattern,
728 'new_pattern_url_0': 'https://url',
655 'new_pattern_url_0': 'https://url',
729 'new_pattern_prefix_0': 'prefix',
656 'new_pattern_prefix_0': 'prefix',
730 'new_pattern_description_0': new_description,
657 'new_pattern_description_0': new_description,
731 'uid': self.uid,
658 'uid': self.uid,
732 'csrf_token': csrf_token
659 'csrf_token': csrf_token
733 }
660 }
734 self.app.post(post_url, post_data, status=302)
661 self.app.post(post_url, post_data, status=302)
735 settings = SettingsModel().get_all_settings()
662 settings = SettingsModel().get_all_settings()
736 assert settings[rc_pattern_key] == pattern
663 assert settings[rc_pattern_key] == pattern
737 assert settings[rc_desc_key] == new_description
664 assert settings[rc_desc_key] == new_description
738
665
739 @request.addfinalizer
666 @request.addfinalizer
740 def cleanup():
667 def cleanup():
741 IssueTrackerSettingsModel().delete_entries(self.uid)
668 IssueTrackerSettingsModel().delete_entries(self.uid)
742
669
743 def test_delete_issuetracker_pattern(
670 def test_delete_issuetracker_pattern(
744 self, autologin_user, backend, csrf_token, settings_util, xhr_header):
671 self, autologin_user, backend, csrf_token, settings_util, xhr_header):
745
672
746 old_pattern = 'issuetracker_pat_deleted'
673 old_pattern = 'issuetracker_pat_deleted'
747 old_uid = md5_safe(old_pattern)
674 old_uid = md5_safe(old_pattern)
748
675
749 post_url = route_path('admin_settings_issuetracker_update')
676 post_url = route_path('admin_settings_issuetracker_update')
750 post_data = {
677 post_data = {
751 'new_pattern_pattern_0': old_pattern,
678 'new_pattern_pattern_0': old_pattern,
752 'new_pattern_url_0': 'http://url',
679 'new_pattern_url_0': 'http://url',
753 'new_pattern_prefix_0': 'prefix',
680 'new_pattern_prefix_0': 'prefix',
754 'new_pattern_description_0': 'description',
681 'new_pattern_description_0': 'description',
755
682
756 'csrf_token': csrf_token
683 'csrf_token': csrf_token
757 }
684 }
758 self.app.post(post_url, post_data, status=302)
685 self.app.post(post_url, post_data, status=302)
759
686
760 post_url = route_path('admin_settings_issuetracker_delete')
687 post_url = route_path('admin_settings_issuetracker_delete')
761 post_data = {
688 post_data = {
762 'uid': old_uid,
689 'uid': old_uid,
763 'csrf_token': csrf_token
690 'csrf_token': csrf_token
764 }
691 }
765 self.app.post(post_url, post_data, extra_environ=xhr_header, status=200)
692 self.app.post(post_url, post_data, extra_environ=xhr_header, status=200)
766 settings = SettingsModel().get_all_settings()
693 settings = SettingsModel().get_all_settings()
767 assert self.PATTERN_KEY+old_uid not in settings
694 assert self.PATTERN_KEY+old_uid not in settings
768 assert self.DESC_KEY + old_uid not in settings
695 assert self.DESC_KEY + old_uid not in settings
@@ -1,171 +1,152 b''
1
1
2 # Copyright (C) 2010-2023 RhodeCode GmbH
2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 #
3 #
4 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
7 #
7 #
8 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
11 # GNU General Public License for more details.
12 #
12 #
13 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
15 #
16 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19
19
20 import pytest
20 import pytest
21
21
22 from rhodecode.model.db import UserGroup, User
22 from rhodecode.model.db import UserGroup, User
23 from rhodecode.model.meta import Session
23 from rhodecode.model.meta import Session
24
24
25 from rhodecode.tests import (
25 from rhodecode.tests import (
26 TestController, TEST_USER_REGULAR_LOGIN, assert_session_flash)
26 TestController, assert_session_flash)
27 from rhodecode.tests.fixture import Fixture
27 from rhodecode.tests.fixture import Fixture
28 from rhodecode.tests.routes import route_path
28
29
29 fixture = Fixture()
30 fixture = Fixture()
30
31
31
32
32 def route_path(name, params=None, **kwargs):
33 import urllib.request
34 import urllib.parse
35 import urllib.error
36 from rhodecode.apps._base import ADMIN_PREFIX
37
38 base_url = {
39 'user_groups': ADMIN_PREFIX + '/user_groups',
40 'user_groups_data': ADMIN_PREFIX + '/user_groups_data',
41 'user_group_members_data': ADMIN_PREFIX + '/user_groups/{user_group_id}/members',
42 'user_groups_new': ADMIN_PREFIX + '/user_groups/new',
43 'user_groups_create': ADMIN_PREFIX + '/user_groups/create',
44 'edit_user_group': ADMIN_PREFIX + '/user_groups/{user_group_id}/edit',
45 }[name].format(**kwargs)
46
47 if params:
48 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
49 return base_url
50
51
52 class TestAdminUserGroupsView(TestController):
33 class TestAdminUserGroupsView(TestController):
53
34
54 def test_show_users(self):
35 def test_show_users(self):
55 self.log_user()
36 self.log_user()
56 self.app.get(route_path('user_groups'))
37 self.app.get(route_path('user_groups'))
57
38
58 def test_show_user_groups_data(self, xhr_header):
39 def test_show_user_groups_data(self, xhr_header):
59 self.log_user()
40 self.log_user()
60 response = self.app.get(route_path(
41 response = self.app.get(route_path(
61 'user_groups_data'), extra_environ=xhr_header)
42 'user_groups_data'), extra_environ=xhr_header)
62
43
63 all_user_groups = UserGroup.query().count()
44 all_user_groups = UserGroup.query().count()
64 assert response.json['recordsTotal'] == all_user_groups
45 assert response.json['recordsTotal'] == all_user_groups
65
46
66 def test_show_user_groups_data_filtered(self, xhr_header):
47 def test_show_user_groups_data_filtered(self, xhr_header):
67 self.log_user()
48 self.log_user()
68 response = self.app.get(route_path(
49 response = self.app.get(route_path(
69 'user_groups_data', params={'search[value]': 'empty_search'}),
50 'user_groups_data', params={'search[value]': 'empty_search'}),
70 extra_environ=xhr_header)
51 extra_environ=xhr_header)
71
52
72 all_user_groups = UserGroup.query().count()
53 all_user_groups = UserGroup.query().count()
73 assert response.json['recordsTotal'] == all_user_groups
54 assert response.json['recordsTotal'] == all_user_groups
74 assert response.json['recordsFiltered'] == 0
55 assert response.json['recordsFiltered'] == 0
75
56
76 def test_usergroup_escape(self, user_util, xhr_header):
57 def test_usergroup_escape(self, user_util, xhr_header):
77 self.log_user()
58 self.log_user()
78
59
79 xss_img = '<img src="/image1" onload="alert(\'Hello, World!\');">'
60 xss_img = '<img src="/image1" onload="alert(\'Hello, World!\');">'
80 user = user_util.create_user()
61 user = user_util.create_user()
81 user.name = xss_img
62 user.name = xss_img
82 user.lastname = xss_img
63 user.lastname = xss_img
83 Session().add(user)
64 Session().add(user)
84 Session().commit()
65 Session().commit()
85
66
86 user_group = user_util.create_user_group()
67 user_group = user_util.create_user_group()
87
68
88 user_group.users_group_name = xss_img
69 user_group.users_group_name = xss_img
89 user_group.user_group_description = '<strong onload="alert();">DESC</strong>'
70 user_group.user_group_description = '<strong onload="alert();">DESC</strong>'
90
71
91 response = self.app.get(
72 response = self.app.get(
92 route_path('user_groups_data'), extra_environ=xhr_header)
73 route_path('user_groups_data'), extra_environ=xhr_header)
93
74
94 response.mustcontain(
75 response.mustcontain(
95 '&lt;strong onload=&#34;alert();&#34;&gt;DESC&lt;/strong&gt;')
76 '&lt;strong onload=&#34;alert();&#34;&gt;DESC&lt;/strong&gt;')
96 response.mustcontain(
77 response.mustcontain(
97 '&lt;img src=&#34;/image1&#34; onload=&#34;'
78 '&lt;img src=&#34;/image1&#34; onload=&#34;'
98 'alert(&#39;Hello, World!&#39;);&#34;&gt;')
79 'alert(&#39;Hello, World!&#39;);&#34;&gt;')
99
80
100 def test_edit_user_group_autocomplete_empty_members(self, xhr_header, user_util):
81 def test_edit_user_group_autocomplete_empty_members(self, xhr_header, user_util):
101 self.log_user()
82 self.log_user()
102 ug = user_util.create_user_group()
83 ug = user_util.create_user_group()
103 response = self.app.get(
84 response = self.app.get(
104 route_path('user_group_members_data', user_group_id=ug.users_group_id),
85 route_path('user_group_members_data', user_group_id=ug.users_group_id),
105 extra_environ=xhr_header)
86 extra_environ=xhr_header)
106
87
107 assert response.json == {'members': []}
88 assert response.json == {'members': []}
108
89
109 def test_edit_user_group_autocomplete_members(self, xhr_header, user_util):
90 def test_edit_user_group_autocomplete_members(self, xhr_header, user_util):
110 self.log_user()
91 self.log_user()
111 members = [u.user_id for u in User.get_all()]
92 members = [u.user_id for u in User.get_all()]
112 ug = user_util.create_user_group(members=members)
93 ug = user_util.create_user_group(members=members)
113 response = self.app.get(
94 response = self.app.get(
114 route_path('user_group_members_data',
95 route_path('user_group_members_data',
115 user_group_id=ug.users_group_id),
96 user_group_id=ug.users_group_id),
116 extra_environ=xhr_header)
97 extra_environ=xhr_header)
117
98
118 assert len(response.json['members']) == len(members)
99 assert len(response.json['members']) == len(members)
119
100
120 def test_creation_page(self):
101 def test_creation_page(self):
121 self.log_user()
102 self.log_user()
122 self.app.get(route_path('user_groups_new'), status=200)
103 self.app.get(route_path('user_groups_new'), status=200)
123
104
124 def test_create(self):
105 def test_create(self):
125 from rhodecode.lib import helpers as h
106 from rhodecode.lib import helpers as h
126
107
127 self.log_user()
108 self.log_user()
128 users_group_name = 'test_user_group'
109 users_group_name = 'test_user_group'
129 response = self.app.post(route_path('user_groups_create'), {
110 response = self.app.post(route_path('user_groups_create'), {
130 'users_group_name': users_group_name,
111 'users_group_name': users_group_name,
131 'user_group_description': 'DESC',
112 'user_group_description': 'DESC',
132 'active': True,
113 'active': True,
133 'csrf_token': self.csrf_token})
114 'csrf_token': self.csrf_token})
134
115
135 user_group_id = UserGroup.get_by_group_name(
116 user_group_id = UserGroup.get_by_group_name(
136 users_group_name).users_group_id
117 users_group_name).users_group_id
137
118
138 user_group_link = h.link_to(
119 user_group_link = h.link_to(
139 users_group_name,
120 users_group_name,
140 route_path('edit_user_group', user_group_id=user_group_id))
121 route_path('edit_user_group', user_group_id=user_group_id))
141
122
142 assert_session_flash(
123 assert_session_flash(
143 response,
124 response,
144 'Created user group %s' % user_group_link)
125 'Created user group %s' % user_group_link)
145
126
146 fixture.destroy_user_group(users_group_name)
127 fixture.destroy_user_group(users_group_name)
147
128
148 def test_create_with_empty_name(self):
129 def test_create_with_empty_name(self):
149 self.log_user()
130 self.log_user()
150
131
151 response = self.app.post(route_path('user_groups_create'), {
132 response = self.app.post(route_path('user_groups_create'), {
152 'users_group_name': '',
133 'users_group_name': '',
153 'user_group_description': 'DESC',
134 'user_group_description': 'DESC',
154 'active': True,
135 'active': True,
155 'csrf_token': self.csrf_token}, status=200)
136 'csrf_token': self.csrf_token}, status=200)
156
137
157 response.mustcontain('Please enter a value')
138 response.mustcontain('Please enter a value')
158
139
159 def test_create_duplicate(self, user_util):
140 def test_create_duplicate(self, user_util):
160 self.log_user()
141 self.log_user()
161
142
162 user_group = user_util.create_user_group()
143 user_group = user_util.create_user_group()
163 duplicate_name = user_group.users_group_name
144 duplicate_name = user_group.users_group_name
164 response = self.app.post(route_path('user_groups_create'), {
145 response = self.app.post(route_path('user_groups_create'), {
165 'users_group_name': duplicate_name,
146 'users_group_name': duplicate_name,
166 'user_group_description': 'DESC',
147 'user_group_description': 'DESC',
167 'active': True,
148 'active': True,
168 'csrf_token': self.csrf_token}, status=200)
149 'csrf_token': self.csrf_token}, status=200)
169
150
170 response.mustcontain(
151 response.mustcontain(
171 'User group `{}` already exists'.format(duplicate_name))
152 'User group `{}` already exists'.format(duplicate_name))
@@ -1,795 +1,727 b''
1
1
2 # Copyright (C) 2010-2023 RhodeCode GmbH
2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 #
3 #
4 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
7 #
7 #
8 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
11 # GNU General Public License for more details.
12 #
12 #
13 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
15 #
16 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19
19
20 import pytest
20 import pytest
21 from sqlalchemy.orm.exc import NoResultFound
21 from sqlalchemy.orm.exc import NoResultFound
22
22
23 from rhodecode.lib import auth
23 from rhodecode.lib import auth
24 from rhodecode.lib import helpers as h
24 from rhodecode.lib import helpers as h
25 from rhodecode.model.db import User, UserApiKeys, UserEmailMap, Repository
25 from rhodecode.model.db import User, UserApiKeys, UserEmailMap, Repository
26 from rhodecode.model.meta import Session
26 from rhodecode.model.meta import Session
27 from rhodecode.model.user import UserModel
27 from rhodecode.model.user import UserModel
28
28
29 from rhodecode.tests import (
29 from rhodecode.tests import (
30 TestController, TEST_USER_REGULAR_LOGIN, assert_session_flash)
30 TestController, TEST_USER_REGULAR_LOGIN, assert_session_flash)
31 from rhodecode.tests.fixture import Fixture
31 from rhodecode.tests.fixture import Fixture
32 from rhodecode.tests.routes import route_path
32
33
33 fixture = Fixture()
34 fixture = Fixture()
34
35
35
36
36 def route_path(name, params=None, **kwargs):
37 import urllib.request
38 import urllib.parse
39 import urllib.error
40 from rhodecode.apps._base import ADMIN_PREFIX
41
42 base_url = {
43 'users':
44 ADMIN_PREFIX + '/users',
45 'users_data':
46 ADMIN_PREFIX + '/users_data',
47 'users_create':
48 ADMIN_PREFIX + '/users/create',
49 'users_new':
50 ADMIN_PREFIX + '/users/new',
51 'user_edit':
52 ADMIN_PREFIX + '/users/{user_id}/edit',
53 'user_edit_advanced':
54 ADMIN_PREFIX + '/users/{user_id}/edit/advanced',
55 'user_edit_global_perms':
56 ADMIN_PREFIX + '/users/{user_id}/edit/global_permissions',
57 'user_edit_global_perms_update':
58 ADMIN_PREFIX + '/users/{user_id}/edit/global_permissions/update',
59 'user_update':
60 ADMIN_PREFIX + '/users/{user_id}/update',
61 'user_delete':
62 ADMIN_PREFIX + '/users/{user_id}/delete',
63 'user_create_personal_repo_group':
64 ADMIN_PREFIX + '/users/{user_id}/create_repo_group',
65
66 'edit_user_auth_tokens':
67 ADMIN_PREFIX + '/users/{user_id}/edit/auth_tokens',
68 'edit_user_auth_tokens_add':
69 ADMIN_PREFIX + '/users/{user_id}/edit/auth_tokens/new',
70 'edit_user_auth_tokens_delete':
71 ADMIN_PREFIX + '/users/{user_id}/edit/auth_tokens/delete',
72
73 'edit_user_emails':
74 ADMIN_PREFIX + '/users/{user_id}/edit/emails',
75 'edit_user_emails_add':
76 ADMIN_PREFIX + '/users/{user_id}/edit/emails/new',
77 'edit_user_emails_delete':
78 ADMIN_PREFIX + '/users/{user_id}/edit/emails/delete',
79
80 'edit_user_ips':
81 ADMIN_PREFIX + '/users/{user_id}/edit/ips',
82 'edit_user_ips_add':
83 ADMIN_PREFIX + '/users/{user_id}/edit/ips/new',
84 'edit_user_ips_delete':
85 ADMIN_PREFIX + '/users/{user_id}/edit/ips/delete',
86
87 'edit_user_perms_summary':
88 ADMIN_PREFIX + '/users/{user_id}/edit/permissions_summary',
89 'edit_user_perms_summary_json':
90 ADMIN_PREFIX + '/users/{user_id}/edit/permissions_summary/json',
91
92 'edit_user_audit_logs':
93 ADMIN_PREFIX + '/users/{user_id}/edit/audit',
94
95 'edit_user_audit_logs_download':
96 ADMIN_PREFIX + '/users/{user_id}/edit/audit/download',
97
98 }[name].format(**kwargs)
99
100 if params:
101 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
102 return base_url
103
104
105 class TestAdminUsersView(TestController):
37 class TestAdminUsersView(TestController):
106
38
107 def test_show_users(self):
39 def test_show_users(self):
108 self.log_user()
40 self.log_user()
109 self.app.get(route_path('users'))
41 self.app.get(route_path('users'))
110
42
111 def test_show_users_data(self, xhr_header):
43 def test_show_users_data(self, xhr_header):
112 self.log_user()
44 self.log_user()
113 response = self.app.get(route_path(
45 response = self.app.get(route_path(
114 'users_data'), extra_environ=xhr_header)
46 'users_data'), extra_environ=xhr_header)
115
47
116 all_users = User.query().filter(
48 all_users = User.query().filter(
117 User.username != User.DEFAULT_USER).count()
49 User.username != User.DEFAULT_USER).count()
118 assert response.json['recordsTotal'] == all_users
50 assert response.json['recordsTotal'] == all_users
119
51
120 def test_show_users_data_filtered(self, xhr_header):
52 def test_show_users_data_filtered(self, xhr_header):
121 self.log_user()
53 self.log_user()
122 response = self.app.get(route_path(
54 response = self.app.get(route_path(
123 'users_data', params={'search[value]': 'empty_search'}),
55 'users_data', params={'search[value]': 'empty_search'}),
124 extra_environ=xhr_header)
56 extra_environ=xhr_header)
125
57
126 all_users = User.query().filter(
58 all_users = User.query().filter(
127 User.username != User.DEFAULT_USER).count()
59 User.username != User.DEFAULT_USER).count()
128 assert response.json['recordsTotal'] == all_users
60 assert response.json['recordsTotal'] == all_users
129 assert response.json['recordsFiltered'] == 0
61 assert response.json['recordsFiltered'] == 0
130
62
131 def test_auth_tokens_default_user(self):
63 def test_auth_tokens_default_user(self):
132 self.log_user()
64 self.log_user()
133 user = User.get_default_user()
65 user = User.get_default_user()
134 response = self.app.get(
66 response = self.app.get(
135 route_path('edit_user_auth_tokens', user_id=user.user_id),
67 route_path('edit_user_auth_tokens', user_id=user.user_id),
136 status=302)
68 status=302)
137
69
138 def test_auth_tokens(self):
70 def test_auth_tokens(self):
139 self.log_user()
71 self.log_user()
140
72
141 user = User.get_by_username(TEST_USER_REGULAR_LOGIN)
73 user = User.get_by_username(TEST_USER_REGULAR_LOGIN)
142 user_id = user.user_id
74 user_id = user.user_id
143 auth_tokens = user.auth_tokens
75 auth_tokens = user.auth_tokens
144 response = self.app.get(
76 response = self.app.get(
145 route_path('edit_user_auth_tokens', user_id=user_id))
77 route_path('edit_user_auth_tokens', user_id=user_id))
146 for token in auth_tokens:
78 for token in auth_tokens:
147 response.mustcontain(token[:4])
79 response.mustcontain(token[:4])
148 response.mustcontain('never')
80 response.mustcontain('never')
149
81
150 @pytest.mark.parametrize("desc, lifetime", [
82 @pytest.mark.parametrize("desc, lifetime", [
151 ('forever', -1),
83 ('forever', -1),
152 ('5mins', 60*5),
84 ('5mins', 60*5),
153 ('30days', 60*60*24*30),
85 ('30days', 60*60*24*30),
154 ])
86 ])
155 def test_add_auth_token(self, desc, lifetime, user_util):
87 def test_add_auth_token(self, desc, lifetime, user_util):
156 self.log_user()
88 self.log_user()
157 user = user_util.create_user()
89 user = user_util.create_user()
158 user_id = user.user_id
90 user_id = user.user_id
159
91
160 response = self.app.post(
92 response = self.app.post(
161 route_path('edit_user_auth_tokens_add', user_id=user_id),
93 route_path('edit_user_auth_tokens_add', user_id=user_id),
162 {'description': desc, 'lifetime': lifetime,
94 {'description': desc, 'lifetime': lifetime,
163 'csrf_token': self.csrf_token})
95 'csrf_token': self.csrf_token})
164 assert_session_flash(response, 'Auth token successfully created')
96 assert_session_flash(response, 'Auth token successfully created')
165
97
166 response = response.follow()
98 response = response.follow()
167 user = User.get(user_id)
99 user = User.get(user_id)
168 for auth_token in user.auth_tokens:
100 for auth_token in user.auth_tokens:
169 response.mustcontain(auth_token[:4])
101 response.mustcontain(auth_token[:4])
170
102
171 def test_delete_auth_token(self, user_util):
103 def test_delete_auth_token(self, user_util):
172 self.log_user()
104 self.log_user()
173 user = user_util.create_user()
105 user = user_util.create_user()
174 user_id = user.user_id
106 user_id = user.user_id
175 keys = user.auth_tokens
107 keys = user.auth_tokens
176 assert 2 == len(keys)
108 assert 2 == len(keys)
177
109
178 response = self.app.post(
110 response = self.app.post(
179 route_path('edit_user_auth_tokens_add', user_id=user_id),
111 route_path('edit_user_auth_tokens_add', user_id=user_id),
180 {'description': 'desc', 'lifetime': -1,
112 {'description': 'desc', 'lifetime': -1,
181 'csrf_token': self.csrf_token})
113 'csrf_token': self.csrf_token})
182 assert_session_flash(response, 'Auth token successfully created')
114 assert_session_flash(response, 'Auth token successfully created')
183 response.follow()
115 response.follow()
184
116
185 # now delete our key
117 # now delete our key
186 keys = UserApiKeys.query().filter(UserApiKeys.user_id == user_id).all()
118 keys = UserApiKeys.query().filter(UserApiKeys.user_id == user_id).all()
187 assert 3 == len(keys)
119 assert 3 == len(keys)
188
120
189 response = self.app.post(
121 response = self.app.post(
190 route_path('edit_user_auth_tokens_delete', user_id=user_id),
122 route_path('edit_user_auth_tokens_delete', user_id=user_id),
191 {'del_auth_token': keys[0].user_api_key_id,
123 {'del_auth_token': keys[0].user_api_key_id,
192 'csrf_token': self.csrf_token})
124 'csrf_token': self.csrf_token})
193
125
194 assert_session_flash(response, 'Auth token successfully deleted')
126 assert_session_flash(response, 'Auth token successfully deleted')
195 keys = UserApiKeys.query().filter(UserApiKeys.user_id == user_id).all()
127 keys = UserApiKeys.query().filter(UserApiKeys.user_id == user_id).all()
196 assert 2 == len(keys)
128 assert 2 == len(keys)
197
129
198 def test_ips(self):
130 def test_ips(self):
199 self.log_user()
131 self.log_user()
200 user = User.get_by_username(TEST_USER_REGULAR_LOGIN)
132 user = User.get_by_username(TEST_USER_REGULAR_LOGIN)
201 response = self.app.get(route_path('edit_user_ips', user_id=user.user_id))
133 response = self.app.get(route_path('edit_user_ips', user_id=user.user_id))
202 response.mustcontain('All IP addresses are allowed')
134 response.mustcontain('All IP addresses are allowed')
203
135
204 @pytest.mark.parametrize("test_name, ip, ip_range, failure", [
136 @pytest.mark.parametrize("test_name, ip, ip_range, failure", [
205 ('127/24', '127.0.0.1/24', '127.0.0.0 - 127.0.0.255', False),
137 ('127/24', '127.0.0.1/24', '127.0.0.0 - 127.0.0.255', False),
206 ('10/32', '10.0.0.10/32', '10.0.0.10 - 10.0.0.10', False),
138 ('10/32', '10.0.0.10/32', '10.0.0.10 - 10.0.0.10', False),
207 ('0/16', '0.0.0.0/16', '0.0.0.0 - 0.0.255.255', False),
139 ('0/16', '0.0.0.0/16', '0.0.0.0 - 0.0.255.255', False),
208 ('0/8', '0.0.0.0/8', '0.0.0.0 - 0.255.255.255', False),
140 ('0/8', '0.0.0.0/8', '0.0.0.0 - 0.255.255.255', False),
209 ('127_bad_mask', '127.0.0.1/99', '127.0.0.1 - 127.0.0.1', True),
141 ('127_bad_mask', '127.0.0.1/99', '127.0.0.1 - 127.0.0.1', True),
210 ('127_bad_ip', 'foobar', 'foobar', True),
142 ('127_bad_ip', 'foobar', 'foobar', True),
211 ])
143 ])
212 def test_ips_add(self, user_util, test_name, ip, ip_range, failure):
144 def test_ips_add(self, user_util, test_name, ip, ip_range, failure):
213 self.log_user()
145 self.log_user()
214 user = user_util.create_user(username=test_name)
146 user = user_util.create_user(username=test_name)
215 user_id = user.user_id
147 user_id = user.user_id
216
148
217 response = self.app.post(
149 response = self.app.post(
218 route_path('edit_user_ips_add', user_id=user_id),
150 route_path('edit_user_ips_add', user_id=user_id),
219 params={'new_ip': ip, 'csrf_token': self.csrf_token})
151 params={'new_ip': ip, 'csrf_token': self.csrf_token})
220
152
221 if failure:
153 if failure:
222 assert_session_flash(
154 assert_session_flash(
223 response, 'Please enter a valid IPv4 or IpV6 address')
155 response, 'Please enter a valid IPv4 or IpV6 address')
224 response = self.app.get(route_path('edit_user_ips', user_id=user_id))
156 response = self.app.get(route_path('edit_user_ips', user_id=user_id))
225
157
226 response.mustcontain(no=[ip])
158 response.mustcontain(no=[ip])
227 response.mustcontain(no=[ip_range])
159 response.mustcontain(no=[ip_range])
228
160
229 else:
161 else:
230 response = self.app.get(route_path('edit_user_ips', user_id=user_id))
162 response = self.app.get(route_path('edit_user_ips', user_id=user_id))
231 response.mustcontain(ip)
163 response.mustcontain(ip)
232 response.mustcontain(ip_range)
164 response.mustcontain(ip_range)
233
165
234 def test_ips_delete(self, user_util):
166 def test_ips_delete(self, user_util):
235 self.log_user()
167 self.log_user()
236 user = user_util.create_user()
168 user = user_util.create_user()
237 user_id = user.user_id
169 user_id = user.user_id
238 ip = '127.0.0.1/32'
170 ip = '127.0.0.1/32'
239 ip_range = '127.0.0.1 - 127.0.0.1'
171 ip_range = '127.0.0.1 - 127.0.0.1'
240 new_ip = UserModel().add_extra_ip(user_id, ip)
172 new_ip = UserModel().add_extra_ip(user_id, ip)
241 Session().commit()
173 Session().commit()
242 new_ip_id = new_ip.ip_id
174 new_ip_id = new_ip.ip_id
243
175
244 response = self.app.get(route_path('edit_user_ips', user_id=user_id))
176 response = self.app.get(route_path('edit_user_ips', user_id=user_id))
245 response.mustcontain(ip)
177 response.mustcontain(ip)
246 response.mustcontain(ip_range)
178 response.mustcontain(ip_range)
247
179
248 self.app.post(
180 self.app.post(
249 route_path('edit_user_ips_delete', user_id=user_id),
181 route_path('edit_user_ips_delete', user_id=user_id),
250 params={'del_ip_id': new_ip_id, 'csrf_token': self.csrf_token})
182 params={'del_ip_id': new_ip_id, 'csrf_token': self.csrf_token})
251
183
252 response = self.app.get(route_path('edit_user_ips', user_id=user_id))
184 response = self.app.get(route_path('edit_user_ips', user_id=user_id))
253 response.mustcontain('All IP addresses are allowed')
185 response.mustcontain('All IP addresses are allowed')
254 response.mustcontain(no=[ip])
186 response.mustcontain(no=[ip])
255 response.mustcontain(no=[ip_range])
187 response.mustcontain(no=[ip_range])
256
188
257 def test_emails(self):
189 def test_emails(self):
258 self.log_user()
190 self.log_user()
259 user = User.get_by_username(TEST_USER_REGULAR_LOGIN)
191 user = User.get_by_username(TEST_USER_REGULAR_LOGIN)
260 response = self.app.get(
192 response = self.app.get(
261 route_path('edit_user_emails', user_id=user.user_id))
193 route_path('edit_user_emails', user_id=user.user_id))
262 response.mustcontain('No additional emails specified')
194 response.mustcontain('No additional emails specified')
263
195
264 def test_emails_add(self, user_util):
196 def test_emails_add(self, user_util):
265 self.log_user()
197 self.log_user()
266 user = user_util.create_user()
198 user = user_util.create_user()
267 user_id = user.user_id
199 user_id = user.user_id
268
200
269 self.app.post(
201 self.app.post(
270 route_path('edit_user_emails_add', user_id=user_id),
202 route_path('edit_user_emails_add', user_id=user_id),
271 params={'new_email': 'example@rhodecode.com',
203 params={'new_email': 'example@rhodecode.com',
272 'csrf_token': self.csrf_token})
204 'csrf_token': self.csrf_token})
273
205
274 response = self.app.get(
206 response = self.app.get(
275 route_path('edit_user_emails', user_id=user_id))
207 route_path('edit_user_emails', user_id=user_id))
276 response.mustcontain('example@rhodecode.com')
208 response.mustcontain('example@rhodecode.com')
277
209
278 def test_emails_add_existing_email(self, user_util, user_regular):
210 def test_emails_add_existing_email(self, user_util, user_regular):
279 existing_email = user_regular.email
211 existing_email = user_regular.email
280
212
281 self.log_user()
213 self.log_user()
282 user = user_util.create_user()
214 user = user_util.create_user()
283 user_id = user.user_id
215 user_id = user.user_id
284
216
285 response = self.app.post(
217 response = self.app.post(
286 route_path('edit_user_emails_add', user_id=user_id),
218 route_path('edit_user_emails_add', user_id=user_id),
287 params={'new_email': existing_email,
219 params={'new_email': existing_email,
288 'csrf_token': self.csrf_token})
220 'csrf_token': self.csrf_token})
289 assert_session_flash(
221 assert_session_flash(
290 response, 'This e-mail address is already taken')
222 response, 'This e-mail address is already taken')
291
223
292 response = self.app.get(
224 response = self.app.get(
293 route_path('edit_user_emails', user_id=user_id))
225 route_path('edit_user_emails', user_id=user_id))
294 response.mustcontain(no=[existing_email])
226 response.mustcontain(no=[existing_email])
295
227
296 def test_emails_delete(self, user_util):
228 def test_emails_delete(self, user_util):
297 self.log_user()
229 self.log_user()
298 user = user_util.create_user()
230 user = user_util.create_user()
299 user_id = user.user_id
231 user_id = user.user_id
300
232
301 self.app.post(
233 self.app.post(
302 route_path('edit_user_emails_add', user_id=user_id),
234 route_path('edit_user_emails_add', user_id=user_id),
303 params={'new_email': 'example@rhodecode.com',
235 params={'new_email': 'example@rhodecode.com',
304 'csrf_token': self.csrf_token})
236 'csrf_token': self.csrf_token})
305
237
306 response = self.app.get(
238 response = self.app.get(
307 route_path('edit_user_emails', user_id=user_id))
239 route_path('edit_user_emails', user_id=user_id))
308 response.mustcontain('example@rhodecode.com')
240 response.mustcontain('example@rhodecode.com')
309
241
310 user_email = UserEmailMap.query()\
242 user_email = UserEmailMap.query()\
311 .filter(UserEmailMap.email == 'example@rhodecode.com') \
243 .filter(UserEmailMap.email == 'example@rhodecode.com') \
312 .filter(UserEmailMap.user_id == user_id)\
244 .filter(UserEmailMap.user_id == user_id)\
313 .one()
245 .one()
314
246
315 del_email_id = user_email.email_id
247 del_email_id = user_email.email_id
316 self.app.post(
248 self.app.post(
317 route_path('edit_user_emails_delete', user_id=user_id),
249 route_path('edit_user_emails_delete', user_id=user_id),
318 params={'del_email_id': del_email_id,
250 params={'del_email_id': del_email_id,
319 'csrf_token': self.csrf_token})
251 'csrf_token': self.csrf_token})
320
252
321 response = self.app.get(
253 response = self.app.get(
322 route_path('edit_user_emails', user_id=user_id))
254 route_path('edit_user_emails', user_id=user_id))
323 response.mustcontain(no=['example@rhodecode.com'])
255 response.mustcontain(no=['example@rhodecode.com'])
324
256
325 def test_create(self, request, xhr_header):
257 def test_create(self, request, xhr_header):
326 self.log_user()
258 self.log_user()
327 username = 'newtestuser'
259 username = 'newtestuser'
328 password = 'test12'
260 password = 'test12'
329 password_confirmation = password
261 password_confirmation = password
330 name = 'name'
262 name = 'name'
331 lastname = 'lastname'
263 lastname = 'lastname'
332 email = 'mail@mail.com'
264 email = 'mail@mail.com'
333
265
334 self.app.get(route_path('users_new'))
266 self.app.get(route_path('users_new'))
335
267
336 response = self.app.post(route_path('users_create'), params={
268 response = self.app.post(route_path('users_create'), params={
337 'username': username,
269 'username': username,
338 'password': password,
270 'password': password,
339 'description': 'mr CTO',
271 'description': 'mr CTO',
340 'password_confirmation': password_confirmation,
272 'password_confirmation': password_confirmation,
341 'firstname': name,
273 'firstname': name,
342 'active': True,
274 'active': True,
343 'lastname': lastname,
275 'lastname': lastname,
344 'extern_name': 'rhodecode',
276 'extern_name': 'rhodecode',
345 'extern_type': 'rhodecode',
277 'extern_type': 'rhodecode',
346 'email': email,
278 'email': email,
347 'csrf_token': self.csrf_token,
279 'csrf_token': self.csrf_token,
348 })
280 })
349 user_link = h.link_to(
281 user_link = h.link_to(
350 username,
282 username,
351 route_path(
283 route_path(
352 'user_edit', user_id=User.get_by_username(username).user_id))
284 'user_edit', user_id=User.get_by_username(username).user_id))
353 assert_session_flash(response, 'Created user %s' % (user_link,))
285 assert_session_flash(response, 'Created user %s' % (user_link,))
354
286
355 @request.addfinalizer
287 @request.addfinalizer
356 def cleanup():
288 def cleanup():
357 fixture.destroy_user(username)
289 fixture.destroy_user(username)
358 Session().commit()
290 Session().commit()
359
291
360 new_user = User.query().filter(User.username == username).one()
292 new_user = User.query().filter(User.username == username).one()
361
293
362 assert new_user.username == username
294 assert new_user.username == username
363 assert auth.check_password(password, new_user.password)
295 assert auth.check_password(password, new_user.password)
364 assert new_user.name == name
296 assert new_user.name == name
365 assert new_user.lastname == lastname
297 assert new_user.lastname == lastname
366 assert new_user.email == email
298 assert new_user.email == email
367
299
368 response = self.app.get(route_path('users_data'),
300 response = self.app.get(route_path('users_data'),
369 extra_environ=xhr_header)
301 extra_environ=xhr_header)
370 response.mustcontain(username)
302 response.mustcontain(username)
371
303
372 def test_create_err(self):
304 def test_create_err(self):
373 self.log_user()
305 self.log_user()
374 username = 'new_user'
306 username = 'new_user'
375 password = ''
307 password = ''
376 name = 'name'
308 name = 'name'
377 lastname = 'lastname'
309 lastname = 'lastname'
378 email = 'errmail.com'
310 email = 'errmail.com'
379
311
380 self.app.get(route_path('users_new'))
312 self.app.get(route_path('users_new'))
381
313
382 response = self.app.post(route_path('users_create'), params={
314 response = self.app.post(route_path('users_create'), params={
383 'username': username,
315 'username': username,
384 'password': password,
316 'password': password,
385 'name': name,
317 'name': name,
386 'active': False,
318 'active': False,
387 'lastname': lastname,
319 'lastname': lastname,
388 'description': 'mr CTO',
320 'description': 'mr CTO',
389 'email': email,
321 'email': email,
390 'csrf_token': self.csrf_token,
322 'csrf_token': self.csrf_token,
391 })
323 })
392
324
393 msg = u'Username "%(username)s" is forbidden'
325 msg = u'Username "%(username)s" is forbidden'
394 msg = h.html_escape(msg % {'username': 'new_user'})
326 msg = h.html_escape(msg % {'username': 'new_user'})
395 response.mustcontain('<span class="error-message">%s</span>' % msg)
327 response.mustcontain('<span class="error-message">%s</span>' % msg)
396 response.mustcontain(
328 response.mustcontain(
397 '<span class="error-message">Please enter a value</span>')
329 '<span class="error-message">Please enter a value</span>')
398 response.mustcontain(
330 response.mustcontain(
399 '<span class="error-message">An email address must contain a'
331 '<span class="error-message">An email address must contain a'
400 ' single @</span>')
332 ' single @</span>')
401
333
402 def get_user():
334 def get_user():
403 Session().query(User).filter(User.username == username).one()
335 Session().query(User).filter(User.username == username).one()
404
336
405 with pytest.raises(NoResultFound):
337 with pytest.raises(NoResultFound):
406 get_user()
338 get_user()
407
339
408 def test_new(self):
340 def test_new(self):
409 self.log_user()
341 self.log_user()
410 self.app.get(route_path('users_new'))
342 self.app.get(route_path('users_new'))
411
343
412 @pytest.mark.parametrize("name, attrs", [
344 @pytest.mark.parametrize("name, attrs", [
413 ('firstname', {'firstname': 'new_username'}),
345 ('firstname', {'firstname': 'new_username'}),
414 ('lastname', {'lastname': 'new_username'}),
346 ('lastname', {'lastname': 'new_username'}),
415 ('admin', {'admin': True}),
347 ('admin', {'admin': True}),
416 ('admin', {'admin': False}),
348 ('admin', {'admin': False}),
417 ('extern_type', {'extern_type': 'ldap'}),
349 ('extern_type', {'extern_type': 'ldap'}),
418 ('extern_type', {'extern_type': None}),
350 ('extern_type', {'extern_type': None}),
419 ('extern_name', {'extern_name': 'test'}),
351 ('extern_name', {'extern_name': 'test'}),
420 ('extern_name', {'extern_name': None}),
352 ('extern_name', {'extern_name': None}),
421 ('active', {'active': False}),
353 ('active', {'active': False}),
422 ('active', {'active': True}),
354 ('active', {'active': True}),
423 ('email', {'email': 'some@email.com'}),
355 ('email', {'email': 'some@email.com'}),
424 ('language', {'language': 'de'}),
356 ('language', {'language': 'de'}),
425 ('language', {'language': 'en'}),
357 ('language', {'language': 'en'}),
426 ('description', {'description': 'hello CTO'}),
358 ('description', {'description': 'hello CTO'}),
427 # ('new_password', {'new_password': 'foobar123',
359 # ('new_password', {'new_password': 'foobar123',
428 # 'password_confirmation': 'foobar123'})
360 # 'password_confirmation': 'foobar123'})
429 ])
361 ])
430 def test_update(self, name, attrs, user_util):
362 def test_update(self, name, attrs, user_util):
431 self.log_user()
363 self.log_user()
432 usr = user_util.create_user(
364 usr = user_util.create_user(
433 password='qweqwe',
365 password='qweqwe',
434 email='testme@rhodecode.org',
366 email='testme@rhodecode.org',
435 extern_type='rhodecode',
367 extern_type='rhodecode',
436 extern_name='xxx',
368 extern_name='xxx',
437 )
369 )
438 user_id = usr.user_id
370 user_id = usr.user_id
439 Session().commit()
371 Session().commit()
440
372
441 params = usr.get_api_data()
373 params = usr.get_api_data()
442 cur_lang = params['language'] or 'en'
374 cur_lang = params['language'] or 'en'
443 params.update({
375 params.update({
444 'password_confirmation': '',
376 'password_confirmation': '',
445 'new_password': '',
377 'new_password': '',
446 'language': cur_lang,
378 'language': cur_lang,
447 'csrf_token': self.csrf_token,
379 'csrf_token': self.csrf_token,
448 })
380 })
449 params.update({'new_password': ''})
381 params.update({'new_password': ''})
450 params.update(attrs)
382 params.update(attrs)
451 if name == 'email':
383 if name == 'email':
452 params['emails'] = [attrs['email']]
384 params['emails'] = [attrs['email']]
453 elif name == 'extern_type':
385 elif name == 'extern_type':
454 # cannot update this via form, expected value is original one
386 # cannot update this via form, expected value is original one
455 params['extern_type'] = "rhodecode"
387 params['extern_type'] = "rhodecode"
456 elif name == 'extern_name':
388 elif name == 'extern_name':
457 # cannot update this via form, expected value is original one
389 # cannot update this via form, expected value is original one
458 params['extern_name'] = 'xxx'
390 params['extern_name'] = 'xxx'
459 # special case since this user is not
391 # special case since this user is not
460 # logged in yet his data is not filled
392 # logged in yet his data is not filled
461 # so we use creation data
393 # so we use creation data
462
394
463 response = self.app.post(
395 response = self.app.post(
464 route_path('user_update', user_id=usr.user_id), params)
396 route_path('user_update', user_id=usr.user_id), params)
465 assert response.status_int == 302
397 assert response.status_int == 302
466 assert_session_flash(response, 'User updated successfully')
398 assert_session_flash(response, 'User updated successfully')
467
399
468 updated_user = User.get(user_id)
400 updated_user = User.get(user_id)
469 updated_params = updated_user.get_api_data()
401 updated_params = updated_user.get_api_data()
470 updated_params.update({'password_confirmation': ''})
402 updated_params.update({'password_confirmation': ''})
471 updated_params.update({'new_password': ''})
403 updated_params.update({'new_password': ''})
472
404
473 del params['csrf_token']
405 del params['csrf_token']
474 assert params == updated_params
406 assert params == updated_params
475
407
476 def test_update_and_migrate_password(
408 def test_update_and_migrate_password(
477 self, autologin_user, real_crypto_backend, user_util):
409 self, autologin_user, real_crypto_backend, user_util):
478
410
479 user = user_util.create_user()
411 user = user_util.create_user()
480 temp_user = user.username
412 temp_user = user.username
481 user.password = auth._RhodeCodeCryptoSha256().hash_create(
413 user.password = auth._RhodeCodeCryptoSha256().hash_create(
482 b'test123')
414 b'test123')
483 Session().add(user)
415 Session().add(user)
484 Session().commit()
416 Session().commit()
485
417
486 params = user.get_api_data()
418 params = user.get_api_data()
487
419
488 params.update({
420 params.update({
489 'password_confirmation': 'qweqwe123',
421 'password_confirmation': 'qweqwe123',
490 'new_password': 'qweqwe123',
422 'new_password': 'qweqwe123',
491 'language': 'en',
423 'language': 'en',
492 'csrf_token': autologin_user.csrf_token,
424 'csrf_token': autologin_user.csrf_token,
493 })
425 })
494
426
495 response = self.app.post(
427 response = self.app.post(
496 route_path('user_update', user_id=user.user_id), params)
428 route_path('user_update', user_id=user.user_id), params)
497 assert response.status_int == 302
429 assert response.status_int == 302
498 assert_session_flash(response, 'User updated successfully')
430 assert_session_flash(response, 'User updated successfully')
499
431
500 # new password should be bcrypted, after log-in and transfer
432 # new password should be bcrypted, after log-in and transfer
501 user = User.get_by_username(temp_user)
433 user = User.get_by_username(temp_user)
502 assert user.password.startswith('$')
434 assert user.password.startswith('$')
503
435
504 updated_user = User.get_by_username(temp_user)
436 updated_user = User.get_by_username(temp_user)
505 updated_params = updated_user.get_api_data()
437 updated_params = updated_user.get_api_data()
506 updated_params.update({'password_confirmation': 'qweqwe123'})
438 updated_params.update({'password_confirmation': 'qweqwe123'})
507 updated_params.update({'new_password': 'qweqwe123'})
439 updated_params.update({'new_password': 'qweqwe123'})
508
440
509 del params['csrf_token']
441 del params['csrf_token']
510 assert params == updated_params
442 assert params == updated_params
511
443
512 def test_delete(self):
444 def test_delete(self):
513 self.log_user()
445 self.log_user()
514 username = 'newtestuserdeleteme'
446 username = 'newtestuserdeleteme'
515
447
516 fixture.create_user(name=username)
448 fixture.create_user(name=username)
517
449
518 new_user = Session().query(User)\
450 new_user = Session().query(User)\
519 .filter(User.username == username).one()
451 .filter(User.username == username).one()
520 response = self.app.post(
452 response = self.app.post(
521 route_path('user_delete', user_id=new_user.user_id),
453 route_path('user_delete', user_id=new_user.user_id),
522 params={'csrf_token': self.csrf_token})
454 params={'csrf_token': self.csrf_token})
523
455
524 assert_session_flash(response, 'Successfully deleted user `{}`'.format(username))
456 assert_session_flash(response, 'Successfully deleted user `{}`'.format(username))
525
457
526 def test_delete_owner_of_repository(self, request, user_util):
458 def test_delete_owner_of_repository(self, request, user_util):
527 self.log_user()
459 self.log_user()
528 obj_name = 'test_repo'
460 obj_name = 'test_repo'
529 usr = user_util.create_user()
461 usr = user_util.create_user()
530 username = usr.username
462 username = usr.username
531 fixture.create_repo(obj_name, cur_user=usr.username)
463 fixture.create_repo(obj_name, cur_user=usr.username)
532
464
533 new_user = Session().query(User)\
465 new_user = Session().query(User)\
534 .filter(User.username == username).one()
466 .filter(User.username == username).one()
535 response = self.app.post(
467 response = self.app.post(
536 route_path('user_delete', user_id=new_user.user_id),
468 route_path('user_delete', user_id=new_user.user_id),
537 params={'csrf_token': self.csrf_token})
469 params={'csrf_token': self.csrf_token})
538
470
539 msg = 'user "%s" still owns 1 repositories and cannot be removed. ' \
471 msg = 'user "%s" still owns 1 repositories and cannot be removed. ' \
540 'Switch owners or remove those repositories:%s' % (username, obj_name)
472 'Switch owners or remove those repositories:%s' % (username, obj_name)
541 assert_session_flash(response, msg)
473 assert_session_flash(response, msg)
542 fixture.destroy_repo(obj_name)
474 fixture.destroy_repo(obj_name)
543
475
544 def test_delete_owner_of_repository_detaching(self, request, user_util):
476 def test_delete_owner_of_repository_detaching(self, request, user_util):
545 self.log_user()
477 self.log_user()
546 obj_name = 'test_repo'
478 obj_name = 'test_repo'
547 usr = user_util.create_user(auto_cleanup=False)
479 usr = user_util.create_user(auto_cleanup=False)
548 username = usr.username
480 username = usr.username
549 fixture.create_repo(obj_name, cur_user=usr.username)
481 fixture.create_repo(obj_name, cur_user=usr.username)
550 Session().commit()
482 Session().commit()
551
483
552 new_user = Session().query(User)\
484 new_user = Session().query(User)\
553 .filter(User.username == username).one()
485 .filter(User.username == username).one()
554 response = self.app.post(
486 response = self.app.post(
555 route_path('user_delete', user_id=new_user.user_id),
487 route_path('user_delete', user_id=new_user.user_id),
556 params={'user_repos': 'detach', 'csrf_token': self.csrf_token})
488 params={'user_repos': 'detach', 'csrf_token': self.csrf_token})
557
489
558 msg = 'Detached 1 repositories'
490 msg = 'Detached 1 repositories'
559 assert_session_flash(response, msg)
491 assert_session_flash(response, msg)
560 fixture.destroy_repo(obj_name)
492 fixture.destroy_repo(obj_name)
561
493
562 def test_delete_owner_of_repository_deleting(self, request, user_util):
494 def test_delete_owner_of_repository_deleting(self, request, user_util):
563 self.log_user()
495 self.log_user()
564 obj_name = 'test_repo'
496 obj_name = 'test_repo'
565 usr = user_util.create_user(auto_cleanup=False)
497 usr = user_util.create_user(auto_cleanup=False)
566 username = usr.username
498 username = usr.username
567 fixture.create_repo(obj_name, cur_user=usr.username)
499 fixture.create_repo(obj_name, cur_user=usr.username)
568
500
569 new_user = Session().query(User)\
501 new_user = Session().query(User)\
570 .filter(User.username == username).one()
502 .filter(User.username == username).one()
571 response = self.app.post(
503 response = self.app.post(
572 route_path('user_delete', user_id=new_user.user_id),
504 route_path('user_delete', user_id=new_user.user_id),
573 params={'user_repos': 'delete', 'csrf_token': self.csrf_token})
505 params={'user_repos': 'delete', 'csrf_token': self.csrf_token})
574
506
575 msg = 'Deleted 1 repositories'
507 msg = 'Deleted 1 repositories'
576 assert_session_flash(response, msg)
508 assert_session_flash(response, msg)
577
509
578 def test_delete_owner_of_repository_group(self, request, user_util):
510 def test_delete_owner_of_repository_group(self, request, user_util):
579 self.log_user()
511 self.log_user()
580 obj_name = 'test_group'
512 obj_name = 'test_group'
581 usr = user_util.create_user()
513 usr = user_util.create_user()
582 username = usr.username
514 username = usr.username
583 fixture.create_repo_group(obj_name, cur_user=usr.username)
515 fixture.create_repo_group(obj_name, cur_user=usr.username)
584
516
585 new_user = Session().query(User)\
517 new_user = Session().query(User)\
586 .filter(User.username == username).one()
518 .filter(User.username == username).one()
587 response = self.app.post(
519 response = self.app.post(
588 route_path('user_delete', user_id=new_user.user_id),
520 route_path('user_delete', user_id=new_user.user_id),
589 params={'csrf_token': self.csrf_token})
521 params={'csrf_token': self.csrf_token})
590
522
591 msg = 'user "%s" still owns 1 repository groups and cannot be removed. ' \
523 msg = 'user "%s" still owns 1 repository groups and cannot be removed. ' \
592 'Switch owners or remove those repository groups:%s' % (username, obj_name)
524 'Switch owners or remove those repository groups:%s' % (username, obj_name)
593 assert_session_flash(response, msg)
525 assert_session_flash(response, msg)
594 fixture.destroy_repo_group(obj_name)
526 fixture.destroy_repo_group(obj_name)
595
527
596 def test_delete_owner_of_repository_group_detaching(self, request, user_util):
528 def test_delete_owner_of_repository_group_detaching(self, request, user_util):
597 self.log_user()
529 self.log_user()
598 obj_name = 'test_group'
530 obj_name = 'test_group'
599 usr = user_util.create_user(auto_cleanup=False)
531 usr = user_util.create_user(auto_cleanup=False)
600 username = usr.username
532 username = usr.username
601 fixture.create_repo_group(obj_name, cur_user=usr.username)
533 fixture.create_repo_group(obj_name, cur_user=usr.username)
602
534
603 new_user = Session().query(User)\
535 new_user = Session().query(User)\
604 .filter(User.username == username).one()
536 .filter(User.username == username).one()
605 response = self.app.post(
537 response = self.app.post(
606 route_path('user_delete', user_id=new_user.user_id),
538 route_path('user_delete', user_id=new_user.user_id),
607 params={'user_repo_groups': 'delete', 'csrf_token': self.csrf_token})
539 params={'user_repo_groups': 'delete', 'csrf_token': self.csrf_token})
608
540
609 msg = 'Deleted 1 repository groups'
541 msg = 'Deleted 1 repository groups'
610 assert_session_flash(response, msg)
542 assert_session_flash(response, msg)
611
543
612 def test_delete_owner_of_repository_group_deleting(self, request, user_util):
544 def test_delete_owner_of_repository_group_deleting(self, request, user_util):
613 self.log_user()
545 self.log_user()
614 obj_name = 'test_group'
546 obj_name = 'test_group'
615 usr = user_util.create_user(auto_cleanup=False)
547 usr = user_util.create_user(auto_cleanup=False)
616 username = usr.username
548 username = usr.username
617 fixture.create_repo_group(obj_name, cur_user=usr.username)
549 fixture.create_repo_group(obj_name, cur_user=usr.username)
618
550
619 new_user = Session().query(User)\
551 new_user = Session().query(User)\
620 .filter(User.username == username).one()
552 .filter(User.username == username).one()
621 response = self.app.post(
553 response = self.app.post(
622 route_path('user_delete', user_id=new_user.user_id),
554 route_path('user_delete', user_id=new_user.user_id),
623 params={'user_repo_groups': 'detach', 'csrf_token': self.csrf_token})
555 params={'user_repo_groups': 'detach', 'csrf_token': self.csrf_token})
624
556
625 msg = 'Detached 1 repository groups'
557 msg = 'Detached 1 repository groups'
626 assert_session_flash(response, msg)
558 assert_session_flash(response, msg)
627 fixture.destroy_repo_group(obj_name)
559 fixture.destroy_repo_group(obj_name)
628
560
629 def test_delete_owner_of_user_group(self, request, user_util):
561 def test_delete_owner_of_user_group(self, request, user_util):
630 self.log_user()
562 self.log_user()
631 obj_name = 'test_user_group'
563 obj_name = 'test_user_group'
632 usr = user_util.create_user()
564 usr = user_util.create_user()
633 username = usr.username
565 username = usr.username
634 fixture.create_user_group(obj_name, cur_user=usr.username)
566 fixture.create_user_group(obj_name, cur_user=usr.username)
635
567
636 new_user = Session().query(User)\
568 new_user = Session().query(User)\
637 .filter(User.username == username).one()
569 .filter(User.username == username).one()
638 response = self.app.post(
570 response = self.app.post(
639 route_path('user_delete', user_id=new_user.user_id),
571 route_path('user_delete', user_id=new_user.user_id),
640 params={'csrf_token': self.csrf_token})
572 params={'csrf_token': self.csrf_token})
641
573
642 msg = 'user "%s" still owns 1 user groups and cannot be removed. ' \
574 msg = 'user "%s" still owns 1 user groups and cannot be removed. ' \
643 'Switch owners or remove those user groups:%s' % (username, obj_name)
575 'Switch owners or remove those user groups:%s' % (username, obj_name)
644 assert_session_flash(response, msg)
576 assert_session_flash(response, msg)
645 fixture.destroy_user_group(obj_name)
577 fixture.destroy_user_group(obj_name)
646
578
647 def test_delete_owner_of_user_group_detaching(self, request, user_util):
579 def test_delete_owner_of_user_group_detaching(self, request, user_util):
648 self.log_user()
580 self.log_user()
649 obj_name = 'test_user_group'
581 obj_name = 'test_user_group'
650 usr = user_util.create_user(auto_cleanup=False)
582 usr = user_util.create_user(auto_cleanup=False)
651 username = usr.username
583 username = usr.username
652 fixture.create_user_group(obj_name, cur_user=usr.username)
584 fixture.create_user_group(obj_name, cur_user=usr.username)
653
585
654 new_user = Session().query(User)\
586 new_user = Session().query(User)\
655 .filter(User.username == username).one()
587 .filter(User.username == username).one()
656 try:
588 try:
657 response = self.app.post(
589 response = self.app.post(
658 route_path('user_delete', user_id=new_user.user_id),
590 route_path('user_delete', user_id=new_user.user_id),
659 params={'user_user_groups': 'detach',
591 params={'user_user_groups': 'detach',
660 'csrf_token': self.csrf_token})
592 'csrf_token': self.csrf_token})
661
593
662 msg = 'Detached 1 user groups'
594 msg = 'Detached 1 user groups'
663 assert_session_flash(response, msg)
595 assert_session_flash(response, msg)
664 finally:
596 finally:
665 fixture.destroy_user_group(obj_name)
597 fixture.destroy_user_group(obj_name)
666
598
667 def test_delete_owner_of_user_group_deleting(self, request, user_util):
599 def test_delete_owner_of_user_group_deleting(self, request, user_util):
668 self.log_user()
600 self.log_user()
669 obj_name = 'test_user_group'
601 obj_name = 'test_user_group'
670 usr = user_util.create_user(auto_cleanup=False)
602 usr = user_util.create_user(auto_cleanup=False)
671 username = usr.username
603 username = usr.username
672 fixture.create_user_group(obj_name, cur_user=usr.username)
604 fixture.create_user_group(obj_name, cur_user=usr.username)
673
605
674 new_user = Session().query(User)\
606 new_user = Session().query(User)\
675 .filter(User.username == username).one()
607 .filter(User.username == username).one()
676 response = self.app.post(
608 response = self.app.post(
677 route_path('user_delete', user_id=new_user.user_id),
609 route_path('user_delete', user_id=new_user.user_id),
678 params={'user_user_groups': 'delete', 'csrf_token': self.csrf_token})
610 params={'user_user_groups': 'delete', 'csrf_token': self.csrf_token})
679
611
680 msg = 'Deleted 1 user groups'
612 msg = 'Deleted 1 user groups'
681 assert_session_flash(response, msg)
613 assert_session_flash(response, msg)
682
614
683 def test_edit(self, user_util):
615 def test_edit(self, user_util):
684 self.log_user()
616 self.log_user()
685 user = user_util.create_user()
617 user = user_util.create_user()
686 self.app.get(route_path('user_edit', user_id=user.user_id))
618 self.app.get(route_path('user_edit', user_id=user.user_id))
687
619
688 def test_edit_default_user_redirect(self):
620 def test_edit_default_user_redirect(self):
689 self.log_user()
621 self.log_user()
690 user = User.get_default_user()
622 user = User.get_default_user()
691 self.app.get(route_path('user_edit', user_id=user.user_id), status=302)
623 self.app.get(route_path('user_edit', user_id=user.user_id), status=302)
692
624
693 @pytest.mark.parametrize(
625 @pytest.mark.parametrize(
694 'repo_create, repo_create_write, user_group_create, repo_group_create,'
626 'repo_create, repo_create_write, user_group_create, repo_group_create,'
695 'fork_create, inherit_default_permissions, expect_error,'
627 'fork_create, inherit_default_permissions, expect_error,'
696 'expect_form_error', [
628 'expect_form_error', [
697 ('hg.create.none', 'hg.create.write_on_repogroup.false',
629 ('hg.create.none', 'hg.create.write_on_repogroup.false',
698 'hg.usergroup.create.false', 'hg.repogroup.create.false',
630 'hg.usergroup.create.false', 'hg.repogroup.create.false',
699 'hg.fork.none', 'hg.inherit_default_perms.false', False, False),
631 'hg.fork.none', 'hg.inherit_default_perms.false', False, False),
700 ('hg.create.repository', 'hg.create.write_on_repogroup.false',
632 ('hg.create.repository', 'hg.create.write_on_repogroup.false',
701 'hg.usergroup.create.false', 'hg.repogroup.create.false',
633 'hg.usergroup.create.false', 'hg.repogroup.create.false',
702 'hg.fork.none', 'hg.inherit_default_perms.false', False, False),
634 'hg.fork.none', 'hg.inherit_default_perms.false', False, False),
703 ('hg.create.repository', 'hg.create.write_on_repogroup.true',
635 ('hg.create.repository', 'hg.create.write_on_repogroup.true',
704 'hg.usergroup.create.true', 'hg.repogroup.create.true',
636 'hg.usergroup.create.true', 'hg.repogroup.create.true',
705 'hg.fork.repository', 'hg.inherit_default_perms.false', False,
637 'hg.fork.repository', 'hg.inherit_default_perms.false', False,
706 False),
638 False),
707 ('hg.create.XXX', 'hg.create.write_on_repogroup.true',
639 ('hg.create.XXX', 'hg.create.write_on_repogroup.true',
708 'hg.usergroup.create.true', 'hg.repogroup.create.true',
640 'hg.usergroup.create.true', 'hg.repogroup.create.true',
709 'hg.fork.repository', 'hg.inherit_default_perms.false', False,
641 'hg.fork.repository', 'hg.inherit_default_perms.false', False,
710 True),
642 True),
711 ('', '', '', '', '', '', True, False),
643 ('', '', '', '', '', '', True, False),
712 ])
644 ])
713 def test_global_perms_on_user(
645 def test_global_perms_on_user(
714 self, repo_create, repo_create_write, user_group_create,
646 self, repo_create, repo_create_write, user_group_create,
715 repo_group_create, fork_create, expect_error, expect_form_error,
647 repo_group_create, fork_create, expect_error, expect_form_error,
716 inherit_default_permissions, user_util):
648 inherit_default_permissions, user_util):
717 self.log_user()
649 self.log_user()
718 user = user_util.create_user()
650 user = user_util.create_user()
719 uid = user.user_id
651 uid = user.user_id
720
652
721 # ENABLE REPO CREATE ON A GROUP
653 # ENABLE REPO CREATE ON A GROUP
722 perm_params = {
654 perm_params = {
723 'inherit_default_permissions': False,
655 'inherit_default_permissions': False,
724 'default_repo_create': repo_create,
656 'default_repo_create': repo_create,
725 'default_repo_create_on_write': repo_create_write,
657 'default_repo_create_on_write': repo_create_write,
726 'default_user_group_create': user_group_create,
658 'default_user_group_create': user_group_create,
727 'default_repo_group_create': repo_group_create,
659 'default_repo_group_create': repo_group_create,
728 'default_fork_create': fork_create,
660 'default_fork_create': fork_create,
729 'default_inherit_default_permissions': inherit_default_permissions,
661 'default_inherit_default_permissions': inherit_default_permissions,
730 'csrf_token': self.csrf_token,
662 'csrf_token': self.csrf_token,
731 }
663 }
732 response = self.app.post(
664 response = self.app.post(
733 route_path('user_edit_global_perms_update', user_id=uid),
665 route_path('user_edit_global_perms_update', user_id=uid),
734 params=perm_params)
666 params=perm_params)
735
667
736 if expect_form_error:
668 if expect_form_error:
737 assert response.status_int == 200
669 assert response.status_int == 200
738 response.mustcontain('Value must be one of')
670 response.mustcontain('Value must be one of')
739 else:
671 else:
740 if expect_error:
672 if expect_error:
741 msg = 'An error occurred during permissions saving'
673 msg = 'An error occurred during permissions saving'
742 else:
674 else:
743 msg = 'User global permissions updated successfully'
675 msg = 'User global permissions updated successfully'
744 ug = User.get(uid)
676 ug = User.get(uid)
745 del perm_params['inherit_default_permissions']
677 del perm_params['inherit_default_permissions']
746 del perm_params['csrf_token']
678 del perm_params['csrf_token']
747 assert perm_params == ug.get_default_perms()
679 assert perm_params == ug.get_default_perms()
748 assert_session_flash(response, msg)
680 assert_session_flash(response, msg)
749
681
750 def test_global_permissions_initial_values(self, user_util):
682 def test_global_permissions_initial_values(self, user_util):
751 self.log_user()
683 self.log_user()
752 user = user_util.create_user()
684 user = user_util.create_user()
753 uid = user.user_id
685 uid = user.user_id
754 response = self.app.get(
686 response = self.app.get(
755 route_path('user_edit_global_perms', user_id=uid))
687 route_path('user_edit_global_perms', user_id=uid))
756 default_user = User.get_default_user()
688 default_user = User.get_default_user()
757 default_permissions = default_user.get_default_perms()
689 default_permissions = default_user.get_default_perms()
758 assert_response = response.assert_response()
690 assert_response = response.assert_response()
759 expected_permissions = (
691 expected_permissions = (
760 'default_repo_create', 'default_repo_create_on_write',
692 'default_repo_create', 'default_repo_create_on_write',
761 'default_fork_create', 'default_repo_group_create',
693 'default_fork_create', 'default_repo_group_create',
762 'default_user_group_create', 'default_inherit_default_permissions')
694 'default_user_group_create', 'default_inherit_default_permissions')
763 for permission in expected_permissions:
695 for permission in expected_permissions:
764 css_selector = '[name={}][checked=checked]'.format(permission)
696 css_selector = '[name={}][checked=checked]'.format(permission)
765 element = assert_response.get_element(css_selector)
697 element = assert_response.get_element(css_selector)
766 assert element.value == default_permissions[permission]
698 assert element.value == default_permissions[permission]
767
699
768 def test_perms_summary_page(self):
700 def test_perms_summary_page(self):
769 user = self.log_user()
701 user = self.log_user()
770 response = self.app.get(
702 response = self.app.get(
771 route_path('edit_user_perms_summary', user_id=user['user_id']))
703 route_path('edit_user_perms_summary', user_id=user['user_id']))
772 for repo in Repository.query().all():
704 for repo in Repository.query().all():
773 response.mustcontain(repo.repo_name)
705 response.mustcontain(repo.repo_name)
774
706
775 def test_perms_summary_page_json(self):
707 def test_perms_summary_page_json(self):
776 user = self.log_user()
708 user = self.log_user()
777 response = self.app.get(
709 response = self.app.get(
778 route_path('edit_user_perms_summary_json', user_id=user['user_id']))
710 route_path('edit_user_perms_summary_json', user_id=user['user_id']))
779 for repo in Repository.query().all():
711 for repo in Repository.query().all():
780 response.mustcontain(repo.repo_name)
712 response.mustcontain(repo.repo_name)
781
713
782 def test_audit_log_page(self):
714 def test_audit_log_page(self):
783 user = self.log_user()
715 user = self.log_user()
784 self.app.get(
716 self.app.get(
785 route_path('edit_user_audit_logs', user_id=user['user_id']))
717 route_path('edit_user_audit_logs', user_id=user['user_id']))
786
718
787 def test_audit_log_page_download(self):
719 def test_audit_log_page_download(self):
788 user = self.log_user()
720 user = self.log_user()
789 user_id = user['user_id']
721 user_id = user['user_id']
790 response = self.app.get(
722 response = self.app.get(
791 route_path('edit_user_audit_logs_download', user_id=user_id))
723 route_path('edit_user_audit_logs_download', user_id=user_id))
792
724
793 assert response.content_disposition == \
725 assert response.content_disposition == \
794 'attachment; filename=user_{}_audit_logs.json'.format(user_id)
726 'attachment; filename=user_{}_audit_logs.json'.format(user_id)
795 assert response.content_type == "application/json"
727 assert response.content_type == "application/json"
@@ -1,177 +1,155 b''
1
1
2 # Copyright (C) 2010-2023 RhodeCode GmbH
2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 #
3 #
4 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
7 #
7 #
8 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
11 # GNU General Public License for more details.
12 #
12 #
13 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
15 #
16 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19
19
20 import pytest
20 import pytest
21
21
22 from rhodecode.model.db import User, UserSshKeys
22 from rhodecode.model.db import User, UserSshKeys
23
23
24 from rhodecode.tests import TestController, assert_session_flash
24 from rhodecode.tests import TestController, assert_session_flash
25 from rhodecode.tests.fixture import Fixture
25 from rhodecode.tests.fixture import Fixture
26 from rhodecode.tests.routes import route_path
26
27
27 fixture = Fixture()
28 fixture = Fixture()
28
29
29
30
30 def route_path(name, params=None, **kwargs):
31 import urllib.request
32 import urllib.parse
33 import urllib.error
34 from rhodecode.apps._base import ADMIN_PREFIX
35
36 base_url = {
37 'edit_user_ssh_keys':
38 ADMIN_PREFIX + '/users/{user_id}/edit/ssh_keys',
39 'edit_user_ssh_keys_generate_keypair':
40 ADMIN_PREFIX + '/users/{user_id}/edit/ssh_keys/generate',
41 'edit_user_ssh_keys_add':
42 ADMIN_PREFIX + '/users/{user_id}/edit/ssh_keys/new',
43 'edit_user_ssh_keys_delete':
44 ADMIN_PREFIX + '/users/{user_id}/edit/ssh_keys/delete',
45
46 }[name].format(**kwargs)
47
48 if params:
49 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
50 return base_url
51
52
53 class TestAdminUsersSshKeysView(TestController):
31 class TestAdminUsersSshKeysView(TestController):
54 INVALID_KEY = """\
32 INVALID_KEY = """\
55 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDk+77sjDzVeB6vevJsuZds1iNU5
33 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDk+77sjDzVeB6vevJsuZds1iNU5
56 LANOa5CU5G/9JYIA6RYsWWMO7mbsR82IUckdqOHmxSykfR1D1TdluyIpQLrwgH5kb
34 LANOa5CU5G/9JYIA6RYsWWMO7mbsR82IUckdqOHmxSykfR1D1TdluyIpQLrwgH5kb
57 n8FkVI8zBMCKakxowvN67B0R7b1BT4PPzW2JlOXei/m9W12ZY484VTow6/B+kf2Q8
35 n8FkVI8zBMCKakxowvN67B0R7b1BT4PPzW2JlOXei/m9W12ZY484VTow6/B+kf2Q8
58 cP8tmCJmKWZma5Em7OTUhvjyQVNz3v7HfeY5Hq0Ci4ECJ59hepFDabJvtAXg9XrI6
36 cP8tmCJmKWZma5Em7OTUhvjyQVNz3v7HfeY5Hq0Ci4ECJ59hepFDabJvtAXg9XrI6
59 jvdphZTc30I4fG8+hBHzpeFxUGvSGNtXPUbwaAY8j/oHYrTpMgkj6pUEFsiKfC5zP
37 jvdphZTc30I4fG8+hBHzpeFxUGvSGNtXPUbwaAY8j/oHYrTpMgkj6pUEFsiKfC5zP
60 qPFR5HyKTCHW0nFUJnZsbyFT5hMiF/hZkJc9A0ZbdSvJwCRQ/g3bmdL
38 qPFR5HyKTCHW0nFUJnZsbyFT5hMiF/hZkJc9A0ZbdSvJwCRQ/g3bmdL
61 your_email@example.com
39 your_email@example.com
62 """
40 """
63 VALID_KEY = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDk+77sjDzVeB6vev' \
41 VALID_KEY = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDk+77sjDzVeB6vev' \
64 'JsuZds1iNU5LANOa5CU5G/9JYIA6RYsWWMO7mbsR82IUckdqOHmxSy' \
42 'JsuZds1iNU5LANOa5CU5G/9JYIA6RYsWWMO7mbsR82IUckdqOHmxSy' \
65 'kfR1D1TdluyIpQLrwgH5kbn8FkVI8zBMCKakxowvN67B0R7b1BT4PP' \
43 'kfR1D1TdluyIpQLrwgH5kbn8FkVI8zBMCKakxowvN67B0R7b1BT4PP' \
66 'zW2JlOXei/m9W12ZY484VTow6/B+kf2Q8cP8tmCJmKWZma5Em7OTUh' \
44 'zW2JlOXei/m9W12ZY484VTow6/B+kf2Q8cP8tmCJmKWZma5Em7OTUh' \
67 'vjyQVNz3v7HfeY5Hq0Ci4ECJ59hepFDabJvtAXg9XrI6jvdphZTc30' \
45 'vjyQVNz3v7HfeY5Hq0Ci4ECJ59hepFDabJvtAXg9XrI6jvdphZTc30' \
68 'I4fG8+hBHzpeFxUGvSGNtXPUbwaAY8j/oHYrTpMgkj6pUEFsiKfC5zPq' \
46 'I4fG8+hBHzpeFxUGvSGNtXPUbwaAY8j/oHYrTpMgkj6pUEFsiKfC5zPq' \
69 'PFR5HyKTCHW0nFUJnZsbyFT5hMiF/hZkJc9A0ZbdSvJwCRQ/g3bmdL ' \
47 'PFR5HyKTCHW0nFUJnZsbyFT5hMiF/hZkJc9A0ZbdSvJwCRQ/g3bmdL ' \
70 'your_email@example.com'
48 'your_email@example.com'
71 FINGERPRINT = 'MD5:01:4f:ad:29:22:6e:01:37:c9:d2:52:26:52:b0:2d:93'
49 FINGERPRINT = 'MD5:01:4f:ad:29:22:6e:01:37:c9:d2:52:26:52:b0:2d:93'
72
50
73 def test_ssh_keys_default_user(self):
51 def test_ssh_keys_default_user(self):
74 self.log_user()
52 self.log_user()
75 user = User.get_default_user()
53 user = User.get_default_user()
76 self.app.get(
54 self.app.get(
77 route_path('edit_user_ssh_keys', user_id=user.user_id),
55 route_path('edit_user_ssh_keys', user_id=user.user_id),
78 status=302)
56 status=302)
79
57
80 def test_add_ssh_key_error(self, user_util):
58 def test_add_ssh_key_error(self, user_util):
81 self.log_user()
59 self.log_user()
82 user = user_util.create_user()
60 user = user_util.create_user()
83 user_id = user.user_id
61 user_id = user.user_id
84
62
85 key_data = self.INVALID_KEY
63 key_data = self.INVALID_KEY
86
64
87 desc = 'MY SSH KEY'
65 desc = 'MY SSH KEY'
88 response = self.app.post(
66 response = self.app.post(
89 route_path('edit_user_ssh_keys_add', user_id=user_id),
67 route_path('edit_user_ssh_keys_add', user_id=user_id),
90 {'description': desc, 'key_data': key_data,
68 {'description': desc, 'key_data': key_data,
91 'csrf_token': self.csrf_token})
69 'csrf_token': self.csrf_token})
92 assert_session_flash(response, 'An error occurred during ssh '
70 assert_session_flash(response, 'An error occurred during ssh '
93 'key saving: Unable to decode the key')
71 'key saving: Unable to decode the key')
94
72
95 def test_ssh_key_duplicate(self, user_util):
73 def test_ssh_key_duplicate(self, user_util):
96 self.log_user()
74 self.log_user()
97 user = user_util.create_user()
75 user = user_util.create_user()
98 user_id = user.user_id
76 user_id = user.user_id
99
77
100 key_data = self.VALID_KEY
78 key_data = self.VALID_KEY
101
79
102 desc = 'MY SSH KEY'
80 desc = 'MY SSH KEY'
103 response = self.app.post(
81 response = self.app.post(
104 route_path('edit_user_ssh_keys_add', user_id=user_id),
82 route_path('edit_user_ssh_keys_add', user_id=user_id),
105 {'description': desc, 'key_data': key_data,
83 {'description': desc, 'key_data': key_data,
106 'csrf_token': self.csrf_token})
84 'csrf_token': self.csrf_token})
107 assert_session_flash(response, 'Ssh Key successfully created')
85 assert_session_flash(response, 'Ssh Key successfully created')
108 response.follow() # flush session flash
86 response.follow() # flush session flash
109
87
110 # add the same key AGAIN
88 # add the same key AGAIN
111 desc = 'MY SSH KEY'
89 desc = 'MY SSH KEY'
112 response = self.app.post(
90 response = self.app.post(
113 route_path('edit_user_ssh_keys_add', user_id=user_id),
91 route_path('edit_user_ssh_keys_add', user_id=user_id),
114 {'description': desc, 'key_data': key_data,
92 {'description': desc, 'key_data': key_data,
115 'csrf_token': self.csrf_token})
93 'csrf_token': self.csrf_token})
116
94
117 err = 'Such key with fingerprint `{}` already exists, ' \
95 err = 'Such key with fingerprint `{}` already exists, ' \
118 'please use a different one'.format(self.FINGERPRINT)
96 'please use a different one'.format(self.FINGERPRINT)
119 assert_session_flash(response, 'An error occurred during ssh key '
97 assert_session_flash(response, 'An error occurred during ssh key '
120 'saving: {}'.format(err))
98 'saving: {}'.format(err))
121
99
122 def test_add_ssh_key(self, user_util):
100 def test_add_ssh_key(self, user_util):
123 self.log_user()
101 self.log_user()
124 user = user_util.create_user()
102 user = user_util.create_user()
125 user_id = user.user_id
103 user_id = user.user_id
126
104
127 key_data = self.VALID_KEY
105 key_data = self.VALID_KEY
128
106
129 desc = 'MY SSH KEY'
107 desc = 'MY SSH KEY'
130 response = self.app.post(
108 response = self.app.post(
131 route_path('edit_user_ssh_keys_add', user_id=user_id),
109 route_path('edit_user_ssh_keys_add', user_id=user_id),
132 {'description': desc, 'key_data': key_data,
110 {'description': desc, 'key_data': key_data,
133 'csrf_token': self.csrf_token})
111 'csrf_token': self.csrf_token})
134 assert_session_flash(response, 'Ssh Key successfully created')
112 assert_session_flash(response, 'Ssh Key successfully created')
135
113
136 response = response.follow()
114 response = response.follow()
137 response.mustcontain(desc)
115 response.mustcontain(desc)
138
116
139 def test_delete_ssh_key(self, user_util):
117 def test_delete_ssh_key(self, user_util):
140 self.log_user()
118 self.log_user()
141 user = user_util.create_user()
119 user = user_util.create_user()
142 user_id = user.user_id
120 user_id = user.user_id
143
121
144 key_data = self.VALID_KEY
122 key_data = self.VALID_KEY
145
123
146 desc = 'MY SSH KEY'
124 desc = 'MY SSH KEY'
147 response = self.app.post(
125 response = self.app.post(
148 route_path('edit_user_ssh_keys_add', user_id=user_id),
126 route_path('edit_user_ssh_keys_add', user_id=user_id),
149 {'description': desc, 'key_data': key_data,
127 {'description': desc, 'key_data': key_data,
150 'csrf_token': self.csrf_token})
128 'csrf_token': self.csrf_token})
151 assert_session_flash(response, 'Ssh Key successfully created')
129 assert_session_flash(response, 'Ssh Key successfully created')
152 response = response.follow() # flush the Session flash
130 response = response.follow() # flush the Session flash
153
131
154 # now delete our key
132 # now delete our key
155 keys = UserSshKeys.query().filter(UserSshKeys.user_id == user_id).all()
133 keys = UserSshKeys.query().filter(UserSshKeys.user_id == user_id).all()
156 assert 1 == len(keys)
134 assert 1 == len(keys)
157
135
158 response = self.app.post(
136 response = self.app.post(
159 route_path('edit_user_ssh_keys_delete', user_id=user_id),
137 route_path('edit_user_ssh_keys_delete', user_id=user_id),
160 {'del_ssh_key': keys[0].ssh_key_id,
138 {'del_ssh_key': keys[0].ssh_key_id,
161 'csrf_token': self.csrf_token})
139 'csrf_token': self.csrf_token})
162
140
163 assert_session_flash(response, 'Ssh key successfully deleted')
141 assert_session_flash(response, 'Ssh key successfully deleted')
164 keys = UserSshKeys.query().filter(UserSshKeys.user_id == user_id).all()
142 keys = UserSshKeys.query().filter(UserSshKeys.user_id == user_id).all()
165 assert 0 == len(keys)
143 assert 0 == len(keys)
166
144
167 def test_generate_keypair(self, user_util):
145 def test_generate_keypair(self, user_util):
168 self.log_user()
146 self.log_user()
169 user = user_util.create_user()
147 user = user_util.create_user()
170 user_id = user.user_id
148 user_id = user.user_id
171
149
172 response = self.app.get(
150 response = self.app.get(
173 route_path('edit_user_ssh_keys_generate_keypair', user_id=user_id))
151 route_path('edit_user_ssh_keys_generate_keypair', user_id=user_id))
174
152
175 response.mustcontain('Private key')
153 response.mustcontain('Private key')
176 response.mustcontain('Public key')
154 response.mustcontain('Public key')
177 response.mustcontain('-----BEGIN PRIVATE KEY-----')
155 response.mustcontain('-----BEGIN PRIVATE KEY-----')
@@ -1,261 +1,246 b''
1 # Copyright (C) 2010-2023 RhodeCode GmbH
1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 #
2 #
3 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
14 #
15 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 import os
18 import os
19 import pytest
19 import pytest
20
20
21 from rhodecode.lib.ext_json import json
21 from rhodecode.lib.ext_json import json
22 from rhodecode.model.auth_token import AuthTokenModel
22 from rhodecode.model.auth_token import AuthTokenModel
23 from rhodecode.model.db import Session, FileStore, Repository, User
23 from rhodecode.model.db import Session, FileStore, Repository, User
24 from rhodecode.tests import TestController
25 from rhodecode.apps.file_store import utils, config_keys
24 from rhodecode.apps.file_store import utils, config_keys
26
25
27
26 from rhodecode.tests import TestController
28 def route_path(name, params=None, **kwargs):
27 from rhodecode.tests.routes import route_path
29 import urllib.request
30 import urllib.parse
31 import urllib.error
32
33 base_url = {
34 'upload_file': '/_file_store/upload',
35 'download_file': '/_file_store/download/{fid}',
36 'download_file_by_token': '/_file_store/token-download/{_auth_token}/{fid}'
37
38 }[name].format(**kwargs)
39
40 if params:
41 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
42 return base_url
43
28
44
29
45 class TestFileStoreViews(TestController):
30 class TestFileStoreViews(TestController):
46
31
47 @pytest.mark.parametrize("fid, content, exists", [
32 @pytest.mark.parametrize("fid, content, exists", [
48 ('abcde-0.jpg', "xxxxx", True),
33 ('abcde-0.jpg', "xxxxx", True),
49 ('abcde-0.exe', "1234567", True),
34 ('abcde-0.exe', "1234567", True),
50 ('abcde-0.jpg', "xxxxx", False),
35 ('abcde-0.jpg', "xxxxx", False),
51 ])
36 ])
52 def test_get_files_from_store(self, fid, content, exists, tmpdir, user_util):
37 def test_get_files_from_store(self, fid, content, exists, tmpdir, user_util):
53 user = self.log_user()
38 user = self.log_user()
54 user_id = user['user_id']
39 user_id = user['user_id']
55 repo_id = user_util.create_repo().repo_id
40 repo_id = user_util.create_repo().repo_id
56 store_path = self.app._pyramid_settings[config_keys.store_path]
41 store_path = self.app._pyramid_settings[config_keys.store_path]
57 store_uid = fid
42 store_uid = fid
58
43
59 if exists:
44 if exists:
60 status = 200
45 status = 200
61 store = utils.get_file_storage({config_keys.store_path: store_path})
46 store = utils.get_file_storage({config_keys.store_path: store_path})
62 filesystem_file = os.path.join(str(tmpdir), fid)
47 filesystem_file = os.path.join(str(tmpdir), fid)
63 with open(filesystem_file, 'wt') as f:
48 with open(filesystem_file, 'wt') as f:
64 f.write(content)
49 f.write(content)
65
50
66 with open(filesystem_file, 'rb') as f:
51 with open(filesystem_file, 'rb') as f:
67 store_uid, metadata = store.save_file(f, fid, extra_metadata={'filename': fid})
52 store_uid, metadata = store.save_file(f, fid, extra_metadata={'filename': fid})
68
53
69 entry = FileStore.create(
54 entry = FileStore.create(
70 file_uid=store_uid, filename=metadata["filename"],
55 file_uid=store_uid, filename=metadata["filename"],
71 file_hash=metadata["sha256"], file_size=metadata["size"],
56 file_hash=metadata["sha256"], file_size=metadata["size"],
72 file_display_name='file_display_name',
57 file_display_name='file_display_name',
73 file_description='repo artifact `{}`'.format(metadata["filename"]),
58 file_description='repo artifact `{}`'.format(metadata["filename"]),
74 check_acl=True, user_id=user_id,
59 check_acl=True, user_id=user_id,
75 scope_repo_id=repo_id
60 scope_repo_id=repo_id
76 )
61 )
77 Session().add(entry)
62 Session().add(entry)
78 Session().commit()
63 Session().commit()
79
64
80 else:
65 else:
81 status = 404
66 status = 404
82
67
83 response = self.app.get(route_path('download_file', fid=store_uid), status=status)
68 response = self.app.get(route_path('download_file', fid=store_uid), status=status)
84
69
85 if exists:
70 if exists:
86 assert response.text == content
71 assert response.text == content
87 file_store_path = os.path.dirname(store.resolve_name(store_uid, store_path)[1])
72 file_store_path = os.path.dirname(store.resolve_name(store_uid, store_path)[1])
88 metadata_file = os.path.join(file_store_path, store_uid + '.meta')
73 metadata_file = os.path.join(file_store_path, store_uid + '.meta')
89 assert os.path.exists(metadata_file)
74 assert os.path.exists(metadata_file)
90 with open(metadata_file, 'rb') as f:
75 with open(metadata_file, 'rb') as f:
91 json_data = json.loads(f.read())
76 json_data = json.loads(f.read())
92
77
93 assert json_data
78 assert json_data
94 assert 'size' in json_data
79 assert 'size' in json_data
95
80
96 def test_upload_files_without_content_to_store(self):
81 def test_upload_files_without_content_to_store(self):
97 self.log_user()
82 self.log_user()
98 response = self.app.post(
83 response = self.app.post(
99 route_path('upload_file'),
84 route_path('upload_file'),
100 params={'csrf_token': self.csrf_token},
85 params={'csrf_token': self.csrf_token},
101 status=200)
86 status=200)
102
87
103 assert response.json == {
88 assert response.json == {
104 'error': 'store_file data field is missing',
89 'error': 'store_file data field is missing',
105 'access_path': None,
90 'access_path': None,
106 'store_fid': None}
91 'store_fid': None}
107
92
108 def test_upload_files_bogus_content_to_store(self):
93 def test_upload_files_bogus_content_to_store(self):
109 self.log_user()
94 self.log_user()
110 response = self.app.post(
95 response = self.app.post(
111 route_path('upload_file'),
96 route_path('upload_file'),
112 params={'csrf_token': self.csrf_token, 'store_file': 'bogus'},
97 params={'csrf_token': self.csrf_token, 'store_file': 'bogus'},
113 status=200)
98 status=200)
114
99
115 assert response.json == {
100 assert response.json == {
116 'error': 'filename cannot be read from the data field',
101 'error': 'filename cannot be read from the data field',
117 'access_path': None,
102 'access_path': None,
118 'store_fid': None}
103 'store_fid': None}
119
104
120 def test_upload_content_to_store(self):
105 def test_upload_content_to_store(self):
121 self.log_user()
106 self.log_user()
122 response = self.app.post(
107 response = self.app.post(
123 route_path('upload_file'),
108 route_path('upload_file'),
124 upload_files=[('store_file', b'myfile.txt', b'SOME CONTENT')],
109 upload_files=[('store_file', b'myfile.txt', b'SOME CONTENT')],
125 params={'csrf_token': self.csrf_token},
110 params={'csrf_token': self.csrf_token},
126 status=200)
111 status=200)
127
112
128 assert response.json['store_fid']
113 assert response.json['store_fid']
129
114
130 @pytest.fixture()
115 @pytest.fixture()
131 def create_artifact_factory(self, tmpdir):
116 def create_artifact_factory(self, tmpdir):
132 def factory(user_id, content):
117 def factory(user_id, content):
133 store_path = self.app._pyramid_settings[config_keys.store_path]
118 store_path = self.app._pyramid_settings[config_keys.store_path]
134 store = utils.get_file_storage({config_keys.store_path: store_path})
119 store = utils.get_file_storage({config_keys.store_path: store_path})
135 fid = 'example.txt'
120 fid = 'example.txt'
136
121
137 filesystem_file = os.path.join(str(tmpdir), fid)
122 filesystem_file = os.path.join(str(tmpdir), fid)
138 with open(filesystem_file, 'wt') as f:
123 with open(filesystem_file, 'wt') as f:
139 f.write(content)
124 f.write(content)
140
125
141 with open(filesystem_file, 'rb') as f:
126 with open(filesystem_file, 'rb') as f:
142 store_uid, metadata = store.save_file(f, fid, extra_metadata={'filename': fid})
127 store_uid, metadata = store.save_file(f, fid, extra_metadata={'filename': fid})
143
128
144 entry = FileStore.create(
129 entry = FileStore.create(
145 file_uid=store_uid, filename=metadata["filename"],
130 file_uid=store_uid, filename=metadata["filename"],
146 file_hash=metadata["sha256"], file_size=metadata["size"],
131 file_hash=metadata["sha256"], file_size=metadata["size"],
147 file_display_name='file_display_name',
132 file_display_name='file_display_name',
148 file_description='repo artifact `{}`'.format(metadata["filename"]),
133 file_description='repo artifact `{}`'.format(metadata["filename"]),
149 check_acl=True, user_id=user_id,
134 check_acl=True, user_id=user_id,
150 )
135 )
151 Session().add(entry)
136 Session().add(entry)
152 Session().commit()
137 Session().commit()
153 return entry
138 return entry
154 return factory
139 return factory
155
140
156 def test_download_file_non_scoped(self, user_util, create_artifact_factory):
141 def test_download_file_non_scoped(self, user_util, create_artifact_factory):
157 user = self.log_user()
142 user = self.log_user()
158 user_id = user['user_id']
143 user_id = user['user_id']
159 content = 'HELLO MY NAME IS ARTIFACT !'
144 content = 'HELLO MY NAME IS ARTIFACT !'
160
145
161 artifact = create_artifact_factory(user_id, content)
146 artifact = create_artifact_factory(user_id, content)
162 file_uid = artifact.file_uid
147 file_uid = artifact.file_uid
163 response = self.app.get(route_path('download_file', fid=file_uid), status=200)
148 response = self.app.get(route_path('download_file', fid=file_uid), status=200)
164 assert response.text == content
149 assert response.text == content
165
150
166 # log-in to new user and test download again
151 # log-in to new user and test download again
167 user = user_util.create_user(password='qweqwe')
152 user = user_util.create_user(password='qweqwe')
168 self.log_user(user.username, 'qweqwe')
153 self.log_user(user.username, 'qweqwe')
169 response = self.app.get(route_path('download_file', fid=file_uid), status=200)
154 response = self.app.get(route_path('download_file', fid=file_uid), status=200)
170 assert response.text == content
155 assert response.text == content
171
156
172 def test_download_file_scoped_to_repo(self, user_util, create_artifact_factory):
157 def test_download_file_scoped_to_repo(self, user_util, create_artifact_factory):
173 user = self.log_user()
158 user = self.log_user()
174 user_id = user['user_id']
159 user_id = user['user_id']
175 content = 'HELLO MY NAME IS ARTIFACT !'
160 content = 'HELLO MY NAME IS ARTIFACT !'
176
161
177 artifact = create_artifact_factory(user_id, content)
162 artifact = create_artifact_factory(user_id, content)
178 # bind to repo
163 # bind to repo
179 repo = user_util.create_repo()
164 repo = user_util.create_repo()
180 repo_id = repo.repo_id
165 repo_id = repo.repo_id
181 artifact.scope_repo_id = repo_id
166 artifact.scope_repo_id = repo_id
182 Session().add(artifact)
167 Session().add(artifact)
183 Session().commit()
168 Session().commit()
184
169
185 file_uid = artifact.file_uid
170 file_uid = artifact.file_uid
186 response = self.app.get(route_path('download_file', fid=file_uid), status=200)
171 response = self.app.get(route_path('download_file', fid=file_uid), status=200)
187 assert response.text == content
172 assert response.text == content
188
173
189 # log-in to new user and test download again
174 # log-in to new user and test download again
190 user = user_util.create_user(password='qweqwe')
175 user = user_util.create_user(password='qweqwe')
191 self.log_user(user.username, 'qweqwe')
176 self.log_user(user.username, 'qweqwe')
192 response = self.app.get(route_path('download_file', fid=file_uid), status=200)
177 response = self.app.get(route_path('download_file', fid=file_uid), status=200)
193 assert response.text == content
178 assert response.text == content
194
179
195 # forbid user the rights to repo
180 # forbid user the rights to repo
196 repo = Repository.get(repo_id)
181 repo = Repository.get(repo_id)
197 user_util.grant_user_permission_to_repo(repo, user, 'repository.none')
182 user_util.grant_user_permission_to_repo(repo, user, 'repository.none')
198 self.app.get(route_path('download_file', fid=file_uid), status=404)
183 self.app.get(route_path('download_file', fid=file_uid), status=404)
199
184
200 def test_download_file_scoped_to_user(self, user_util, create_artifact_factory):
185 def test_download_file_scoped_to_user(self, user_util, create_artifact_factory):
201 user = self.log_user()
186 user = self.log_user()
202 user_id = user['user_id']
187 user_id = user['user_id']
203 content = 'HELLO MY NAME IS ARTIFACT !'
188 content = 'HELLO MY NAME IS ARTIFACT !'
204
189
205 artifact = create_artifact_factory(user_id, content)
190 artifact = create_artifact_factory(user_id, content)
206 # bind to user
191 # bind to user
207 user = user_util.create_user(password='qweqwe')
192 user = user_util.create_user(password='qweqwe')
208
193
209 artifact.scope_user_id = user.user_id
194 artifact.scope_user_id = user.user_id
210 Session().add(artifact)
195 Session().add(artifact)
211 Session().commit()
196 Session().commit()
212
197
213 # artifact creator doesn't have access since it's bind to another user
198 # artifact creator doesn't have access since it's bind to another user
214 file_uid = artifact.file_uid
199 file_uid = artifact.file_uid
215 self.app.get(route_path('download_file', fid=file_uid), status=404)
200 self.app.get(route_path('download_file', fid=file_uid), status=404)
216
201
217 # log-in to new user and test download again, should be ok since we're bind to this artifact
202 # log-in to new user and test download again, should be ok since we're bind to this artifact
218 self.log_user(user.username, 'qweqwe')
203 self.log_user(user.username, 'qweqwe')
219 response = self.app.get(route_path('download_file', fid=file_uid), status=200)
204 response = self.app.get(route_path('download_file', fid=file_uid), status=200)
220 assert response.text == content
205 assert response.text == content
221
206
222 def test_download_file_scoped_to_repo_with_bad_token(self, user_util, create_artifact_factory):
207 def test_download_file_scoped_to_repo_with_bad_token(self, user_util, create_artifact_factory):
223 user_id = User.get_first_super_admin().user_id
208 user_id = User.get_first_super_admin().user_id
224 content = 'HELLO MY NAME IS ARTIFACT !'
209 content = 'HELLO MY NAME IS ARTIFACT !'
225
210
226 artifact = create_artifact_factory(user_id, content)
211 artifact = create_artifact_factory(user_id, content)
227 # bind to repo
212 # bind to repo
228 repo = user_util.create_repo()
213 repo = user_util.create_repo()
229 repo_id = repo.repo_id
214 repo_id = repo.repo_id
230 artifact.scope_repo_id = repo_id
215 artifact.scope_repo_id = repo_id
231 Session().add(artifact)
216 Session().add(artifact)
232 Session().commit()
217 Session().commit()
233
218
234 file_uid = artifact.file_uid
219 file_uid = artifact.file_uid
235 self.app.get(route_path('download_file_by_token',
220 self.app.get(route_path('download_file_by_token',
236 _auth_token='bogus', fid=file_uid), status=302)
221 _auth_token='bogus', fid=file_uid), status=302)
237
222
238 def test_download_file_scoped_to_repo_with_token(self, user_util, create_artifact_factory):
223 def test_download_file_scoped_to_repo_with_token(self, user_util, create_artifact_factory):
239 user = User.get_first_super_admin()
224 user = User.get_first_super_admin()
240 AuthTokenModel().create(user, 'test artifact token',
225 AuthTokenModel().create(user, 'test artifact token',
241 role=AuthTokenModel.cls.ROLE_ARTIFACT_DOWNLOAD)
226 role=AuthTokenModel.cls.ROLE_ARTIFACT_DOWNLOAD)
242
227
243 user = User.get_first_super_admin()
228 user = User.get_first_super_admin()
244 artifact_token = user.artifact_token
229 artifact_token = user.artifact_token
245
230
246 user_id = User.get_first_super_admin().user_id
231 user_id = User.get_first_super_admin().user_id
247 content = 'HELLO MY NAME IS ARTIFACT !'
232 content = 'HELLO MY NAME IS ARTIFACT !'
248
233
249 artifact = create_artifact_factory(user_id, content)
234 artifact = create_artifact_factory(user_id, content)
250 # bind to repo
235 # bind to repo
251 repo = user_util.create_repo()
236 repo = user_util.create_repo()
252 repo_id = repo.repo_id
237 repo_id = repo.repo_id
253 artifact.scope_repo_id = repo_id
238 artifact.scope_repo_id = repo_id
254 Session().add(artifact)
239 Session().add(artifact)
255 Session().commit()
240 Session().commit()
256
241
257 file_uid = artifact.file_uid
242 file_uid = artifact.file_uid
258 response = self.app.get(
243 response = self.app.get(
259 route_path('download_file_by_token',
244 route_path('download_file_by_token',
260 _auth_token=artifact_token, fid=file_uid), status=200)
245 _auth_token=artifact_token, fid=file_uid), status=200)
261 assert response.text == content
246 assert response.text == content
@@ -1,389 +1,365 b''
1 # Copyright (C) 2010-2023 RhodeCode GmbH
1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 #
2 #
3 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
14 #
15 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19 import mock
19 import mock
20 import pytest
20 import pytest
21
21
22 from rhodecode.lib import helpers as h
22 from rhodecode.lib import helpers as h
23 from rhodecode.model.db import User, Gist
23 from rhodecode.model.db import User, Gist
24 from rhodecode.model.gist import GistModel
24 from rhodecode.model.gist import GistModel
25 from rhodecode.model.meta import Session
25 from rhodecode.model.meta import Session
26 from rhodecode.tests import (
26 from rhodecode.tests import (
27 TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS,
27 TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS,
28 TestController, assert_session_flash)
28 TestController, assert_session_flash)
29
29 from rhodecode.tests.routes import route_path
30
31 def route_path(name, params=None, **kwargs):
32 import urllib.parse
33 import urllib.error
34 from rhodecode.apps._base import ADMIN_PREFIX
35
36 base_url = {
37 'gists_show': ADMIN_PREFIX + '/gists',
38 'gists_new': ADMIN_PREFIX + '/gists/new',
39 'gists_create': ADMIN_PREFIX + '/gists/create',
40 'gist_show': ADMIN_PREFIX + '/gists/{gist_id}',
41 'gist_delete': ADMIN_PREFIX + '/gists/{gist_id}/delete',
42 'gist_edit': ADMIN_PREFIX + '/gists/{gist_id}/edit',
43 'gist_edit_check_revision': ADMIN_PREFIX + '/gists/{gist_id}/edit/check_revision',
44 'gist_update': ADMIN_PREFIX + '/gists/{gist_id}/update',
45 'gist_show_rev': ADMIN_PREFIX + '/gists/{gist_id}/rev/{revision}',
46 'gist_show_formatted': ADMIN_PREFIX + '/gists/{gist_id}/rev/{revision}/{format}',
47 'gist_show_formatted_path': ADMIN_PREFIX + '/gists/{gist_id}/rev/{revision}/{format}/{f_path}',
48
49 }[name].format(**kwargs)
50
51 if params:
52 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
53 return base_url
54
30
55
31
56 class GistUtility(object):
32 class GistUtility(object):
57
33
58 def __init__(self):
34 def __init__(self):
59 self._gist_ids = []
35 self._gist_ids = []
60
36
61 def __call__(
37 def __call__(
62 self, f_name: bytes, content: bytes = b'some gist', lifetime=-1,
38 self, f_name: bytes, content: bytes = b'some gist', lifetime=-1,
63 description='gist-desc', gist_type='public',
39 description='gist-desc', gist_type='public',
64 acl_level=Gist.GIST_PUBLIC, owner=TEST_USER_ADMIN_LOGIN):
40 acl_level=Gist.GIST_PUBLIC, owner=TEST_USER_ADMIN_LOGIN):
65 gist_mapping = {
41 gist_mapping = {
66 f_name: {'content': content}
42 f_name: {'content': content}
67 }
43 }
68 user = User.get_by_username(owner)
44 user = User.get_by_username(owner)
69 gist = GistModel().create(
45 gist = GistModel().create(
70 description, owner=user, gist_mapping=gist_mapping,
46 description, owner=user, gist_mapping=gist_mapping,
71 gist_type=gist_type, lifetime=lifetime, gist_acl_level=acl_level)
47 gist_type=gist_type, lifetime=lifetime, gist_acl_level=acl_level)
72 Session().commit()
48 Session().commit()
73 self._gist_ids.append(gist.gist_id)
49 self._gist_ids.append(gist.gist_id)
74 return gist
50 return gist
75
51
76 def cleanup(self):
52 def cleanup(self):
77 for gist_id in self._gist_ids:
53 for gist_id in self._gist_ids:
78 gist = Gist.get(gist_id)
54 gist = Gist.get(gist_id)
79 if gist:
55 if gist:
80 Session().delete(gist)
56 Session().delete(gist)
81
57
82 Session().commit()
58 Session().commit()
83
59
84
60
85 @pytest.fixture()
61 @pytest.fixture()
86 def create_gist(request):
62 def create_gist(request):
87 gist_utility = GistUtility()
63 gist_utility = GistUtility()
88 request.addfinalizer(gist_utility.cleanup)
64 request.addfinalizer(gist_utility.cleanup)
89 return gist_utility
65 return gist_utility
90
66
91
67
92 class TestGistsController(TestController):
68 class TestGistsController(TestController):
93
69
94 def test_index_empty(self, create_gist):
70 def test_index_empty(self, create_gist):
95 self.log_user()
71 self.log_user()
96 response = self.app.get(route_path('gists_show'))
72 response = self.app.get(route_path('gists_show'))
97 response.mustcontain('var gist_data = [];')
73 response.mustcontain('var gist_data = [];')
98
74
99 def test_index(self, create_gist):
75 def test_index(self, create_gist):
100 self.log_user()
76 self.log_user()
101 g1 = create_gist(b'gist1')
77 g1 = create_gist(b'gist1')
102 g2 = create_gist(b'gist2', lifetime=1400)
78 g2 = create_gist(b'gist2', lifetime=1400)
103 g3 = create_gist(b'gist3', description='gist3-desc')
79 g3 = create_gist(b'gist3', description='gist3-desc')
104 g4 = create_gist(b'gist4', gist_type='private').gist_access_id
80 g4 = create_gist(b'gist4', gist_type='private').gist_access_id
105 response = self.app.get(route_path('gists_show'))
81 response = self.app.get(route_path('gists_show'))
106
82
107 response.mustcontain(g1.gist_access_id)
83 response.mustcontain(g1.gist_access_id)
108 response.mustcontain(g2.gist_access_id)
84 response.mustcontain(g2.gist_access_id)
109 response.mustcontain(g3.gist_access_id)
85 response.mustcontain(g3.gist_access_id)
110 response.mustcontain('gist3-desc')
86 response.mustcontain('gist3-desc')
111 response.mustcontain(no=[g4])
87 response.mustcontain(no=[g4])
112
88
113 # Expiration information should be visible
89 # Expiration information should be visible
114 expires_tag = str(h.age_component(h.time_to_utcdatetime(g2.gist_expires)))
90 expires_tag = str(h.age_component(h.time_to_utcdatetime(g2.gist_expires)))
115 response.mustcontain(expires_tag.replace('"', '\\"'))
91 response.mustcontain(expires_tag.replace('"', '\\"'))
116
92
117 def test_index_private_gists(self, create_gist):
93 def test_index_private_gists(self, create_gist):
118 self.log_user()
94 self.log_user()
119 gist = create_gist(b'gist5', gist_type='private')
95 gist = create_gist(b'gist5', gist_type='private')
120 response = self.app.get(route_path('gists_show', params=dict(private=1)))
96 response = self.app.get(route_path('gists_show', params=dict(private=1)))
121
97
122 # and privates
98 # and privates
123 response.mustcontain(gist.gist_access_id)
99 response.mustcontain(gist.gist_access_id)
124
100
125 def test_index_show_all(self, create_gist):
101 def test_index_show_all(self, create_gist):
126 self.log_user()
102 self.log_user()
127 create_gist(b'gist1')
103 create_gist(b'gist1')
128 create_gist(b'gist2', lifetime=1400)
104 create_gist(b'gist2', lifetime=1400)
129 create_gist(b'gist3', description='gist3-desc')
105 create_gist(b'gist3', description='gist3-desc')
130 create_gist(b'gist4', gist_type='private')
106 create_gist(b'gist4', gist_type='private')
131
107
132 response = self.app.get(route_path('gists_show', params=dict(all=1)))
108 response = self.app.get(route_path('gists_show', params=dict(all=1)))
133
109
134 assert len(GistModel.get_all()) == 4
110 assert len(GistModel.get_all()) == 4
135 # and privates
111 # and privates
136 for gist in GistModel.get_all():
112 for gist in GistModel.get_all():
137 response.mustcontain(gist.gist_access_id)
113 response.mustcontain(gist.gist_access_id)
138
114
139 def test_index_show_all_hidden_from_regular(self, create_gist):
115 def test_index_show_all_hidden_from_regular(self, create_gist):
140 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
116 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
141 create_gist(b'gist2', gist_type='private')
117 create_gist(b'gist2', gist_type='private')
142 create_gist(b'gist3', gist_type='private')
118 create_gist(b'gist3', gist_type='private')
143 create_gist(b'gist4', gist_type='private')
119 create_gist(b'gist4', gist_type='private')
144
120
145 response = self.app.get(route_path('gists_show', params=dict(all=1)))
121 response = self.app.get(route_path('gists_show', params=dict(all=1)))
146
122
147 assert len(GistModel.get_all()) == 3
123 assert len(GistModel.get_all()) == 3
148 # since we don't have access to private in this view, we
124 # since we don't have access to private in this view, we
149 # should see nothing
125 # should see nothing
150 for gist in GistModel.get_all():
126 for gist in GistModel.get_all():
151 response.mustcontain(no=[gist.gist_access_id])
127 response.mustcontain(no=[gist.gist_access_id])
152
128
153 def test_create(self):
129 def test_create(self):
154 self.log_user()
130 self.log_user()
155 response = self.app.post(
131 response = self.app.post(
156 route_path('gists_create'),
132 route_path('gists_create'),
157 params={'lifetime': -1,
133 params={'lifetime': -1,
158 'content': 'gist test',
134 'content': 'gist test',
159 'filename': 'foo',
135 'filename': 'foo',
160 'gist_type': 'public',
136 'gist_type': 'public',
161 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
137 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
162 'csrf_token': self.csrf_token},
138 'csrf_token': self.csrf_token},
163 status=302)
139 status=302)
164 response = response.follow()
140 response = response.follow()
165 response.mustcontain('added file: foo')
141 response.mustcontain('added file: foo')
166 response.mustcontain('gist test')
142 response.mustcontain('gist test')
167
143
168 def test_create_with_path_with_dirs(self):
144 def test_create_with_path_with_dirs(self):
169 self.log_user()
145 self.log_user()
170 response = self.app.post(
146 response = self.app.post(
171 route_path('gists_create'),
147 route_path('gists_create'),
172 params={'lifetime': -1,
148 params={'lifetime': -1,
173 'content': 'gist test',
149 'content': 'gist test',
174 'filename': '/home/foo',
150 'filename': '/home/foo',
175 'gist_type': 'public',
151 'gist_type': 'public',
176 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
152 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
177 'csrf_token': self.csrf_token},
153 'csrf_token': self.csrf_token},
178 status=200)
154 status=200)
179 response.mustcontain('Filename /home/foo cannot be inside a directory')
155 response.mustcontain('Filename /home/foo cannot be inside a directory')
180
156
181 def test_access_expired_gist(self, create_gist):
157 def test_access_expired_gist(self, create_gist):
182 self.log_user()
158 self.log_user()
183 gist = create_gist(b'never-see-me')
159 gist = create_gist(b'never-see-me')
184 gist.gist_expires = 0 # 1970
160 gist.gist_expires = 0 # 1970
185 Session().add(gist)
161 Session().add(gist)
186 Session().commit()
162 Session().commit()
187
163
188 self.app.get(route_path('gist_show', gist_id=gist.gist_access_id),
164 self.app.get(route_path('gist_show', gist_id=gist.gist_access_id),
189 status=404)
165 status=404)
190
166
191 def test_create_private(self):
167 def test_create_private(self):
192 self.log_user()
168 self.log_user()
193 response = self.app.post(
169 response = self.app.post(
194 route_path('gists_create'),
170 route_path('gists_create'),
195 params={'lifetime': -1,
171 params={'lifetime': -1,
196 'content': 'private gist test',
172 'content': 'private gist test',
197 'filename': 'private-foo',
173 'filename': 'private-foo',
198 'gist_type': 'private',
174 'gist_type': 'private',
199 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
175 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
200 'csrf_token': self.csrf_token},
176 'csrf_token': self.csrf_token},
201 status=302)
177 status=302)
202 response = response.follow()
178 response = response.follow()
203 response.mustcontain('added file: private-foo<')
179 response.mustcontain('added file: private-foo<')
204 response.mustcontain('private gist test')
180 response.mustcontain('private gist test')
205 response.mustcontain('Private Gist')
181 response.mustcontain('Private Gist')
206 # Make sure private gists are not indexed by robots
182 # Make sure private gists are not indexed by robots
207 response.mustcontain(
183 response.mustcontain(
208 '<meta name="robots" content="noindex, nofollow">')
184 '<meta name="robots" content="noindex, nofollow">')
209
185
210 def test_create_private_acl_private(self):
186 def test_create_private_acl_private(self):
211 self.log_user()
187 self.log_user()
212 response = self.app.post(
188 response = self.app.post(
213 route_path('gists_create'),
189 route_path('gists_create'),
214 params={'lifetime': -1,
190 params={'lifetime': -1,
215 'content': 'private gist test',
191 'content': 'private gist test',
216 'filename': 'private-foo',
192 'filename': 'private-foo',
217 'gist_type': 'private',
193 'gist_type': 'private',
218 'gist_acl_level': Gist.ACL_LEVEL_PRIVATE,
194 'gist_acl_level': Gist.ACL_LEVEL_PRIVATE,
219 'csrf_token': self.csrf_token},
195 'csrf_token': self.csrf_token},
220 status=302)
196 status=302)
221 response = response.follow()
197 response = response.follow()
222 response.mustcontain('added file: private-foo<')
198 response.mustcontain('added file: private-foo<')
223 response.mustcontain('private gist test')
199 response.mustcontain('private gist test')
224 response.mustcontain('Private Gist')
200 response.mustcontain('Private Gist')
225 # Make sure private gists are not indexed by robots
201 # Make sure private gists are not indexed by robots
226 response.mustcontain(
202 response.mustcontain(
227 '<meta name="robots" content="noindex, nofollow">')
203 '<meta name="robots" content="noindex, nofollow">')
228
204
229 def test_create_with_description(self):
205 def test_create_with_description(self):
230 self.log_user()
206 self.log_user()
231 response = self.app.post(
207 response = self.app.post(
232 route_path('gists_create'),
208 route_path('gists_create'),
233 params={'lifetime': -1,
209 params={'lifetime': -1,
234 'content': 'gist test',
210 'content': 'gist test',
235 'filename': 'foo-desc',
211 'filename': 'foo-desc',
236 'description': 'gist-desc',
212 'description': 'gist-desc',
237 'gist_type': 'public',
213 'gist_type': 'public',
238 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
214 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
239 'csrf_token': self.csrf_token},
215 'csrf_token': self.csrf_token},
240 status=302)
216 status=302)
241 response = response.follow()
217 response = response.follow()
242 response.mustcontain('added file: foo-desc')
218 response.mustcontain('added file: foo-desc')
243 response.mustcontain('gist test')
219 response.mustcontain('gist test')
244 response.mustcontain('gist-desc')
220 response.mustcontain('gist-desc')
245
221
246 def test_create_public_with_anonymous_access(self):
222 def test_create_public_with_anonymous_access(self):
247 self.log_user()
223 self.log_user()
248 params = {
224 params = {
249 'lifetime': -1,
225 'lifetime': -1,
250 'content': 'gist test',
226 'content': 'gist test',
251 'filename': 'foo-desc',
227 'filename': 'foo-desc',
252 'description': 'gist-desc',
228 'description': 'gist-desc',
253 'gist_type': 'public',
229 'gist_type': 'public',
254 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
230 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
255 'csrf_token': self.csrf_token
231 'csrf_token': self.csrf_token
256 }
232 }
257 response = self.app.post(
233 response = self.app.post(
258 route_path('gists_create'), params=params, status=302)
234 route_path('gists_create'), params=params, status=302)
259 self.logout_user()
235 self.logout_user()
260 response = response.follow()
236 response = response.follow()
261 response.mustcontain('added file: foo-desc')
237 response.mustcontain('added file: foo-desc')
262 response.mustcontain('gist test')
238 response.mustcontain('gist test')
263 response.mustcontain('gist-desc')
239 response.mustcontain('gist-desc')
264
240
265 def test_new(self):
241 def test_new(self):
266 self.log_user()
242 self.log_user()
267 self.app.get(route_path('gists_new'))
243 self.app.get(route_path('gists_new'))
268
244
269 def test_delete(self, create_gist):
245 def test_delete(self, create_gist):
270 self.log_user()
246 self.log_user()
271 gist = create_gist(b'delete-me')
247 gist = create_gist(b'delete-me')
272 response = self.app.post(
248 response = self.app.post(
273 route_path('gist_delete', gist_id=gist.gist_id),
249 route_path('gist_delete', gist_id=gist.gist_id),
274 params={'csrf_token': self.csrf_token})
250 params={'csrf_token': self.csrf_token})
275 assert_session_flash(response, 'Deleted gist %s' % gist.gist_id)
251 assert_session_flash(response, 'Deleted gist %s' % gist.gist_id)
276
252
277 def test_delete_normal_user_his_gist(self, create_gist):
253 def test_delete_normal_user_his_gist(self, create_gist):
278 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
254 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
279 gist = create_gist(b'delete-me', owner=TEST_USER_REGULAR_LOGIN)
255 gist = create_gist(b'delete-me', owner=TEST_USER_REGULAR_LOGIN)
280
256
281 response = self.app.post(
257 response = self.app.post(
282 route_path('gist_delete', gist_id=gist.gist_id),
258 route_path('gist_delete', gist_id=gist.gist_id),
283 params={'csrf_token': self.csrf_token})
259 params={'csrf_token': self.csrf_token})
284 assert_session_flash(response, 'Deleted gist %s' % gist.gist_id)
260 assert_session_flash(response, 'Deleted gist %s' % gist.gist_id)
285
261
286 def test_delete_normal_user_not_his_own_gist(self, create_gist):
262 def test_delete_normal_user_not_his_own_gist(self, create_gist):
287 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
263 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
288 gist = create_gist(b'delete-me-2')
264 gist = create_gist(b'delete-me-2')
289
265
290 self.app.post(
266 self.app.post(
291 route_path('gist_delete', gist_id=gist.gist_id),
267 route_path('gist_delete', gist_id=gist.gist_id),
292 params={'csrf_token': self.csrf_token}, status=404)
268 params={'csrf_token': self.csrf_token}, status=404)
293
269
294 def test_show(self, create_gist):
270 def test_show(self, create_gist):
295 gist = create_gist(b'gist-show-me')
271 gist = create_gist(b'gist-show-me')
296 response = self.app.get(route_path('gist_show', gist_id=gist.gist_access_id))
272 response = self.app.get(route_path('gist_show', gist_id=gist.gist_access_id))
297
273
298 response.mustcontain('added file: gist-show-me<')
274 response.mustcontain('added file: gist-show-me<')
299
275
300 assert_response = response.assert_response()
276 assert_response = response.assert_response()
301 assert_response.element_equals_to(
277 assert_response.element_equals_to(
302 'div.rc-user span.user',
278 'div.rc-user span.user',
303 '<a href="/_profiles/test_admin">test_admin</a>')
279 '<a href="/_profiles/test_admin">test_admin</a>')
304
280
305 response.mustcontain('gist-desc')
281 response.mustcontain('gist-desc')
306
282
307 def test_show_without_hg(self, create_gist):
283 def test_show_without_hg(self, create_gist):
308 with mock.patch(
284 with mock.patch(
309 'rhodecode.lib.vcs.settings.ALIASES', ['git']):
285 'rhodecode.lib.vcs.settings.ALIASES', ['git']):
310 gist = create_gist(b'gist-show-me-again')
286 gist = create_gist(b'gist-show-me-again')
311 self.app.get(
287 self.app.get(
312 route_path('gist_show', gist_id=gist.gist_access_id), status=200)
288 route_path('gist_show', gist_id=gist.gist_access_id), status=200)
313
289
314 def test_show_acl_private(self, create_gist):
290 def test_show_acl_private(self, create_gist):
315 gist = create_gist(b'gist-show-me-only-when-im-logged-in',
291 gist = create_gist(b'gist-show-me-only-when-im-logged-in',
316 acl_level=Gist.ACL_LEVEL_PRIVATE)
292 acl_level=Gist.ACL_LEVEL_PRIVATE)
317 self.app.get(
293 self.app.get(
318 route_path('gist_show', gist_id=gist.gist_access_id), status=404)
294 route_path('gist_show', gist_id=gist.gist_access_id), status=404)
319
295
320 # now we log-in we should see thi gist
296 # now we log-in we should see thi gist
321 self.log_user()
297 self.log_user()
322 response = self.app.get(
298 response = self.app.get(
323 route_path('gist_show', gist_id=gist.gist_access_id))
299 route_path('gist_show', gist_id=gist.gist_access_id))
324 response.mustcontain('added file: gist-show-me-only-when-im-logged-in')
300 response.mustcontain('added file: gist-show-me-only-when-im-logged-in')
325
301
326 assert_response = response.assert_response()
302 assert_response = response.assert_response()
327 assert_response.element_equals_to(
303 assert_response.element_equals_to(
328 'div.rc-user span.user',
304 'div.rc-user span.user',
329 '<a href="/_profiles/test_admin">test_admin</a>')
305 '<a href="/_profiles/test_admin">test_admin</a>')
330 response.mustcontain('gist-desc')
306 response.mustcontain('gist-desc')
331
307
332 def test_show_as_raw(self, create_gist):
308 def test_show_as_raw(self, create_gist):
333 gist = create_gist(b'gist-show-me', content=b'GIST CONTENT')
309 gist = create_gist(b'gist-show-me', content=b'GIST CONTENT')
334 response = self.app.get(
310 response = self.app.get(
335 route_path('gist_show_formatted',
311 route_path('gist_show_formatted',
336 gist_id=gist.gist_access_id, revision='tip',
312 gist_id=gist.gist_access_id, revision='tip',
337 format='raw'))
313 format='raw'))
338 assert response.text == 'GIST CONTENT'
314 assert response.text == 'GIST CONTENT'
339
315
340 def test_show_as_raw_individual_file(self, create_gist):
316 def test_show_as_raw_individual_file(self, create_gist):
341 gist = create_gist(b'gist-show-me-raw', content=b'GIST BODY')
317 gist = create_gist(b'gist-show-me-raw', content=b'GIST BODY')
342 response = self.app.get(
318 response = self.app.get(
343 route_path('gist_show_formatted_path',
319 route_path('gist_show_formatted_path',
344 gist_id=gist.gist_access_id, format='raw',
320 gist_id=gist.gist_access_id, format='raw',
345 revision='tip', f_path='gist-show-me-raw'))
321 revision='tip', f_path='gist-show-me-raw'))
346 assert response.text == 'GIST BODY'
322 assert response.text == 'GIST BODY'
347
323
348 def test_edit_page(self, create_gist):
324 def test_edit_page(self, create_gist):
349 self.log_user()
325 self.log_user()
350 gist = create_gist(b'gist-for-edit', content=b'GIST EDIT BODY')
326 gist = create_gist(b'gist-for-edit', content=b'GIST EDIT BODY')
351 response = self.app.get(route_path('gist_edit', gist_id=gist.gist_access_id))
327 response = self.app.get(route_path('gist_edit', gist_id=gist.gist_access_id))
352 response.mustcontain('GIST EDIT BODY')
328 response.mustcontain('GIST EDIT BODY')
353
329
354 def test_edit_page_non_logged_user(self, create_gist):
330 def test_edit_page_non_logged_user(self, create_gist):
355 gist = create_gist(b'gist-for-edit', content=b'GIST EDIT BODY')
331 gist = create_gist(b'gist-for-edit', content=b'GIST EDIT BODY')
356 self.app.get(route_path('gist_edit', gist_id=gist.gist_access_id),
332 self.app.get(route_path('gist_edit', gist_id=gist.gist_access_id),
357 status=302)
333 status=302)
358
334
359 def test_edit_normal_user_his_gist(self, create_gist):
335 def test_edit_normal_user_his_gist(self, create_gist):
360 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
336 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
361 gist = create_gist(b'gist-for-edit', owner=TEST_USER_REGULAR_LOGIN)
337 gist = create_gist(b'gist-for-edit', owner=TEST_USER_REGULAR_LOGIN)
362 self.app.get(route_path('gist_edit', gist_id=gist.gist_access_id,
338 self.app.get(route_path('gist_edit', gist_id=gist.gist_access_id,
363 status=200))
339 status=200))
364
340
365 def test_edit_normal_user_not_his_own_gist(self, create_gist):
341 def test_edit_normal_user_not_his_own_gist(self, create_gist):
366 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
342 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
367 gist = create_gist(b'delete-me')
343 gist = create_gist(b'delete-me')
368 self.app.get(route_path('gist_edit', gist_id=gist.gist_access_id),
344 self.app.get(route_path('gist_edit', gist_id=gist.gist_access_id),
369 status=404)
345 status=404)
370
346
371 def test_user_first_name_is_escaped(self, user_util, create_gist):
347 def test_user_first_name_is_escaped(self, user_util, create_gist):
372 xss_atack_string = '"><script>alert(\'First Name\')</script>'
348 xss_atack_string = '"><script>alert(\'First Name\')</script>'
373 xss_escaped_string = h.html_escape(h.escape(xss_atack_string))
349 xss_escaped_string = h.html_escape(h.escape(xss_atack_string))
374 password = 'test'
350 password = 'test'
375 user = user_util.create_user(
351 user = user_util.create_user(
376 firstname=xss_atack_string, password=password)
352 firstname=xss_atack_string, password=password)
377 create_gist(b'gist', gist_type='public', owner=user.username)
353 create_gist(b'gist', gist_type='public', owner=user.username)
378 response = self.app.get(route_path('gists_show'))
354 response = self.app.get(route_path('gists_show'))
379 response.mustcontain(xss_escaped_string)
355 response.mustcontain(xss_escaped_string)
380
356
381 def test_user_last_name_is_escaped(self, user_util, create_gist):
357 def test_user_last_name_is_escaped(self, user_util, create_gist):
382 xss_atack_string = '"><script>alert(\'Last Name\')</script>'
358 xss_atack_string = '"><script>alert(\'Last Name\')</script>'
383 xss_escaped_string = h.html_escape(h.escape(xss_atack_string))
359 xss_escaped_string = h.html_escape(h.escape(xss_atack_string))
384 password = 'test'
360 password = 'test'
385 user = user_util.create_user(
361 user = user_util.create_user(
386 lastname=xss_atack_string, password=password)
362 lastname=xss_atack_string, password=password)
387 create_gist(b'gist', gist_type='public', owner=user.username)
363 create_gist(b'gist', gist_type='public', owner=user.username)
388 response = self.app.get(route_path('gists_show'))
364 response = self.app.get(route_path('gists_show'))
389 response.mustcontain(xss_escaped_string)
365 response.mustcontain(xss_escaped_string)
@@ -1,179 +1,167 b''
1 # Copyright (C) 2016-2023 RhodeCode GmbH
1 # Copyright (C) 2016-2023 RhodeCode GmbH
2 #
2 #
3 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
14 #
15 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19 import pytest
19 import pytest
20
20
21 from . import assert_and_get_main_filter_content
21 from . import assert_and_get_main_filter_content
22 from rhodecode.tests import TestController, TEST_USER_ADMIN_LOGIN
23 from rhodecode.tests.fixture import Fixture
24
22
25 from rhodecode.lib.utils import map_groups
23 from rhodecode.lib.utils import map_groups
26 from rhodecode.lib.ext_json import json
24 from rhodecode.lib.ext_json import json
27 from rhodecode.model.repo import RepoModel
25 from rhodecode.model.repo import RepoModel
28 from rhodecode.model.repo_group import RepoGroupModel
26 from rhodecode.model.repo_group import RepoGroupModel
29 from rhodecode.model.db import Session, Repository, RepoGroup
27 from rhodecode.model.db import Session, Repository, RepoGroup
30
28
31 fixture = Fixture()
29 from rhodecode.tests import TestController, TEST_USER_ADMIN_LOGIN
32
30 from rhodecode.tests.fixture import Fixture
33
31 from rhodecode.tests.routes import route_path
34 def route_path(name, params=None, **kwargs):
35 import urllib.request
36 import urllib.parse
37 import urllib.error
38
32
39 base_url = {
33 fixture = Fixture()
40 'goto_switcher_data': '/_goto_data',
41 }[name].format(**kwargs)
42
43 if params:
44 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
45 return base_url
46
34
47
35
48 class TestGotoSwitcherData(TestController):
36 class TestGotoSwitcherData(TestController):
49
37
50 required_repos_with_groups = [
38 required_repos_with_groups = [
51 'abc',
39 'abc',
52 'abc-fork',
40 'abc-fork',
53 'forks/abcd',
41 'forks/abcd',
54 'abcd',
42 'abcd',
55 'abcde',
43 'abcde',
56 'a/abc',
44 'a/abc',
57 'aa/abc',
45 'aa/abc',
58 'aaa/abc',
46 'aaa/abc',
59 'aaaa/abc',
47 'aaaa/abc',
60 'repos_abc/aaa/abc',
48 'repos_abc/aaa/abc',
61 'abc_repos/abc',
49 'abc_repos/abc',
62 'abc_repos/abcd',
50 'abc_repos/abcd',
63 'xxx/xyz',
51 'xxx/xyz',
64 'forked-abc/a/abc'
52 'forked-abc/a/abc'
65 ]
53 ]
66
54
67 @pytest.fixture(autouse=True, scope='class')
55 @pytest.fixture(autouse=True, scope='class')
68 def prepare(self, request, baseapp):
56 def prepare(self, request, baseapp):
69 for repo_and_group in self.required_repos_with_groups:
57 for repo_and_group in self.required_repos_with_groups:
70 # create structure of groups and return the last group
58 # create structure of groups and return the last group
71
59
72 repo_group = map_groups(repo_and_group)
60 repo_group = map_groups(repo_and_group)
73
61
74 RepoModel()._create_repo(
62 RepoModel()._create_repo(
75 repo_and_group, 'hg', 'test-ac', TEST_USER_ADMIN_LOGIN,
63 repo_and_group, 'hg', 'test-ac', TEST_USER_ADMIN_LOGIN,
76 repo_group=getattr(repo_group, 'group_id', None))
64 repo_group=getattr(repo_group, 'group_id', None))
77
65
78 Session().commit()
66 Session().commit()
79
67
80 request.addfinalizer(self.cleanup)
68 request.addfinalizer(self.cleanup)
81
69
82 def cleanup(self):
70 def cleanup(self):
83 # first delete all repos
71 # first delete all repos
84 for repo_and_groups in self.required_repos_with_groups:
72 for repo_and_groups in self.required_repos_with_groups:
85 repo = Repository.get_by_repo_name(repo_and_groups)
73 repo = Repository.get_by_repo_name(repo_and_groups)
86 if repo:
74 if repo:
87 RepoModel().delete(repo)
75 RepoModel().delete(repo)
88 Session().commit()
76 Session().commit()
89
77
90 # then delete all empty groups
78 # then delete all empty groups
91 for repo_and_groups in self.required_repos_with_groups:
79 for repo_and_groups in self.required_repos_with_groups:
92 if '/' in repo_and_groups:
80 if '/' in repo_and_groups:
93 r_group = repo_and_groups.rsplit('/', 1)[0]
81 r_group = repo_and_groups.rsplit('/', 1)[0]
94 repo_group = RepoGroup.get_by_group_name(r_group)
82 repo_group = RepoGroup.get_by_group_name(r_group)
95 if not repo_group:
83 if not repo_group:
96 continue
84 continue
97 parents = repo_group.parents
85 parents = repo_group.parents
98 RepoGroupModel().delete(repo_group, force_delete=True)
86 RepoGroupModel().delete(repo_group, force_delete=True)
99 Session().commit()
87 Session().commit()
100
88
101 for el in reversed(parents):
89 for el in reversed(parents):
102 RepoGroupModel().delete(el, force_delete=True)
90 RepoGroupModel().delete(el, force_delete=True)
103 Session().commit()
91 Session().commit()
104
92
105 def test_empty_query(self, xhr_header):
93 def test_empty_query(self, xhr_header):
106 self.log_user()
94 self.log_user()
107
95
108 response = self.app.get(
96 response = self.app.get(
109 route_path('goto_switcher_data'),
97 route_path('goto_switcher_data'),
110 extra_environ=xhr_header, status=200)
98 extra_environ=xhr_header, status=200)
111 result = json.loads(response.body)['suggestions']
99 result = json.loads(response.body)['suggestions']
112
100
113 assert result == []
101 assert result == []
114
102
115 def test_returns_list_of_repos_and_groups_filtered(self, xhr_header):
103 def test_returns_list_of_repos_and_groups_filtered(self, xhr_header):
116 self.log_user()
104 self.log_user()
117
105
118 response = self.app.get(
106 response = self.app.get(
119 route_path('goto_switcher_data'),
107 route_path('goto_switcher_data'),
120 params={'query': 'abc'},
108 params={'query': 'abc'},
121 extra_environ=xhr_header, status=200)
109 extra_environ=xhr_header, status=200)
122 result = json.loads(response.body)['suggestions']
110 result = json.loads(response.body)['suggestions']
123
111
124 repos, groups, users, commits = assert_and_get_main_filter_content(result)
112 repos, groups, users, commits = assert_and_get_main_filter_content(result)
125
113
126 assert len(repos) == 13
114 assert len(repos) == 13
127 assert len(groups) == 5
115 assert len(groups) == 5
128 assert len(users) == 0
116 assert len(users) == 0
129 assert len(commits) == 0
117 assert len(commits) == 0
130
118
131 def test_returns_list_of_users_filtered(self, xhr_header):
119 def test_returns_list_of_users_filtered(self, xhr_header):
132 self.log_user()
120 self.log_user()
133
121
134 response = self.app.get(
122 response = self.app.get(
135 route_path('goto_switcher_data'),
123 route_path('goto_switcher_data'),
136 params={'query': 'user:admin'},
124 params={'query': 'user:admin'},
137 extra_environ=xhr_header, status=200)
125 extra_environ=xhr_header, status=200)
138 result = json.loads(response.body)['suggestions']
126 result = json.loads(response.body)['suggestions']
139
127
140 repos, groups, users, commits = assert_and_get_main_filter_content(result)
128 repos, groups, users, commits = assert_and_get_main_filter_content(result)
141
129
142 assert len(repos) == 0
130 assert len(repos) == 0
143 assert len(groups) == 0
131 assert len(groups) == 0
144 assert len(users) == 1
132 assert len(users) == 1
145 assert len(commits) == 0
133 assert len(commits) == 0
146
134
147 def test_returns_list_of_commits_filtered(self, xhr_header):
135 def test_returns_list_of_commits_filtered(self, xhr_header):
148 self.log_user()
136 self.log_user()
149
137
150 response = self.app.get(
138 response = self.app.get(
151 route_path('goto_switcher_data'),
139 route_path('goto_switcher_data'),
152 params={'query': 'commit:e8'},
140 params={'query': 'commit:e8'},
153 extra_environ=xhr_header, status=200)
141 extra_environ=xhr_header, status=200)
154 result = json.loads(response.body)['suggestions']
142 result = json.loads(response.body)['suggestions']
155
143
156 repos, groups, users, commits = assert_and_get_main_filter_content(result)
144 repos, groups, users, commits = assert_and_get_main_filter_content(result)
157
145
158 assert len(repos) == 0
146 assert len(repos) == 0
159 assert len(groups) == 0
147 assert len(groups) == 0
160 assert len(users) == 0
148 assert len(users) == 0
161 assert len(commits) == 5
149 assert len(commits) == 5
162
150
163 def test_returns_list_of_properly_sorted_and_filtered(self, xhr_header):
151 def test_returns_list_of_properly_sorted_and_filtered(self, xhr_header):
164 self.log_user()
152 self.log_user()
165
153
166 response = self.app.get(
154 response = self.app.get(
167 route_path('goto_switcher_data'),
155 route_path('goto_switcher_data'),
168 params={'query': 'abc'},
156 params={'query': 'abc'},
169 extra_environ=xhr_header, status=200)
157 extra_environ=xhr_header, status=200)
170 result = json.loads(response.body)['suggestions']
158 result = json.loads(response.body)['suggestions']
171
159
172 repos, groups, users, commits = assert_and_get_main_filter_content(result)
160 repos, groups, users, commits = assert_and_get_main_filter_content(result)
173
161
174 test_repos = [x['value_display'] for x in repos[:4]]
162 test_repos = [x['value_display'] for x in repos[:4]]
175 assert ['abc', 'abcd', 'a/abc', 'abcde'] == test_repos
163 assert ['abc', 'abcd', 'a/abc', 'abcde'] == test_repos
176
164
177 test_groups = [x['value_display'] for x in groups[:4]]
165 test_groups = [x['value_display'] for x in groups[:4]]
178 assert ['abc_repos', 'repos_abc',
166 assert ['abc_repos', 'repos_abc',
179 'forked-abc', 'forked-abc/a'] == test_groups
167 'forked-abc', 'forked-abc/a'] == test_groups
@@ -1,95 +1,83 b''
1 # Copyright (C) 2016-2023 RhodeCode GmbH
1 # Copyright (C) 2016-2023 RhodeCode GmbH
2 #
2 #
3 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
14 #
15 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19 from . import assert_and_get_repo_list_content
19 from . import assert_and_get_repo_list_content
20 from rhodecode.tests import TestController
20
21 from rhodecode.tests.fixture import Fixture
22 from rhodecode.model.db import Repository
21 from rhodecode.model.db import Repository
23 from rhodecode.lib.ext_json import json
22 from rhodecode.lib.ext_json import json
24
23
24 from rhodecode.tests import TestController
25 from rhodecode.tests.fixture import Fixture
26 from rhodecode.tests.routes import route_path
25
27
26 fixture = Fixture()
28 fixture = Fixture()
27
29
28
30
29 def route_path(name, params=None, **kwargs):
30 import urllib.request
31 import urllib.parse
32 import urllib.error
33
34 base_url = {
35 'repo_list_data': '/_repos',
36 }[name].format(**kwargs)
37
38 if params:
39 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
40 return base_url
41
42
43 class TestRepoListData(TestController):
31 class TestRepoListData(TestController):
44
32
45 def test_returns_list_of_repos_and_groups(self, xhr_header):
33 def test_returns_list_of_repos_and_groups(self, xhr_header):
46 self.log_user()
34 self.log_user()
47
35
48 response = self.app.get(
36 response = self.app.get(
49 route_path('repo_list_data'),
37 route_path('repo_list_data'),
50 extra_environ=xhr_header, status=200)
38 extra_environ=xhr_header, status=200)
51 result = json.loads(response.body)['results']
39 result = json.loads(response.body)['results']
52
40
53 repos = assert_and_get_repo_list_content(result)
41 repos = assert_and_get_repo_list_content(result)
54
42
55 assert len(repos) == len(Repository.get_all())
43 assert len(repos) == len(Repository.get_all())
56
44
57 def test_returns_list_of_repos_and_groups_filtered(self, xhr_header):
45 def test_returns_list_of_repos_and_groups_filtered(self, xhr_header):
58 self.log_user()
46 self.log_user()
59
47
60 response = self.app.get(
48 response = self.app.get(
61 route_path('repo_list_data'),
49 route_path('repo_list_data'),
62 params={'query': 'vcs_test_git'},
50 params={'query': 'vcs_test_git'},
63 extra_environ=xhr_header, status=200)
51 extra_environ=xhr_header, status=200)
64 result = json.loads(response.body)['results']
52 result = json.loads(response.body)['results']
65
53
66 repos = assert_and_get_repo_list_content(result)
54 repos = assert_and_get_repo_list_content(result)
67
55
68 assert len(repos) == len(Repository.query().filter(
56 assert len(repos) == len(Repository.query().filter(
69 Repository.repo_name.ilike('%vcs_test_git%')).all())
57 Repository.repo_name.ilike('%vcs_test_git%')).all())
70
58
71 def test_returns_list_of_repos_and_groups_filtered_with_type(self, xhr_header):
59 def test_returns_list_of_repos_and_groups_filtered_with_type(self, xhr_header):
72 self.log_user()
60 self.log_user()
73
61
74 response = self.app.get(
62 response = self.app.get(
75 route_path('repo_list_data'),
63 route_path('repo_list_data'),
76 params={'query': 'vcs_test_git', 'repo_type': 'git'},
64 params={'query': 'vcs_test_git', 'repo_type': 'git'},
77 extra_environ=xhr_header, status=200)
65 extra_environ=xhr_header, status=200)
78 result = json.loads(response.body)['results']
66 result = json.loads(response.body)['results']
79
67
80 repos = assert_and_get_repo_list_content(result)
68 repos = assert_and_get_repo_list_content(result)
81
69
82 assert len(repos) == len(Repository.query().filter(
70 assert len(repos) == len(Repository.query().filter(
83 Repository.repo_name.ilike('%vcs_test_git%')).all())
71 Repository.repo_name.ilike('%vcs_test_git%')).all())
84
72
85 def test_returns_list_of_repos_non_ascii_query(self, xhr_header):
73 def test_returns_list_of_repos_non_ascii_query(self, xhr_header):
86 self.log_user()
74 self.log_user()
87 response = self.app.get(
75 response = self.app.get(
88 route_path('repo_list_data'),
76 route_path('repo_list_data'),
89 params={'query': 'ć_vcs_test_ą', 'repo_type': 'git'},
77 params={'query': 'ć_vcs_test_ą', 'repo_type': 'git'},
90 extra_environ=xhr_header, status=200)
78 extra_environ=xhr_header, status=200)
91 result = json.loads(response.body)['results']
79 result = json.loads(response.body)['results']
92
80
93 repos = assert_and_get_repo_list_content(result)
81 repos = assert_and_get_repo_list_content(result)
94
82
95 assert len(repos) == 0
83 assert len(repos) == 0
@@ -1,110 +1,97 b''
1 # Copyright (C) 2016-2023 RhodeCode GmbH
1 # Copyright (C) 2016-2023 RhodeCode GmbH
2 #
2 #
3 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
14 #
15 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 import pytest
18 import pytest
19
19
20 from rhodecode.lib.ext_json import json
21
20 from rhodecode.tests import TestController
22 from rhodecode.tests import TestController
21 from rhodecode.tests.fixture import Fixture
23 from rhodecode.tests.fixture import Fixture
22 from rhodecode.lib.ext_json import json
24 from rhodecode.tests.routes import route_path
23
25
24 fixture = Fixture()
26 fixture = Fixture()
25
27
26
28
27 def route_path(name, params=None, **kwargs):
28 import urllib.request
29 import urllib.parse
30 import urllib.error
31
32 base_url = {
33 'user_autocomplete_data': '/_users',
34 'user_group_autocomplete_data': '/_user_groups'
35 }[name].format(**kwargs)
36
37 if params:
38 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
39 return base_url
40
41
42 class TestUserAutocompleteData(TestController):
29 class TestUserAutocompleteData(TestController):
43
30
44 def test_returns_list_of_users(self, user_util, xhr_header):
31 def test_returns_list_of_users(self, user_util, xhr_header):
45 self.log_user()
32 self.log_user()
46 user = user_util.create_user(active=True)
33 user = user_util.create_user(active=True)
47 user_name = user.username
34 user_name = user.username
48 response = self.app.get(
35 response = self.app.get(
49 route_path('user_autocomplete_data'),
36 route_path('user_autocomplete_data'),
50 extra_environ=xhr_header, status=200)
37 extra_environ=xhr_header, status=200)
51 result = json.loads(response.body)
38 result = json.loads(response.body)
52 values = [suggestion['value'] for suggestion in result['suggestions']]
39 values = [suggestion['value'] for suggestion in result['suggestions']]
53 assert user_name in values
40 assert user_name in values
54
41
55 def test_returns_inactive_users_when_active_flag_sent(
42 def test_returns_inactive_users_when_active_flag_sent(
56 self, user_util, xhr_header):
43 self, user_util, xhr_header):
57 self.log_user()
44 self.log_user()
58 user = user_util.create_user(active=False)
45 user = user_util.create_user(active=False)
59 user_name = user.username
46 user_name = user.username
60
47
61 response = self.app.get(
48 response = self.app.get(
62 route_path('user_autocomplete_data',
49 route_path('user_autocomplete_data',
63 params=dict(user_groups='true', active='0')),
50 params=dict(user_groups='true', active='0')),
64 extra_environ=xhr_header, status=200)
51 extra_environ=xhr_header, status=200)
65 result = json.loads(response.body)
52 result = json.loads(response.body)
66 values = [suggestion['value'] for suggestion in result['suggestions']]
53 values = [suggestion['value'] for suggestion in result['suggestions']]
67 assert user_name in values
54 assert user_name in values
68
55
69 response = self.app.get(
56 response = self.app.get(
70 route_path('user_autocomplete_data',
57 route_path('user_autocomplete_data',
71 params=dict(user_groups='true', active='1')),
58 params=dict(user_groups='true', active='1')),
72 extra_environ=xhr_header, status=200)
59 extra_environ=xhr_header, status=200)
73 result = json.loads(response.body)
60 result = json.loads(response.body)
74 values = [suggestion['value'] for suggestion in result['suggestions']]
61 values = [suggestion['value'] for suggestion in result['suggestions']]
75 assert user_name not in values
62 assert user_name not in values
76
63
77 def test_returns_groups_when_user_groups_flag_sent(
64 def test_returns_groups_when_user_groups_flag_sent(
78 self, user_util, xhr_header):
65 self, user_util, xhr_header):
79 self.log_user()
66 self.log_user()
80 group = user_util.create_user_group(user_groups_active=True)
67 group = user_util.create_user_group(user_groups_active=True)
81 group_name = group.users_group_name
68 group_name = group.users_group_name
82 response = self.app.get(
69 response = self.app.get(
83 route_path('user_autocomplete_data',
70 route_path('user_autocomplete_data',
84 params=dict(user_groups='true')),
71 params=dict(user_groups='true')),
85 extra_environ=xhr_header, status=200)
72 extra_environ=xhr_header, status=200)
86 result = json.loads(response.body)
73 result = json.loads(response.body)
87 values = [suggestion['value'] for suggestion in result['suggestions']]
74 values = [suggestion['value'] for suggestion in result['suggestions']]
88 assert group_name in values
75 assert group_name in values
89
76
90 @pytest.mark.parametrize('query, count', [
77 @pytest.mark.parametrize('query, count', [
91 ('hello1', 0),
78 ('hello1', 0),
92 ('dev', 2),
79 ('dev', 2),
93 ])
80 ])
94 def test_result_is_limited_when_query_is_sent(self, user_util, xhr_header,
81 def test_result_is_limited_when_query_is_sent(self, user_util, xhr_header,
95 query, count):
82 query, count):
96 self.log_user()
83 self.log_user()
97
84
98 user_util._test_name = 'dev-test'
85 user_util._test_name = 'dev-test'
99 user_util.create_user()
86 user_util.create_user()
100
87
101 user_util._test_name = 'dev-group-test'
88 user_util._test_name = 'dev-group-test'
102 user_util.create_user_group()
89 user_util.create_user_group()
103
90
104 response = self.app.get(
91 response = self.app.get(
105 route_path('user_autocomplete_data',
92 route_path('user_autocomplete_data',
106 params=dict(user_groups='true', query=query)),
93 params=dict(user_groups='true', query=query)),
107 extra_environ=xhr_header, status=200)
94 extra_environ=xhr_header, status=200)
108
95
109 result = json.loads(response.body)
96 result = json.loads(response.body)
110 assert len(result['suggestions']) == count
97 assert len(result['suggestions']) == count
@@ -1,116 +1,102 b''
1 # Copyright (C) 2016-2023 RhodeCode GmbH
1 # Copyright (C) 2016-2023 RhodeCode GmbH
2 #
2 #
3 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
14 #
15 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19
19
20 # Copyright (C) 2016-2023 RhodeCode GmbH
20 # Copyright (C) 2016-2023 RhodeCode GmbH
21 #
21 #
22 # This program is free software: you can redistribute it and/or modify
22 # This program is free software: you can redistribute it and/or modify
23 # it under the terms of the GNU Affero General Public License, version 3
23 # it under the terms of the GNU Affero General Public License, version 3
24 # (only), as published by the Free Software Foundation.
24 # (only), as published by the Free Software Foundation.
25 #
25 #
26 # This program is distributed in the hope that it will be useful,
26 # This program is distributed in the hope that it will be useful,
27 # but WITHOUT ANY WARRANTY; without even the implied warranty of
27 # but WITHOUT ANY WARRANTY; without even the implied warranty of
28 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
28 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
29 # GNU General Public License for more details.
29 # GNU General Public License for more details.
30 #
30 #
31 # You should have received a copy of the GNU Affero General Public License
31 # You should have received a copy of the GNU Affero General Public License
32 # along with this program. If not, see <http://www.gnu.org/licenses/>.
32 # along with this program. If not, see <http://www.gnu.org/licenses/>.
33 #
33 #
34 # This program is dual-licensed. If you wish to learn more about the
34 # This program is dual-licensed. If you wish to learn more about the
35 # RhodeCode Enterprise Edition, including its added features, Support services,
35 # RhodeCode Enterprise Edition, including its added features, Support services,
36 # and proprietary license terms, please see https://rhodecode.com/licenses/
36 # and proprietary license terms, please see https://rhodecode.com/licenses/
37
37
38 import pytest
38 import pytest
39
39
40 from rhodecode.lib.ext_json import json
41
40 from rhodecode.tests import TestController
42 from rhodecode.tests import TestController
41 from rhodecode.tests.fixture import Fixture
43 from rhodecode.tests.fixture import Fixture
42 from rhodecode.lib.ext_json import json
44 from rhodecode.tests.routes import route_path
43
44
45
45 fixture = Fixture()
46 fixture = Fixture()
46
47
47
48
48 def route_path(name, params=None, **kwargs):
49 import urllib.request
50 import urllib.parse
51 import urllib.error
52
53 base_url = {
54 'user_autocomplete_data': '/_users',
55 'user_group_autocomplete_data': '/_user_groups'
56 }[name].format(**kwargs)
57
58 if params:
59 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
60 return base_url
61
62
63 class TestUserGroupAutocompleteData(TestController):
49 class TestUserGroupAutocompleteData(TestController):
64
50
65 def test_returns_list_of_user_groups(self, user_util, xhr_header):
51 def test_returns_list_of_user_groups(self, user_util, xhr_header):
66 self.log_user()
52 self.log_user()
67 user_group = user_util.create_user_group(active=True)
53 user_group = user_util.create_user_group(active=True)
68 user_group_name = user_group.users_group_name
54 user_group_name = user_group.users_group_name
69 response = self.app.get(
55 response = self.app.get(
70 route_path('user_group_autocomplete_data'),
56 route_path('user_group_autocomplete_data'),
71 extra_environ=xhr_header, status=200)
57 extra_environ=xhr_header, status=200)
72 result = json.loads(response.body)
58 result = json.loads(response.body)
73 values = [suggestion['value'] for suggestion in result['suggestions']]
59 values = [suggestion['value'] for suggestion in result['suggestions']]
74 assert user_group_name in values
60 assert user_group_name in values
75
61
76 def test_returns_inactive_user_groups_when_active_flag_sent(
62 def test_returns_inactive_user_groups_when_active_flag_sent(
77 self, user_util, xhr_header):
63 self, user_util, xhr_header):
78 self.log_user()
64 self.log_user()
79 user_group = user_util.create_user_group(active=False)
65 user_group = user_util.create_user_group(active=False)
80 user_group_name = user_group.users_group_name
66 user_group_name = user_group.users_group_name
81
67
82 response = self.app.get(
68 response = self.app.get(
83 route_path('user_group_autocomplete_data',
69 route_path('user_group_autocomplete_data',
84 params=dict(active='0')),
70 params=dict(active='0')),
85 extra_environ=xhr_header, status=200)
71 extra_environ=xhr_header, status=200)
86 result = json.loads(response.body)
72 result = json.loads(response.body)
87 values = [suggestion['value'] for suggestion in result['suggestions']]
73 values = [suggestion['value'] for suggestion in result['suggestions']]
88 assert user_group_name in values
74 assert user_group_name in values
89
75
90 response = self.app.get(
76 response = self.app.get(
91 route_path('user_group_autocomplete_data',
77 route_path('user_group_autocomplete_data',
92 params=dict(active='1')),
78 params=dict(active='1')),
93 extra_environ=xhr_header, status=200)
79 extra_environ=xhr_header, status=200)
94 result = json.loads(response.body)
80 result = json.loads(response.body)
95 values = [suggestion['value'] for suggestion in result['suggestions']]
81 values = [suggestion['value'] for suggestion in result['suggestions']]
96 assert user_group_name not in values
82 assert user_group_name not in values
97
83
98 @pytest.mark.parametrize('query, count', [
84 @pytest.mark.parametrize('query, count', [
99 ('hello1', 0),
85 ('hello1', 0),
100 ('dev', 1),
86 ('dev', 1),
101 ])
87 ])
102 def test_result_is_limited_when_query_is_sent(self, user_util, xhr_header, query, count):
88 def test_result_is_limited_when_query_is_sent(self, user_util, xhr_header, query, count):
103 self.log_user()
89 self.log_user()
104
90
105 user_util._test_name = 'dev-test'
91 user_util._test_name = 'dev-test'
106 user_util.create_user_group()
92 user_util.create_user_group()
107
93
108 response = self.app.get(
94 response = self.app.get(
109 route_path('user_group_autocomplete_data',
95 route_path('user_group_autocomplete_data',
110 params=dict(user_groups='true',
96 params=dict(user_groups='true',
111 query=query)),
97 query=query)),
112 extra_environ=xhr_header, status=200)
98 extra_environ=xhr_header, status=200)
113
99
114 result = json.loads(response.body)
100 result = json.loads(response.body)
115
101
116 assert len(result['suggestions']) == count
102 assert len(result['suggestions']) == count
@@ -1,177 +1,167 b''
1 # Copyright (C) 2010-2023 RhodeCode GmbH
1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 #
2 #
3 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
14 #
15 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19
19
20 import pytest
20 import pytest
21
21
22 import rhodecode
22 import rhodecode
23 from rhodecode.model.db import Repository, RepoGroup, User
23 from rhodecode.model.db import Repository, RepoGroup, User
24 from rhodecode.model.meta import Session
24 from rhodecode.model.meta import Session
25 from rhodecode.model.repo import RepoModel
26 from rhodecode.model.repo_group import RepoGroupModel
27 from rhodecode.model.settings import SettingsModel
25 from rhodecode.model.settings import SettingsModel
28 from rhodecode.tests import TestController
26 from rhodecode.tests import TestController
29 from rhodecode.tests.fixture import Fixture
27 from rhodecode.tests.fixture import Fixture
30 from rhodecode.lib import helpers as h
28 from rhodecode.tests.routes import route_path
31
32 fixture = Fixture()
33
29
34
30
35 def route_path(name, **kwargs):
31 fixture = Fixture()
36 return {
37 'home': '/',
38 'main_page_repos_data': '/_home_repos',
39 'main_page_repo_groups_data': '/_home_repo_groups',
40 'repo_group_home': '/{repo_group_name}'
41 }[name].format(**kwargs)
42
32
43
33
44 class TestHomeController(TestController):
34 class TestHomeController(TestController):
45
35
46 def test_index(self):
36 def test_index(self):
47 self.log_user()
37 self.log_user()
48 response = self.app.get(route_path('home'))
38 response = self.app.get(route_path('home'))
49 # if global permission is set
39 # if global permission is set
50 response.mustcontain('New Repository')
40 response.mustcontain('New Repository')
51
41
52 def test_index_grid_repos(self, xhr_header):
42 def test_index_grid_repos(self, xhr_header):
53 self.log_user()
43 self.log_user()
54 response = self.app.get(route_path('main_page_repos_data'), extra_environ=xhr_header)
44 response = self.app.get(route_path('main_page_repos_data'), extra_environ=xhr_header)
55 # search for objects inside the JavaScript JSON
45 # search for objects inside the JavaScript JSON
56 for obj in Repository.getAll():
46 for obj in Repository.getAll():
57 response.mustcontain('<a href=\\"/{}\\">'.format(obj.repo_name))
47 response.mustcontain('<a href=\\"/{}\\">'.format(obj.repo_name))
58
48
59 def test_index_grid_repo_groups(self, xhr_header):
49 def test_index_grid_repo_groups(self, xhr_header):
60 self.log_user()
50 self.log_user()
61 response = self.app.get(route_path('main_page_repo_groups_data'),
51 response = self.app.get(route_path('main_page_repo_groups_data'),
62 extra_environ=xhr_header,)
52 extra_environ=xhr_header,)
63
53
64 # search for objects inside the JavaScript JSON
54 # search for objects inside the JavaScript JSON
65 for obj in RepoGroup.getAll():
55 for obj in RepoGroup.getAll():
66 response.mustcontain('<a href=\\"/{}\\">'.format(obj.group_name))
56 response.mustcontain('<a href=\\"/{}\\">'.format(obj.group_name))
67
57
68 def test_index_grid_repo_groups_without_access(self, xhr_header, user_util):
58 def test_index_grid_repo_groups_without_access(self, xhr_header, user_util):
69 user = user_util.create_user(password='qweqwe')
59 user = user_util.create_user(password='qweqwe')
70 group_ok = user_util.create_repo_group(owner=user)
60 group_ok = user_util.create_repo_group(owner=user)
71 group_id_ok = group_ok.group_id
61 group_id_ok = group_ok.group_id
72
62
73 group_forbidden = user_util.create_repo_group(owner=User.get_first_super_admin())
63 group_forbidden = user_util.create_repo_group(owner=User.get_first_super_admin())
74 group_id_forbidden = group_forbidden.group_id
64 group_id_forbidden = group_forbidden.group_id
75
65
76 user_util.grant_user_permission_to_repo_group(group_forbidden, user, 'group.none')
66 user_util.grant_user_permission_to_repo_group(group_forbidden, user, 'group.none')
77 self.log_user(user.username, 'qweqwe')
67 self.log_user(user.username, 'qweqwe')
78
68
79 self.app.get(route_path('main_page_repo_groups_data'),
69 self.app.get(route_path('main_page_repo_groups_data'),
80 extra_environ=xhr_header,
70 extra_environ=xhr_header,
81 params={'repo_group_id': group_id_ok}, status=200)
71 params={'repo_group_id': group_id_ok}, status=200)
82
72
83 self.app.get(route_path('main_page_repo_groups_data'),
73 self.app.get(route_path('main_page_repo_groups_data'),
84 extra_environ=xhr_header,
74 extra_environ=xhr_header,
85 params={'repo_group_id': group_id_forbidden}, status=404)
75 params={'repo_group_id': group_id_forbidden}, status=404)
86
76
87 def test_index_contains_statics_with_ver(self):
77 def test_index_contains_statics_with_ver(self):
88 from rhodecode.lib.base import calculate_version_hash
78 from rhodecode.lib.base import calculate_version_hash
89
79
90 self.log_user()
80 self.log_user()
91 response = self.app.get(route_path('home'))
81 response = self.app.get(route_path('home'))
92
82
93 rhodecode_version_hash = calculate_version_hash(
83 rhodecode_version_hash = calculate_version_hash(
94 {'beaker.session.secret': 'test-rc-uytcxaz'})
84 {'beaker.session.secret': 'test-rc-uytcxaz'})
95 response.mustcontain('style.css?ver={0}'.format(rhodecode_version_hash))
85 response.mustcontain('style.css?ver={0}'.format(rhodecode_version_hash))
96 response.mustcontain('scripts.min.js?ver={0}'.format(rhodecode_version_hash))
86 response.mustcontain('scripts.min.js?ver={0}'.format(rhodecode_version_hash))
97
87
98 def test_index_contains_backend_specific_details(self, backend, xhr_header):
88 def test_index_contains_backend_specific_details(self, backend, xhr_header):
99 self.log_user()
89 self.log_user()
100 response = self.app.get(route_path('main_page_repos_data'), extra_environ=xhr_header)
90 response = self.app.get(route_path('main_page_repos_data'), extra_environ=xhr_header)
101 tip = backend.repo.get_commit().raw_id
91 tip = backend.repo.get_commit().raw_id
102
92
103 # html in javascript variable:
93 # html in javascript variable:
104 response.mustcontain(r'<i class=\"icon-%s\"' % (backend.alias, ))
94 response.mustcontain(r'<i class=\"icon-%s\"' % (backend.alias, ))
105 response.mustcontain(r'href=\"/%s\"' % (backend.repo_name, ))
95 response.mustcontain(r'href=\"/%s\"' % (backend.repo_name, ))
106
96
107 response.mustcontain("""/%s/changeset/%s""" % (backend.repo_name, tip))
97 response.mustcontain("""/%s/changeset/%s""" % (backend.repo_name, tip))
108 response.mustcontain("""Added a symlink""")
98 response.mustcontain("""Added a symlink""")
109
99
110 def test_index_with_anonymous_access_disabled(self):
100 def test_index_with_anonymous_access_disabled(self):
111 with fixture.anon_access(False):
101 with fixture.anon_access(False):
112 response = self.app.get(route_path('home'), status=302)
102 response = self.app.get(route_path('home'), status=302)
113 assert 'login' in response.location
103 assert 'login' in response.location
114
104
115 def test_index_page_on_groups_with_wrong_group_id(self, autologin_user, xhr_header):
105 def test_index_page_on_groups_with_wrong_group_id(self, autologin_user, xhr_header):
116 group_id = 918123
106 group_id = 918123
117 self.app.get(
107 self.app.get(
118 route_path('main_page_repo_groups_data'),
108 route_path('main_page_repo_groups_data'),
119 params={'repo_group_id': group_id},
109 params={'repo_group_id': group_id},
120 status=404, extra_environ=xhr_header)
110 status=404, extra_environ=xhr_header)
121
111
122 def test_index_page_on_groups(self, autologin_user, user_util, xhr_header):
112 def test_index_page_on_groups(self, autologin_user, user_util, xhr_header):
123 gr = user_util.create_repo_group()
113 gr = user_util.create_repo_group()
124 repo = user_util.create_repo(parent=gr)
114 repo = user_util.create_repo(parent=gr)
125 repo_name = repo.repo_name
115 repo_name = repo.repo_name
126 group_id = gr.group_id
116 group_id = gr.group_id
127
117
128 response = self.app.get(route_path(
118 response = self.app.get(route_path(
129 'repo_group_home', repo_group_name=gr.group_name))
119 'repo_group_home', repo_group_name=gr.group_name))
130 response.mustcontain('d.repo_group_id = {}'.format(group_id))
120 response.mustcontain('d.repo_group_id = {}'.format(group_id))
131
121
132 response = self.app.get(
122 response = self.app.get(
133 route_path('main_page_repos_data'),
123 route_path('main_page_repos_data'),
134 params={'repo_group_id': group_id},
124 params={'repo_group_id': group_id},
135 extra_environ=xhr_header,)
125 extra_environ=xhr_header,)
136 response.mustcontain(repo_name)
126 response.mustcontain(repo_name)
137
127
138 def test_index_page_on_group_with_trailing_slash(self, autologin_user, user_util, xhr_header):
128 def test_index_page_on_group_with_trailing_slash(self, autologin_user, user_util, xhr_header):
139 gr = user_util.create_repo_group()
129 gr = user_util.create_repo_group()
140 repo = user_util.create_repo(parent=gr)
130 repo = user_util.create_repo(parent=gr)
141 repo_name = repo.repo_name
131 repo_name = repo.repo_name
142 group_id = gr.group_id
132 group_id = gr.group_id
143
133
144 response = self.app.get(route_path(
134 response = self.app.get(route_path(
145 'repo_group_home', repo_group_name=gr.group_name+'/'))
135 'repo_group_home', repo_group_name=gr.group_name+'/'))
146 response.mustcontain('d.repo_group_id = {}'.format(group_id))
136 response.mustcontain('d.repo_group_id = {}'.format(group_id))
147
137
148 response = self.app.get(
138 response = self.app.get(
149 route_path('main_page_repos_data'),
139 route_path('main_page_repos_data'),
150 params={'repo_group_id': group_id},
140 params={'repo_group_id': group_id},
151 extra_environ=xhr_header, )
141 extra_environ=xhr_header, )
152 response.mustcontain(repo_name)
142 response.mustcontain(repo_name)
153
143
154 @pytest.mark.parametrize("name, state", [
144 @pytest.mark.parametrize("name, state", [
155 ('Disabled', False),
145 ('Disabled', False),
156 ('Enabled', True),
146 ('Enabled', True),
157 ])
147 ])
158 def test_index_show_version(self, autologin_user, name, state):
148 def test_index_show_version(self, autologin_user, name, state):
159 version_string = 'RhodeCode %s' % rhodecode.__version__
149 version_string = 'RhodeCode %s' % rhodecode.__version__
160
150
161 sett = SettingsModel().create_or_update_setting(
151 sett = SettingsModel().create_or_update_setting(
162 'show_version', state, 'bool')
152 'show_version', state, 'bool')
163 Session().add(sett)
153 Session().add(sett)
164 Session().commit()
154 Session().commit()
165 SettingsModel().invalidate_settings_cache(hard=True)
155 SettingsModel().invalidate_settings_cache(hard=True)
166
156
167 response = self.app.get(route_path('home'))
157 response = self.app.get(route_path('home'))
168 if state is True:
158 if state is True:
169 response.mustcontain(version_string)
159 response.mustcontain(version_string)
170 if state is False:
160 if state is False:
171 response.mustcontain(no=[version_string])
161 response.mustcontain(no=[version_string])
172
162
173 def test_logout_form_contains_csrf(self, autologin_user, csrf_token):
163 def test_logout_form_contains_csrf(self, autologin_user, csrf_token):
174 response = self.app.get(route_path('home'))
164 response = self.app.get(route_path('home'))
175 assert_response = response.assert_response()
165 assert_response = response.assert_response()
176 element = assert_response.get_element('.logout [name=csrf_token]')
166 element = assert_response.get_element('.logout [name=csrf_token]')
177 assert element.value == csrf_token
167 assert element.value == csrf_token
@@ -1,107 +1,87 b''
1
1
2 # Copyright (C) 2010-2023 RhodeCode GmbH
2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 #
3 #
4 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
7 #
7 #
8 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
11 # GNU General Public License for more details.
12 #
12 #
13 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
15 #
16 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19
19
20 import datetime
20 import datetime
21
21
22 import pytest
22 import pytest
23
23
24 from rhodecode.apps._base import ADMIN_PREFIX
24 from rhodecode.apps._base import ADMIN_PREFIX
25 from rhodecode.tests import TestController
26 from rhodecode.model.db import UserFollowing, Repository
25 from rhodecode.model.db import UserFollowing, Repository
27
26
28
27 from rhodecode.tests import TestController
29 def route_path(name, params=None, **kwargs):
28 from rhodecode.tests.routes import route_path
30 import urllib.request
31 import urllib.parse
32 import urllib.error
33
34 base_url = {
35 'journal': ADMIN_PREFIX + '/journal',
36 'journal_rss': ADMIN_PREFIX + '/journal/rss',
37 'journal_atom': ADMIN_PREFIX + '/journal/atom',
38 'journal_public': ADMIN_PREFIX + '/public_journal',
39 'journal_public_atom': ADMIN_PREFIX + '/public_journal/atom',
40 'journal_public_atom_old': ADMIN_PREFIX + '/public_journal_atom',
41 'journal_public_rss': ADMIN_PREFIX + '/public_journal/rss',
42 'journal_public_rss_old': ADMIN_PREFIX + '/public_journal_rss',
43 'toggle_following': ADMIN_PREFIX + '/toggle_following',
44 }[name].format(**kwargs)
45
46 if params:
47 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
48 return base_url
49
29
50
30
51 class TestJournalViews(TestController):
31 class TestJournalViews(TestController):
52
32
53 def test_journal(self):
33 def test_journal(self):
54 self.log_user()
34 self.log_user()
55 response = self.app.get(route_path('journal'))
35 response = self.app.get(route_path('journal'))
56 # response.mustcontain(
36 # response.mustcontain(
57 # """<div class="journal_day">%s</div>""" % datetime.date.today())
37 # """<div class="journal_day">%s</div>""" % datetime.date.today())
58
38
59 @pytest.mark.parametrize("feed_type, content_type", [
39 @pytest.mark.parametrize("feed_type, content_type", [
60 ('rss', "application/rss+xml"),
40 ('rss', "application/rss+xml"),
61 ('atom', "application/atom+xml")
41 ('atom', "application/atom+xml")
62 ])
42 ])
63 def test_journal_feed(self, feed_type, content_type):
43 def test_journal_feed(self, feed_type, content_type):
64 self.log_user()
44 self.log_user()
65 response = self.app.get(
45 response = self.app.get(
66 route_path(
46 route_path(
67 'journal_{}'.format(feed_type)),
47 'journal_{}'.format(feed_type)),
68 status=200)
48 status=200)
69
49
70 assert response.content_type == content_type
50 assert response.content_type == content_type
71
51
72 def test_toggle_following_repository(self, backend):
52 def test_toggle_following_repository(self, backend):
73 user = self.log_user()
53 user = self.log_user()
74 repo = Repository.get_by_repo_name(backend.repo_name)
54 repo = Repository.get_by_repo_name(backend.repo_name)
75 repo_id = repo.repo_id
55 repo_id = repo.repo_id
76 self.app.post(
56 self.app.post(
77 route_path('toggle_following'), {'follows_repo_id': repo_id,
57 route_path('toggle_following'), {'follows_repo_id': repo_id,
78 'csrf_token': self.csrf_token})
58 'csrf_token': self.csrf_token})
79
59
80 followings = UserFollowing.query()\
60 followings = UserFollowing.query()\
81 .filter(UserFollowing.user_id == user['user_id'])\
61 .filter(UserFollowing.user_id == user['user_id'])\
82 .filter(UserFollowing.follows_repo_id == repo_id).all()
62 .filter(UserFollowing.follows_repo_id == repo_id).all()
83
63
84 assert len(followings) == 0
64 assert len(followings) == 0
85
65
86 self.app.post(
66 self.app.post(
87 route_path('toggle_following'), {'follows_repo_id': repo_id,
67 route_path('toggle_following'), {'follows_repo_id': repo_id,
88 'csrf_token': self.csrf_token})
68 'csrf_token': self.csrf_token})
89
69
90 followings = UserFollowing.query()\
70 followings = UserFollowing.query()\
91 .filter(UserFollowing.user_id == user['user_id'])\
71 .filter(UserFollowing.user_id == user['user_id'])\
92 .filter(UserFollowing.follows_repo_id == repo_id).all()
72 .filter(UserFollowing.follows_repo_id == repo_id).all()
93
73
94 assert len(followings) == 1
74 assert len(followings) == 1
95
75
96 @pytest.mark.parametrize("feed_type, content_type", [
76 @pytest.mark.parametrize("feed_type, content_type", [
97 ('rss', "application/rss+xml"),
77 ('rss', "application/rss+xml"),
98 ('atom', "application/atom+xml")
78 ('atom', "application/atom+xml")
99 ])
79 ])
100 def test_public_journal_feed(self, feed_type, content_type):
80 def test_public_journal_feed(self, feed_type, content_type):
101 self.log_user()
81 self.log_user()
102 response = self.app.get(
82 response = self.app.get(
103 route_path(
83 route_path(
104 'journal_public_{}'.format(feed_type)),
84 'journal_public_{}'.format(feed_type)),
105 status=200)
85 status=200)
106
86
107 assert response.content_type == content_type
87 assert response.content_type == content_type
@@ -1,607 +1,581 b''
1 # Copyright (C) 2010-2023 RhodeCode GmbH
1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 #
2 #
3 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
14 #
15 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19 import urllib.parse
19 import urllib.parse
20
20
21 import mock
21 import mock
22 import pytest
22 import pytest
23
23
24 from rhodecode.tests import (
24
25 assert_session_flash, HG_REPO, TEST_USER_ADMIN_LOGIN,
26 no_newline_id_generator)
27 from rhodecode.tests.fixture import Fixture
28 from rhodecode.lib.auth import check_password
25 from rhodecode.lib.auth import check_password
29 from rhodecode.lib import helpers as h
26 from rhodecode.lib import helpers as h
30 from rhodecode.model.auth_token import AuthTokenModel
27 from rhodecode.model.auth_token import AuthTokenModel
31 from rhodecode.model.db import User, Notification, UserApiKeys
28 from rhodecode.model.db import User, Notification, UserApiKeys
32 from rhodecode.model.meta import Session
29 from rhodecode.model.meta import Session
33
30
31 from rhodecode.tests import (
32 assert_session_flash, HG_REPO, TEST_USER_ADMIN_LOGIN,
33 no_newline_id_generator)
34 from rhodecode.tests.fixture import Fixture
35 from rhodecode.tests.routes import route_path
36
34 fixture = Fixture()
37 fixture = Fixture()
35
38
36 whitelist_view = ['RepoCommitsView:repo_commit_raw']
39 whitelist_view = ['RepoCommitsView:repo_commit_raw']
37
40
38
41
39 def route_path(name, params=None, **kwargs):
40 import urllib.request
41 import urllib.parse
42 import urllib.error
43 from rhodecode.apps._base import ADMIN_PREFIX
44
45 base_url = {
46 'login': ADMIN_PREFIX + '/login',
47 'logout': ADMIN_PREFIX + '/logout',
48 'register': ADMIN_PREFIX + '/register',
49 'reset_password':
50 ADMIN_PREFIX + '/password_reset',
51 'reset_password_confirmation':
52 ADMIN_PREFIX + '/password_reset_confirmation',
53
54 'admin_permissions_application':
55 ADMIN_PREFIX + '/permissions/application',
56 'admin_permissions_application_update':
57 ADMIN_PREFIX + '/permissions/application/update',
58
59 'repo_commit_raw': '/{repo_name}/raw-changeset/{commit_id}'
60
61 }[name].format(**kwargs)
62
63 if params:
64 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
65 return base_url
66
67
68 @pytest.mark.usefixtures('app')
42 @pytest.mark.usefixtures('app')
69 class TestLoginController(object):
43 class TestLoginController(object):
70 destroy_users = set()
44 destroy_users = set()
71
45
72 @classmethod
46 @classmethod
73 def teardown_class(cls):
47 def teardown_class(cls):
74 fixture.destroy_users(cls.destroy_users)
48 fixture.destroy_users(cls.destroy_users)
75
49
76 def teardown_method(self, method):
50 def teardown_method(self, method):
77 for n in Notification.query().all():
51 for n in Notification.query().all():
78 Session().delete(n)
52 Session().delete(n)
79
53
80 Session().commit()
54 Session().commit()
81 assert Notification.query().all() == []
55 assert Notification.query().all() == []
82
56
83 def test_index(self):
57 def test_index(self):
84 response = self.app.get(route_path('login'))
58 response = self.app.get(route_path('login'))
85 assert response.status == '200 OK'
59 assert response.status == '200 OK'
86 # Test response...
60 # Test response...
87
61
88 def test_login_admin_ok(self):
62 def test_login_admin_ok(self):
89 response = self.app.post(route_path('login'),
63 response = self.app.post(route_path('login'),
90 {'username': 'test_admin',
64 {'username': 'test_admin',
91 'password': 'test12'}, status=302)
65 'password': 'test12'}, status=302)
92 response = response.follow()
66 response = response.follow()
93 session = response.get_session_from_response()
67 session = response.get_session_from_response()
94 username = session['rhodecode_user'].get('username')
68 username = session['rhodecode_user'].get('username')
95 assert username == 'test_admin'
69 assert username == 'test_admin'
96 response.mustcontain('logout')
70 response.mustcontain('logout')
97
71
98 def test_login_regular_ok(self):
72 def test_login_regular_ok(self):
99 response = self.app.post(route_path('login'),
73 response = self.app.post(route_path('login'),
100 {'username': 'test_regular',
74 {'username': 'test_regular',
101 'password': 'test12'}, status=302)
75 'password': 'test12'}, status=302)
102
76
103 response = response.follow()
77 response = response.follow()
104 session = response.get_session_from_response()
78 session = response.get_session_from_response()
105 username = session['rhodecode_user'].get('username')
79 username = session['rhodecode_user'].get('username')
106 assert username == 'test_regular'
80 assert username == 'test_regular'
107 response.mustcontain('logout')
81 response.mustcontain('logout')
108
82
109 def test_login_regular_forbidden_when_super_admin_restriction(self):
83 def test_login_regular_forbidden_when_super_admin_restriction(self):
110 from rhodecode.authentication.plugins.auth_rhodecode import RhodeCodeAuthPlugin
84 from rhodecode.authentication.plugins.auth_rhodecode import RhodeCodeAuthPlugin
111 with fixture.auth_restriction(self.app._pyramid_registry,
85 with fixture.auth_restriction(self.app._pyramid_registry,
112 RhodeCodeAuthPlugin.AUTH_RESTRICTION_SUPER_ADMIN):
86 RhodeCodeAuthPlugin.AUTH_RESTRICTION_SUPER_ADMIN):
113 response = self.app.post(route_path('login'),
87 response = self.app.post(route_path('login'),
114 {'username': 'test_regular',
88 {'username': 'test_regular',
115 'password': 'test12'})
89 'password': 'test12'})
116
90
117 response.mustcontain('invalid user name')
91 response.mustcontain('invalid user name')
118 response.mustcontain('invalid password')
92 response.mustcontain('invalid password')
119
93
120 def test_login_regular_forbidden_when_scope_restriction(self):
94 def test_login_regular_forbidden_when_scope_restriction(self):
121 from rhodecode.authentication.plugins.auth_rhodecode import RhodeCodeAuthPlugin
95 from rhodecode.authentication.plugins.auth_rhodecode import RhodeCodeAuthPlugin
122 with fixture.scope_restriction(self.app._pyramid_registry,
96 with fixture.scope_restriction(self.app._pyramid_registry,
123 RhodeCodeAuthPlugin.AUTH_RESTRICTION_SCOPE_VCS):
97 RhodeCodeAuthPlugin.AUTH_RESTRICTION_SCOPE_VCS):
124 response = self.app.post(route_path('login'),
98 response = self.app.post(route_path('login'),
125 {'username': 'test_regular',
99 {'username': 'test_regular',
126 'password': 'test12'})
100 'password': 'test12'})
127
101
128 response.mustcontain('invalid user name')
102 response.mustcontain('invalid user name')
129 response.mustcontain('invalid password')
103 response.mustcontain('invalid password')
130
104
131 def test_login_ok_came_from(self):
105 def test_login_ok_came_from(self):
132 test_came_from = '/_admin/users?branch=stable'
106 test_came_from = '/_admin/users?branch=stable'
133 _url = '{}?came_from={}'.format(route_path('login'), test_came_from)
107 _url = '{}?came_from={}'.format(route_path('login'), test_came_from)
134 response = self.app.post(
108 response = self.app.post(
135 _url, {'username': 'test_admin', 'password': 'test12'}, status=302)
109 _url, {'username': 'test_admin', 'password': 'test12'}, status=302)
136
110
137 assert 'branch=stable' in response.location
111 assert 'branch=stable' in response.location
138 response = response.follow()
112 response = response.follow()
139
113
140 assert response.status == '200 OK'
114 assert response.status == '200 OK'
141 response.mustcontain('Users administration')
115 response.mustcontain('Users administration')
142
116
143 def test_redirect_to_login_with_get_args(self):
117 def test_redirect_to_login_with_get_args(self):
144 with fixture.anon_access(False):
118 with fixture.anon_access(False):
145 kwargs = {'branch': 'stable'}
119 kwargs = {'branch': 'stable'}
146 response = self.app.get(
120 response = self.app.get(
147 h.route_path('repo_summary', repo_name=HG_REPO, _query=kwargs),
121 h.route_path('repo_summary', repo_name=HG_REPO, _query=kwargs),
148 status=302)
122 status=302)
149
123
150 response_query = urllib.parse.parse_qsl(response.location)
124 response_query = urllib.parse.parse_qsl(response.location)
151 assert 'branch=stable' in response_query[0][1]
125 assert 'branch=stable' in response_query[0][1]
152
126
153 def test_login_form_with_get_args(self):
127 def test_login_form_with_get_args(self):
154 _url = '{}?came_from=/_admin/users,branch=stable'.format(route_path('login'))
128 _url = '{}?came_from=/_admin/users,branch=stable'.format(route_path('login'))
155 response = self.app.get(_url)
129 response = self.app.get(_url)
156 assert 'branch%3Dstable' in response.form.action
130 assert 'branch%3Dstable' in response.form.action
157
131
158 @pytest.mark.parametrize("url_came_from", [
132 @pytest.mark.parametrize("url_came_from", [
159 'data:text/html,<script>window.alert("xss")</script>',
133 'data:text/html,<script>window.alert("xss")</script>',
160 'mailto:test@rhodecode.org',
134 'mailto:test@rhodecode.org',
161 'file:///etc/passwd',
135 'file:///etc/passwd',
162 'ftp://some.ftp.server',
136 'ftp://some.ftp.server',
163 'http://other.domain',
137 'http://other.domain',
164 ], ids=no_newline_id_generator)
138 ], ids=no_newline_id_generator)
165 def test_login_bad_came_froms(self, url_came_from):
139 def test_login_bad_came_froms(self, url_came_from):
166 _url = '{}?came_from={}'.format(route_path('login'), url_came_from)
140 _url = '{}?came_from={}'.format(route_path('login'), url_came_from)
167 response = self.app.post(
141 response = self.app.post(
168 _url, {'username': 'test_admin', 'password': 'test12'}, status=302)
142 _url, {'username': 'test_admin', 'password': 'test12'}, status=302)
169 assert response.status == '302 Found'
143 assert response.status == '302 Found'
170 response = response.follow()
144 response = response.follow()
171 assert response.status == '200 OK'
145 assert response.status == '200 OK'
172 assert response.request.path == '/'
146 assert response.request.path == '/'
173
147
174 @pytest.mark.xfail(reason="newline params changed behaviour in python3")
148 @pytest.mark.xfail(reason="newline params changed behaviour in python3")
175 @pytest.mark.parametrize("url_came_from", [
149 @pytest.mark.parametrize("url_came_from", [
176 '/\r\nX-Forwarded-Host: \rhttp://example.org',
150 '/\r\nX-Forwarded-Host: \rhttp://example.org',
177 ], ids=no_newline_id_generator)
151 ], ids=no_newline_id_generator)
178 def test_login_bad_came_froms_404(self, url_came_from):
152 def test_login_bad_came_froms_404(self, url_came_from):
179 _url = '{}?came_from={}'.format(route_path('login'), url_came_from)
153 _url = '{}?came_from={}'.format(route_path('login'), url_came_from)
180 response = self.app.post(
154 response = self.app.post(
181 _url, {'username': 'test_admin', 'password': 'test12'}, status=302)
155 _url, {'username': 'test_admin', 'password': 'test12'}, status=302)
182
156
183 response = response.follow()
157 response = response.follow()
184 assert response.status == '404 Not Found'
158 assert response.status == '404 Not Found'
185
159
186 def test_login_short_password(self):
160 def test_login_short_password(self):
187 response = self.app.post(route_path('login'),
161 response = self.app.post(route_path('login'),
188 {'username': 'test_admin',
162 {'username': 'test_admin',
189 'password': 'as'})
163 'password': 'as'})
190 assert response.status == '200 OK'
164 assert response.status == '200 OK'
191
165
192 response.mustcontain('Enter 3 characters or more')
166 response.mustcontain('Enter 3 characters or more')
193
167
194 def test_login_wrong_non_ascii_password(self, user_regular):
168 def test_login_wrong_non_ascii_password(self, user_regular):
195 response = self.app.post(
169 response = self.app.post(
196 route_path('login'),
170 route_path('login'),
197 {'username': user_regular.username,
171 {'username': user_regular.username,
198 'password': 'invalid-non-asci\xe4'.encode('utf8')})
172 'password': 'invalid-non-asci\xe4'.encode('utf8')})
199
173
200 response.mustcontain('invalid user name')
174 response.mustcontain('invalid user name')
201 response.mustcontain('invalid password')
175 response.mustcontain('invalid password')
202
176
203 def test_login_with_non_ascii_password(self, user_util):
177 def test_login_with_non_ascii_password(self, user_util):
204 password = u'valid-non-ascii\xe4'
178 password = u'valid-non-ascii\xe4'
205 user = user_util.create_user(password=password)
179 user = user_util.create_user(password=password)
206 response = self.app.post(
180 response = self.app.post(
207 route_path('login'),
181 route_path('login'),
208 {'username': user.username,
182 {'username': user.username,
209 'password': password})
183 'password': password})
210 assert response.status_code == 302
184 assert response.status_code == 302
211
185
212 def test_login_wrong_username_password(self):
186 def test_login_wrong_username_password(self):
213 response = self.app.post(route_path('login'),
187 response = self.app.post(route_path('login'),
214 {'username': 'error',
188 {'username': 'error',
215 'password': 'test12'})
189 'password': 'test12'})
216
190
217 response.mustcontain('invalid user name')
191 response.mustcontain('invalid user name')
218 response.mustcontain('invalid password')
192 response.mustcontain('invalid password')
219
193
220 def test_login_admin_ok_password_migration(self, real_crypto_backend):
194 def test_login_admin_ok_password_migration(self, real_crypto_backend):
221 from rhodecode.lib import auth
195 from rhodecode.lib import auth
222
196
223 # create new user, with sha256 password
197 # create new user, with sha256 password
224 temp_user = 'test_admin_sha256'
198 temp_user = 'test_admin_sha256'
225 user = fixture.create_user(temp_user)
199 user = fixture.create_user(temp_user)
226 user.password = auth._RhodeCodeCryptoSha256().hash_create(
200 user.password = auth._RhodeCodeCryptoSha256().hash_create(
227 b'test123')
201 b'test123')
228 Session().add(user)
202 Session().add(user)
229 Session().commit()
203 Session().commit()
230 self.destroy_users.add(temp_user)
204 self.destroy_users.add(temp_user)
231 response = self.app.post(route_path('login'),
205 response = self.app.post(route_path('login'),
232 {'username': temp_user,
206 {'username': temp_user,
233 'password': 'test123'}, status=302)
207 'password': 'test123'}, status=302)
234
208
235 response = response.follow()
209 response = response.follow()
236 session = response.get_session_from_response()
210 session = response.get_session_from_response()
237 username = session['rhodecode_user'].get('username')
211 username = session['rhodecode_user'].get('username')
238 assert username == temp_user
212 assert username == temp_user
239 response.mustcontain('logout')
213 response.mustcontain('logout')
240
214
241 # new password should be bcrypted, after log-in and transfer
215 # new password should be bcrypted, after log-in and transfer
242 user = User.get_by_username(temp_user)
216 user = User.get_by_username(temp_user)
243 assert user.password.startswith('$')
217 assert user.password.startswith('$')
244
218
245 # REGISTRATIONS
219 # REGISTRATIONS
246 def test_register(self):
220 def test_register(self):
247 response = self.app.get(route_path('register'))
221 response = self.app.get(route_path('register'))
248 response.mustcontain('Create an Account')
222 response.mustcontain('Create an Account')
249
223
250 def test_register_err_same_username(self):
224 def test_register_err_same_username(self):
251 uname = 'test_admin'
225 uname = 'test_admin'
252 response = self.app.post(
226 response = self.app.post(
253 route_path('register'),
227 route_path('register'),
254 {
228 {
255 'username': uname,
229 'username': uname,
256 'password': 'test12',
230 'password': 'test12',
257 'password_confirmation': 'test12',
231 'password_confirmation': 'test12',
258 'email': 'goodmail@domain.com',
232 'email': 'goodmail@domain.com',
259 'firstname': 'test',
233 'firstname': 'test',
260 'lastname': 'test'
234 'lastname': 'test'
261 }
235 }
262 )
236 )
263
237
264 assertr = response.assert_response()
238 assertr = response.assert_response()
265 msg = 'Username "%(username)s" already exists'
239 msg = 'Username "%(username)s" already exists'
266 msg = msg % {'username': uname}
240 msg = msg % {'username': uname}
267 assertr.element_contains('#username+.error-message', msg)
241 assertr.element_contains('#username+.error-message', msg)
268
242
269 def test_register_err_same_email(self):
243 def test_register_err_same_email(self):
270 response = self.app.post(
244 response = self.app.post(
271 route_path('register'),
245 route_path('register'),
272 {
246 {
273 'username': 'test_admin_0',
247 'username': 'test_admin_0',
274 'password': 'test12',
248 'password': 'test12',
275 'password_confirmation': 'test12',
249 'password_confirmation': 'test12',
276 'email': 'test_admin@mail.com',
250 'email': 'test_admin@mail.com',
277 'firstname': 'test',
251 'firstname': 'test',
278 'lastname': 'test'
252 'lastname': 'test'
279 }
253 }
280 )
254 )
281
255
282 assertr = response.assert_response()
256 assertr = response.assert_response()
283 msg = u'This e-mail address is already taken'
257 msg = u'This e-mail address is already taken'
284 assertr.element_contains('#email+.error-message', msg)
258 assertr.element_contains('#email+.error-message', msg)
285
259
286 def test_register_err_same_email_case_sensitive(self):
260 def test_register_err_same_email_case_sensitive(self):
287 response = self.app.post(
261 response = self.app.post(
288 route_path('register'),
262 route_path('register'),
289 {
263 {
290 'username': 'test_admin_1',
264 'username': 'test_admin_1',
291 'password': 'test12',
265 'password': 'test12',
292 'password_confirmation': 'test12',
266 'password_confirmation': 'test12',
293 'email': 'TesT_Admin@mail.COM',
267 'email': 'TesT_Admin@mail.COM',
294 'firstname': 'test',
268 'firstname': 'test',
295 'lastname': 'test'
269 'lastname': 'test'
296 }
270 }
297 )
271 )
298 assertr = response.assert_response()
272 assertr = response.assert_response()
299 msg = u'This e-mail address is already taken'
273 msg = u'This e-mail address is already taken'
300 assertr.element_contains('#email+.error-message', msg)
274 assertr.element_contains('#email+.error-message', msg)
301
275
302 def test_register_err_wrong_data(self):
276 def test_register_err_wrong_data(self):
303 response = self.app.post(
277 response = self.app.post(
304 route_path('register'),
278 route_path('register'),
305 {
279 {
306 'username': 'xs',
280 'username': 'xs',
307 'password': 'test',
281 'password': 'test',
308 'password_confirmation': 'test',
282 'password_confirmation': 'test',
309 'email': 'goodmailm',
283 'email': 'goodmailm',
310 'firstname': 'test',
284 'firstname': 'test',
311 'lastname': 'test'
285 'lastname': 'test'
312 }
286 }
313 )
287 )
314 assert response.status == '200 OK'
288 assert response.status == '200 OK'
315 response.mustcontain('An email address must contain a single @')
289 response.mustcontain('An email address must contain a single @')
316 response.mustcontain('Enter a value 6 characters long or more')
290 response.mustcontain('Enter a value 6 characters long or more')
317
291
318 def test_register_err_username(self):
292 def test_register_err_username(self):
319 response = self.app.post(
293 response = self.app.post(
320 route_path('register'),
294 route_path('register'),
321 {
295 {
322 'username': 'error user',
296 'username': 'error user',
323 'password': 'test12',
297 'password': 'test12',
324 'password_confirmation': 'test12',
298 'password_confirmation': 'test12',
325 'email': 'goodmailm',
299 'email': 'goodmailm',
326 'firstname': 'test',
300 'firstname': 'test',
327 'lastname': 'test'
301 'lastname': 'test'
328 }
302 }
329 )
303 )
330
304
331 response.mustcontain('An email address must contain a single @')
305 response.mustcontain('An email address must contain a single @')
332 response.mustcontain(
306 response.mustcontain(
333 'Username may only contain '
307 'Username may only contain '
334 'alphanumeric characters underscores, '
308 'alphanumeric characters underscores, '
335 'periods or dashes and must begin with '
309 'periods or dashes and must begin with '
336 'alphanumeric character')
310 'alphanumeric character')
337
311
338 def test_register_err_case_sensitive(self):
312 def test_register_err_case_sensitive(self):
339 usr = 'Test_Admin'
313 usr = 'Test_Admin'
340 response = self.app.post(
314 response = self.app.post(
341 route_path('register'),
315 route_path('register'),
342 {
316 {
343 'username': usr,
317 'username': usr,
344 'password': 'test12',
318 'password': 'test12',
345 'password_confirmation': 'test12',
319 'password_confirmation': 'test12',
346 'email': 'goodmailm',
320 'email': 'goodmailm',
347 'firstname': 'test',
321 'firstname': 'test',
348 'lastname': 'test'
322 'lastname': 'test'
349 }
323 }
350 )
324 )
351
325
352 assertr = response.assert_response()
326 assertr = response.assert_response()
353 msg = u'Username "%(username)s" already exists'
327 msg = u'Username "%(username)s" already exists'
354 msg = msg % {'username': usr}
328 msg = msg % {'username': usr}
355 assertr.element_contains('#username+.error-message', msg)
329 assertr.element_contains('#username+.error-message', msg)
356
330
357 def test_register_special_chars(self):
331 def test_register_special_chars(self):
358 response = self.app.post(
332 response = self.app.post(
359 route_path('register'),
333 route_path('register'),
360 {
334 {
361 'username': 'xxxaxn',
335 'username': 'xxxaxn',
362 'password': 'ąćźżąśśśś',
336 'password': 'ąćźżąśśśś',
363 'password_confirmation': 'ąćźżąśśśś',
337 'password_confirmation': 'ąćźżąśśśś',
364 'email': 'goodmailm@test.plx',
338 'email': 'goodmailm@test.plx',
365 'firstname': 'test',
339 'firstname': 'test',
366 'lastname': 'test'
340 'lastname': 'test'
367 }
341 }
368 )
342 )
369
343
370 msg = u'Invalid characters (non-ascii) in password'
344 msg = u'Invalid characters (non-ascii) in password'
371 response.mustcontain(msg)
345 response.mustcontain(msg)
372
346
373 def test_register_password_mismatch(self):
347 def test_register_password_mismatch(self):
374 response = self.app.post(
348 response = self.app.post(
375 route_path('register'),
349 route_path('register'),
376 {
350 {
377 'username': 'xs',
351 'username': 'xs',
378 'password': '123qwe',
352 'password': '123qwe',
379 'password_confirmation': 'qwe123',
353 'password_confirmation': 'qwe123',
380 'email': 'goodmailm@test.plxa',
354 'email': 'goodmailm@test.plxa',
381 'firstname': 'test',
355 'firstname': 'test',
382 'lastname': 'test'
356 'lastname': 'test'
383 }
357 }
384 )
358 )
385 msg = u'Passwords do not match'
359 msg = u'Passwords do not match'
386 response.mustcontain(msg)
360 response.mustcontain(msg)
387
361
388 def test_register_ok(self):
362 def test_register_ok(self):
389 username = 'test_regular4'
363 username = 'test_regular4'
390 password = 'qweqwe'
364 password = 'qweqwe'
391 email = 'marcin@test.com'
365 email = 'marcin@test.com'
392 name = 'testname'
366 name = 'testname'
393 lastname = 'testlastname'
367 lastname = 'testlastname'
394
368
395 # this initializes a session
369 # this initializes a session
396 response = self.app.get(route_path('register'))
370 response = self.app.get(route_path('register'))
397 response.mustcontain('Create an Account')
371 response.mustcontain('Create an Account')
398
372
399
373
400 response = self.app.post(
374 response = self.app.post(
401 route_path('register'),
375 route_path('register'),
402 {
376 {
403 'username': username,
377 'username': username,
404 'password': password,
378 'password': password,
405 'password_confirmation': password,
379 'password_confirmation': password,
406 'email': email,
380 'email': email,
407 'firstname': name,
381 'firstname': name,
408 'lastname': lastname,
382 'lastname': lastname,
409 'admin': True
383 'admin': True
410 },
384 },
411 status=302
385 status=302
412 ) # This should be overridden
386 ) # This should be overridden
413
387
414 assert_session_flash(
388 assert_session_flash(
415 response, 'You have successfully registered with RhodeCode. You can log-in now.')
389 response, 'You have successfully registered with RhodeCode. You can log-in now.')
416
390
417 ret = Session().query(User).filter(
391 ret = Session().query(User).filter(
418 User.username == 'test_regular4').one()
392 User.username == 'test_regular4').one()
419 assert ret.username == username
393 assert ret.username == username
420 assert check_password(password, ret.password)
394 assert check_password(password, ret.password)
421 assert ret.email == email
395 assert ret.email == email
422 assert ret.name == name
396 assert ret.name == name
423 assert ret.lastname == lastname
397 assert ret.lastname == lastname
424 assert ret.auth_tokens is not None
398 assert ret.auth_tokens is not None
425 assert not ret.admin
399 assert not ret.admin
426
400
427 def test_forgot_password_wrong_mail(self):
401 def test_forgot_password_wrong_mail(self):
428 bad_email = 'marcin@wrongmail.org'
402 bad_email = 'marcin@wrongmail.org'
429 # this initializes a session
403 # this initializes a session
430 self.app.get(route_path('reset_password'))
404 self.app.get(route_path('reset_password'))
431
405
432 response = self.app.post(
406 response = self.app.post(
433 route_path('reset_password'), {'email': bad_email, }
407 route_path('reset_password'), {'email': bad_email, }
434 )
408 )
435 assert_session_flash(response,
409 assert_session_flash(response,
436 'If such email exists, a password reset link was sent to it.')
410 'If such email exists, a password reset link was sent to it.')
437
411
438 def test_forgot_password(self, user_util):
412 def test_forgot_password(self, user_util):
439 # this initializes a session
413 # this initializes a session
440 self.app.get(route_path('reset_password'))
414 self.app.get(route_path('reset_password'))
441
415
442 user = user_util.create_user()
416 user = user_util.create_user()
443 user_id = user.user_id
417 user_id = user.user_id
444 email = user.email
418 email = user.email
445
419
446 response = self.app.post(route_path('reset_password'), {'email': email, })
420 response = self.app.post(route_path('reset_password'), {'email': email, })
447
421
448 assert_session_flash(response,
422 assert_session_flash(response,
449 'If such email exists, a password reset link was sent to it.')
423 'If such email exists, a password reset link was sent to it.')
450
424
451 # BAD KEY
425 # BAD KEY
452 confirm_url = '{}?key={}'.format(route_path('reset_password_confirmation'), 'badkey')
426 confirm_url = '{}?key={}'.format(route_path('reset_password_confirmation'), 'badkey')
453 response = self.app.get(confirm_url, status=302)
427 response = self.app.get(confirm_url, status=302)
454 assert response.location.endswith(route_path('reset_password'))
428 assert response.location.endswith(route_path('reset_password'))
455 assert_session_flash(response, 'Given reset token is invalid')
429 assert_session_flash(response, 'Given reset token is invalid')
456
430
457 response.follow() # cleanup flash
431 response.follow() # cleanup flash
458
432
459 # GOOD KEY
433 # GOOD KEY
460 key = UserApiKeys.query()\
434 key = UserApiKeys.query()\
461 .filter(UserApiKeys.user_id == user_id)\
435 .filter(UserApiKeys.user_id == user_id)\
462 .filter(UserApiKeys.role == UserApiKeys.ROLE_PASSWORD_RESET)\
436 .filter(UserApiKeys.role == UserApiKeys.ROLE_PASSWORD_RESET)\
463 .first()
437 .first()
464
438
465 assert key
439 assert key
466
440
467 confirm_url = '{}?key={}'.format(route_path('reset_password_confirmation'), key.api_key)
441 confirm_url = '{}?key={}'.format(route_path('reset_password_confirmation'), key.api_key)
468 response = self.app.get(confirm_url)
442 response = self.app.get(confirm_url)
469 assert response.status == '302 Found'
443 assert response.status == '302 Found'
470 assert response.location.endswith(route_path('login'))
444 assert response.location.endswith(route_path('login'))
471
445
472 assert_session_flash(
446 assert_session_flash(
473 response,
447 response,
474 'Your password reset was successful, '
448 'Your password reset was successful, '
475 'a new password has been sent to your email')
449 'a new password has been sent to your email')
476
450
477 response.follow()
451 response.follow()
478
452
479 def _get_api_whitelist(self, values=None):
453 def _get_api_whitelist(self, values=None):
480 config = {'api_access_controllers_whitelist': values or []}
454 config = {'api_access_controllers_whitelist': values or []}
481 return config
455 return config
482
456
483 @pytest.mark.parametrize("test_name, auth_token", [
457 @pytest.mark.parametrize("test_name, auth_token", [
484 ('none', None),
458 ('none', None),
485 ('empty_string', ''),
459 ('empty_string', ''),
486 ('fake_number', '123456'),
460 ('fake_number', '123456'),
487 ('proper_auth_token', None)
461 ('proper_auth_token', None)
488 ])
462 ])
489 def test_access_not_whitelisted_page_via_auth_token(
463 def test_access_not_whitelisted_page_via_auth_token(
490 self, test_name, auth_token, user_admin):
464 self, test_name, auth_token, user_admin):
491
465
492 whitelist = self._get_api_whitelist([])
466 whitelist = self._get_api_whitelist([])
493 with mock.patch.dict('rhodecode.CONFIG', whitelist):
467 with mock.patch.dict('rhodecode.CONFIG', whitelist):
494 assert [] == whitelist['api_access_controllers_whitelist']
468 assert [] == whitelist['api_access_controllers_whitelist']
495 if test_name == 'proper_auth_token':
469 if test_name == 'proper_auth_token':
496 # use builtin if api_key is None
470 # use builtin if api_key is None
497 auth_token = user_admin.api_key
471 auth_token = user_admin.api_key
498
472
499 with fixture.anon_access(False):
473 with fixture.anon_access(False):
500 # webtest uses linter to check if response is bytes,
474 # webtest uses linter to check if response is bytes,
501 # and we use memoryview here as a wrapper, quick turn-off
475 # and we use memoryview here as a wrapper, quick turn-off
502 self.app.lint = False
476 self.app.lint = False
503
477
504 self.app.get(
478 self.app.get(
505 route_path('repo_commit_raw',
479 route_path('repo_commit_raw',
506 repo_name=HG_REPO, commit_id='tip',
480 repo_name=HG_REPO, commit_id='tip',
507 params=dict(api_key=auth_token)),
481 params=dict(api_key=auth_token)),
508 status=302)
482 status=302)
509
483
510 @pytest.mark.parametrize("test_name, auth_token, code", [
484 @pytest.mark.parametrize("test_name, auth_token, code", [
511 ('none', None, 302),
485 ('none', None, 302),
512 ('empty_string', '', 302),
486 ('empty_string', '', 302),
513 ('fake_number', '123456', 302),
487 ('fake_number', '123456', 302),
514 ('proper_auth_token', None, 200)
488 ('proper_auth_token', None, 200)
515 ])
489 ])
516 def test_access_whitelisted_page_via_auth_token(
490 def test_access_whitelisted_page_via_auth_token(
517 self, test_name, auth_token, code, user_admin):
491 self, test_name, auth_token, code, user_admin):
518
492
519 whitelist = self._get_api_whitelist(whitelist_view)
493 whitelist = self._get_api_whitelist(whitelist_view)
520
494
521 with mock.patch.dict('rhodecode.CONFIG', whitelist):
495 with mock.patch.dict('rhodecode.CONFIG', whitelist):
522 assert whitelist_view == whitelist['api_access_controllers_whitelist']
496 assert whitelist_view == whitelist['api_access_controllers_whitelist']
523
497
524 if test_name == 'proper_auth_token':
498 if test_name == 'proper_auth_token':
525 auth_token = user_admin.api_key
499 auth_token = user_admin.api_key
526 assert auth_token
500 assert auth_token
527
501
528 with fixture.anon_access(False):
502 with fixture.anon_access(False):
529 # webtest uses linter to check if response is bytes,
503 # webtest uses linter to check if response is bytes,
530 # and we use memoryview here as a wrapper, quick turn-off
504 # and we use memoryview here as a wrapper, quick turn-off
531 self.app.lint = False
505 self.app.lint = False
532 self.app.get(
506 self.app.get(
533 route_path('repo_commit_raw',
507 route_path('repo_commit_raw',
534 repo_name=HG_REPO, commit_id='tip',
508 repo_name=HG_REPO, commit_id='tip',
535 params=dict(api_key=auth_token)),
509 params=dict(api_key=auth_token)),
536 status=code)
510 status=code)
537
511
538 @pytest.mark.parametrize("test_name, auth_token, code", [
512 @pytest.mark.parametrize("test_name, auth_token, code", [
539 ('proper_auth_token', None, 200),
513 ('proper_auth_token', None, 200),
540 ('wrong_auth_token', '123456', 302),
514 ('wrong_auth_token', '123456', 302),
541 ])
515 ])
542 def test_access_whitelisted_page_via_auth_token_bound_to_token(
516 def test_access_whitelisted_page_via_auth_token_bound_to_token(
543 self, test_name, auth_token, code, user_admin):
517 self, test_name, auth_token, code, user_admin):
544
518
545 expected_token = auth_token
519 expected_token = auth_token
546 if test_name == 'proper_auth_token':
520 if test_name == 'proper_auth_token':
547 auth_token = user_admin.api_key
521 auth_token = user_admin.api_key
548 expected_token = auth_token
522 expected_token = auth_token
549 assert auth_token
523 assert auth_token
550
524
551 whitelist = self._get_api_whitelist([
525 whitelist = self._get_api_whitelist([
552 'RepoCommitsView:repo_commit_raw@{}'.format(expected_token)])
526 'RepoCommitsView:repo_commit_raw@{}'.format(expected_token)])
553
527
554 with mock.patch.dict('rhodecode.CONFIG', whitelist):
528 with mock.patch.dict('rhodecode.CONFIG', whitelist):
555
529
556 with fixture.anon_access(False):
530 with fixture.anon_access(False):
557 # webtest uses linter to check if response is bytes,
531 # webtest uses linter to check if response is bytes,
558 # and we use memoryview here as a wrapper, quick turn-off
532 # and we use memoryview here as a wrapper, quick turn-off
559 self.app.lint = False
533 self.app.lint = False
560
534
561 self.app.get(
535 self.app.get(
562 route_path('repo_commit_raw',
536 route_path('repo_commit_raw',
563 repo_name=HG_REPO, commit_id='tip',
537 repo_name=HG_REPO, commit_id='tip',
564 params=dict(api_key=auth_token)),
538 params=dict(api_key=auth_token)),
565 status=code)
539 status=code)
566
540
567 def test_access_page_via_extra_auth_token(self):
541 def test_access_page_via_extra_auth_token(self):
568 whitelist = self._get_api_whitelist(whitelist_view)
542 whitelist = self._get_api_whitelist(whitelist_view)
569 with mock.patch.dict('rhodecode.CONFIG', whitelist):
543 with mock.patch.dict('rhodecode.CONFIG', whitelist):
570 assert whitelist_view == \
544 assert whitelist_view == \
571 whitelist['api_access_controllers_whitelist']
545 whitelist['api_access_controllers_whitelist']
572
546
573 new_auth_token = AuthTokenModel().create(
547 new_auth_token = AuthTokenModel().create(
574 TEST_USER_ADMIN_LOGIN, 'test')
548 TEST_USER_ADMIN_LOGIN, 'test')
575 Session().commit()
549 Session().commit()
576 with fixture.anon_access(False):
550 with fixture.anon_access(False):
577 # webtest uses linter to check if response is bytes,
551 # webtest uses linter to check if response is bytes,
578 # and we use memoryview here as a wrapper, quick turn-off
552 # and we use memoryview here as a wrapper, quick turn-off
579 self.app.lint = False
553 self.app.lint = False
580 self.app.get(
554 self.app.get(
581 route_path('repo_commit_raw',
555 route_path('repo_commit_raw',
582 repo_name=HG_REPO, commit_id='tip',
556 repo_name=HG_REPO, commit_id='tip',
583 params=dict(api_key=new_auth_token.api_key)),
557 params=dict(api_key=new_auth_token.api_key)),
584 status=200)
558 status=200)
585
559
586 def test_access_page_via_expired_auth_token(self):
560 def test_access_page_via_expired_auth_token(self):
587 whitelist = self._get_api_whitelist(whitelist_view)
561 whitelist = self._get_api_whitelist(whitelist_view)
588 with mock.patch.dict('rhodecode.CONFIG', whitelist):
562 with mock.patch.dict('rhodecode.CONFIG', whitelist):
589 assert whitelist_view == \
563 assert whitelist_view == \
590 whitelist['api_access_controllers_whitelist']
564 whitelist['api_access_controllers_whitelist']
591
565
592 new_auth_token = AuthTokenModel().create(
566 new_auth_token = AuthTokenModel().create(
593 TEST_USER_ADMIN_LOGIN, 'test')
567 TEST_USER_ADMIN_LOGIN, 'test')
594 Session().commit()
568 Session().commit()
595 # patch the api key and make it expired
569 # patch the api key and make it expired
596 new_auth_token.expires = 0
570 new_auth_token.expires = 0
597 Session().add(new_auth_token)
571 Session().add(new_auth_token)
598 Session().commit()
572 Session().commit()
599 with fixture.anon_access(False):
573 with fixture.anon_access(False):
600 # webtest uses linter to check if response is bytes,
574 # webtest uses linter to check if response is bytes,
601 # and we use memoryview here as a wrapper, quick turn-off
575 # and we use memoryview here as a wrapper, quick turn-off
602 self.app.lint = False
576 self.app.lint = False
603 self.app.get(
577 self.app.get(
604 route_path('repo_commit_raw',
578 route_path('repo_commit_raw',
605 repo_name=HG_REPO, commit_id='tip',
579 repo_name=HG_REPO, commit_id='tip',
606 params=dict(api_key=new_auth_token.api_key)),
580 params=dict(api_key=new_auth_token.api_key)),
607 status=302)
581 status=302)
@@ -1,118 +1,94 b''
1 # Copyright (C) 2010-2023 RhodeCode GmbH
1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 #
2 #
3 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
14 #
15 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19 import pytest
19 import pytest
20
20
21 from rhodecode.lib import helpers as h
21 from rhodecode.lib import helpers as h
22 from rhodecode.tests import (
22 from rhodecode.tests import (
23 TestController, clear_cache_regions,
23 TestController, clear_cache_regions,
24 TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS)
24 TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS)
25 from rhodecode.tests.fixture import Fixture
25 from rhodecode.tests.fixture import Fixture
26 from rhodecode.tests.utils import AssertResponse
26 from rhodecode.tests.utils import AssertResponse
27
27 from rhodecode.tests.routes import route_path
28 fixture = Fixture()
29
28
30
29
31 def route_path(name, params=None, **kwargs):
30 fixture = Fixture()
32 import urllib.request
33 import urllib.parse
34 import urllib.error
35 from rhodecode.apps._base import ADMIN_PREFIX
36
37 base_url = {
38 'login': ADMIN_PREFIX + '/login',
39 'logout': ADMIN_PREFIX + '/logout',
40 'register': ADMIN_PREFIX + '/register',
41 'reset_password':
42 ADMIN_PREFIX + '/password_reset',
43 'reset_password_confirmation':
44 ADMIN_PREFIX + '/password_reset_confirmation',
45
46 'admin_permissions_application':
47 ADMIN_PREFIX + '/permissions/application',
48 'admin_permissions_application_update':
49 ADMIN_PREFIX + '/permissions/application/update',
50 }[name].format(**kwargs)
51
52 if params:
53 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
54 return base_url
55
31
56
32
57 class TestPasswordReset(TestController):
33 class TestPasswordReset(TestController):
58
34
59 @pytest.mark.parametrize(
35 @pytest.mark.parametrize(
60 'pwd_reset_setting, show_link, show_reset', [
36 'pwd_reset_setting, show_link, show_reset', [
61 ('hg.password_reset.enabled', True, True),
37 ('hg.password_reset.enabled', True, True),
62 ('hg.password_reset.hidden', False, True),
38 ('hg.password_reset.hidden', False, True),
63 ('hg.password_reset.disabled', False, False),
39 ('hg.password_reset.disabled', False, False),
64 ])
40 ])
65 def test_password_reset_settings(
41 def test_password_reset_settings(
66 self, pwd_reset_setting, show_link, show_reset):
42 self, pwd_reset_setting, show_link, show_reset):
67 clear_cache_regions()
43 clear_cache_regions()
68 self.log_user(TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS)
44 self.log_user(TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS)
69 params = {
45 params = {
70 'csrf_token': self.csrf_token,
46 'csrf_token': self.csrf_token,
71 'anonymous': 'True',
47 'anonymous': 'True',
72 'default_register': 'hg.register.auto_activate',
48 'default_register': 'hg.register.auto_activate',
73 'default_register_message': '',
49 'default_register_message': '',
74 'default_password_reset': pwd_reset_setting,
50 'default_password_reset': pwd_reset_setting,
75 'default_extern_activate': 'hg.extern_activate.auto',
51 'default_extern_activate': 'hg.extern_activate.auto',
76 }
52 }
77 resp = self.app.post(
53 resp = self.app.post(
78 route_path('admin_permissions_application_update'), params=params)
54 route_path('admin_permissions_application_update'), params=params)
79 self.logout_user()
55 self.logout_user()
80
56
81 login_page = self.app.get(route_path('login'))
57 login_page = self.app.get(route_path('login'))
82 asr_login = AssertResponse(login_page)
58 asr_login = AssertResponse(login_page)
83
59
84 if show_link:
60 if show_link:
85 asr_login.one_element_exists('a.pwd_reset')
61 asr_login.one_element_exists('a.pwd_reset')
86 else:
62 else:
87 asr_login.no_element_exists('a.pwd_reset')
63 asr_login.no_element_exists('a.pwd_reset')
88
64
89 response = self.app.get(route_path('reset_password'))
65 response = self.app.get(route_path('reset_password'))
90
66
91 assert_response = response.assert_response()
67 assert_response = response.assert_response()
92 if show_reset:
68 if show_reset:
93 response.mustcontain('Send password reset email')
69 response.mustcontain('Send password reset email')
94 assert_response.one_element_exists('#email')
70 assert_response.one_element_exists('#email')
95 assert_response.one_element_exists('#send')
71 assert_response.one_element_exists('#send')
96 else:
72 else:
97 response.mustcontain('Password reset is disabled.')
73 response.mustcontain('Password reset is disabled.')
98 assert_response.no_element_exists('#email')
74 assert_response.no_element_exists('#email')
99 assert_response.no_element_exists('#send')
75 assert_response.no_element_exists('#send')
100
76
101 def test_password_form_disabled(self):
77 def test_password_form_disabled(self):
102 self.log_user(TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS)
78 self.log_user(TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS)
103 params = {
79 params = {
104 'csrf_token': self.csrf_token,
80 'csrf_token': self.csrf_token,
105 'anonymous': 'True',
81 'anonymous': 'True',
106 'default_register': 'hg.register.auto_activate',
82 'default_register': 'hg.register.auto_activate',
107 'default_register_message': '',
83 'default_register_message': '',
108 'default_password_reset': 'hg.password_reset.disabled',
84 'default_password_reset': 'hg.password_reset.disabled',
109 'default_extern_activate': 'hg.extern_activate.auto',
85 'default_extern_activate': 'hg.extern_activate.auto',
110 }
86 }
111 self.app.post(route_path('admin_permissions_application_update'), params=params)
87 self.app.post(route_path('admin_permissions_application_update'), params=params)
112 self.logout_user()
88 self.logout_user()
113
89
114 response = self.app.post(
90 response = self.app.post(
115 route_path('reset_password'), {'email': 'lisa@rhodecode.com',}
91 route_path('reset_password'), {'email': 'lisa@rhodecode.com',}
116 )
92 )
117 response = response.follow()
93 response = response.follow()
118 response.mustcontain('Password reset is disabled.')
94 response.mustcontain('Password reset is disabled.')
@@ -1,109 +1,98 b''
1 # Copyright (C) 2010-2023 RhodeCode GmbH
1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 #
2 #
3 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
14 #
15 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19 import pytest
19 import pytest
20
20
21 from rhodecode.apps._base import ADMIN_PREFIX
21 from rhodecode.apps._base import ADMIN_PREFIX
22 from rhodecode.model.db import User
22 from rhodecode.model.db import User
23 from rhodecode.tests import (
23 from rhodecode.tests import (
24 TestController, route_path_generator, assert_session_flash)
24 TestController, assert_session_flash)
25 from rhodecode.tests.fixture import Fixture
25 from rhodecode.tests.fixture import Fixture
26 from rhodecode.tests.utils import AssertResponse
26 from rhodecode.tests.routes import route_path
27
28 fixture = Fixture()
29
27
30
28
31 def route_path(name, params=None, **kwargs):
29 fixture = Fixture()
32 url_defs = {
33 'my_account_auth_tokens':
34 ADMIN_PREFIX + '/my_account/auth_tokens',
35 'my_account_auth_tokens_add':
36 ADMIN_PREFIX + '/my_account/auth_tokens/new',
37 'my_account_auth_tokens_delete':
38 ADMIN_PREFIX + '/my_account/auth_tokens/delete',
39 }
40 return route_path_generator(url_defs, name=name, params=params, **kwargs)
41
30
42
31
43 class TestMyAccountAuthTokens(TestController):
32 class TestMyAccountAuthTokens(TestController):
44
33
45 def test_my_account_auth_tokens(self):
34 def test_my_account_auth_tokens(self):
46 usr = self.log_user('test_regular2', 'test12')
35 usr = self.log_user('test_regular2', 'test12')
47 user = User.get(usr['user_id'])
36 user = User.get(usr['user_id'])
48 response = self.app.get(route_path('my_account_auth_tokens'))
37 response = self.app.get(route_path('my_account_auth_tokens'))
49 for token in user.auth_tokens:
38 for token in user.auth_tokens:
50 response.mustcontain(token[:4])
39 response.mustcontain(token[:4])
51 response.mustcontain('never')
40 response.mustcontain('never')
52
41
53 def test_my_account_add_auth_tokens_wrong_csrf(self, user_util):
42 def test_my_account_add_auth_tokens_wrong_csrf(self, user_util):
54 user = user_util.create_user(password='qweqwe')
43 user = user_util.create_user(password='qweqwe')
55 self.log_user(user.username, 'qweqwe')
44 self.log_user(user.username, 'qweqwe')
56
45
57 self.app.post(
46 self.app.post(
58 route_path('my_account_auth_tokens_add'),
47 route_path('my_account_auth_tokens_add'),
59 {'description': 'desc', 'lifetime': -1}, status=403)
48 {'description': 'desc', 'lifetime': -1}, status=403)
60
49
61 @pytest.mark.parametrize("desc, lifetime", [
50 @pytest.mark.parametrize("desc, lifetime", [
62 ('forever', -1),
51 ('forever', -1),
63 ('5mins', 60*5),
52 ('5mins', 60*5),
64 ('30days', 60*60*24*30),
53 ('30days', 60*60*24*30),
65 ])
54 ])
66 def test_my_account_add_auth_tokens(self, desc, lifetime, user_util):
55 def test_my_account_add_auth_tokens(self, desc, lifetime, user_util):
67 user = user_util.create_user(password='qweqwe')
56 user = user_util.create_user(password='qweqwe')
68 user_id = user.user_id
57 user_id = user.user_id
69 self.log_user(user.username, 'qweqwe')
58 self.log_user(user.username, 'qweqwe')
70
59
71 response = self.app.post(
60 response = self.app.post(
72 route_path('my_account_auth_tokens_add'),
61 route_path('my_account_auth_tokens_add'),
73 {'description': desc, 'lifetime': lifetime,
62 {'description': desc, 'lifetime': lifetime,
74 'csrf_token': self.csrf_token})
63 'csrf_token': self.csrf_token})
75 assert_session_flash(response, 'Auth token successfully created')
64 assert_session_flash(response, 'Auth token successfully created')
76
65
77 response = response.follow()
66 response = response.follow()
78 user = User.get(user_id)
67 user = User.get(user_id)
79 for auth_token in user.auth_tokens:
68 for auth_token in user.auth_tokens:
80 response.mustcontain(auth_token[:4])
69 response.mustcontain(auth_token[:4])
81
70
82 def test_my_account_delete_auth_token(self, user_util):
71 def test_my_account_delete_auth_token(self, user_util):
83 user = user_util.create_user(password='qweqwe')
72 user = user_util.create_user(password='qweqwe')
84 user_id = user.user_id
73 user_id = user.user_id
85 self.log_user(user.username, 'qweqwe')
74 self.log_user(user.username, 'qweqwe')
86
75
87 user = User.get(user_id)
76 user = User.get(user_id)
88 keys = user.get_auth_tokens()
77 keys = user.get_auth_tokens()
89 assert 2 == len(keys)
78 assert 2 == len(keys)
90
79
91 response = self.app.post(
80 response = self.app.post(
92 route_path('my_account_auth_tokens_add'),
81 route_path('my_account_auth_tokens_add'),
93 {'description': 'desc', 'lifetime': -1,
82 {'description': 'desc', 'lifetime': -1,
94 'csrf_token': self.csrf_token})
83 'csrf_token': self.csrf_token})
95 assert_session_flash(response, 'Auth token successfully created')
84 assert_session_flash(response, 'Auth token successfully created')
96 response.follow()
85 response.follow()
97
86
98 user = User.get(user_id)
87 user = User.get(user_id)
99 keys = user.get_auth_tokens()
88 keys = user.get_auth_tokens()
100 assert 3 == len(keys)
89 assert 3 == len(keys)
101
90
102 response = self.app.post(
91 response = self.app.post(
103 route_path('my_account_auth_tokens_delete'),
92 route_path('my_account_auth_tokens_delete'),
104 {'del_auth_token': keys[0].user_api_key_id, 'csrf_token': self.csrf_token})
93 {'del_auth_token': keys[0].user_api_key_id, 'csrf_token': self.csrf_token})
105 assert_session_flash(response, 'Auth token successfully deleted')
94 assert_session_flash(response, 'Auth token successfully deleted')
106
95
107 user = User.get(user_id)
96 user = User.get(user_id)
108 keys = user.auth_tokens
97 keys = user.auth_tokens
109 assert 2 == len(keys)
98 assert 2 == len(keys)
@@ -1,208 +1,190 b''
1 # Copyright (C) 2016-2023 RhodeCode GmbH
1 # Copyright (C) 2016-2023 RhodeCode GmbH
2 #
2 #
3 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
14 #
15 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19
19
20 # Copyright (C) 2016-2023 RhodeCode GmbH
20 # Copyright (C) 2016-2023 RhodeCode GmbH
21 #
21 #
22 # This program is free software: you can redistribute it and/or modify
22 # This program is free software: you can redistribute it and/or modify
23 # it under the terms of the GNU Affero General Public License, version 3
23 # it under the terms of the GNU Affero General Public License, version 3
24 # (only), as published by the Free Software Foundation.
24 # (only), as published by the Free Software Foundation.
25 #
25 #
26 # This program is distributed in the hope that it will be useful,
26 # This program is distributed in the hope that it will be useful,
27 # but WITHOUT ANY WARRANTY; without even the implied warranty of
27 # but WITHOUT ANY WARRANTY; without even the implied warranty of
28 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
28 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
29 # GNU General Public License for more details.
29 # GNU General Public License for more details.
30 #
30 #
31 # You should have received a copy of the GNU Affero General Public License
31 # You should have received a copy of the GNU Affero General Public License
32 # along with this program. If not, see <http://www.gnu.org/licenses/>.
32 # along with this program. If not, see <http://www.gnu.org/licenses/>.
33 #
33 #
34 # This program is dual-licensed. If you wish to learn more about the
34 # This program is dual-licensed. If you wish to learn more about the
35 # RhodeCode Enterprise Edition, including its added features, Support services,
35 # RhodeCode Enterprise Edition, including its added features, Support services,
36 # and proprietary license terms, please see https://rhodecode.com/licenses/
36 # and proprietary license terms, please see https://rhodecode.com/licenses/
37
37
38 import pytest
38 import pytest
39
39
40 from rhodecode.model.db import User
40 from rhodecode.model.db import User
41 from rhodecode.tests import TestController, assert_session_flash
41 from rhodecode.tests import TestController, assert_session_flash
42 from rhodecode.lib import helpers as h
42 from rhodecode.tests.routes import route_path
43
44
45 def route_path(name, params=None, **kwargs):
46 import urllib.request
47 import urllib.parse
48 import urllib.error
49 from rhodecode.apps._base import ADMIN_PREFIX
50
51 base_url = {
52 'my_account_edit': ADMIN_PREFIX + '/my_account/edit',
53 'my_account_update': ADMIN_PREFIX + '/my_account/update',
54 'my_account_pullrequests': ADMIN_PREFIX + '/my_account/pull_requests',
55 'my_account_pullrequests_data': ADMIN_PREFIX + '/my_account/pull_requests/data',
56 }[name].format(**kwargs)
57
58 if params:
59 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
60 return base_url
61
43
62
44
63 class TestMyAccountEdit(TestController):
45 class TestMyAccountEdit(TestController):
64
46
65 def test_my_account_edit(self):
47 def test_my_account_edit(self):
66 self.log_user()
48 self.log_user()
67 response = self.app.get(route_path('my_account_edit'))
49 response = self.app.get(route_path('my_account_edit'))
68
50
69 response.mustcontain('value="test_admin')
51 response.mustcontain('value="test_admin')
70
52
71 @pytest.mark.backends("git", "hg")
53 @pytest.mark.backends("git", "hg")
72 def test_my_account_my_pullrequests(self, pr_util):
54 def test_my_account_my_pullrequests(self, pr_util):
73 self.log_user()
55 self.log_user()
74 response = self.app.get(route_path('my_account_pullrequests'))
56 response = self.app.get(route_path('my_account_pullrequests'))
75 response.mustcontain('There are currently no open pull '
57 response.mustcontain('There are currently no open pull '
76 'requests requiring your participation.')
58 'requests requiring your participation.')
77
59
78 @pytest.mark.backends("git", "hg")
60 @pytest.mark.backends("git", "hg")
79 @pytest.mark.parametrize('params, expected_title', [
61 @pytest.mark.parametrize('params, expected_title', [
80 ({'closed': 1}, 'Closed'),
62 ({'closed': 1}, 'Closed'),
81 ({'awaiting_my_review': 1}, 'Awaiting my review'),
63 ({'awaiting_my_review': 1}, 'Awaiting my review'),
82 ])
64 ])
83 def test_my_account_my_pullrequests_data(self, pr_util, xhr_header, params, expected_title):
65 def test_my_account_my_pullrequests_data(self, pr_util, xhr_header, params, expected_title):
84 self.log_user()
66 self.log_user()
85 response = self.app.get(route_path('my_account_pullrequests_data'),
67 response = self.app.get(route_path('my_account_pullrequests_data'),
86 extra_environ=xhr_header)
68 extra_environ=xhr_header)
87 assert response.json == {
69 assert response.json == {
88 'data': [], 'draw': None,
70 'data': [], 'draw': None,
89 'recordsFiltered': 0, 'recordsTotal': 0}
71 'recordsFiltered': 0, 'recordsTotal': 0}
90
72
91 pr = pr_util.create_pull_request(title='TestMyAccountPR')
73 pr = pr_util.create_pull_request(title='TestMyAccountPR')
92 expected = {
74 expected = {
93 'author_raw': 'RhodeCode Admin',
75 'author_raw': 'RhodeCode Admin',
94 'name_raw': pr.pull_request_id
76 'name_raw': pr.pull_request_id
95 }
77 }
96 response = self.app.get(route_path('my_account_pullrequests_data'),
78 response = self.app.get(route_path('my_account_pullrequests_data'),
97 extra_environ=xhr_header)
79 extra_environ=xhr_header)
98 assert response.json['recordsTotal'] == 1
80 assert response.json['recordsTotal'] == 1
99 assert response.json['data'][0]['author_raw'] == expected['author_raw']
81 assert response.json['data'][0]['author_raw'] == expected['author_raw']
100
82
101 assert response.json['data'][0]['author_raw'] == expected['author_raw']
83 assert response.json['data'][0]['author_raw'] == expected['author_raw']
102 assert response.json['data'][0]['name_raw'] == expected['name_raw']
84 assert response.json['data'][0]['name_raw'] == expected['name_raw']
103
85
104 @pytest.mark.parametrize(
86 @pytest.mark.parametrize(
105 "name, attrs", [
87 "name, attrs", [
106 ('firstname', {'firstname': 'new_username'}),
88 ('firstname', {'firstname': 'new_username'}),
107 ('lastname', {'lastname': 'new_username'}),
89 ('lastname', {'lastname': 'new_username'}),
108 ('admin', {'admin': True}),
90 ('admin', {'admin': True}),
109 ('admin', {'admin': False}),
91 ('admin', {'admin': False}),
110 ('extern_type', {'extern_type': 'ldap'}),
92 ('extern_type', {'extern_type': 'ldap'}),
111 ('extern_type', {'extern_type': None}),
93 ('extern_type', {'extern_type': None}),
112 # ('extern_name', {'extern_name': 'test'}),
94 # ('extern_name', {'extern_name': 'test'}),
113 # ('extern_name', {'extern_name': None}),
95 # ('extern_name', {'extern_name': None}),
114 ('active', {'active': False}),
96 ('active', {'active': False}),
115 ('active', {'active': True}),
97 ('active', {'active': True}),
116 ('email', {'email': 'some@email.com'}),
98 ('email', {'email': 'some@email.com'}),
117 ])
99 ])
118 def test_my_account_update(self, name, attrs, user_util):
100 def test_my_account_update(self, name, attrs, user_util):
119 usr = user_util.create_user(password='qweqwe')
101 usr = user_util.create_user(password='qweqwe')
120 params = usr.get_api_data() # current user data
102 params = usr.get_api_data() # current user data
121 user_id = usr.user_id
103 user_id = usr.user_id
122 self.log_user(
104 self.log_user(
123 username=usr.username, password='qweqwe')
105 username=usr.username, password='qweqwe')
124
106
125 params.update({'password_confirmation': ''})
107 params.update({'password_confirmation': ''})
126 params.update({'new_password': ''})
108 params.update({'new_password': ''})
127 params.update({'extern_type': 'rhodecode'})
109 params.update({'extern_type': 'rhodecode'})
128 params.update({'extern_name': 'rhodecode'})
110 params.update({'extern_name': 'rhodecode'})
129 params.update({'csrf_token': self.csrf_token})
111 params.update({'csrf_token': self.csrf_token})
130
112
131 params.update(attrs)
113 params.update(attrs)
132 # my account page cannot set language param yet, only for admins
114 # my account page cannot set language param yet, only for admins
133 del params['language']
115 del params['language']
134 if name == 'email':
116 if name == 'email':
135 uem = user_util.create_additional_user_email(usr, attrs['email'])
117 uem = user_util.create_additional_user_email(usr, attrs['email'])
136 email_before = User.get(user_id).email
118 email_before = User.get(user_id).email
137
119
138 response = self.app.post(route_path('my_account_update'), params)
120 response = self.app.post(route_path('my_account_update'), params)
139
121
140 assert_session_flash(
122 assert_session_flash(
141 response, 'Your account was updated successfully')
123 response, 'Your account was updated successfully')
142
124
143 del params['csrf_token']
125 del params['csrf_token']
144
126
145 updated_user = User.get(user_id)
127 updated_user = User.get(user_id)
146 updated_params = updated_user.get_api_data()
128 updated_params = updated_user.get_api_data()
147 updated_params.update({'password_confirmation': ''})
129 updated_params.update({'password_confirmation': ''})
148 updated_params.update({'new_password': ''})
130 updated_params.update({'new_password': ''})
149
131
150 params['last_login'] = updated_params['last_login']
132 params['last_login'] = updated_params['last_login']
151 params['last_activity'] = updated_params['last_activity']
133 params['last_activity'] = updated_params['last_activity']
152 # my account page cannot set language param yet, only for admins
134 # my account page cannot set language param yet, only for admins
153 # but we get this info from API anyway
135 # but we get this info from API anyway
154 params['language'] = updated_params['language']
136 params['language'] = updated_params['language']
155
137
156 if name == 'email':
138 if name == 'email':
157 params['emails'] = [attrs['email'], email_before]
139 params['emails'] = [attrs['email'], email_before]
158 if name == 'extern_type':
140 if name == 'extern_type':
159 # cannot update this via form, expected value is original one
141 # cannot update this via form, expected value is original one
160 params['extern_type'] = "rhodecode"
142 params['extern_type'] = "rhodecode"
161 if name == 'extern_name':
143 if name == 'extern_name':
162 # cannot update this via form, expected value is original one
144 # cannot update this via form, expected value is original one
163 params['extern_name'] = str(user_id)
145 params['extern_name'] = str(user_id)
164 if name == 'active':
146 if name == 'active':
165 # my account cannot deactivate account
147 # my account cannot deactivate account
166 params['active'] = True
148 params['active'] = True
167 if name == 'admin':
149 if name == 'admin':
168 # my account cannot make you an admin !
150 # my account cannot make you an admin !
169 params['admin'] = False
151 params['admin'] = False
170
152
171 assert params == updated_params
153 assert params == updated_params
172
154
173 def test_my_account_update_err_email_not_exists_in_emails(self):
155 def test_my_account_update_err_email_not_exists_in_emails(self):
174 self.log_user()
156 self.log_user()
175
157
176 new_email = 'test_regular@mail.com' # not in emails
158 new_email = 'test_regular@mail.com' # not in emails
177 params = {
159 params = {
178 'username': 'test_admin',
160 'username': 'test_admin',
179 'new_password': 'test12',
161 'new_password': 'test12',
180 'password_confirmation': 'test122',
162 'password_confirmation': 'test122',
181 'firstname': 'NewName',
163 'firstname': 'NewName',
182 'lastname': 'NewLastname',
164 'lastname': 'NewLastname',
183 'email': new_email,
165 'email': new_email,
184 'csrf_token': self.csrf_token,
166 'csrf_token': self.csrf_token,
185 }
167 }
186
168
187 response = self.app.post(route_path('my_account_update'),
169 response = self.app.post(route_path('my_account_update'),
188 params=params)
170 params=params)
189
171
190 response.mustcontain('"test_regular@mail.com" is not one of test_admin@mail.com')
172 response.mustcontain('"test_regular@mail.com" is not one of test_admin@mail.com')
191
173
192 def test_my_account_update_bad_email_address(self):
174 def test_my_account_update_bad_email_address(self):
193 self.log_user('test_regular2', 'test12')
175 self.log_user('test_regular2', 'test12')
194
176
195 new_email = 'newmail.pl'
177 new_email = 'newmail.pl'
196 params = {
178 params = {
197 'username': 'test_admin',
179 'username': 'test_admin',
198 'new_password': 'test12',
180 'new_password': 'test12',
199 'password_confirmation': 'test122',
181 'password_confirmation': 'test122',
200 'firstname': 'NewName',
182 'firstname': 'NewName',
201 'lastname': 'NewLastname',
183 'lastname': 'NewLastname',
202 'email': new_email,
184 'email': new_email,
203 'csrf_token': self.csrf_token,
185 'csrf_token': self.csrf_token,
204 }
186 }
205 response = self.app.post(route_path('my_account_update'),
187 response = self.app.post(route_path('my_account_update'),
206 params=params)
188 params=params)
207
189
208 response.mustcontain('"newmail.pl" is not one of test_regular2@mail.com')
190 response.mustcontain('"newmail.pl" is not one of test_regular2@mail.com')
@@ -1,75 +1,66 b''
1 # Copyright (C) 2010-2023 RhodeCode GmbH
1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 #
2 #
3 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
14 #
15 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19 import pytest
19 import pytest
20
20
21 from rhodecode.apps._base import ADMIN_PREFIX
21 from rhodecode.apps._base import ADMIN_PREFIX
22 from rhodecode.model.db import User, UserEmailMap
22 from rhodecode.model.db import User, UserEmailMap
23 from rhodecode.tests import (
23 from rhodecode.tests import (
24 TestController, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_EMAIL,
24 TestController, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_EMAIL,
25 assert_session_flash, TEST_USER_REGULAR_PASS)
25 assert_session_flash, TEST_USER_REGULAR_PASS)
26 from rhodecode.tests.fixture import Fixture
26 from rhodecode.tests.fixture import Fixture
27
27 from rhodecode.tests.routes import route_path
28 fixture = Fixture()
29
28
30
29
31 def route_path(name, **kwargs):
30 fixture = Fixture()
32 return {
33 'my_account_emails':
34 ADMIN_PREFIX + '/my_account/emails',
35 'my_account_emails_add':
36 ADMIN_PREFIX + '/my_account/emails/new',
37 'my_account_emails_delete':
38 ADMIN_PREFIX + '/my_account/emails/delete',
39 }[name].format(**kwargs)
40
31
41
32
42 class TestMyAccountEmails(TestController):
33 class TestMyAccountEmails(TestController):
43 def test_my_account_my_emails(self):
34 def test_my_account_my_emails(self):
44 self.log_user()
35 self.log_user()
45 response = self.app.get(route_path('my_account_emails'))
36 response = self.app.get(route_path('my_account_emails'))
46 response.mustcontain('No additional emails specified')
37 response.mustcontain('No additional emails specified')
47
38
48 def test_my_account_my_emails_add_remove(self):
39 def test_my_account_my_emails_add_remove(self):
49 self.log_user()
40 self.log_user()
50 response = self.app.get(route_path('my_account_emails'))
41 response = self.app.get(route_path('my_account_emails'))
51 response.mustcontain('No additional emails specified')
42 response.mustcontain('No additional emails specified')
52
43
53 response = self.app.post(route_path('my_account_emails_add'),
44 response = self.app.post(route_path('my_account_emails_add'),
54 {'email': 'foo@barz.com',
45 {'email': 'foo@barz.com',
55 'current_password': TEST_USER_REGULAR_PASS,
46 'current_password': TEST_USER_REGULAR_PASS,
56 'csrf_token': self.csrf_token})
47 'csrf_token': self.csrf_token})
57
48
58 response = self.app.get(route_path('my_account_emails'))
49 response = self.app.get(route_path('my_account_emails'))
59
50
60 email_id = UserEmailMap.query().filter(
51 email_id = UserEmailMap.query().filter(
61 UserEmailMap.user == User.get_by_username(
52 UserEmailMap.user == User.get_by_username(
62 TEST_USER_ADMIN_LOGIN)).filter(
53 TEST_USER_ADMIN_LOGIN)).filter(
63 UserEmailMap.email == 'foo@barz.com').one().email_id
54 UserEmailMap.email == 'foo@barz.com').one().email_id
64
55
65 response.mustcontain('foo@barz.com')
56 response.mustcontain('foo@barz.com')
66 response.mustcontain('<input id="del_email_id" name="del_email_id" '
57 response.mustcontain('<input id="del_email_id" name="del_email_id" '
67 'type="hidden" value="%s" />' % email_id)
58 'type="hidden" value="%s" />' % email_id)
68
59
69 response = self.app.post(
60 response = self.app.post(
70 route_path('my_account_emails_delete'), {
61 route_path('my_account_emails_delete'), {
71 'del_email_id': email_id,
62 'del_email_id': email_id,
72 'csrf_token': self.csrf_token})
63 'csrf_token': self.csrf_token})
73 assert_session_flash(response, 'Email successfully deleted')
64 assert_session_flash(response, 'Email successfully deleted')
74 response = self.app.get(route_path('my_account_emails'))
65 response = self.app.get(route_path('my_account_emails'))
75 response.mustcontain('No additional emails specified')
66 response.mustcontain('No additional emails specified')
@@ -1,207 +1,186 b''
1 # Copyright (C) 2010-2023 RhodeCode GmbH
1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 #
2 #
3 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
14 #
15 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19 import pytest
19 import pytest
20
20
21 from rhodecode.apps._base import ADMIN_PREFIX
22 from rhodecode.tests import (
21 from rhodecode.tests import (
23 TestController, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS,
22 TestController, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS,
24 TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS)
23 TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS)
25 from rhodecode.tests.fixture import Fixture
24 from rhodecode.tests.fixture import Fixture
25 from rhodecode.tests.routes import route_path
26
26
27 from rhodecode.model.db import Notification, User
27 from rhodecode.model.db import Notification, User
28 from rhodecode.model.user import UserModel
29 from rhodecode.model.notification import NotificationModel
28 from rhodecode.model.notification import NotificationModel
30 from rhodecode.model.meta import Session
29 from rhodecode.model.meta import Session
31
30
32 fixture = Fixture()
31 fixture = Fixture()
33
32
34
33
35 def route_path(name, params=None, **kwargs):
36 import urllib.request
37 import urllib.parse
38 import urllib.error
39 from rhodecode.apps._base import ADMIN_PREFIX
40
41 base_url = {
42 'notifications_show_all': ADMIN_PREFIX + '/notifications',
43 'notifications_mark_all_read': ADMIN_PREFIX + '/notifications_mark_all_read',
44 'notifications_show': ADMIN_PREFIX + '/notifications/{notification_id}',
45 'notifications_update': ADMIN_PREFIX + '/notifications/{notification_id}/update',
46 'notifications_delete': ADMIN_PREFIX + '/notifications/{notification_id}/delete',
47
48 }[name].format(**kwargs)
49
50 if params:
51 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
52 return base_url
53
54
55 class TestNotificationsController(TestController):
34 class TestNotificationsController(TestController):
56
35
57 def teardown_method(self, method):
36 def teardown_method(self, method):
58 for n in Notification.query().all():
37 for n in Notification.query().all():
59 inst = Notification.get(n.notification_id)
38 inst = Notification.get(n.notification_id)
60 Session().delete(inst)
39 Session().delete(inst)
61 Session().commit()
40 Session().commit()
62
41
63 def test_mark_all_read(self, user_util):
42 def test_mark_all_read(self, user_util):
64 user = user_util.create_user(password='qweqwe')
43 user = user_util.create_user(password='qweqwe')
65 self.log_user(user.username, 'qweqwe')
44 self.log_user(user.username, 'qweqwe')
66
45
67 self.app.post(
46 self.app.post(
68 route_path('notifications_mark_all_read'), status=302,
47 route_path('notifications_mark_all_read'), status=302,
69 params={'csrf_token': self.csrf_token}
48 params={'csrf_token': self.csrf_token}
70 )
49 )
71
50
72 def test_show_all(self, user_util):
51 def test_show_all(self, user_util):
73 user = user_util.create_user(password='qweqwe')
52 user = user_util.create_user(password='qweqwe')
74 user_id = user.user_id
53 user_id = user.user_id
75 self.log_user(user.username, 'qweqwe')
54 self.log_user(user.username, 'qweqwe')
76
55
77 response = self.app.get(
56 response = self.app.get(
78 route_path('notifications_show_all', params={'type': 'all'}))
57 route_path('notifications_show_all', params={'type': 'all'}))
79 response.mustcontain(
58 response.mustcontain(
80 '<div class="table">No notifications here yet</div>')
59 '<div class="table">No notifications here yet</div>')
81
60
82 notification = NotificationModel().create(
61 notification = NotificationModel().create(
83 created_by=user_id, notification_subject=u'test_notification_1',
62 created_by=user_id, notification_subject=u'test_notification_1',
84 notification_body=u'notification_1', recipients=[user_id])
63 notification_body=u'notification_1', recipients=[user_id])
85 Session().commit()
64 Session().commit()
86 notification_id = notification.notification_id
65 notification_id = notification.notification_id
87
66
88 response = self.app.get(route_path('notifications_show_all',
67 response = self.app.get(route_path('notifications_show_all',
89 params={'type': 'all'}))
68 params={'type': 'all'}))
90 response.mustcontain('id="notification_%s"' % notification_id)
69 response.mustcontain('id="notification_%s"' % notification_id)
91
70
92 def test_show_unread(self, user_util):
71 def test_show_unread(self, user_util):
93 user = user_util.create_user(password='qweqwe')
72 user = user_util.create_user(password='qweqwe')
94 user_id = user.user_id
73 user_id = user.user_id
95 self.log_user(user.username, 'qweqwe')
74 self.log_user(user.username, 'qweqwe')
96
75
97 response = self.app.get(route_path('notifications_show_all'))
76 response = self.app.get(route_path('notifications_show_all'))
98 response.mustcontain(
77 response.mustcontain(
99 '<div class="table">No notifications here yet</div>')
78 '<div class="table">No notifications here yet</div>')
100
79
101 notification = NotificationModel().create(
80 notification = NotificationModel().create(
102 created_by=user_id, notification_subject=u'test_notification_1',
81 created_by=user_id, notification_subject=u'test_notification_1',
103 notification_body=u'notification_1', recipients=[user_id])
82 notification_body=u'notification_1', recipients=[user_id])
104
83
105 # mark the USER notification as unread
84 # mark the USER notification as unread
106 user_notification = NotificationModel().get_user_notification(
85 user_notification = NotificationModel().get_user_notification(
107 user_id, notification)
86 user_id, notification)
108 user_notification.read = False
87 user_notification.read = False
109
88
110 Session().commit()
89 Session().commit()
111 notification_id = notification.notification_id
90 notification_id = notification.notification_id
112
91
113 response = self.app.get(route_path('notifications_show_all'))
92 response = self.app.get(route_path('notifications_show_all'))
114 response.mustcontain('id="notification_%s"' % notification_id)
93 response.mustcontain('id="notification_%s"' % notification_id)
115 response.mustcontain('<div class="desc unread')
94 response.mustcontain('<div class="desc unread')
116
95
117 @pytest.mark.parametrize('user,password', [
96 @pytest.mark.parametrize('user,password', [
118 (TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS),
97 (TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS),
119 (TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS),
98 (TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS),
120 ])
99 ])
121 def test_delete(self, user, password, user_util):
100 def test_delete(self, user, password, user_util):
122 self.log_user(user, password)
101 self.log_user(user, password)
123 cur_user = self._get_logged_user()
102 cur_user = self._get_logged_user()
124
103
125 u1 = user_util.create_user()
104 u1 = user_util.create_user()
126 u2 = user_util.create_user()
105 u2 = user_util.create_user()
127
106
128 # make notifications
107 # make notifications
129 notification = NotificationModel().create(
108 notification = NotificationModel().create(
130 created_by=cur_user, notification_subject=u'test',
109 created_by=cur_user, notification_subject=u'test',
131 notification_body=u'hi there', recipients=[cur_user, u1, u2])
110 notification_body=u'hi there', recipients=[cur_user, u1, u2])
132 Session().commit()
111 Session().commit()
133 u1 = User.get(u1.user_id)
112 u1 = User.get(u1.user_id)
134 u2 = User.get(u2.user_id)
113 u2 = User.get(u2.user_id)
135
114
136 # check DB
115 # check DB
137 def get_notif(un):
116 def get_notif(un):
138 return [x.notification for x in un]
117 return [x.notification for x in un]
139 assert get_notif(cur_user.notifications) == [notification]
118 assert get_notif(cur_user.notifications) == [notification]
140 assert get_notif(u1.notifications) == [notification]
119 assert get_notif(u1.notifications) == [notification]
141 assert get_notif(u2.notifications) == [notification]
120 assert get_notif(u2.notifications) == [notification]
142 cur_usr_id = cur_user.user_id
121 cur_usr_id = cur_user.user_id
143
122
144 response = self.app.post(
123 response = self.app.post(
145 route_path('notifications_delete',
124 route_path('notifications_delete',
146 notification_id=notification.notification_id),
125 notification_id=notification.notification_id),
147 params={'csrf_token': self.csrf_token})
126 params={'csrf_token': self.csrf_token})
148 assert response.json == 'ok'
127 assert response.json == 'ok'
149
128
150 cur_user = User.get(cur_usr_id)
129 cur_user = User.get(cur_usr_id)
151 assert cur_user.notifications == []
130 assert cur_user.notifications == []
152
131
153 @pytest.mark.parametrize('user,password', [
132 @pytest.mark.parametrize('user,password', [
154 (TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS),
133 (TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS),
155 (TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS),
134 (TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS),
156 ])
135 ])
157 def test_show(self, user, password, user_util):
136 def test_show(self, user, password, user_util):
158 self.log_user(user, password)
137 self.log_user(user, password)
159 cur_user = self._get_logged_user()
138 cur_user = self._get_logged_user()
160 u1 = user_util.create_user()
139 u1 = user_util.create_user()
161 u2 = user_util.create_user()
140 u2 = user_util.create_user()
162
141
163 subject = u'test'
142 subject = u'test'
164 notif_body = u'hi there'
143 notif_body = u'hi there'
165 notification = NotificationModel().create(
144 notification = NotificationModel().create(
166 created_by=cur_user, notification_subject=subject,
145 created_by=cur_user, notification_subject=subject,
167 notification_body=notif_body, recipients=[cur_user, u1, u2])
146 notification_body=notif_body, recipients=[cur_user, u1, u2])
168 Session().commit()
147 Session().commit()
169
148
170 response = self.app.get(
149 response = self.app.get(
171 route_path('notifications_show',
150 route_path('notifications_show',
172 notification_id=notification.notification_id))
151 notification_id=notification.notification_id))
173
152
174 response.mustcontain(subject)
153 response.mustcontain(subject)
175 response.mustcontain(notif_body)
154 response.mustcontain(notif_body)
176
155
177 @pytest.mark.parametrize('user,password', [
156 @pytest.mark.parametrize('user,password', [
178 (TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS),
157 (TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS),
179 (TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS),
158 (TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS),
180 ])
159 ])
181 def test_update(self, user, password, user_util):
160 def test_update(self, user, password, user_util):
182 self.log_user(user, password)
161 self.log_user(user, password)
183 cur_user = self._get_logged_user()
162 cur_user = self._get_logged_user()
184 u1 = user_util.create_user()
163 u1 = user_util.create_user()
185 u2 = user_util.create_user()
164 u2 = user_util.create_user()
186
165
187 # make notifications
166 # make notifications
188 recipients = [cur_user, u1, u2]
167 recipients = [cur_user, u1, u2]
189 notification = NotificationModel().create(
168 notification = NotificationModel().create(
190 created_by=cur_user, notification_subject=u'test',
169 created_by=cur_user, notification_subject=u'test',
191 notification_body=u'hi there', recipients=recipients)
170 notification_body=u'hi there', recipients=recipients)
192 Session().commit()
171 Session().commit()
193
172
194 for u_obj in recipients:
173 for u_obj in recipients:
195 # if it's current user, he has his message already read
174 # if it's current user, he has his message already read
196 read = u_obj.username == user
175 read = u_obj.username == user
197 assert len(u_obj.notifications) == 1
176 assert len(u_obj.notifications) == 1
198 assert u_obj.notifications[0].read == read
177 assert u_obj.notifications[0].read == read
199
178
200 response = self.app.post(
179 response = self.app.post(
201 route_path('notifications_update',
180 route_path('notifications_update',
202 notification_id=notification.notification_id),
181 notification_id=notification.notification_id),
203 params={'csrf_token': self.csrf_token})
182 params={'csrf_token': self.csrf_token})
204 assert response.json == 'ok'
183 assert response.json == 'ok'
205
184
206 cur_user = self._get_logged_user()
185 cur_user = self._get_logged_user()
207 assert True is cur_user.notifications[0].read
186 assert True is cur_user.notifications[0].read
@@ -1,143 +1,134 b''
1
1
2 # Copyright (C) 2010-2023 RhodeCode GmbH
2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 #
3 #
4 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
7 #
7 #
8 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
11 # GNU General Public License for more details.
12 #
12 #
13 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
15 #
16 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19
19
20 import pytest
20 import pytest
21 import mock
21 import mock
22
22
23 from rhodecode.apps._base import ADMIN_PREFIX
23 from rhodecode.apps._base import ADMIN_PREFIX
24 from rhodecode.lib import helpers as h
24 from rhodecode.lib import helpers as h
25 from rhodecode.lib.auth import check_password
25 from rhodecode.lib.auth import check_password
26 from rhodecode.model.meta import Session
26 from rhodecode.model.meta import Session
27 from rhodecode.model.user import UserModel
27 from rhodecode.model.user import UserModel
28 from rhodecode.tests import assert_session_flash
28 from rhodecode.tests import assert_session_flash
29 from rhodecode.tests.fixture import Fixture, TestController, error_function
29 from rhodecode.tests.fixture import Fixture, TestController, error_function
30 from rhodecode.tests.routes import route_path
30
31
31 fixture = Fixture()
32 fixture = Fixture()
32
33
33
34
34 def route_path(name, **kwargs):
35 return {
36 'home': '/',
37 'my_account_password':
38 ADMIN_PREFIX + '/my_account/password',
39 'my_account_password_update':
40 ADMIN_PREFIX + '/my_account/password/update',
41 }[name].format(**kwargs)
42
43
44 test_user_1 = 'testme'
35 test_user_1 = 'testme'
45 test_user_1_password = '0jd83nHNS/d23n'
36 test_user_1_password = '0jd83nHNS/d23n'
46
37
47
38
48 class TestMyAccountPassword(TestController):
39 class TestMyAccountPassword(TestController):
49 def test_valid_change_password(self, user_util):
40 def test_valid_change_password(self, user_util):
50 new_password = 'my_new_valid_password'
41 new_password = 'my_new_valid_password'
51 user = user_util.create_user(password=test_user_1_password)
42 user = user_util.create_user(password=test_user_1_password)
52 self.log_user(user.username, test_user_1_password)
43 self.log_user(user.username, test_user_1_password)
53
44
54 form_data = [
45 form_data = [
55 ('current_password', test_user_1_password),
46 ('current_password', test_user_1_password),
56 ('__start__', 'new_password:mapping'),
47 ('__start__', 'new_password:mapping'),
57 ('new_password', new_password),
48 ('new_password', new_password),
58 ('new_password-confirm', new_password),
49 ('new_password-confirm', new_password),
59 ('__end__', 'new_password:mapping'),
50 ('__end__', 'new_password:mapping'),
60 ('csrf_token', self.csrf_token),
51 ('csrf_token', self.csrf_token),
61 ]
52 ]
62 response = self.app.post(
53 response = self.app.post(
63 route_path('my_account_password_update'), form_data).follow()
54 route_path('my_account_password_update'), form_data).follow()
64 assert 'Successfully updated password' in response
55 assert 'Successfully updated password' in response
65
56
66 # check_password depends on user being in session
57 # check_password depends on user being in session
67 Session().add(user)
58 Session().add(user)
68 try:
59 try:
69 assert check_password(new_password, user.password)
60 assert check_password(new_password, user.password)
70 finally:
61 finally:
71 Session().expunge(user)
62 Session().expunge(user)
72
63
73 @pytest.mark.parametrize('current_pw, new_pw, confirm_pw', [
64 @pytest.mark.parametrize('current_pw, new_pw, confirm_pw', [
74 ('', 'abcdef123', 'abcdef123'),
65 ('', 'abcdef123', 'abcdef123'),
75 ('wrong_pw', 'abcdef123', 'abcdef123'),
66 ('wrong_pw', 'abcdef123', 'abcdef123'),
76 (test_user_1_password, test_user_1_password, test_user_1_password),
67 (test_user_1_password, test_user_1_password, test_user_1_password),
77 (test_user_1_password, '', ''),
68 (test_user_1_password, '', ''),
78 (test_user_1_password, 'abcdef123', ''),
69 (test_user_1_password, 'abcdef123', ''),
79 (test_user_1_password, '', 'abcdef123'),
70 (test_user_1_password, '', 'abcdef123'),
80 (test_user_1_password, 'not_the', 'same_pw'),
71 (test_user_1_password, 'not_the', 'same_pw'),
81 (test_user_1_password, 'short', 'short'),
72 (test_user_1_password, 'short', 'short'),
82 ])
73 ])
83 def test_invalid_change_password(self, current_pw, new_pw, confirm_pw,
74 def test_invalid_change_password(self, current_pw, new_pw, confirm_pw,
84 user_util):
75 user_util):
85 user = user_util.create_user(password=test_user_1_password)
76 user = user_util.create_user(password=test_user_1_password)
86 self.log_user(user.username, test_user_1_password)
77 self.log_user(user.username, test_user_1_password)
87
78
88 form_data = [
79 form_data = [
89 ('current_password', current_pw),
80 ('current_password', current_pw),
90 ('__start__', 'new_password:mapping'),
81 ('__start__', 'new_password:mapping'),
91 ('new_password', new_pw),
82 ('new_password', new_pw),
92 ('new_password-confirm', confirm_pw),
83 ('new_password-confirm', confirm_pw),
93 ('__end__', 'new_password:mapping'),
84 ('__end__', 'new_password:mapping'),
94 ('csrf_token', self.csrf_token),
85 ('csrf_token', self.csrf_token),
95 ]
86 ]
96 response = self.app.post(
87 response = self.app.post(
97 route_path('my_account_password_update'), form_data)
88 route_path('my_account_password_update'), form_data)
98
89
99 assert_response = response.assert_response()
90 assert_response = response.assert_response()
100 assert assert_response.get_elements('.error-block')
91 assert assert_response.get_elements('.error-block')
101
92
102 @mock.patch.object(UserModel, 'update_user', error_function)
93 @mock.patch.object(UserModel, 'update_user', error_function)
103 def test_invalid_change_password_exception(self, user_util):
94 def test_invalid_change_password_exception(self, user_util):
104 user = user_util.create_user(password=test_user_1_password)
95 user = user_util.create_user(password=test_user_1_password)
105 self.log_user(user.username, test_user_1_password)
96 self.log_user(user.username, test_user_1_password)
106
97
107 form_data = [
98 form_data = [
108 ('current_password', test_user_1_password),
99 ('current_password', test_user_1_password),
109 ('__start__', 'new_password:mapping'),
100 ('__start__', 'new_password:mapping'),
110 ('new_password', '123456'),
101 ('new_password', '123456'),
111 ('new_password-confirm', '123456'),
102 ('new_password-confirm', '123456'),
112 ('__end__', 'new_password:mapping'),
103 ('__end__', 'new_password:mapping'),
113 ('csrf_token', self.csrf_token),
104 ('csrf_token', self.csrf_token),
114 ]
105 ]
115 response = self.app.post(
106 response = self.app.post(
116 route_path('my_account_password_update'), form_data)
107 route_path('my_account_password_update'), form_data)
117 assert_session_flash(
108 assert_session_flash(
118 response, 'Error occurred during update of user password')
109 response, 'Error occurred during update of user password')
119
110
120 def test_password_is_updated_in_session_on_password_change(self, user_util):
111 def test_password_is_updated_in_session_on_password_change(self, user_util):
121 old_password = 'abcdef123'
112 old_password = 'abcdef123'
122 new_password = 'abcdef124'
113 new_password = 'abcdef124'
123
114
124 user = user_util.create_user(password=old_password)
115 user = user_util.create_user(password=old_password)
125 session = self.log_user(user.username, old_password)
116 session = self.log_user(user.username, old_password)
126 old_password_hash = session['password']
117 old_password_hash = session['password']
127
118
128 form_data = [
119 form_data = [
129 ('current_password', old_password),
120 ('current_password', old_password),
130 ('__start__', 'new_password:mapping'),
121 ('__start__', 'new_password:mapping'),
131 ('new_password', new_password),
122 ('new_password', new_password),
132 ('new_password-confirm', new_password),
123 ('new_password-confirm', new_password),
133 ('__end__', 'new_password:mapping'),
124 ('__end__', 'new_password:mapping'),
134 ('csrf_token', self.csrf_token),
125 ('csrf_token', self.csrf_token),
135 ]
126 ]
136 self.app.post(
127 self.app.post(
137 route_path('my_account_password_update'), form_data)
128 route_path('my_account_password_update'), form_data)
138
129
139 response = self.app.get(route_path('home'))
130 response = self.app.get(route_path('home'))
140 session = response.get_session_from_response()
131 session = response.get_session_from_response()
141 new_password_hash = session['rhodecode_user']['password']
132 new_password_hash = session['rhodecode_user']['password']
142
133
143 assert old_password_hash != new_password_hash No newline at end of file
134 assert old_password_hash != new_password_hash
@@ -1,54 +1,45 b''
1
1
2 # Copyright (C) 2010-2023 RhodeCode GmbH
2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 #
3 #
4 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
7 #
7 #
8 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
11 # GNU General Public License for more details.
12 #
12 #
13 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
15 #
16 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19
19
20 import pytest
21
22 from rhodecode.apps._base import ADMIN_PREFIX
23 from rhodecode.tests import (
20 from rhodecode.tests import (
24 TestController, TEST_USER_ADMIN_LOGIN,
21 TestController, TEST_USER_ADMIN_LOGIN,
25 TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
22 TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
26 from rhodecode.tests.fixture import Fixture
23 from rhodecode.tests.fixture import Fixture
24 from rhodecode.tests.routes import route_path
27
25
28 fixture = Fixture()
26 fixture = Fixture()
29
27
30
28
31 def route_path(name, **kwargs):
32 return {
33 'my_account':
34 ADMIN_PREFIX + '/my_account/profile',
35 }[name].format(**kwargs)
36
37
38 class TestMyAccountProfile(TestController):
29 class TestMyAccountProfile(TestController):
39
30
40 def test_my_account(self):
31 def test_my_account(self):
41 self.log_user()
32 self.log_user()
42 response = self.app.get(route_path('my_account'))
33 response = self.app.get(route_path('my_account'))
43
34
44 response.mustcontain(TEST_USER_ADMIN_LOGIN)
35 response.mustcontain(TEST_USER_ADMIN_LOGIN)
45 response.mustcontain('href="/_admin/my_account/edit"')
36 response.mustcontain('href="/_admin/my_account/edit"')
46 response.mustcontain('Photo')
37 response.mustcontain('Photo')
47
38
48 def test_my_account_regular_user(self):
39 def test_my_account_regular_user(self):
49 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
40 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
50 response = self.app.get(route_path('my_account'))
41 response = self.app.get(route_path('my_account'))
51
42
52 response.mustcontain(TEST_USER_REGULAR_LOGIN)
43 response.mustcontain(TEST_USER_REGULAR_LOGIN)
53 response.mustcontain('href="/_admin/my_account/edit"')
44 response.mustcontain('href="/_admin/my_account/edit"')
54 response.mustcontain('Photo')
45 response.mustcontain('Photo')
@@ -1,74 +1,57 b''
1
1
2 # Copyright (C) 2010-2023 RhodeCode GmbH
2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 #
3 #
4 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
7 #
7 #
8 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
11 # GNU General Public License for more details.
12 #
12 #
13 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
15 #
16 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19
19
20 import pytest
20 from rhodecode.model.db import User, Repository, UserFollowing
21
21 from rhodecode.tests import TestController, TEST_USER_ADMIN_LOGIN
22 from rhodecode.apps._base import ADMIN_PREFIX
23 from rhodecode.model.db import User, UserEmailMap, Repository, UserFollowing
24 from rhodecode.tests import (
25 TestController, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_EMAIL,
26 assert_session_flash)
27 from rhodecode.tests.fixture import Fixture
22 from rhodecode.tests.fixture import Fixture
23 from rhodecode.tests.routes import route_path
28
24
29 fixture = Fixture()
25 fixture = Fixture()
30
26
31
27
32 def route_path(name, **kwargs):
33 return {
34 'my_account_repos':
35 ADMIN_PREFIX + '/my_account/repos',
36 'my_account_watched':
37 ADMIN_PREFIX + '/my_account/watched',
38 'my_account_perms':
39 ADMIN_PREFIX + '/my_account/perms',
40 'my_account_notifications':
41 ADMIN_PREFIX + '/my_account/notifications',
42 }[name].format(**kwargs)
43
44
45 class TestMyAccountSimpleViews(TestController):
28 class TestMyAccountSimpleViews(TestController):
46
29
47 def test_my_account_my_repos(self, autologin_user):
30 def test_my_account_my_repos(self, autologin_user):
48 response = self.app.get(route_path('my_account_repos'))
31 response = self.app.get(route_path('my_account_repos'))
49 repos = Repository.query().filter(
32 repos = Repository.query().filter(
50 Repository.user == User.get_by_username(
33 Repository.user == User.get_by_username(
51 TEST_USER_ADMIN_LOGIN)).all()
34 TEST_USER_ADMIN_LOGIN)).all()
52 for repo in repos:
35 for repo in repos:
53 response.mustcontain(f'"name_raw":"{repo.repo_name}"')
36 response.mustcontain(f'"name_raw":"{repo.repo_name}"')
54
37
55 def test_my_account_my_watched(self, autologin_user):
38 def test_my_account_my_watched(self, autologin_user):
56 response = self.app.get(route_path('my_account_watched'))
39 response = self.app.get(route_path('my_account_watched'))
57
40
58 repos = UserFollowing.query().filter(
41 repos = UserFollowing.query().filter(
59 UserFollowing.user == User.get_by_username(
42 UserFollowing.user == User.get_by_username(
60 TEST_USER_ADMIN_LOGIN)).all()
43 TEST_USER_ADMIN_LOGIN)).all()
61 for repo in repos:
44 for repo in repos:
62 response.mustcontain(f'"name_raw":"{repo.follows_repository.repo_name}"')
45 response.mustcontain(f'"name_raw":"{repo.follows_repository.repo_name}"')
63
46
64 def test_my_account_perms(self, autologin_user):
47 def test_my_account_perms(self, autologin_user):
65 response = self.app.get(route_path('my_account_perms'))
48 response = self.app.get(route_path('my_account_perms'))
66 assert_response = response.assert_response()
49 assert_response = response.assert_response()
67 assert assert_response.get_elements('.perm_tag.none')
50 assert assert_response.get_elements('.perm_tag.none')
68 assert assert_response.get_elements('.perm_tag.read')
51 assert assert_response.get_elements('.perm_tag.read')
69 assert assert_response.get_elements('.perm_tag.write')
52 assert assert_response.get_elements('.perm_tag.write')
70 assert assert_response.get_elements('.perm_tag.admin')
53 assert assert_response.get_elements('.perm_tag.admin')
71
54
72 def test_my_account_notifications(self, autologin_user):
55 def test_my_account_notifications(self, autologin_user):
73 response = self.app.get(route_path('my_account_notifications'))
56 response = self.app.get(route_path('my_account_notifications'))
74 response.mustcontain('Test flash message')
57 response.mustcontain('Test flash message')
@@ -1,164 +1,142 b''
1
1
2 # Copyright (C) 2010-2023 RhodeCode GmbH
2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 #
3 #
4 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
7 #
7 #
8 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
11 # GNU General Public License for more details.
12 #
12 #
13 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
15 #
16 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19
19
20 import pytest
21
20
22 from rhodecode.model.db import User, UserSshKeys
21 from rhodecode.model.db import User, UserSshKeys
23
22
24 from rhodecode.tests import TestController, assert_session_flash
23 from rhodecode.tests import TestController, assert_session_flash
25 from rhodecode.tests.fixture import Fixture
24 from rhodecode.tests.fixture import Fixture
25 from rhodecode.tests.routes import route_path
26
26
27 fixture = Fixture()
27 fixture = Fixture()
28
28
29
29
30 def route_path(name, params=None, **kwargs):
31 import urllib.request
32 import urllib.parse
33 import urllib.error
34 from rhodecode.apps._base import ADMIN_PREFIX
35
36 base_url = {
37 'my_account_ssh_keys':
38 ADMIN_PREFIX + '/my_account/ssh_keys',
39 'my_account_ssh_keys_generate':
40 ADMIN_PREFIX + '/my_account/ssh_keys/generate',
41 'my_account_ssh_keys_add':
42 ADMIN_PREFIX + '/my_account/ssh_keys/new',
43 'my_account_ssh_keys_delete':
44 ADMIN_PREFIX + '/my_account/ssh_keys/delete',
45 }[name].format(**kwargs)
46
47 if params:
48 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
49 return base_url
50
51
52 class TestMyAccountSshKeysView(TestController):
30 class TestMyAccountSshKeysView(TestController):
53 INVALID_KEY = """\
31 INVALID_KEY = """\
54 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDk+77sjDzVeB6vevJsuZds1iNU5
32 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDk+77sjDzVeB6vevJsuZds1iNU5
55 LANOa5CU5G/9JYIA6RYsWWMO7mbsR82IUckdqOHmxSykfR1D1TdluyIpQLrwgH5kb
33 LANOa5CU5G/9JYIA6RYsWWMO7mbsR82IUckdqOHmxSykfR1D1TdluyIpQLrwgH5kb
56 n8FkVI8zBMCKakxowvN67B0R7b1BT4PPzW2JlOXei/m9W12ZY484VTow6/B+kf2Q8
34 n8FkVI8zBMCKakxowvN67B0R7b1BT4PPzW2JlOXei/m9W12ZY484VTow6/B+kf2Q8
57 cP8tmCJmKWZma5Em7OTUhvjyQVNz3v7HfeY5Hq0Ci4ECJ59hepFDabJvtAXg9XrI6
35 cP8tmCJmKWZma5Em7OTUhvjyQVNz3v7HfeY5Hq0Ci4ECJ59hepFDabJvtAXg9XrI6
58 jvdphZTc30I4fG8+hBHzpeFxUGvSGNtXPUbwaAY8j/oHYrTpMgkj6pUEFsiKfC5zP
36 jvdphZTc30I4fG8+hBHzpeFxUGvSGNtXPUbwaAY8j/oHYrTpMgkj6pUEFsiKfC5zP
59 qPFR5HyKTCHW0nFUJnZsbyFT5hMiF/hZkJc9A0ZbdSvJwCRQ/g3bmdL
37 qPFR5HyKTCHW0nFUJnZsbyFT5hMiF/hZkJc9A0ZbdSvJwCRQ/g3bmdL
60 your_email@example.com
38 your_email@example.com
61 """
39 """
62 VALID_KEY = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDk+77sjDzVeB6vev' \
40 VALID_KEY = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDk+77sjDzVeB6vev' \
63 'JsuZds1iNU5LANOa5CU5G/9JYIA6RYsWWMO7mbsR82IUckdqOHmxSy' \
41 'JsuZds1iNU5LANOa5CU5G/9JYIA6RYsWWMO7mbsR82IUckdqOHmxSy' \
64 'kfR1D1TdluyIpQLrwgH5kbn8FkVI8zBMCKakxowvN67B0R7b1BT4PP' \
42 'kfR1D1TdluyIpQLrwgH5kbn8FkVI8zBMCKakxowvN67B0R7b1BT4PP' \
65 'zW2JlOXei/m9W12ZY484VTow6/B+kf2Q8cP8tmCJmKWZma5Em7OTUh' \
43 'zW2JlOXei/m9W12ZY484VTow6/B+kf2Q8cP8tmCJmKWZma5Em7OTUh' \
66 'vjyQVNz3v7HfeY5Hq0Ci4ECJ59hepFDabJvtAXg9XrI6jvdphZTc30' \
44 'vjyQVNz3v7HfeY5Hq0Ci4ECJ59hepFDabJvtAXg9XrI6jvdphZTc30' \
67 'I4fG8+hBHzpeFxUGvSGNtXPUbwaAY8j/oHYrTpMgkj6pUEFsiKfC5zPq' \
45 'I4fG8+hBHzpeFxUGvSGNtXPUbwaAY8j/oHYrTpMgkj6pUEFsiKfC5zPq' \
68 'PFR5HyKTCHW0nFUJnZsbyFT5hMiF/hZkJc9A0ZbdSvJwCRQ/g3bmdL ' \
46 'PFR5HyKTCHW0nFUJnZsbyFT5hMiF/hZkJc9A0ZbdSvJwCRQ/g3bmdL ' \
69 'your_email@example.com'
47 'your_email@example.com'
70 FINGERPRINT = 'MD5:01:4f:ad:29:22:6e:01:37:c9:d2:52:26:52:b0:2d:93'
48 FINGERPRINT = 'MD5:01:4f:ad:29:22:6e:01:37:c9:d2:52:26:52:b0:2d:93'
71
49
72 def test_add_ssh_key_error(self, user_util):
50 def test_add_ssh_key_error(self, user_util):
73 user = user_util.create_user(password='qweqwe')
51 user = user_util.create_user(password='qweqwe')
74 self.log_user(user.username, 'qweqwe')
52 self.log_user(user.username, 'qweqwe')
75
53
76 key_data = self.INVALID_KEY
54 key_data = self.INVALID_KEY
77
55
78 desc = 'MY SSH KEY'
56 desc = 'MY SSH KEY'
79 response = self.app.post(
57 response = self.app.post(
80 route_path('my_account_ssh_keys_add'),
58 route_path('my_account_ssh_keys_add'),
81 {'description': desc, 'key_data': key_data,
59 {'description': desc, 'key_data': key_data,
82 'csrf_token': self.csrf_token})
60 'csrf_token': self.csrf_token})
83 assert_session_flash(response, 'An error occurred during ssh '
61 assert_session_flash(response, 'An error occurred during ssh '
84 'key saving: Unable to decode the key')
62 'key saving: Unable to decode the key')
85
63
86 def test_ssh_key_duplicate(self, user_util):
64 def test_ssh_key_duplicate(self, user_util):
87 user = user_util.create_user(password='qweqwe')
65 user = user_util.create_user(password='qweqwe')
88 self.log_user(user.username, 'qweqwe')
66 self.log_user(user.username, 'qweqwe')
89 key_data = self.VALID_KEY
67 key_data = self.VALID_KEY
90
68
91 desc = 'MY SSH KEY'
69 desc = 'MY SSH KEY'
92 response = self.app.post(
70 response = self.app.post(
93 route_path('my_account_ssh_keys_add'),
71 route_path('my_account_ssh_keys_add'),
94 {'description': desc, 'key_data': key_data,
72 {'description': desc, 'key_data': key_data,
95 'csrf_token': self.csrf_token})
73 'csrf_token': self.csrf_token})
96 assert_session_flash(response, 'Ssh Key successfully created')
74 assert_session_flash(response, 'Ssh Key successfully created')
97 response.follow() # flush session flash
75 response.follow() # flush session flash
98
76
99 # add the same key AGAIN
77 # add the same key AGAIN
100 desc = 'MY SSH KEY'
78 desc = 'MY SSH KEY'
101 response = self.app.post(
79 response = self.app.post(
102 route_path('my_account_ssh_keys_add'),
80 route_path('my_account_ssh_keys_add'),
103 {'description': desc, 'key_data': key_data,
81 {'description': desc, 'key_data': key_data,
104 'csrf_token': self.csrf_token})
82 'csrf_token': self.csrf_token})
105
83
106 err = 'Such key with fingerprint `{}` already exists, ' \
84 err = 'Such key with fingerprint `{}` already exists, ' \
107 'please use a different one'.format(self.FINGERPRINT)
85 'please use a different one'.format(self.FINGERPRINT)
108 assert_session_flash(response, 'An error occurred during ssh key '
86 assert_session_flash(response, 'An error occurred during ssh key '
109 'saving: {}'.format(err))
87 'saving: {}'.format(err))
110
88
111 def test_add_ssh_key(self, user_util):
89 def test_add_ssh_key(self, user_util):
112 user = user_util.create_user(password='qweqwe')
90 user = user_util.create_user(password='qweqwe')
113 self.log_user(user.username, 'qweqwe')
91 self.log_user(user.username, 'qweqwe')
114
92
115 key_data = self.VALID_KEY
93 key_data = self.VALID_KEY
116
94
117 desc = 'MY SSH KEY'
95 desc = 'MY SSH KEY'
118 response = self.app.post(
96 response = self.app.post(
119 route_path('my_account_ssh_keys_add'),
97 route_path('my_account_ssh_keys_add'),
120 {'description': desc, 'key_data': key_data,
98 {'description': desc, 'key_data': key_data,
121 'csrf_token': self.csrf_token})
99 'csrf_token': self.csrf_token})
122 assert_session_flash(response, 'Ssh Key successfully created')
100 assert_session_flash(response, 'Ssh Key successfully created')
123
101
124 response = response.follow()
102 response = response.follow()
125 response.mustcontain(desc)
103 response.mustcontain(desc)
126
104
127 def test_delete_ssh_key(self, user_util):
105 def test_delete_ssh_key(self, user_util):
128 user = user_util.create_user(password='qweqwe')
106 user = user_util.create_user(password='qweqwe')
129 user_id = user.user_id
107 user_id = user.user_id
130 self.log_user(user.username, 'qweqwe')
108 self.log_user(user.username, 'qweqwe')
131
109
132 key_data = self.VALID_KEY
110 key_data = self.VALID_KEY
133
111
134 desc = 'MY SSH KEY'
112 desc = 'MY SSH KEY'
135 response = self.app.post(
113 response = self.app.post(
136 route_path('my_account_ssh_keys_add'),
114 route_path('my_account_ssh_keys_add'),
137 {'description': desc, 'key_data': key_data,
115 {'description': desc, 'key_data': key_data,
138 'csrf_token': self.csrf_token})
116 'csrf_token': self.csrf_token})
139 assert_session_flash(response, 'Ssh Key successfully created')
117 assert_session_flash(response, 'Ssh Key successfully created')
140 response = response.follow() # flush the Session flash
118 response = response.follow() # flush the Session flash
141
119
142 # now delete our key
120 # now delete our key
143 keys = UserSshKeys.query().filter(UserSshKeys.user_id == user_id).all()
121 keys = UserSshKeys.query().filter(UserSshKeys.user_id == user_id).all()
144 assert 1 == len(keys)
122 assert 1 == len(keys)
145
123
146 response = self.app.post(
124 response = self.app.post(
147 route_path('my_account_ssh_keys_delete'),
125 route_path('my_account_ssh_keys_delete'),
148 {'del_ssh_key': keys[0].ssh_key_id,
126 {'del_ssh_key': keys[0].ssh_key_id,
149 'csrf_token': self.csrf_token})
127 'csrf_token': self.csrf_token})
150
128
151 assert_session_flash(response, 'Ssh key successfully deleted')
129 assert_session_flash(response, 'Ssh key successfully deleted')
152 keys = UserSshKeys.query().filter(UserSshKeys.user_id == user_id).all()
130 keys = UserSshKeys.query().filter(UserSshKeys.user_id == user_id).all()
153 assert 0 == len(keys)
131 assert 0 == len(keys)
154
132
155 def test_generate_keypair(self, user_util):
133 def test_generate_keypair(self, user_util):
156 user = user_util.create_user(password='qweqwe')
134 user = user_util.create_user(password='qweqwe')
157 self.log_user(user.username, 'qweqwe')
135 self.log_user(user.username, 'qweqwe')
158
136
159 response = self.app.get(
137 response = self.app.get(
160 route_path('my_account_ssh_keys_generate'))
138 route_path('my_account_ssh_keys_generate'))
161
139
162 response.mustcontain('Private key')
140 response.mustcontain('Private key')
163 response.mustcontain('Public key')
141 response.mustcontain('Public key')
164 response.mustcontain('-----BEGIN PRIVATE KEY-----')
142 response.mustcontain('-----BEGIN PRIVATE KEY-----')
@@ -1,89 +1,73 b''
1 # Copyright (C) 2010-2023 RhodeCode GmbH
1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 #
2 #
3 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
14 #
15 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19 import pytest
19 import pytest
20
20
21 from rhodecode.tests import assert_session_flash
21 from rhodecode.tests import assert_session_flash
22
22 from rhodecode.tests.routes import route_path
23
24 def route_path(name, params=None, **kwargs):
25 import urllib.request
26 import urllib.parse
27 import urllib.error
28
29 base_url = {
30 'edit_repo_group_advanced':
31 '/{repo_group_name}/_settings/advanced',
32 'edit_repo_group_advanced_delete':
33 '/{repo_group_name}/_settings/advanced/delete',
34 }[name].format(**kwargs)
35
36 if params:
37 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
38 return base_url
39
23
40
24
41 @pytest.mark.usefixtures("app")
25 @pytest.mark.usefixtures("app")
42 class TestRepoGroupsAdvancedView(object):
26 class TestRepoGroupsAdvancedView(object):
43
27
44 @pytest.mark.parametrize('repo_group_name', [
28 @pytest.mark.parametrize('repo_group_name', [
45 'gro',
29 'gro',
46 '12345',
30 '12345',
47 ])
31 ])
48 def test_show_advanced_settings(self, autologin_user, user_util, repo_group_name):
32 def test_show_advanced_settings(self, autologin_user, user_util, repo_group_name):
49 user_util._test_name = repo_group_name
33 user_util._test_name = repo_group_name
50 gr = user_util.create_repo_group()
34 gr = user_util.create_repo_group()
51 self.app.get(
35 self.app.get(
52 route_path('edit_repo_group_advanced',
36 route_path('edit_repo_group_advanced',
53 repo_group_name=gr.group_name))
37 repo_group_name=gr.group_name))
54
38
55 def test_show_advanced_settings_delete(self, autologin_user, user_util,
39 def test_show_advanced_settings_delete(self, autologin_user, user_util,
56 csrf_token):
40 csrf_token):
57 gr = user_util.create_repo_group(auto_cleanup=False)
41 gr = user_util.create_repo_group(auto_cleanup=False)
58 repo_group_name = gr.group_name
42 repo_group_name = gr.group_name
59
43
60 params = dict(
44 params = dict(
61 csrf_token=csrf_token
45 csrf_token=csrf_token
62 )
46 )
63 response = self.app.post(
47 response = self.app.post(
64 route_path('edit_repo_group_advanced_delete',
48 route_path('edit_repo_group_advanced_delete',
65 repo_group_name=repo_group_name), params=params)
49 repo_group_name=repo_group_name), params=params)
66 assert_session_flash(
50 assert_session_flash(
67 response, 'Removed repository group `{}`'.format(repo_group_name))
51 response, 'Removed repository group `{}`'.format(repo_group_name))
68
52
69 def test_delete_not_possible_with_objects_inside(self, autologin_user,
53 def test_delete_not_possible_with_objects_inside(self, autologin_user,
70 repo_groups, csrf_token):
54 repo_groups, csrf_token):
71 zombie_group, parent_group, child_group = repo_groups
55 zombie_group, parent_group, child_group = repo_groups
72
56
73 response = self.app.get(
57 response = self.app.get(
74 route_path('edit_repo_group_advanced',
58 route_path('edit_repo_group_advanced',
75 repo_group_name=parent_group.group_name))
59 repo_group_name=parent_group.group_name))
76
60
77 response.mustcontain(
61 response.mustcontain(
78 'This repository group includes 1 children repository group')
62 'This repository group includes 1 children repository group')
79
63
80 params = dict(
64 params = dict(
81 csrf_token=csrf_token
65 csrf_token=csrf_token
82 )
66 )
83 response = self.app.post(
67 response = self.app.post(
84 route_path('edit_repo_group_advanced_delete',
68 route_path('edit_repo_group_advanced_delete',
85 repo_group_name=parent_group.group_name), params=params)
69 repo_group_name=parent_group.group_name), params=params)
86
70
87 assert_session_flash(
71 assert_session_flash(
88 response, 'This repository group contains 1 subgroup '
72 response, 'This repository group contains 1 subgroup '
89 'and cannot be deleted')
73 'and cannot be deleted')
@@ -1,86 +1,70 b''
1 # Copyright (C) 2010-2023 RhodeCode GmbH
1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 #
2 #
3 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
14 #
15 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19 import pytest
19 import pytest
20
20
21 from rhodecode.tests.utils import permission_update_data_generator
21 from rhodecode.tests.utils import permission_update_data_generator
22
22 from rhodecode.tests.routes import route_path
23
24 def route_path(name, params=None, **kwargs):
25 import urllib.request
26 import urllib.parse
27 import urllib.error
28
29 base_url = {
30 'edit_repo_group_perms':
31 '/{repo_group_name:}/_settings/permissions',
32 'edit_repo_group_perms_update':
33 '/{repo_group_name}/_settings/permissions/update',
34 }[name].format(**kwargs)
35
36 if params:
37 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
38 return base_url
39
23
40
24
41 @pytest.mark.usefixtures("app")
25 @pytest.mark.usefixtures("app")
42 class TestRepoGroupPermissionsView(object):
26 class TestRepoGroupPermissionsView(object):
43
27
44 def test_edit_perms_view(self, user_util, autologin_user):
28 def test_edit_perms_view(self, user_util, autologin_user):
45 repo_group = user_util.create_repo_group()
29 repo_group = user_util.create_repo_group()
46
30
47 self.app.get(
31 self.app.get(
48 route_path('edit_repo_group_perms',
32 route_path('edit_repo_group_perms',
49 repo_group_name=repo_group.group_name), status=200)
33 repo_group_name=repo_group.group_name), status=200)
50
34
51 def test_update_permissions(self, csrf_token, user_util):
35 def test_update_permissions(self, csrf_token, user_util):
52 repo_group = user_util.create_repo_group()
36 repo_group = user_util.create_repo_group()
53 repo_group_name = repo_group.group_name
37 repo_group_name = repo_group.group_name
54 user = user_util.create_user()
38 user = user_util.create_user()
55 user_id = user.user_id
39 user_id = user.user_id
56 username = user.username
40 username = user.username
57
41
58 # grant new
42 # grant new
59 form_data = permission_update_data_generator(
43 form_data = permission_update_data_generator(
60 csrf_token,
44 csrf_token,
61 default='group.write',
45 default='group.write',
62 grant=[(user_id, 'group.write', username, 'user')])
46 grant=[(user_id, 'group.write', username, 'user')])
63
47
64 # recursive flag required for repo groups
48 # recursive flag required for repo groups
65 form_data.extend([('recursive', u'none')])
49 form_data.extend([('recursive', u'none')])
66
50
67 response = self.app.post(
51 response = self.app.post(
68 route_path('edit_repo_group_perms_update',
52 route_path('edit_repo_group_perms_update',
69 repo_group_name=repo_group_name), form_data).follow()
53 repo_group_name=repo_group_name), form_data).follow()
70
54
71 assert 'Repository Group permissions updated' in response
55 assert 'Repository Group permissions updated' in response
72
56
73 # revoke given
57 # revoke given
74 form_data = permission_update_data_generator(
58 form_data = permission_update_data_generator(
75 csrf_token,
59 csrf_token,
76 default='group.read',
60 default='group.read',
77 revoke=[(user_id, 'user')])
61 revoke=[(user_id, 'user')])
78
62
79 # recursive flag required for repo groups
63 # recursive flag required for repo groups
80 form_data.extend([('recursive', u'none')])
64 form_data.extend([('recursive', u'none')])
81
65
82 response = self.app.post(
66 response = self.app.post(
83 route_path('edit_repo_group_perms_update',
67 route_path('edit_repo_group_perms_update',
84 repo_group_name=repo_group_name), form_data).follow()
68 repo_group_name=repo_group_name), form_data).follow()
85
69
86 assert 'Repository Group permissions updated' in response
70 assert 'Repository Group permissions updated' in response
@@ -1,91 +1,78 b''
1
1
2 # Copyright (C) 2010-2023 RhodeCode GmbH
2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 #
3 #
4 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
7 #
7 #
8 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
11 # GNU General Public License for more details.
12 #
12 #
13 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
15 #
16 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19
19
20 import pytest
20 import pytest
21
21
22 from rhodecode.tests import assert_session_flash
22 from rhodecode.tests import assert_session_flash
23
23 from rhodecode.tests.routes import route_path
24
25 def route_path(name, params=None, **kwargs):
26 import urllib.request
27 import urllib.parse
28 import urllib.error
29
24
30 base_url = {
31 'edit_repo_group': '/{repo_group_name}/_edit',
32 # Update is POST to the above url
33 }[name].format(**kwargs)
34
35 if params:
36 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
37 return base_url
38
25
39
26
40 @pytest.mark.usefixtures("app")
27 @pytest.mark.usefixtures("app")
41 class TestRepoGroupsSettingsView(object):
28 class TestRepoGroupsSettingsView(object):
42
29
43 @pytest.mark.parametrize('repo_group_name', [
30 @pytest.mark.parametrize('repo_group_name', [
44 'gro',
31 'gro',
45 u'12345',
32 u'12345',
46 ])
33 ])
47 def test_edit(self, user_util, autologin_user, repo_group_name):
34 def test_edit(self, user_util, autologin_user, repo_group_name):
48 user_util._test_name = repo_group_name
35 user_util._test_name = repo_group_name
49 repo_group = user_util.create_repo_group()
36 repo_group = user_util.create_repo_group()
50
37
51 self.app.get(
38 self.app.get(
52 route_path('edit_repo_group', repo_group_name=repo_group.group_name),
39 route_path('edit_repo_group', repo_group_name=repo_group.group_name),
53 status=200)
40 status=200)
54
41
55 def test_update(self, csrf_token, autologin_user, user_util, rc_fixture):
42 def test_update(self, csrf_token, autologin_user, user_util, rc_fixture):
56 repo_group = user_util.create_repo_group()
43 repo_group = user_util.create_repo_group()
57 repo_group_name = repo_group.group_name
44 repo_group_name = repo_group.group_name
58
45
59 description = 'description for newly created repo group'
46 description = 'description for newly created repo group'
60 form_data = rc_fixture._get_group_create_params(
47 form_data = rc_fixture._get_group_create_params(
61 group_name=repo_group.group_name,
48 group_name=repo_group.group_name,
62 group_description=description,
49 group_description=description,
63 csrf_token=csrf_token,
50 csrf_token=csrf_token,
64 repo_group_name=repo_group.group_name,
51 repo_group_name=repo_group.group_name,
65 repo_group_owner=repo_group.user.username)
52 repo_group_owner=repo_group.user.username)
66
53
67 response = self.app.post(
54 response = self.app.post(
68 route_path('edit_repo_group',
55 route_path('edit_repo_group',
69 repo_group_name=repo_group.group_name),
56 repo_group_name=repo_group.group_name),
70 form_data,
57 form_data,
71 status=302)
58 status=302)
72
59
73 assert_session_flash(
60 assert_session_flash(
74 response, 'Repository Group `{}` updated successfully'.format(
61 response, 'Repository Group `{}` updated successfully'.format(
75 repo_group_name))
62 repo_group_name))
76
63
77 def test_update_fails_when_parent_pointing_to_self(
64 def test_update_fails_when_parent_pointing_to_self(
78 self, csrf_token, user_util, autologin_user, rc_fixture):
65 self, csrf_token, user_util, autologin_user, rc_fixture):
79 group = user_util.create_repo_group()
66 group = user_util.create_repo_group()
80 response = self.app.post(
67 response = self.app.post(
81 route_path('edit_repo_group', repo_group_name=group.group_name),
68 route_path('edit_repo_group', repo_group_name=group.group_name),
82 rc_fixture._get_group_create_params(
69 rc_fixture._get_group_create_params(
83 repo_group_name=group.group_name,
70 repo_group_name=group.group_name,
84 repo_group_owner=group.user.username,
71 repo_group_owner=group.user.username,
85 repo_group=group.group_id,
72 repo_group=group.group_id,
86 csrf_token=csrf_token),
73 csrf_token=csrf_token),
87 status=200
74 status=200
88 )
75 )
89 response.mustcontain(
76 response.mustcontain(
90 '<span class="error-message">"{}" is not one of -1'.format(
77 '<span class="error-message">"{}" is not one of -1'.format(
91 group.group_id))
78 group.group_id))
@@ -1,84 +1,69 b''
1 # Copyright (C) 2010-2023 RhodeCode GmbH
1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 #
2 #
3 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
14 #
15 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19 import pytest
19 import pytest
20 from rhodecode.model.db import Repository
20 from rhodecode.tests.routes import route_path
21
22
23 def route_path(name, params=None, **kwargs):
24 import urllib.request
25 import urllib.parse
26 import urllib.error
27
28 base_url = {
29 'pullrequest_show_all': '/{repo_name}/pull-request',
30 'pullrequest_show_all_data': '/{repo_name}/pull-request-data',
31 }[name].format(**kwargs)
32
33 if params:
34 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
35 return base_url
36
21
37
22
38 @pytest.mark.backends("git", "hg")
23 @pytest.mark.backends("git", "hg")
39 @pytest.mark.usefixtures('autologin_user', 'app')
24 @pytest.mark.usefixtures('autologin_user', 'app')
40 class TestPullRequestList(object):
25 class TestPullRequestList(object):
41
26
42 @pytest.mark.parametrize('params, expected_title', [
27 @pytest.mark.parametrize('params, expected_title', [
43 ({'source': 0, 'closed': 1}, 'Closed'),
28 ({'source': 0, 'closed': 1}, 'Closed'),
44 ({'source': 0, 'my': 1}, 'Created by me'),
29 ({'source': 0, 'my': 1}, 'Created by me'),
45 ({'source': 0, 'awaiting_review': 1}, 'Awaiting review'),
30 ({'source': 0, 'awaiting_review': 1}, 'Awaiting review'),
46 ({'source': 0, 'awaiting_my_review': 1}, 'Awaiting my review'),
31 ({'source': 0, 'awaiting_my_review': 1}, 'Awaiting my review'),
47 ({'source': 1}, 'From this repo'),
32 ({'source': 1}, 'From this repo'),
48 ])
33 ])
49 def test_showing_list_page(self, backend, pr_util, params, expected_title):
34 def test_showing_list_page(self, backend, pr_util, params, expected_title):
50 pull_request = pr_util.create_pull_request()
35 pull_request = pr_util.create_pull_request()
51
36
52 response = self.app.get(
37 response = self.app.get(
53 route_path('pullrequest_show_all',
38 route_path('pullrequest_show_all',
54 repo_name=pull_request.target_repo.repo_name,
39 repo_name=pull_request.target_repo.repo_name,
55 params=params))
40 params=params))
56
41
57 assert_response = response.assert_response()
42 assert_response = response.assert_response()
58
43
59 element = assert_response.get_element('.title .active')
44 element = assert_response.get_element('.title .active')
60 element_text = element.text_content()
45 element_text = element.text_content()
61 assert expected_title == element_text
46 assert expected_title == element_text
62
47
63 def test_showing_list_page_data(self, backend, pr_util, xhr_header):
48 def test_showing_list_page_data(self, backend, pr_util, xhr_header):
64 pull_request = pr_util.create_pull_request()
49 pull_request = pr_util.create_pull_request()
65 response = self.app.get(
50 response = self.app.get(
66 route_path('pullrequest_show_all_data',
51 route_path('pullrequest_show_all_data',
67 repo_name=pull_request.target_repo.repo_name),
52 repo_name=pull_request.target_repo.repo_name),
68 extra_environ=xhr_header)
53 extra_environ=xhr_header)
69
54
70 assert response.json['recordsTotal'] == 1
55 assert response.json['recordsTotal'] == 1
71 assert response.json['data'][0]['description'] == 'Description'
56 assert response.json['data'][0]['description'] == 'Description'
72
57
73 def test_description_is_escaped_on_index_page(self, backend, pr_util, xhr_header):
58 def test_description_is_escaped_on_index_page(self, backend, pr_util, xhr_header):
74 xss_description = "<script>alert('Hi!')</script>"
59 xss_description = "<script>alert('Hi!')</script>"
75 pull_request = pr_util.create_pull_request(description=xss_description)
60 pull_request = pr_util.create_pull_request(description=xss_description)
76
61
77 response = self.app.get(
62 response = self.app.get(
78 route_path('pullrequest_show_all_data',
63 route_path('pullrequest_show_all_data',
79 repo_name=pull_request.target_repo.repo_name),
64 repo_name=pull_request.target_repo.repo_name),
80 extra_environ=xhr_header)
65 extra_environ=xhr_header)
81
66
82 assert response.json['recordsTotal'] == 1
67 assert response.json['recordsTotal'] == 1
83 assert response.json['data'][0]['description'] == \
68 assert response.json['data'][0]['description'] == \
84 "&lt;script&gt;alert(&#39;Hi!&#39;)&lt;/script&gt;"
69 "&lt;script&gt;alert(&#39;Hi!&#39;)&lt;/script&gt;"
@@ -1,52 +1,40 b''
1 # Copyright (C) 2010-2023 RhodeCode GmbH
1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 #
2 #
3 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
14 #
15 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19 import pytest
19 import pytest
20 from rhodecode.model.db import Repository
20 from rhodecode.model.db import Repository
21
21 from rhodecode.tests.routes import route_path
22
23 def route_path(name, params=None, **kwargs):
24 import urllib.request
25 import urllib.parse
26 import urllib.error
27
22
28 base_url = {
29 'bookmarks_home': '/{repo_name}/bookmarks',
30 }[name].format(**kwargs)
31
32 if params:
33 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
34 return base_url
35
23
36
24
37 @pytest.mark.usefixtures('autologin_user', 'app')
25 @pytest.mark.usefixtures('autologin_user', 'app')
38 class TestBookmarks(object):
26 class TestBookmarks(object):
39
27
40 def test_index(self, backend):
28 def test_index(self, backend):
41 if backend.alias == 'hg':
29 if backend.alias == 'hg':
42 response = self.app.get(
30 response = self.app.get(
43 route_path('bookmarks_home', repo_name=backend.repo_name))
31 route_path('bookmarks_home', repo_name=backend.repo_name))
44
32
45 repo = Repository.get_by_repo_name(backend.repo_name)
33 repo = Repository.get_by_repo_name(backend.repo_name)
46 for commit_id, obj_name in repo.scm_instance().bookmarks.items():
34 for commit_id, obj_name in repo.scm_instance().bookmarks.items():
47 assert commit_id in response
35 assert commit_id in response
48 assert obj_name in response
36 assert obj_name in response
49 else:
37 else:
50 self.app.get(
38 self.app.get(
51 route_path('bookmarks_home', repo_name=backend.repo_name),
39 route_path('bookmarks_home', repo_name=backend.repo_name),
52 status=404)
40 status=404)
@@ -1,48 +1,35 b''
1 # Copyright (C) 2010-2023 RhodeCode GmbH
1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 #
2 #
3 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
14 #
15 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19 import pytest
19 import pytest
20 from rhodecode.model.db import Repository
20 from rhodecode.model.db import Repository
21
21 from rhodecode.tests.routes import route_path
22
23 def route_path(name, params=None, **kwargs):
24 import urllib.request
25 import urllib.parse
26 import urllib.error
27
28 base_url = {
29 'branches_home': '/{repo_name}/branches',
30 }[name].format(**kwargs)
31
32 if params:
33 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
34 return base_url
35
22
36
23
37 @pytest.mark.usefixtures('autologin_user', 'app')
24 @pytest.mark.usefixtures('autologin_user', 'app')
38 class TestBranchesController(object):
25 class TestBranchesController(object):
39
26
40 def test_index(self, backend):
27 def test_index(self, backend):
41 response = self.app.get(
28 response = self.app.get(
42 route_path('branches_home', repo_name=backend.repo_name))
29 route_path('branches_home', repo_name=backend.repo_name))
43
30
44 repo = Repository.get_by_repo_name(backend.repo_name)
31 repo = Repository.get_by_repo_name(backend.repo_name)
45
32
46 for commit_id, obj_name in repo.scm_instance().branches.items():
33 for commit_id, obj_name in repo.scm_instance().branches.items():
47 assert commit_id in response
34 assert commit_id in response
48 assert obj_name in response
35 assert obj_name in response
@@ -1,219 +1,204 b''
1 # Copyright (C) 2010-2023 RhodeCode GmbH
1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 #
2 #
3 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
14 #
15 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19 import re
19 import re
20
20
21 import pytest
21 import pytest
22
22
23 from rhodecode.apps.repository.views.repo_changelog import DEFAULT_CHANGELOG_SIZE
23 from rhodecode.apps.repository.views.repo_changelog import DEFAULT_CHANGELOG_SIZE
24 from rhodecode.tests import TestController
24 from rhodecode.tests import TestController
25
25 from rhodecode.tests.routes import route_path
26 MATCH_HASH = re.compile(r'<span class="commit_hash">r(\d+):[\da-f]+</span>')
27
26
28
27
29 def route_path(name, params=None, **kwargs):
28 MATCH_HASH = re.compile(r'<span class="commit_hash">r(\d+):[\da-f]+</span>')
30 import urllib.request
31 import urllib.parse
32 import urllib.error
33
34 base_url = {
35 'repo_changelog': '/{repo_name}/changelog',
36 'repo_commits': '/{repo_name}/commits',
37 'repo_commits_file': '/{repo_name}/commits/{commit_id}/{f_path}',
38 'repo_commits_elements': '/{repo_name}/commits_elements',
39 }[name].format(**kwargs)
40
41 if params:
42 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
43 return base_url
44
29
45
30
46 def assert_commits_on_page(response, indexes):
31 def assert_commits_on_page(response, indexes):
47 found_indexes = [int(idx) for idx in MATCH_HASH.findall(response.text)]
32 found_indexes = [int(idx) for idx in MATCH_HASH.findall(response.text)]
48 assert found_indexes == indexes
33 assert found_indexes == indexes
49
34
50
35
51 class TestChangelogController(TestController):
36 class TestChangelogController(TestController):
52
37
53 def test_commits_page(self, backend):
38 def test_commits_page(self, backend):
54 self.log_user()
39 self.log_user()
55 response = self.app.get(
40 response = self.app.get(
56 route_path('repo_commits', repo_name=backend.repo_name))
41 route_path('repo_commits', repo_name=backend.repo_name))
57
42
58 first_idx = -1
43 first_idx = -1
59 last_idx = -DEFAULT_CHANGELOG_SIZE
44 last_idx = -DEFAULT_CHANGELOG_SIZE
60 self.assert_commit_range_on_page(response, first_idx, last_idx, backend)
45 self.assert_commit_range_on_page(response, first_idx, last_idx, backend)
61
46
62 def test_changelog(self, backend):
47 def test_changelog(self, backend):
63 self.log_user()
48 self.log_user()
64 response = self.app.get(
49 response = self.app.get(
65 route_path('repo_changelog', repo_name=backend.repo_name))
50 route_path('repo_changelog', repo_name=backend.repo_name))
66
51
67 first_idx = -1
52 first_idx = -1
68 last_idx = -DEFAULT_CHANGELOG_SIZE
53 last_idx = -DEFAULT_CHANGELOG_SIZE
69 self.assert_commit_range_on_page(
54 self.assert_commit_range_on_page(
70 response, first_idx, last_idx, backend)
55 response, first_idx, last_idx, backend)
71
56
72 @pytest.mark.backends("hg", "git")
57 @pytest.mark.backends("hg", "git")
73 def test_changelog_filtered_by_branch(self, backend):
58 def test_changelog_filtered_by_branch(self, backend):
74 self.log_user()
59 self.log_user()
75 self.app.get(
60 self.app.get(
76 route_path('repo_changelog', repo_name=backend.repo_name,
61 route_path('repo_changelog', repo_name=backend.repo_name,
77 params=dict(branch=backend.default_branch_name)),
62 params=dict(branch=backend.default_branch_name)),
78 status=200)
63 status=200)
79
64
80 @pytest.mark.backends("hg", "git")
65 @pytest.mark.backends("hg", "git")
81 def test_commits_filtered_by_branch(self, backend):
66 def test_commits_filtered_by_branch(self, backend):
82 self.log_user()
67 self.log_user()
83 self.app.get(
68 self.app.get(
84 route_path('repo_commits', repo_name=backend.repo_name,
69 route_path('repo_commits', repo_name=backend.repo_name,
85 params=dict(branch=backend.default_branch_name)),
70 params=dict(branch=backend.default_branch_name)),
86 status=200)
71 status=200)
87
72
88 @pytest.mark.backends("svn")
73 @pytest.mark.backends("svn")
89 def test_changelog_filtered_by_branch_svn(self, autologin_user, backend):
74 def test_changelog_filtered_by_branch_svn(self, autologin_user, backend):
90 repo = backend['svn-simple-layout']
75 repo = backend['svn-simple-layout']
91 response = self.app.get(
76 response = self.app.get(
92 route_path('repo_changelog', repo_name=repo.repo_name,
77 route_path('repo_changelog', repo_name=repo.repo_name,
93 params=dict(branch='trunk')),
78 params=dict(branch='trunk')),
94 status=200)
79 status=200)
95
80
96 assert_commits_on_page(response, indexes=[15, 12, 7, 3, 2, 1])
81 assert_commits_on_page(response, indexes=[15, 12, 7, 3, 2, 1])
97
82
98 def test_commits_filtered_by_wrong_branch(self, backend):
83 def test_commits_filtered_by_wrong_branch(self, backend):
99 self.log_user()
84 self.log_user()
100 branch = 'wrong-branch-name'
85 branch = 'wrong-branch-name'
101 response = self.app.get(
86 response = self.app.get(
102 route_path('repo_commits', repo_name=backend.repo_name,
87 route_path('repo_commits', repo_name=backend.repo_name,
103 params=dict(branch=branch)),
88 params=dict(branch=branch)),
104 status=302)
89 status=302)
105 expected_url = '/{repo}/commits/{branch}'.format(
90 expected_url = '/{repo}/commits/{branch}'.format(
106 repo=backend.repo_name, branch=branch)
91 repo=backend.repo_name, branch=branch)
107 assert expected_url in response.location
92 assert expected_url in response.location
108 response = response.follow()
93 response = response.follow()
109 expected_warning = f'Branch {branch} is not found.'
94 expected_warning = f'Branch {branch} is not found.'
110 assert expected_warning in response.text
95 assert expected_warning in response.text
111
96
112 @pytest.mark.xfail_backends("svn", reason="Depends on branch support")
97 @pytest.mark.xfail_backends("svn", reason="Depends on branch support")
113 def test_changelog_filtered_by_branch_with_merges(self, autologin_user, backend):
98 def test_changelog_filtered_by_branch_with_merges(self, autologin_user, backend):
114
99
115 # Note: The changelog of branch "b" does not contain the commit "a1"
100 # Note: The changelog of branch "b" does not contain the commit "a1"
116 # although this is a parent of commit "b1". And branch "b" has commits
101 # although this is a parent of commit "b1". And branch "b" has commits
117 # which have a smaller index than commit "a1".
102 # which have a smaller index than commit "a1".
118 commits = [
103 commits = [
119 {'message': 'a'},
104 {'message': 'a'},
120 {'message': 'b', 'branch': 'b'},
105 {'message': 'b', 'branch': 'b'},
121 {'message': 'a1', 'parents': ['a']},
106 {'message': 'a1', 'parents': ['a']},
122 {'message': 'b1', 'branch': 'b', 'parents': ['b', 'a1']},
107 {'message': 'b1', 'branch': 'b', 'parents': ['b', 'a1']},
123 ]
108 ]
124 backend.create_repo(commits)
109 backend.create_repo(commits)
125
110
126 self.app.get(
111 self.app.get(
127 route_path('repo_changelog', repo_name=backend.repo_name,
112 route_path('repo_changelog', repo_name=backend.repo_name,
128 params=dict(branch='b')),
113 params=dict(branch='b')),
129 status=200)
114 status=200)
130
115
131 @pytest.mark.backends("hg")
116 @pytest.mark.backends("hg")
132 def test_commits_closed_branches(self, autologin_user, backend):
117 def test_commits_closed_branches(self, autologin_user, backend):
133 repo = backend['closed_branch']
118 repo = backend['closed_branch']
134 response = self.app.get(
119 response = self.app.get(
135 route_path('repo_commits', repo_name=repo.repo_name,
120 route_path('repo_commits', repo_name=repo.repo_name,
136 params=dict(branch='experimental')),
121 params=dict(branch='experimental')),
137 status=200)
122 status=200)
138
123
139 assert_commits_on_page(response, indexes=[3, 1])
124 assert_commits_on_page(response, indexes=[3, 1])
140
125
141 def test_changelog_pagination(self, backend):
126 def test_changelog_pagination(self, backend):
142 self.log_user()
127 self.log_user()
143 # pagination, walk up to page 6
128 # pagination, walk up to page 6
144 changelog_url = route_path(
129 changelog_url = route_path(
145 'repo_commits', repo_name=backend.repo_name)
130 'repo_commits', repo_name=backend.repo_name)
146
131
147 for page in range(1, 7):
132 for page in range(1, 7):
148 response = self.app.get(changelog_url, {'page': page})
133 response = self.app.get(changelog_url, {'page': page})
149
134
150 first_idx = -DEFAULT_CHANGELOG_SIZE * (page - 1) - 1
135 first_idx = -DEFAULT_CHANGELOG_SIZE * (page - 1) - 1
151 last_idx = -DEFAULT_CHANGELOG_SIZE * page
136 last_idx = -DEFAULT_CHANGELOG_SIZE * page
152 self.assert_commit_range_on_page(response, first_idx, last_idx, backend)
137 self.assert_commit_range_on_page(response, first_idx, last_idx, backend)
153
138
154 def assert_commit_range_on_page(
139 def assert_commit_range_on_page(
155 self, response, first_idx, last_idx, backend):
140 self, response, first_idx, last_idx, backend):
156 input_template = (
141 input_template = (
157 """<input class="commit-range" """
142 """<input class="commit-range" """
158 """data-commit-id="%(raw_id)s" data-commit-idx="%(idx)s" """
143 """data-commit-id="%(raw_id)s" data-commit-idx="%(idx)s" """
159 """data-short-id="%(short_id)s" id="%(raw_id)s" """
144 """data-short-id="%(short_id)s" id="%(raw_id)s" """
160 """name="%(raw_id)s" type="checkbox" value="1" />"""
145 """name="%(raw_id)s" type="checkbox" value="1" />"""
161 )
146 )
162
147
163 commit_span_template = """<span class="commit_hash">r%s:%s</span>"""
148 commit_span_template = """<span class="commit_hash">r%s:%s</span>"""
164 repo = backend.repo
149 repo = backend.repo
165
150
166 first_commit_on_page = repo.get_commit(commit_idx=first_idx)
151 first_commit_on_page = repo.get_commit(commit_idx=first_idx)
167 response.mustcontain(
152 response.mustcontain(
168 input_template % {'raw_id': first_commit_on_page.raw_id,
153 input_template % {'raw_id': first_commit_on_page.raw_id,
169 'idx': first_commit_on_page.idx,
154 'idx': first_commit_on_page.idx,
170 'short_id': first_commit_on_page.short_id})
155 'short_id': first_commit_on_page.short_id})
171
156
172 response.mustcontain(commit_span_template % (
157 response.mustcontain(commit_span_template % (
173 first_commit_on_page.idx, first_commit_on_page.short_id)
158 first_commit_on_page.idx, first_commit_on_page.short_id)
174 )
159 )
175
160
176 last_commit_on_page = repo.get_commit(commit_idx=last_idx)
161 last_commit_on_page = repo.get_commit(commit_idx=last_idx)
177 response.mustcontain(
162 response.mustcontain(
178 input_template % {'raw_id': last_commit_on_page.raw_id,
163 input_template % {'raw_id': last_commit_on_page.raw_id,
179 'idx': last_commit_on_page.idx,
164 'idx': last_commit_on_page.idx,
180 'short_id': last_commit_on_page.short_id})
165 'short_id': last_commit_on_page.short_id})
181 response.mustcontain(commit_span_template % (
166 response.mustcontain(commit_span_template % (
182 last_commit_on_page.idx, last_commit_on_page.short_id)
167 last_commit_on_page.idx, last_commit_on_page.short_id)
183 )
168 )
184
169
185 first_commit_of_next_page = repo.get_commit(commit_idx=last_idx - 1)
170 first_commit_of_next_page = repo.get_commit(commit_idx=last_idx - 1)
186 first_span_of_next_page = commit_span_template % (
171 first_span_of_next_page = commit_span_template % (
187 first_commit_of_next_page.idx, first_commit_of_next_page.short_id)
172 first_commit_of_next_page.idx, first_commit_of_next_page.short_id)
188 assert first_span_of_next_page not in response
173 assert first_span_of_next_page not in response
189
174
190 @pytest.mark.parametrize('test_path', [
175 @pytest.mark.parametrize('test_path', [
191 'vcs/exceptions.py',
176 'vcs/exceptions.py',
192 '/vcs/exceptions.py',
177 '/vcs/exceptions.py',
193 '//vcs/exceptions.py'
178 '//vcs/exceptions.py'
194 ])
179 ])
195 def test_commits_with_filenode(self, backend, test_path):
180 def test_commits_with_filenode(self, backend, test_path):
196 self.log_user()
181 self.log_user()
197 response = self.app.get(
182 response = self.app.get(
198 route_path('repo_commits_file', repo_name=backend.repo_name,
183 route_path('repo_commits_file', repo_name=backend.repo_name,
199 commit_id='tip', f_path=test_path),
184 commit_id='tip', f_path=test_path),
200 )
185 )
201
186
202 # history commits messages
187 # history commits messages
203 response.mustcontain('Added exceptions module, this time for real')
188 response.mustcontain('Added exceptions module, this time for real')
204 response.mustcontain('Added not implemented hg backend test case')
189 response.mustcontain('Added not implemented hg backend test case')
205 response.mustcontain('Added BaseChangeset class')
190 response.mustcontain('Added BaseChangeset class')
206
191
207 def test_commits_with_filenode_that_is_dirnode(self, backend):
192 def test_commits_with_filenode_that_is_dirnode(self, backend):
208 self.log_user()
193 self.log_user()
209 self.app.get(
194 self.app.get(
210 route_path('repo_commits_file', repo_name=backend.repo_name,
195 route_path('repo_commits_file', repo_name=backend.repo_name,
211 commit_id='tip', f_path='/tests'),
196 commit_id='tip', f_path='/tests'),
212 status=302)
197 status=302)
213
198
214 def test_commits_with_filenode_not_existing(self, backend):
199 def test_commits_with_filenode_not_existing(self, backend):
215 self.log_user()
200 self.log_user()
216 self.app.get(
201 self.app.get(
217 route_path('repo_commits_file', repo_name=backend.repo_name,
202 route_path('repo_commits_file', repo_name=backend.repo_name,
218 commit_id='tip', f_path='wrong_path'),
203 commit_id='tip', f_path='wrong_path'),
219 status=302)
204 status=302)
@@ -1,494 +1,477 b''
1 # Copyright (C) 2010-2023 RhodeCode GmbH
1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 #
2 #
3 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
14 #
15 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19 import pytest
19 import pytest
20
20
21 from rhodecode.tests import TestController
21 from rhodecode.tests import TestController
22
22 from rhodecode.tests.routes import route_path
23 from rhodecode.model.db import ChangesetComment, Notification
23 from rhodecode.model.db import ChangesetComment, Notification
24 from rhodecode.model.meta import Session
24 from rhodecode.model.meta import Session
25 from rhodecode.lib import helpers as h
25 from rhodecode.lib import helpers as h
26
26
27
27
28 def route_path(name, params=None, **kwargs):
29 import urllib.request
30 import urllib.parse
31 import urllib.error
32
33 base_url = {
34 'repo_commit': '/{repo_name}/changeset/{commit_id}',
35 'repo_commit_comment_create': '/{repo_name}/changeset/{commit_id}/comment/create',
36 'repo_commit_comment_preview': '/{repo_name}/changeset/{commit_id}/comment/preview',
37 'repo_commit_comment_delete': '/{repo_name}/changeset/{commit_id}/comment/{comment_id}/delete',
38 'repo_commit_comment_edit': '/{repo_name}/changeset/{commit_id}/comment/{comment_id}/edit',
39 }[name].format(**kwargs)
40
41 if params:
42 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
43 return base_url
44
45
28
46 @pytest.mark.backends("git", "hg", "svn")
29 @pytest.mark.backends("git", "hg", "svn")
47 class TestRepoCommitCommentsView(TestController):
30 class TestRepoCommitCommentsView(TestController):
48
31
49 @pytest.fixture(autouse=True)
32 @pytest.fixture(autouse=True)
50 def prepare(self, request, baseapp):
33 def prepare(self, request, baseapp):
51 for x in ChangesetComment.query().all():
34 for x in ChangesetComment.query().all():
52 Session().delete(x)
35 Session().delete(x)
53 Session().commit()
36 Session().commit()
54
37
55 for x in Notification.query().all():
38 for x in Notification.query().all():
56 Session().delete(x)
39 Session().delete(x)
57 Session().commit()
40 Session().commit()
58
41
59 request.addfinalizer(self.cleanup)
42 request.addfinalizer(self.cleanup)
60
43
61 def cleanup(self):
44 def cleanup(self):
62 for x in ChangesetComment.query().all():
45 for x in ChangesetComment.query().all():
63 Session().delete(x)
46 Session().delete(x)
64 Session().commit()
47 Session().commit()
65
48
66 for x in Notification.query().all():
49 for x in Notification.query().all():
67 Session().delete(x)
50 Session().delete(x)
68 Session().commit()
51 Session().commit()
69
52
70 @pytest.mark.parametrize('comment_type', ChangesetComment.COMMENT_TYPES)
53 @pytest.mark.parametrize('comment_type', ChangesetComment.COMMENT_TYPES)
71 def test_create(self, comment_type, backend):
54 def test_create(self, comment_type, backend):
72 self.log_user()
55 self.log_user()
73 commit = backend.repo.get_commit('300')
56 commit = backend.repo.get_commit('300')
74 commit_id = commit.raw_id
57 commit_id = commit.raw_id
75 text = 'CommentOnCommit'
58 text = 'CommentOnCommit'
76
59
77 params = {'text': text, 'csrf_token': self.csrf_token,
60 params = {'text': text, 'csrf_token': self.csrf_token,
78 'comment_type': comment_type}
61 'comment_type': comment_type}
79 self.app.post(
62 self.app.post(
80 route_path('repo_commit_comment_create',
63 route_path('repo_commit_comment_create',
81 repo_name=backend.repo_name, commit_id=commit_id),
64 repo_name=backend.repo_name, commit_id=commit_id),
82 params=params)
65 params=params)
83
66
84 response = self.app.get(
67 response = self.app.get(
85 route_path('repo_commit',
68 route_path('repo_commit',
86 repo_name=backend.repo_name, commit_id=commit_id))
69 repo_name=backend.repo_name, commit_id=commit_id))
87
70
88 # test DB
71 # test DB
89 assert ChangesetComment.query().count() == 1
72 assert ChangesetComment.query().count() == 1
90 assert_comment_links(response, ChangesetComment.query().count(), 0)
73 assert_comment_links(response, ChangesetComment.query().count(), 0)
91
74
92 assert Notification.query().count() == 1
75 assert Notification.query().count() == 1
93 assert ChangesetComment.query().count() == 1
76 assert ChangesetComment.query().count() == 1
94
77
95 notification = Notification.query().all()[0]
78 notification = Notification.query().all()[0]
96
79
97 comment_id = ChangesetComment.query().first().comment_id
80 comment_id = ChangesetComment.query().first().comment_id
98 assert notification.type_ == Notification.TYPE_CHANGESET_COMMENT
81 assert notification.type_ == Notification.TYPE_CHANGESET_COMMENT
99
82
100 author = notification.created_by_user.username_and_name
83 author = notification.created_by_user.username_and_name
101 sbj = '@{0} left a {1} on commit `{2}` in the `{3}` repository'.format(
84 sbj = '@{0} left a {1} on commit `{2}` in the `{3}` repository'.format(
102 author, comment_type, h.show_id(commit), backend.repo_name)
85 author, comment_type, h.show_id(commit), backend.repo_name)
103 assert sbj == notification.subject
86 assert sbj == notification.subject
104
87
105 lnk = ('/{0}/changeset/{1}#comment-{2}'.format(
88 lnk = ('/{0}/changeset/{1}#comment-{2}'.format(
106 backend.repo_name, commit_id, comment_id))
89 backend.repo_name, commit_id, comment_id))
107 assert lnk in notification.body
90 assert lnk in notification.body
108
91
109 @pytest.mark.parametrize('comment_type', ChangesetComment.COMMENT_TYPES)
92 @pytest.mark.parametrize('comment_type', ChangesetComment.COMMENT_TYPES)
110 def test_create_inline(self, comment_type, backend):
93 def test_create_inline(self, comment_type, backend):
111 self.log_user()
94 self.log_user()
112 commit = backend.repo.get_commit('300')
95 commit = backend.repo.get_commit('300')
113 commit_id = commit.raw_id
96 commit_id = commit.raw_id
114 text = 'CommentOnCommit'
97 text = 'CommentOnCommit'
115 f_path = 'vcs/web/simplevcs/views/repository.py'
98 f_path = 'vcs/web/simplevcs/views/repository.py'
116 line = 'n1'
99 line = 'n1'
117
100
118 params = {'text': text, 'f_path': f_path, 'line': line,
101 params = {'text': text, 'f_path': f_path, 'line': line,
119 'comment_type': comment_type,
102 'comment_type': comment_type,
120 'csrf_token': self.csrf_token}
103 'csrf_token': self.csrf_token}
121
104
122 self.app.post(
105 self.app.post(
123 route_path('repo_commit_comment_create',
106 route_path('repo_commit_comment_create',
124 repo_name=backend.repo_name, commit_id=commit_id),
107 repo_name=backend.repo_name, commit_id=commit_id),
125 params=params)
108 params=params)
126
109
127 response = self.app.get(
110 response = self.app.get(
128 route_path('repo_commit',
111 route_path('repo_commit',
129 repo_name=backend.repo_name, commit_id=commit_id))
112 repo_name=backend.repo_name, commit_id=commit_id))
130
113
131 # test DB
114 # test DB
132 assert ChangesetComment.query().count() == 1
115 assert ChangesetComment.query().count() == 1
133 assert_comment_links(response, 0, ChangesetComment.query().count())
116 assert_comment_links(response, 0, ChangesetComment.query().count())
134
117
135 if backend.alias == 'svn':
118 if backend.alias == 'svn':
136 response.mustcontain(
119 response.mustcontain(
137 '''data-f-path="vcs/commands/summary.py" '''
120 '''data-f-path="vcs/commands/summary.py" '''
138 '''data-anchor-id="c-300-ad05457a43f8"'''
121 '''data-anchor-id="c-300-ad05457a43f8"'''
139 )
122 )
140 if backend.alias == 'git':
123 if backend.alias == 'git':
141 response.mustcontain(
124 response.mustcontain(
142 '''data-f-path="vcs/backends/hg.py" '''
125 '''data-f-path="vcs/backends/hg.py" '''
143 '''data-anchor-id="c-883e775e89ea-9c390eb52cd6"'''
126 '''data-anchor-id="c-883e775e89ea-9c390eb52cd6"'''
144 )
127 )
145
128
146 if backend.alias == 'hg':
129 if backend.alias == 'hg':
147 response.mustcontain(
130 response.mustcontain(
148 '''data-f-path="vcs/backends/hg.py" '''
131 '''data-f-path="vcs/backends/hg.py" '''
149 '''data-anchor-id="c-e58d85a3973b-9c390eb52cd6"'''
132 '''data-anchor-id="c-e58d85a3973b-9c390eb52cd6"'''
150 )
133 )
151
134
152 assert Notification.query().count() == 1
135 assert Notification.query().count() == 1
153 assert ChangesetComment.query().count() == 1
136 assert ChangesetComment.query().count() == 1
154
137
155 notification = Notification.query().all()[0]
138 notification = Notification.query().all()[0]
156 comment = ChangesetComment.query().first()
139 comment = ChangesetComment.query().first()
157 assert notification.type_ == Notification.TYPE_CHANGESET_COMMENT
140 assert notification.type_ == Notification.TYPE_CHANGESET_COMMENT
158
141
159 assert comment.revision == commit_id
142 assert comment.revision == commit_id
160
143
161 author = notification.created_by_user.username_and_name
144 author = notification.created_by_user.username_and_name
162 sbj = '@{0} left a {1} on file `{2}` in commit `{3}` in the `{4}` repository'.format(
145 sbj = '@{0} left a {1} on file `{2}` in commit `{3}` in the `{4}` repository'.format(
163 author, comment_type, f_path, h.show_id(commit), backend.repo_name)
146 author, comment_type, f_path, h.show_id(commit), backend.repo_name)
164
147
165 assert sbj == notification.subject
148 assert sbj == notification.subject
166
149
167 lnk = ('/{0}/changeset/{1}#comment-{2}'.format(
150 lnk = ('/{0}/changeset/{1}#comment-{2}'.format(
168 backend.repo_name, commit_id, comment.comment_id))
151 backend.repo_name, commit_id, comment.comment_id))
169 assert lnk in notification.body
152 assert lnk in notification.body
170 assert 'on line n1' in notification.body
153 assert 'on line n1' in notification.body
171
154
172 def test_create_with_mention(self, backend):
155 def test_create_with_mention(self, backend):
173 self.log_user()
156 self.log_user()
174
157
175 commit_id = backend.repo.get_commit('300').raw_id
158 commit_id = backend.repo.get_commit('300').raw_id
176 text = '@test_regular check CommentOnCommit'
159 text = '@test_regular check CommentOnCommit'
177
160
178 params = {'text': text, 'csrf_token': self.csrf_token}
161 params = {'text': text, 'csrf_token': self.csrf_token}
179 self.app.post(
162 self.app.post(
180 route_path('repo_commit_comment_create',
163 route_path('repo_commit_comment_create',
181 repo_name=backend.repo_name, commit_id=commit_id),
164 repo_name=backend.repo_name, commit_id=commit_id),
182 params=params)
165 params=params)
183
166
184 response = self.app.get(
167 response = self.app.get(
185 route_path('repo_commit',
168 route_path('repo_commit',
186 repo_name=backend.repo_name, commit_id=commit_id))
169 repo_name=backend.repo_name, commit_id=commit_id))
187 # test DB
170 # test DB
188 assert ChangesetComment.query().count() == 1
171 assert ChangesetComment.query().count() == 1
189 assert_comment_links(response, ChangesetComment.query().count(), 0)
172 assert_comment_links(response, ChangesetComment.query().count(), 0)
190
173
191 notification = Notification.query().one()
174 notification = Notification.query().one()
192
175
193 assert len(notification.recipients) == 2
176 assert len(notification.recipients) == 2
194 users = [x.username for x in notification.recipients]
177 users = [x.username for x in notification.recipients]
195
178
196 # test_regular gets notification by @mention
179 # test_regular gets notification by @mention
197 assert sorted(users) == ['test_admin', 'test_regular']
180 assert sorted(users) == ['test_admin', 'test_regular']
198
181
199 def test_create_with_status_change(self, backend):
182 def test_create_with_status_change(self, backend):
200 self.log_user()
183 self.log_user()
201 commit = backend.repo.get_commit('300')
184 commit = backend.repo.get_commit('300')
202 commit_id = commit.raw_id
185 commit_id = commit.raw_id
203 text = 'CommentOnCommit'
186 text = 'CommentOnCommit'
204 f_path = 'vcs/web/simplevcs/views/repository.py'
187 f_path = 'vcs/web/simplevcs/views/repository.py'
205 line = 'n1'
188 line = 'n1'
206
189
207 params = {'text': text, 'changeset_status': 'approved',
190 params = {'text': text, 'changeset_status': 'approved',
208 'csrf_token': self.csrf_token}
191 'csrf_token': self.csrf_token}
209
192
210 self.app.post(
193 self.app.post(
211 route_path(
194 route_path(
212 'repo_commit_comment_create',
195 'repo_commit_comment_create',
213 repo_name=backend.repo_name, commit_id=commit_id),
196 repo_name=backend.repo_name, commit_id=commit_id),
214 params=params)
197 params=params)
215
198
216 response = self.app.get(
199 response = self.app.get(
217 route_path('repo_commit',
200 route_path('repo_commit',
218 repo_name=backend.repo_name, commit_id=commit_id))
201 repo_name=backend.repo_name, commit_id=commit_id))
219
202
220 # test DB
203 # test DB
221 assert ChangesetComment.query().count() == 1
204 assert ChangesetComment.query().count() == 1
222 assert_comment_links(response, ChangesetComment.query().count(), 0)
205 assert_comment_links(response, ChangesetComment.query().count(), 0)
223
206
224 assert Notification.query().count() == 1
207 assert Notification.query().count() == 1
225 assert ChangesetComment.query().count() == 1
208 assert ChangesetComment.query().count() == 1
226
209
227 notification = Notification.query().all()[0]
210 notification = Notification.query().all()[0]
228
211
229 comment_id = ChangesetComment.query().first().comment_id
212 comment_id = ChangesetComment.query().first().comment_id
230 assert notification.type_ == Notification.TYPE_CHANGESET_COMMENT
213 assert notification.type_ == Notification.TYPE_CHANGESET_COMMENT
231
214
232 author = notification.created_by_user.username_and_name
215 author = notification.created_by_user.username_and_name
233 sbj = '[status: Approved] @{0} left a note on commit `{1}` in the `{2}` repository'.format(
216 sbj = '[status: Approved] @{0} left a note on commit `{1}` in the `{2}` repository'.format(
234 author, h.show_id(commit), backend.repo_name)
217 author, h.show_id(commit), backend.repo_name)
235 assert sbj == notification.subject
218 assert sbj == notification.subject
236
219
237 lnk = ('/{0}/changeset/{1}#comment-{2}'.format(
220 lnk = ('/{0}/changeset/{1}#comment-{2}'.format(
238 backend.repo_name, commit_id, comment_id))
221 backend.repo_name, commit_id, comment_id))
239 assert lnk in notification.body
222 assert lnk in notification.body
240
223
241 def test_delete(self, backend):
224 def test_delete(self, backend):
242 self.log_user()
225 self.log_user()
243 commit_id = backend.repo.get_commit('300').raw_id
226 commit_id = backend.repo.get_commit('300').raw_id
244 text = 'CommentOnCommit'
227 text = 'CommentOnCommit'
245
228
246 params = {'text': text, 'csrf_token': self.csrf_token}
229 params = {'text': text, 'csrf_token': self.csrf_token}
247 self.app.post(
230 self.app.post(
248 route_path(
231 route_path(
249 'repo_commit_comment_create',
232 'repo_commit_comment_create',
250 repo_name=backend.repo_name, commit_id=commit_id),
233 repo_name=backend.repo_name, commit_id=commit_id),
251 params=params)
234 params=params)
252
235
253 comments = ChangesetComment.query().all()
236 comments = ChangesetComment.query().all()
254 assert len(comments) == 1
237 assert len(comments) == 1
255 comment_id = comments[0].comment_id
238 comment_id = comments[0].comment_id
256
239
257 self.app.post(
240 self.app.post(
258 route_path('repo_commit_comment_delete',
241 route_path('repo_commit_comment_delete',
259 repo_name=backend.repo_name,
242 repo_name=backend.repo_name,
260 commit_id=commit_id,
243 commit_id=commit_id,
261 comment_id=comment_id),
244 comment_id=comment_id),
262 params={'csrf_token': self.csrf_token})
245 params={'csrf_token': self.csrf_token})
263
246
264 comments = ChangesetComment.query().all()
247 comments = ChangesetComment.query().all()
265 assert len(comments) == 0
248 assert len(comments) == 0
266
249
267 response = self.app.get(
250 response = self.app.get(
268 route_path('repo_commit',
251 route_path('repo_commit',
269 repo_name=backend.repo_name, commit_id=commit_id))
252 repo_name=backend.repo_name, commit_id=commit_id))
270 assert_comment_links(response, 0, 0)
253 assert_comment_links(response, 0, 0)
271
254
272 def test_edit(self, backend):
255 def test_edit(self, backend):
273 self.log_user()
256 self.log_user()
274 commit_id = backend.repo.get_commit('300').raw_id
257 commit_id = backend.repo.get_commit('300').raw_id
275 text = 'CommentOnCommit'
258 text = 'CommentOnCommit'
276
259
277 params = {'text': text, 'csrf_token': self.csrf_token}
260 params = {'text': text, 'csrf_token': self.csrf_token}
278 self.app.post(
261 self.app.post(
279 route_path(
262 route_path(
280 'repo_commit_comment_create',
263 'repo_commit_comment_create',
281 repo_name=backend.repo_name, commit_id=commit_id),
264 repo_name=backend.repo_name, commit_id=commit_id),
282 params=params)
265 params=params)
283
266
284 comments = ChangesetComment.query().all()
267 comments = ChangesetComment.query().all()
285 assert len(comments) == 1
268 assert len(comments) == 1
286 comment_id = comments[0].comment_id
269 comment_id = comments[0].comment_id
287 test_text = 'test_text'
270 test_text = 'test_text'
288 self.app.post(
271 self.app.post(
289 route_path(
272 route_path(
290 'repo_commit_comment_edit',
273 'repo_commit_comment_edit',
291 repo_name=backend.repo_name,
274 repo_name=backend.repo_name,
292 commit_id=commit_id,
275 commit_id=commit_id,
293 comment_id=comment_id,
276 comment_id=comment_id,
294 ),
277 ),
295 params={
278 params={
296 'csrf_token': self.csrf_token,
279 'csrf_token': self.csrf_token,
297 'text': test_text,
280 'text': test_text,
298 'version': '0',
281 'version': '0',
299 })
282 })
300
283
301 text_form_db = ChangesetComment.query().filter(
284 text_form_db = ChangesetComment.query().filter(
302 ChangesetComment.comment_id == comment_id).first().text
285 ChangesetComment.comment_id == comment_id).first().text
303 assert test_text == text_form_db
286 assert test_text == text_form_db
304
287
305 def test_edit_without_change(self, backend):
288 def test_edit_without_change(self, backend):
306 self.log_user()
289 self.log_user()
307 commit_id = backend.repo.get_commit('300').raw_id
290 commit_id = backend.repo.get_commit('300').raw_id
308 text = 'CommentOnCommit'
291 text = 'CommentOnCommit'
309
292
310 params = {'text': text, 'csrf_token': self.csrf_token}
293 params = {'text': text, 'csrf_token': self.csrf_token}
311 self.app.post(
294 self.app.post(
312 route_path(
295 route_path(
313 'repo_commit_comment_create',
296 'repo_commit_comment_create',
314 repo_name=backend.repo_name, commit_id=commit_id),
297 repo_name=backend.repo_name, commit_id=commit_id),
315 params=params)
298 params=params)
316
299
317 comments = ChangesetComment.query().all()
300 comments = ChangesetComment.query().all()
318 assert len(comments) == 1
301 assert len(comments) == 1
319 comment_id = comments[0].comment_id
302 comment_id = comments[0].comment_id
320
303
321 response = self.app.post(
304 response = self.app.post(
322 route_path(
305 route_path(
323 'repo_commit_comment_edit',
306 'repo_commit_comment_edit',
324 repo_name=backend.repo_name,
307 repo_name=backend.repo_name,
325 commit_id=commit_id,
308 commit_id=commit_id,
326 comment_id=comment_id,
309 comment_id=comment_id,
327 ),
310 ),
328 params={
311 params={
329 'csrf_token': self.csrf_token,
312 'csrf_token': self.csrf_token,
330 'text': text,
313 'text': text,
331 'version': '0',
314 'version': '0',
332 },
315 },
333 status=404,
316 status=404,
334 )
317 )
335 assert response.status_int == 404
318 assert response.status_int == 404
336
319
337 def test_edit_try_edit_already_edited(self, backend):
320 def test_edit_try_edit_already_edited(self, backend):
338 self.log_user()
321 self.log_user()
339 commit_id = backend.repo.get_commit('300').raw_id
322 commit_id = backend.repo.get_commit('300').raw_id
340 text = 'CommentOnCommit'
323 text = 'CommentOnCommit'
341
324
342 params = {'text': text, 'csrf_token': self.csrf_token}
325 params = {'text': text, 'csrf_token': self.csrf_token}
343 self.app.post(
326 self.app.post(
344 route_path(
327 route_path(
345 'repo_commit_comment_create',
328 'repo_commit_comment_create',
346 repo_name=backend.repo_name, commit_id=commit_id
329 repo_name=backend.repo_name, commit_id=commit_id
347 ),
330 ),
348 params=params,
331 params=params,
349 )
332 )
350
333
351 comments = ChangesetComment.query().all()
334 comments = ChangesetComment.query().all()
352 assert len(comments) == 1
335 assert len(comments) == 1
353 comment_id = comments[0].comment_id
336 comment_id = comments[0].comment_id
354 test_text = 'test_text'
337 test_text = 'test_text'
355 self.app.post(
338 self.app.post(
356 route_path(
339 route_path(
357 'repo_commit_comment_edit',
340 'repo_commit_comment_edit',
358 repo_name=backend.repo_name,
341 repo_name=backend.repo_name,
359 commit_id=commit_id,
342 commit_id=commit_id,
360 comment_id=comment_id,
343 comment_id=comment_id,
361 ),
344 ),
362 params={
345 params={
363 'csrf_token': self.csrf_token,
346 'csrf_token': self.csrf_token,
364 'text': test_text,
347 'text': test_text,
365 'version': '0',
348 'version': '0',
366 }
349 }
367 )
350 )
368 test_text_v2 = 'test_v2'
351 test_text_v2 = 'test_v2'
369 response = self.app.post(
352 response = self.app.post(
370 route_path(
353 route_path(
371 'repo_commit_comment_edit',
354 'repo_commit_comment_edit',
372 repo_name=backend.repo_name,
355 repo_name=backend.repo_name,
373 commit_id=commit_id,
356 commit_id=commit_id,
374 comment_id=comment_id,
357 comment_id=comment_id,
375 ),
358 ),
376 params={
359 params={
377 'csrf_token': self.csrf_token,
360 'csrf_token': self.csrf_token,
378 'text': test_text_v2,
361 'text': test_text_v2,
379 'version': '0',
362 'version': '0',
380 },
363 },
381 status=409,
364 status=409,
382 )
365 )
383 assert response.status_int == 409
366 assert response.status_int == 409
384
367
385 text_form_db = ChangesetComment.query().filter(
368 text_form_db = ChangesetComment.query().filter(
386 ChangesetComment.comment_id == comment_id).first().text
369 ChangesetComment.comment_id == comment_id).first().text
387
370
388 assert test_text == text_form_db
371 assert test_text == text_form_db
389 assert test_text_v2 != text_form_db
372 assert test_text_v2 != text_form_db
390
373
391 def test_edit_forbidden_for_immutable_comments(self, backend):
374 def test_edit_forbidden_for_immutable_comments(self, backend):
392 self.log_user()
375 self.log_user()
393 commit_id = backend.repo.get_commit('300').raw_id
376 commit_id = backend.repo.get_commit('300').raw_id
394 text = 'CommentOnCommit'
377 text = 'CommentOnCommit'
395
378
396 params = {'text': text, 'csrf_token': self.csrf_token, 'version': '0'}
379 params = {'text': text, 'csrf_token': self.csrf_token, 'version': '0'}
397 self.app.post(
380 self.app.post(
398 route_path(
381 route_path(
399 'repo_commit_comment_create',
382 'repo_commit_comment_create',
400 repo_name=backend.repo_name,
383 repo_name=backend.repo_name,
401 commit_id=commit_id,
384 commit_id=commit_id,
402 ),
385 ),
403 params=params
386 params=params
404 )
387 )
405
388
406 comments = ChangesetComment.query().all()
389 comments = ChangesetComment.query().all()
407 assert len(comments) == 1
390 assert len(comments) == 1
408 comment_id = comments[0].comment_id
391 comment_id = comments[0].comment_id
409
392
410 comment = ChangesetComment.get(comment_id)
393 comment = ChangesetComment.get(comment_id)
411 comment.immutable_state = ChangesetComment.OP_IMMUTABLE
394 comment.immutable_state = ChangesetComment.OP_IMMUTABLE
412 Session().add(comment)
395 Session().add(comment)
413 Session().commit()
396 Session().commit()
414
397
415 response = self.app.post(
398 response = self.app.post(
416 route_path(
399 route_path(
417 'repo_commit_comment_edit',
400 'repo_commit_comment_edit',
418 repo_name=backend.repo_name,
401 repo_name=backend.repo_name,
419 commit_id=commit_id,
402 commit_id=commit_id,
420 comment_id=comment_id,
403 comment_id=comment_id,
421 ),
404 ),
422 params={
405 params={
423 'csrf_token': self.csrf_token,
406 'csrf_token': self.csrf_token,
424 'text': 'test_text',
407 'text': 'test_text',
425 },
408 },
426 status=403,
409 status=403,
427 )
410 )
428 assert response.status_int == 403
411 assert response.status_int == 403
429
412
430 def test_delete_forbidden_for_immutable_comments(self, backend):
413 def test_delete_forbidden_for_immutable_comments(self, backend):
431 self.log_user()
414 self.log_user()
432 commit_id = backend.repo.get_commit('300').raw_id
415 commit_id = backend.repo.get_commit('300').raw_id
433 text = 'CommentOnCommit'
416 text = 'CommentOnCommit'
434
417
435 params = {'text': text, 'csrf_token': self.csrf_token}
418 params = {'text': text, 'csrf_token': self.csrf_token}
436 self.app.post(
419 self.app.post(
437 route_path(
420 route_path(
438 'repo_commit_comment_create',
421 'repo_commit_comment_create',
439 repo_name=backend.repo_name, commit_id=commit_id),
422 repo_name=backend.repo_name, commit_id=commit_id),
440 params=params)
423 params=params)
441
424
442 comments = ChangesetComment.query().all()
425 comments = ChangesetComment.query().all()
443 assert len(comments) == 1
426 assert len(comments) == 1
444 comment_id = comments[0].comment_id
427 comment_id = comments[0].comment_id
445
428
446 comment = ChangesetComment.get(comment_id)
429 comment = ChangesetComment.get(comment_id)
447 comment.immutable_state = ChangesetComment.OP_IMMUTABLE
430 comment.immutable_state = ChangesetComment.OP_IMMUTABLE
448 Session().add(comment)
431 Session().add(comment)
449 Session().commit()
432 Session().commit()
450
433
451 self.app.post(
434 self.app.post(
452 route_path('repo_commit_comment_delete',
435 route_path('repo_commit_comment_delete',
453 repo_name=backend.repo_name,
436 repo_name=backend.repo_name,
454 commit_id=commit_id,
437 commit_id=commit_id,
455 comment_id=comment_id),
438 comment_id=comment_id),
456 params={'csrf_token': self.csrf_token},
439 params={'csrf_token': self.csrf_token},
457 status=403)
440 status=403)
458
441
459 @pytest.mark.parametrize('renderer, text_input, output', [
442 @pytest.mark.parametrize('renderer, text_input, output', [
460 ('rst', 'plain text', '<p>plain text</p>'),
443 ('rst', 'plain text', '<p>plain text</p>'),
461 ('rst', 'header\n======', '<h1 class="title">header</h1>'),
444 ('rst', 'header\n======', '<h1 class="title">header</h1>'),
462 ('rst', '*italics*', '<em>italics</em>'),
445 ('rst', '*italics*', '<em>italics</em>'),
463 ('rst', '**bold**', '<strong>bold</strong>'),
446 ('rst', '**bold**', '<strong>bold</strong>'),
464 ('markdown', 'plain text', '<p>plain text</p>'),
447 ('markdown', 'plain text', '<p>plain text</p>'),
465 ('markdown', '# header', '<h1>header</h1>'),
448 ('markdown', '# header', '<h1>header</h1>'),
466 ('markdown', '*italics*', '<em>italics</em>'),
449 ('markdown', '*italics*', '<em>italics</em>'),
467 ('markdown', '**bold**', '<strong>bold</strong>'),
450 ('markdown', '**bold**', '<strong>bold</strong>'),
468 ], ids=['rst-plain', 'rst-header', 'rst-italics', 'rst-bold', 'md-plain',
451 ], ids=['rst-plain', 'rst-header', 'rst-italics', 'rst-bold', 'md-plain',
469 'md-header', 'md-italics', 'md-bold', ])
452 'md-header', 'md-italics', 'md-bold', ])
470 def test_preview(self, renderer, text_input, output, backend, xhr_header):
453 def test_preview(self, renderer, text_input, output, backend, xhr_header):
471 self.log_user()
454 self.log_user()
472 params = {
455 params = {
473 'renderer': renderer,
456 'renderer': renderer,
474 'text': text_input,
457 'text': text_input,
475 'csrf_token': self.csrf_token
458 'csrf_token': self.csrf_token
476 }
459 }
477 commit_id = '0' * 16 # fake this for tests
460 commit_id = '0' * 16 # fake this for tests
478 response = self.app.post(
461 response = self.app.post(
479 route_path('repo_commit_comment_preview',
462 route_path('repo_commit_comment_preview',
480 repo_name=backend.repo_name, commit_id=commit_id,),
463 repo_name=backend.repo_name, commit_id=commit_id,),
481 params=params,
464 params=params,
482 extra_environ=xhr_header)
465 extra_environ=xhr_header)
483
466
484 response.mustcontain(output)
467 response.mustcontain(output)
485
468
486
469
487 def assert_comment_links(response, comments, inline_comments):
470 def assert_comment_links(response, comments, inline_comments):
488 response.mustcontain(
471 response.mustcontain(
489 '<span class="display-none" id="general-comments-count">{}</span>'.format(comments))
472 '<span class="display-none" id="general-comments-count">{}</span>'.format(comments))
490 response.mustcontain(
473 response.mustcontain(
491 '<span class="display-none" id="inline-comments-count">{}</span>'.format(inline_comments))
474 '<span class="display-none" id="inline-comments-count">{}</span>'.format(inline_comments))
492
475
493
476
494
477
@@ -1,336 +1,316 b''
1
1
2 # Copyright (C) 2010-2023 RhodeCode GmbH
2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 #
3 #
4 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
7 #
7 #
8 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
11 # GNU General Public License for more details.
12 #
12 #
13 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
15 #
16 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19
19
20 import pytest
20 import pytest
21
21
22 from rhodecode.apps.repository.tests.test_repo_compare import ComparePage
22 from rhodecode.apps.repository.tests.test_repo_compare import ComparePage
23 from rhodecode.lib.helpers import _shorten_commit_id
23 from rhodecode.lib.helpers import _shorten_commit_id
24
24 from rhodecode.tests.routes import route_path
25
26 def route_path(name, params=None, **kwargs):
27 import urllib.request
28 import urllib.parse
29 import urllib.error
30
31 base_url = {
32 'repo_commit': '/{repo_name}/changeset/{commit_id}',
33 'repo_commit_children': '/{repo_name}/changeset_children/{commit_id}',
34 'repo_commit_parents': '/{repo_name}/changeset_parents/{commit_id}',
35 'repo_commit_raw': '/{repo_name}/changeset-diff/{commit_id}',
36 'repo_commit_patch': '/{repo_name}/changeset-patch/{commit_id}',
37 'repo_commit_download': '/{repo_name}/changeset-download/{commit_id}',
38 'repo_commit_data': '/{repo_name}/changeset-data/{commit_id}',
39 'repo_compare': '/{repo_name}/compare/{source_ref_type}@{source_ref}...{target_ref_type}@{target_ref}',
40 }[name].format(**kwargs)
41
42 if params:
43 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
44 return base_url
45
25
46
26
47 @pytest.mark.usefixtures("app")
27 @pytest.mark.usefixtures("app")
48 class TestRepoCommitView(object):
28 class TestRepoCommitView(object):
49
29
50 def test_show_commit(self, backend):
30 def test_show_commit(self, backend):
51 commit_id = self.commit_id[backend.alias]
31 commit_id = self.commit_id[backend.alias]
52 response = self.app.get(route_path(
32 response = self.app.get(route_path(
53 'repo_commit', repo_name=backend.repo_name, commit_id=commit_id))
33 'repo_commit', repo_name=backend.repo_name, commit_id=commit_id))
54 response.mustcontain('Added a symlink')
34 response.mustcontain('Added a symlink')
55 response.mustcontain(commit_id)
35 response.mustcontain(commit_id)
56 response.mustcontain('No newline at end of file')
36 response.mustcontain('No newline at end of file')
57
37
58 def test_show_raw(self, backend):
38 def test_show_raw(self, backend):
59 commit_id = self.commit_id[backend.alias]
39 commit_id = self.commit_id[backend.alias]
60 # webtest uses linter to check if response is bytes,
40 # webtest uses linter to check if response is bytes,
61 # and we use memoryview here as a wrapper, quick turn-off
41 # and we use memoryview here as a wrapper, quick turn-off
62 self.app.lint = False
42 self.app.lint = False
63
43
64 response = self.app.get(route_path(
44 response = self.app.get(route_path(
65 'repo_commit_raw',
45 'repo_commit_raw',
66 repo_name=backend.repo_name, commit_id=commit_id))
46 repo_name=backend.repo_name, commit_id=commit_id))
67 assert response.body == self.diffs[backend.alias]
47 assert response.body == self.diffs[backend.alias]
68
48
69 def test_show_raw_patch(self, backend):
49 def test_show_raw_patch(self, backend):
70 response = self.app.get(route_path(
50 response = self.app.get(route_path(
71 'repo_commit_patch', repo_name=backend.repo_name,
51 'repo_commit_patch', repo_name=backend.repo_name,
72 commit_id=self.commit_id[backend.alias]))
52 commit_id=self.commit_id[backend.alias]))
73 assert response.body == self.patches[backend.alias]
53 assert response.body == self.patches[backend.alias]
74
54
75 def test_commit_download(self, backend):
55 def test_commit_download(self, backend):
76 # webtest uses linter to check if response is bytes,
56 # webtest uses linter to check if response is bytes,
77 # and we use memoryview here as a wrapper, quick turn-off
57 # and we use memoryview here as a wrapper, quick turn-off
78 self.app.lint = False
58 self.app.lint = False
79
59
80 response = self.app.get(route_path(
60 response = self.app.get(route_path(
81 'repo_commit_download',
61 'repo_commit_download',
82 repo_name=backend.repo_name,
62 repo_name=backend.repo_name,
83 commit_id=self.commit_id[backend.alias]))
63 commit_id=self.commit_id[backend.alias]))
84 assert response.body == self.diffs[backend.alias]
64 assert response.body == self.diffs[backend.alias]
85
65
86 def test_single_commit_page_different_ops(self, backend):
66 def test_single_commit_page_different_ops(self, backend):
87 commit_id = {
67 commit_id = {
88 'hg': '603d6c72c46d953420c89d36372f08d9f305f5dd',
68 'hg': '603d6c72c46d953420c89d36372f08d9f305f5dd',
89 'git': '03fa803d7e9fb14daa9a3089e0d1494eda75d986',
69 'git': '03fa803d7e9fb14daa9a3089e0d1494eda75d986',
90 'svn': '337',
70 'svn': '337',
91 }
71 }
92 diff_stat = {
72 diff_stat = {
93 'hg': (21, 943, 288),
73 'hg': (21, 943, 288),
94 'git': (20, 941, 286),
74 'git': (20, 941, 286),
95 'svn': (21, 943, 288),
75 'svn': (21, 943, 288),
96 }
76 }
97
77
98 commit_id = commit_id[backend.alias]
78 commit_id = commit_id[backend.alias]
99 response = self.app.get(route_path(
79 response = self.app.get(route_path(
100 'repo_commit',
80 'repo_commit',
101 repo_name=backend.repo_name, commit_id=commit_id))
81 repo_name=backend.repo_name, commit_id=commit_id))
102
82
103 response.mustcontain(_shorten_commit_id(commit_id))
83 response.mustcontain(_shorten_commit_id(commit_id))
104
84
105 compare_page = ComparePage(response)
85 compare_page = ComparePage(response)
106 file_changes = diff_stat[backend.alias]
86 file_changes = diff_stat[backend.alias]
107 compare_page.contains_change_summary(*file_changes)
87 compare_page.contains_change_summary(*file_changes)
108
88
109 # files op files
89 # files op files
110 response.mustcontain('File not present at commit: %s' %
90 response.mustcontain('File not present at commit: %s' %
111 _shorten_commit_id(commit_id))
91 _shorten_commit_id(commit_id))
112
92
113 # svn uses a different filename
93 # svn uses a different filename
114 if backend.alias == 'svn':
94 if backend.alias == 'svn':
115 response.mustcontain('new file 10644')
95 response.mustcontain('new file 10644')
116 else:
96 else:
117 response.mustcontain('new file 100644')
97 response.mustcontain('new file 100644')
118 response.mustcontain('Changed theme to ADC theme') # commit msg
98 response.mustcontain('Changed theme to ADC theme') # commit msg
119
99
120 self._check_new_diff_menus(response, right_menu=True)
100 self._check_new_diff_menus(response, right_menu=True)
121
101
122 def test_commit_range_page_different_ops(self, backend):
102 def test_commit_range_page_different_ops(self, backend):
123 commit_id_range = {
103 commit_id_range = {
124 'hg': (
104 'hg': (
125 '25d7e49c18b159446cadfa506a5cf8ad1cb04067',
105 '25d7e49c18b159446cadfa506a5cf8ad1cb04067',
126 '603d6c72c46d953420c89d36372f08d9f305f5dd'),
106 '603d6c72c46d953420c89d36372f08d9f305f5dd'),
127 'git': (
107 'git': (
128 '6fc9270775aaf5544c1deb014f4ddd60c952fcbb',
108 '6fc9270775aaf5544c1deb014f4ddd60c952fcbb',
129 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'),
109 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'),
130 'svn': (
110 'svn': (
131 '335',
111 '335',
132 '337'),
112 '337'),
133 }
113 }
134 commit_ids = commit_id_range[backend.alias]
114 commit_ids = commit_id_range[backend.alias]
135 commit_id = '%s...%s' % (commit_ids[0], commit_ids[1])
115 commit_id = '%s...%s' % (commit_ids[0], commit_ids[1])
136 response = self.app.get(route_path(
116 response = self.app.get(route_path(
137 'repo_commit',
117 'repo_commit',
138 repo_name=backend.repo_name, commit_id=commit_id))
118 repo_name=backend.repo_name, commit_id=commit_id))
139
119
140 response.mustcontain(_shorten_commit_id(commit_ids[0]))
120 response.mustcontain(_shorten_commit_id(commit_ids[0]))
141 response.mustcontain(_shorten_commit_id(commit_ids[1]))
121 response.mustcontain(_shorten_commit_id(commit_ids[1]))
142
122
143 compare_page = ComparePage(response)
123 compare_page = ComparePage(response)
144
124
145 # svn is special
125 # svn is special
146 if backend.alias == 'svn':
126 if backend.alias == 'svn':
147 response.mustcontain('new file 10644')
127 response.mustcontain('new file 10644')
148 for file_changes in [(1, 5, 1), (12, 236, 22), (21, 943, 288)]:
128 for file_changes in [(1, 5, 1), (12, 236, 22), (21, 943, 288)]:
149 compare_page.contains_change_summary(*file_changes)
129 compare_page.contains_change_summary(*file_changes)
150 elif backend.alias == 'git':
130 elif backend.alias == 'git':
151 response.mustcontain('new file 100644')
131 response.mustcontain('new file 100644')
152 for file_changes in [(12, 222, 20), (20, 941, 286)]:
132 for file_changes in [(12, 222, 20), (20, 941, 286)]:
153 compare_page.contains_change_summary(*file_changes)
133 compare_page.contains_change_summary(*file_changes)
154 else:
134 else:
155 response.mustcontain('new file 100644')
135 response.mustcontain('new file 100644')
156 for file_changes in [(12, 222, 20), (21, 943, 288)]:
136 for file_changes in [(12, 222, 20), (21, 943, 288)]:
157 compare_page.contains_change_summary(*file_changes)
137 compare_page.contains_change_summary(*file_changes)
158
138
159 # files op files
139 # files op files
160 response.mustcontain('File not present at commit: %s' % _shorten_commit_id(commit_ids[1]))
140 response.mustcontain('File not present at commit: %s' % _shorten_commit_id(commit_ids[1]))
161 response.mustcontain('Added docstrings to vcs.cli') # commit msg
141 response.mustcontain('Added docstrings to vcs.cli') # commit msg
162 response.mustcontain('Changed theme to ADC theme') # commit msg
142 response.mustcontain('Changed theme to ADC theme') # commit msg
163
143
164 self._check_new_diff_menus(response)
144 self._check_new_diff_menus(response)
165
145
166 def test_combined_compare_commit_page_different_ops(self, backend):
146 def test_combined_compare_commit_page_different_ops(self, backend):
167 commit_id_range = {
147 commit_id_range = {
168 'hg': (
148 'hg': (
169 '4fdd71e9427417b2e904e0464c634fdee85ec5a7',
149 '4fdd71e9427417b2e904e0464c634fdee85ec5a7',
170 '603d6c72c46d953420c89d36372f08d9f305f5dd'),
150 '603d6c72c46d953420c89d36372f08d9f305f5dd'),
171 'git': (
151 'git': (
172 'f5fbf9cfd5f1f1be146f6d3b38bcd791a7480c13',
152 'f5fbf9cfd5f1f1be146f6d3b38bcd791a7480c13',
173 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'),
153 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'),
174 'svn': (
154 'svn': (
175 '335',
155 '335',
176 '337'),
156 '337'),
177 }
157 }
178 commit_ids = commit_id_range[backend.alias]
158 commit_ids = commit_id_range[backend.alias]
179 response = self.app.get(route_path(
159 response = self.app.get(route_path(
180 'repo_compare',
160 'repo_compare',
181 repo_name=backend.repo_name,
161 repo_name=backend.repo_name,
182 source_ref_type='rev', source_ref=commit_ids[0],
162 source_ref_type='rev', source_ref=commit_ids[0],
183 target_ref_type='rev', target_ref=commit_ids[1], ))
163 target_ref_type='rev', target_ref=commit_ids[1], ))
184
164
185 response.mustcontain(_shorten_commit_id(commit_ids[0]))
165 response.mustcontain(_shorten_commit_id(commit_ids[0]))
186 response.mustcontain(_shorten_commit_id(commit_ids[1]))
166 response.mustcontain(_shorten_commit_id(commit_ids[1]))
187
167
188 # files op files
168 # files op files
189 response.mustcontain('File not present at commit: %s' %
169 response.mustcontain('File not present at commit: %s' %
190 _shorten_commit_id(commit_ids[1]))
170 _shorten_commit_id(commit_ids[1]))
191
171
192 compare_page = ComparePage(response)
172 compare_page = ComparePage(response)
193
173
194 # svn is special
174 # svn is special
195 if backend.alias == 'svn':
175 if backend.alias == 'svn':
196 response.mustcontain('new file 10644')
176 response.mustcontain('new file 10644')
197 file_changes = (32, 1179, 310)
177 file_changes = (32, 1179, 310)
198 compare_page.contains_change_summary(*file_changes)
178 compare_page.contains_change_summary(*file_changes)
199 elif backend.alias == 'git':
179 elif backend.alias == 'git':
200 response.mustcontain('new file 100644')
180 response.mustcontain('new file 100644')
201 file_changes = (31, 1163, 306)
181 file_changes = (31, 1163, 306)
202 compare_page.contains_change_summary(*file_changes)
182 compare_page.contains_change_summary(*file_changes)
203 else:
183 else:
204 response.mustcontain('new file 100644')
184 response.mustcontain('new file 100644')
205 file_changes = (32, 1165, 308)
185 file_changes = (32, 1165, 308)
206 compare_page.contains_change_summary(*file_changes)
186 compare_page.contains_change_summary(*file_changes)
207
187
208 response.mustcontain('Added docstrings to vcs.cli') # commit msg
188 response.mustcontain('Added docstrings to vcs.cli') # commit msg
209 response.mustcontain('Changed theme to ADC theme') # commit msg
189 response.mustcontain('Changed theme to ADC theme') # commit msg
210
190
211 self._check_new_diff_menus(response)
191 self._check_new_diff_menus(response)
212
192
213 def test_changeset_range(self, backend):
193 def test_changeset_range(self, backend):
214 self._check_changeset_range(
194 self._check_changeset_range(
215 backend, self.commit_id_range, self.commit_id_range_result)
195 backend, self.commit_id_range, self.commit_id_range_result)
216
196
217 def test_changeset_range_with_initial_commit(self, backend):
197 def test_changeset_range_with_initial_commit(self, backend):
218 commit_id_range = {
198 commit_id_range = {
219 'hg': (
199 'hg': (
220 'b986218ba1c9b0d6a259fac9b050b1724ed8e545'
200 'b986218ba1c9b0d6a259fac9b050b1724ed8e545'
221 '...6cba7170863a2411822803fa77a0a264f1310b35'),
201 '...6cba7170863a2411822803fa77a0a264f1310b35'),
222 'git': (
202 'git': (
223 'c1214f7e79e02fc37156ff215cd71275450cffc3'
203 'c1214f7e79e02fc37156ff215cd71275450cffc3'
224 '...fa6600f6848800641328adbf7811fd2372c02ab2'),
204 '...fa6600f6848800641328adbf7811fd2372c02ab2'),
225 'svn': '1...3',
205 'svn': '1...3',
226 }
206 }
227 commit_id_range_result = {
207 commit_id_range_result = {
228 'hg': ['b986218ba1c9', '3d8f361e72ab', '6cba7170863a'],
208 'hg': ['b986218ba1c9', '3d8f361e72ab', '6cba7170863a'],
229 'git': ['c1214f7e79e0', '38b5fe81f109', 'fa6600f68488'],
209 'git': ['c1214f7e79e0', '38b5fe81f109', 'fa6600f68488'],
230 'svn': ['1', '2', '3'],
210 'svn': ['1', '2', '3'],
231 }
211 }
232 self._check_changeset_range(
212 self._check_changeset_range(
233 backend, commit_id_range, commit_id_range_result)
213 backend, commit_id_range, commit_id_range_result)
234
214
235 def _check_changeset_range(
215 def _check_changeset_range(
236 self, backend, commit_id_ranges, commit_id_range_result):
216 self, backend, commit_id_ranges, commit_id_range_result):
237 response = self.app.get(
217 response = self.app.get(
238 route_path('repo_commit',
218 route_path('repo_commit',
239 repo_name=backend.repo_name,
219 repo_name=backend.repo_name,
240 commit_id=commit_id_ranges[backend.alias]))
220 commit_id=commit_id_ranges[backend.alias]))
241
221
242 expected_result = commit_id_range_result[backend.alias]
222 expected_result = commit_id_range_result[backend.alias]
243 response.mustcontain('{} commits'.format(len(expected_result)))
223 response.mustcontain('{} commits'.format(len(expected_result)))
244 for commit_id in expected_result:
224 for commit_id in expected_result:
245 response.mustcontain(commit_id)
225 response.mustcontain(commit_id)
246
226
247 commit_id = {
227 commit_id = {
248 'hg': '2062ec7beeeaf9f44a1c25c41479565040b930b2',
228 'hg': '2062ec7beeeaf9f44a1c25c41479565040b930b2',
249 'svn': '393',
229 'svn': '393',
250 'git': 'fd627b9e0dd80b47be81af07c4a98518244ed2f7',
230 'git': 'fd627b9e0dd80b47be81af07c4a98518244ed2f7',
251 }
231 }
252
232
253 commit_id_range = {
233 commit_id_range = {
254 'hg': (
234 'hg': (
255 'a53d9201d4bc278910d416d94941b7ea007ecd52'
235 'a53d9201d4bc278910d416d94941b7ea007ecd52'
256 '...2062ec7beeeaf9f44a1c25c41479565040b930b2'),
236 '...2062ec7beeeaf9f44a1c25c41479565040b930b2'),
257 'git': (
237 'git': (
258 '7ab37bc680b4aa72c34d07b230c866c28e9fc204'
238 '7ab37bc680b4aa72c34d07b230c866c28e9fc204'
259 '...fd627b9e0dd80b47be81af07c4a98518244ed2f7'),
239 '...fd627b9e0dd80b47be81af07c4a98518244ed2f7'),
260 'svn': '391...393',
240 'svn': '391...393',
261 }
241 }
262
242
263 commit_id_range_result = {
243 commit_id_range_result = {
264 'hg': ['a53d9201d4bc', '96507bd11ecc', '2062ec7beeea'],
244 'hg': ['a53d9201d4bc', '96507bd11ecc', '2062ec7beeea'],
265 'git': ['7ab37bc680b4', '5f2c6ee19592', 'fd627b9e0dd8'],
245 'git': ['7ab37bc680b4', '5f2c6ee19592', 'fd627b9e0dd8'],
266 'svn': ['391', '392', '393'],
246 'svn': ['391', '392', '393'],
267 }
247 }
268
248
269 diffs = {
249 diffs = {
270 'hg': br"""diff --git a/README b/README
250 'hg': br"""diff --git a/README b/README
271 new file mode 120000
251 new file mode 120000
272 --- /dev/null
252 --- /dev/null
273 +++ b/README
253 +++ b/README
274 @@ -0,0 +1,1 @@
254 @@ -0,0 +1,1 @@
275 +README.rst
255 +README.rst
276 \ No newline at end of file
256 \ No newline at end of file
277 """,
257 """,
278 'git': br"""diff --git a/README b/README
258 'git': br"""diff --git a/README b/README
279 new file mode 120000
259 new file mode 120000
280 index 0000000..92cacd2
260 index 0000000..92cacd2
281 --- /dev/null
261 --- /dev/null
282 +++ b/README
262 +++ b/README
283 @@ -0,0 +1 @@
263 @@ -0,0 +1 @@
284 +README.rst
264 +README.rst
285 \ No newline at end of file
265 \ No newline at end of file
286 """,
266 """,
287 'svn': b"""Index: README
267 'svn': b"""Index: README
288 ===================================================================
268 ===================================================================
289 diff --git a/README b/README
269 diff --git a/README b/README
290 new file mode 10644
270 new file mode 10644
291 --- /dev/null\t(revision 0)
271 --- /dev/null\t(revision 0)
292 +++ b/README\t(revision 393)
272 +++ b/README\t(revision 393)
293 @@ -0,0 +1 @@
273 @@ -0,0 +1 @@
294 +link README.rst
274 +link README.rst
295 \\ No newline at end of file
275 \\ No newline at end of file
296 """,
276 """,
297 }
277 }
298
278
299 patches = {
279 patches = {
300 'hg': br"""# HG changeset patch
280 'hg': br"""# HG changeset patch
301 # User Marcin Kuzminski <marcin@python-works.com>
281 # User Marcin Kuzminski <marcin@python-works.com>
302 # Date 2014-01-07 12:21:40
282 # Date 2014-01-07 12:21:40
303 # Node ID 2062ec7beeeaf9f44a1c25c41479565040b930b2
283 # Node ID 2062ec7beeeaf9f44a1c25c41479565040b930b2
304 # Parent 96507bd11ecc815ebc6270fdf6db110928c09c1e
284 # Parent 96507bd11ecc815ebc6270fdf6db110928c09c1e
305
285
306 Added a symlink
286 Added a symlink
307
287
308 """ + diffs['hg'],
288 """ + diffs['hg'],
309 'git': br"""From fd627b9e0dd80b47be81af07c4a98518244ed2f7 2014-01-07 12:22:20
289 'git': br"""From fd627b9e0dd80b47be81af07c4a98518244ed2f7 2014-01-07 12:22:20
310 From: Marcin Kuzminski <marcin@python-works.com>
290 From: Marcin Kuzminski <marcin@python-works.com>
311 Date: 2014-01-07 12:22:20
291 Date: 2014-01-07 12:22:20
312 Subject: [PATCH] Added a symlink
292 Subject: [PATCH] Added a symlink
313
293
314 ---
294 ---
315
295
316 """ + diffs['git'],
296 """ + diffs['git'],
317 'svn': br"""# SVN changeset patch
297 'svn': br"""# SVN changeset patch
318 # User marcin
298 # User marcin
319 # Date 2014-09-02 12:25:22.071142
299 # Date 2014-09-02 12:25:22.071142
320 # Revision 393
300 # Revision 393
321
301
322 Added a symlink
302 Added a symlink
323
303
324 """ + diffs['svn'],
304 """ + diffs['svn'],
325 }
305 }
326
306
327 def _check_new_diff_menus(self, response, right_menu=False,):
307 def _check_new_diff_menus(self, response, right_menu=False,):
328 # individual file diff menus
308 # individual file diff menus
329 for elem in ['Show file before', 'Show file after']:
309 for elem in ['Show file before', 'Show file after']:
330 response.mustcontain(elem)
310 response.mustcontain(elem)
331
311
332 # right pane diff menus
312 # right pane diff menus
333 if right_menu:
313 if right_menu:
334 for elem in ['Hide whitespace changes', 'Toggle wide diff',
314 for elem in ['Hide whitespace changes', 'Toggle wide diff',
335 'Show full context diff']:
315 'Show full context diff']:
336 response.mustcontain(elem)
316 response.mustcontain(elem)
@@ -1,670 +1,656 b''
1
1
2 # Copyright (C) 2010-2023 RhodeCode GmbH
2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 #
3 #
4 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
7 #
7 #
8 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
11 # GNU General Public License for more details.
12 #
12 #
13 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
15 #
16 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19
19
20 import mock
20 import mock
21 import pytest
21 import pytest
22 import lxml.html
22 import lxml.html
23
23
24 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
24 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
25 from rhodecode.tests import assert_session_flash
25 from rhodecode.tests import assert_session_flash
26 from rhodecode.tests.utils import AssertResponse, commit_change
26 from rhodecode.tests.utils import AssertResponse, commit_change
27
27 from rhodecode.tests.routes import route_path
28
29 def route_path(name, params=None, **kwargs):
30 import urllib.request
31 import urllib.parse
32 import urllib.error
33
34 base_url = {
35 'repo_compare_select': '/{repo_name}/compare',
36 'repo_compare': '/{repo_name}/compare/{source_ref_type}@{source_ref}...{target_ref_type}@{target_ref}',
37 }[name].format(**kwargs)
38
39 if params:
40 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
41 return base_url
42
28
43
29
44 @pytest.mark.usefixtures("autologin_user", "app")
30 @pytest.mark.usefixtures("autologin_user", "app")
45 class TestCompareView(object):
31 class TestCompareView(object):
46
32
47 def test_compare_index_is_reached_at_least_once(self, backend):
33 def test_compare_index_is_reached_at_least_once(self, backend):
48 repo = backend.repo
34 repo = backend.repo
49 self.app.get(
35 self.app.get(
50 route_path('repo_compare_select', repo_name=repo.repo_name))
36 route_path('repo_compare_select', repo_name=repo.repo_name))
51
37
52 @pytest.mark.xfail_backends("svn", reason="Requires pull")
38 @pytest.mark.xfail_backends("svn", reason="Requires pull")
53 def test_compare_remote_with_different_commit_indexes(self, backend):
39 def test_compare_remote_with_different_commit_indexes(self, backend):
54 # Preparing the following repository structure:
40 # Preparing the following repository structure:
55 #
41 #
56 # Origin repository has two commits:
42 # Origin repository has two commits:
57 #
43 #
58 # 0 1
44 # 0 1
59 # A -- D
45 # A -- D
60 #
46 #
61 # The fork of it has a few more commits and "D" has a commit index
47 # The fork of it has a few more commits and "D" has a commit index
62 # which does not exist in origin.
48 # which does not exist in origin.
63 #
49 #
64 # 0 1 2 3 4
50 # 0 1 2 3 4
65 # A -- -- -- D -- E
51 # A -- -- -- D -- E
66 # \- B -- C
52 # \- B -- C
67 #
53 #
68
54
69 fork = backend.create_repo()
55 fork = backend.create_repo()
70 origin = backend.create_repo()
56 origin = backend.create_repo()
71
57
72 # prepare fork
58 # prepare fork
73 commit0 = commit_change(
59 commit0 = commit_change(
74 fork.repo_name, filename=b'file1', content=b'A',
60 fork.repo_name, filename=b'file1', content=b'A',
75 message='A - Initial Commit', vcs_type=backend.alias, parent=None, newfile=True)
61 message='A - Initial Commit', vcs_type=backend.alias, parent=None, newfile=True)
76
62
77 commit1 = commit_change(
63 commit1 = commit_change(
78 fork.repo_name, filename=b'file1', content=b'B',
64 fork.repo_name, filename=b'file1', content=b'B',
79 message='B, child of A', vcs_type=backend.alias, parent=commit0)
65 message='B, child of A', vcs_type=backend.alias, parent=commit0)
80
66
81 commit_change( # commit 2
67 commit_change( # commit 2
82 fork.repo_name, filename=b'file1', content=b'C',
68 fork.repo_name, filename=b'file1', content=b'C',
83 message='C, child of B', vcs_type=backend.alias, parent=commit1)
69 message='C, child of B', vcs_type=backend.alias, parent=commit1)
84
70
85 commit3 = commit_change(
71 commit3 = commit_change(
86 fork.repo_name, filename=b'file1', content=b'D',
72 fork.repo_name, filename=b'file1', content=b'D',
87 message='D, child of A', vcs_type=backend.alias, parent=commit0)
73 message='D, child of A', vcs_type=backend.alias, parent=commit0)
88
74
89 commit4 = commit_change(
75 commit4 = commit_change(
90 fork.repo_name, filename=b'file1', content=b'E',
76 fork.repo_name, filename=b'file1', content=b'E',
91 message='E, child of D', vcs_type=backend.alias, parent=commit3)
77 message='E, child of D', vcs_type=backend.alias, parent=commit3)
92
78
93 # prepare origin repository, taking just the history up to D
79 # prepare origin repository, taking just the history up to D
94
80
95 origin_repo = origin.scm_instance(cache=False)
81 origin_repo = origin.scm_instance(cache=False)
96 origin_repo.config.clear_section('hooks')
82 origin_repo.config.clear_section('hooks')
97 origin_repo.pull(fork.repo_full_path, commit_ids=[commit3.raw_id])
83 origin_repo.pull(fork.repo_full_path, commit_ids=[commit3.raw_id])
98 origin_repo = origin.scm_instance(cache=False) # cache rebuild
84 origin_repo = origin.scm_instance(cache=False) # cache rebuild
99
85
100 # Verify test fixture setup
86 # Verify test fixture setup
101 # This does not work for git
87 # This does not work for git
102 if backend.alias != 'git':
88 if backend.alias != 'git':
103 assert 5 == len(fork.scm_instance(cache=False).commit_ids)
89 assert 5 == len(fork.scm_instance(cache=False).commit_ids)
104 assert 2 == len(origin_repo.commit_ids)
90 assert 2 == len(origin_repo.commit_ids)
105
91
106 # Comparing the revisions
92 # Comparing the revisions
107 response = self.app.get(
93 response = self.app.get(
108 route_path('repo_compare',
94 route_path('repo_compare',
109 repo_name=origin.repo_name,
95 repo_name=origin.repo_name,
110 source_ref_type="rev", source_ref=commit3.raw_id,
96 source_ref_type="rev", source_ref=commit3.raw_id,
111 target_ref_type="rev", target_ref=commit4.raw_id,
97 target_ref_type="rev", target_ref=commit4.raw_id,
112 params=dict(merge='1', target_repo=fork.repo_name)
98 params=dict(merge='1', target_repo=fork.repo_name)
113 ),
99 ),
114 status=200)
100 status=200)
115
101
116 compare_page = ComparePage(response)
102 compare_page = ComparePage(response)
117 compare_page.contains_commits([commit4])
103 compare_page.contains_commits([commit4])
118
104
119 @pytest.mark.xfail_backends("svn", reason="Depends on branch support")
105 @pytest.mark.xfail_backends("svn", reason="Depends on branch support")
120 def test_compare_forks_on_branch_extra_commits(self, backend):
106 def test_compare_forks_on_branch_extra_commits(self, backend):
121 repo1 = backend.create_repo()
107 repo1 = backend.create_repo()
122
108
123 # commit something !
109 # commit something !
124 commit0 = commit_change(
110 commit0 = commit_change(
125 repo1.repo_name, filename=b'file1', content=b'line1\n',
111 repo1.repo_name, filename=b'file1', content=b'line1\n',
126 message='commit1', vcs_type=backend.alias, parent=None,
112 message='commit1', vcs_type=backend.alias, parent=None,
127 newfile=True)
113 newfile=True)
128
114
129 # fork this repo
115 # fork this repo
130 repo2 = backend.create_fork()
116 repo2 = backend.create_fork()
131
117
132 # add two extra commit into fork
118 # add two extra commit into fork
133 commit1 = commit_change(
119 commit1 = commit_change(
134 repo2.repo_name, filename=b'file1', content=b'line1\nline2\n',
120 repo2.repo_name, filename=b'file1', content=b'line1\nline2\n',
135 message='commit2', vcs_type=backend.alias, parent=commit0)
121 message='commit2', vcs_type=backend.alias, parent=commit0)
136
122
137 commit2 = commit_change(
123 commit2 = commit_change(
138 repo2.repo_name, filename=b'file1', content=b'line1\nline2\nline3\n',
124 repo2.repo_name, filename=b'file1', content=b'line1\nline2\nline3\n',
139 message='commit3', vcs_type=backend.alias, parent=commit1)
125 message='commit3', vcs_type=backend.alias, parent=commit1)
140
126
141 commit_id1 = repo1.scm_instance().DEFAULT_BRANCH_NAME
127 commit_id1 = repo1.scm_instance().DEFAULT_BRANCH_NAME
142 commit_id2 = repo2.scm_instance().DEFAULT_BRANCH_NAME
128 commit_id2 = repo2.scm_instance().DEFAULT_BRANCH_NAME
143
129
144 response = self.app.get(
130 response = self.app.get(
145 route_path('repo_compare',
131 route_path('repo_compare',
146 repo_name=repo1.repo_name,
132 repo_name=repo1.repo_name,
147 source_ref_type="branch", source_ref=commit_id2,
133 source_ref_type="branch", source_ref=commit_id2,
148 target_ref_type="branch", target_ref=commit_id1,
134 target_ref_type="branch", target_ref=commit_id1,
149 params=dict(merge='1', target_repo=repo2.repo_name)
135 params=dict(merge='1', target_repo=repo2.repo_name)
150 ))
136 ))
151
137
152 response.mustcontain('%s@%s' % (repo1.repo_name, commit_id2))
138 response.mustcontain('%s@%s' % (repo1.repo_name, commit_id2))
153 response.mustcontain('%s@%s' % (repo2.repo_name, commit_id1))
139 response.mustcontain('%s@%s' % (repo2.repo_name, commit_id1))
154
140
155 compare_page = ComparePage(response)
141 compare_page = ComparePage(response)
156 compare_page.contains_change_summary(1, 2, 0)
142 compare_page.contains_change_summary(1, 2, 0)
157 compare_page.contains_commits([commit1, commit2])
143 compare_page.contains_commits([commit1, commit2])
158
144
159 anchor = 'a_c-{}-826e8142e6ba'.format(commit0.short_id)
145 anchor = 'a_c-{}-826e8142e6ba'.format(commit0.short_id)
160 compare_page.contains_file_links_and_anchors([('file1', anchor), ])
146 compare_page.contains_file_links_and_anchors([('file1', anchor), ])
161
147
162 # Swap is removed when comparing branches since it's a PR feature and
148 # Swap is removed when comparing branches since it's a PR feature and
163 # it is then a preview mode
149 # it is then a preview mode
164 compare_page.swap_is_hidden()
150 compare_page.swap_is_hidden()
165 compare_page.target_source_are_disabled()
151 compare_page.target_source_are_disabled()
166
152
167 @pytest.mark.xfail_backends("svn", reason="Depends on branch support")
153 @pytest.mark.xfail_backends("svn", reason="Depends on branch support")
168 def test_compare_forks_on_branch_extra_commits_origin_has_incomming(self, backend):
154 def test_compare_forks_on_branch_extra_commits_origin_has_incoming(self, backend):
169 repo1 = backend.create_repo()
155 repo1 = backend.create_repo()
170
156
171 # commit something !
157 # commit something !
172 commit0 = commit_change(
158 commit0 = commit_change(
173 repo1.repo_name, filename=b'file1', content=b'line1\n',
159 repo1.repo_name, filename=b'file1', content=b'line1\n',
174 message='commit1', vcs_type=backend.alias, parent=None,
160 message='commit1', vcs_type=backend.alias, parent=None,
175 newfile=True)
161 newfile=True)
176
162
177 # fork this repo
163 # fork this repo
178 repo2 = backend.create_fork()
164 repo2 = backend.create_fork()
179
165
180 # now commit something to origin repo
166 # now commit something to origin repo
181 commit_change(
167 commit_change(
182 repo1.repo_name, filename=b'file2', content=b'line1file2\n',
168 repo1.repo_name, filename=b'file2', content=b'line1file2\n',
183 message='commit2', vcs_type=backend.alias, parent=commit0,
169 message='commit2', vcs_type=backend.alias, parent=commit0,
184 newfile=True)
170 newfile=True)
185
171
186 # add two extra commit into fork
172 # add two extra commit into fork
187 commit1 = commit_change(
173 commit1 = commit_change(
188 repo2.repo_name, filename=b'file1', content=b'line1\nline2\n',
174 repo2.repo_name, filename=b'file1', content=b'line1\nline2\n',
189 message='commit2', vcs_type=backend.alias, parent=commit0)
175 message='commit2', vcs_type=backend.alias, parent=commit0)
190
176
191 commit2 = commit_change(
177 commit2 = commit_change(
192 repo2.repo_name, filename=b'file1', content=b'line1\nline2\nline3\n',
178 repo2.repo_name, filename=b'file1', content=b'line1\nline2\nline3\n',
193 message='commit3', vcs_type=backend.alias, parent=commit1)
179 message='commit3', vcs_type=backend.alias, parent=commit1)
194
180
195 commit_id1 = repo1.scm_instance().DEFAULT_BRANCH_NAME
181 commit_id1 = repo1.scm_instance().DEFAULT_BRANCH_NAME
196 commit_id2 = repo2.scm_instance().DEFAULT_BRANCH_NAME
182 commit_id2 = repo2.scm_instance().DEFAULT_BRANCH_NAME
197
183
198 response = self.app.get(
184 response = self.app.get(
199 route_path('repo_compare',
185 route_path('repo_compare',
200 repo_name=repo1.repo_name,
186 repo_name=repo1.repo_name,
201 source_ref_type="branch", source_ref=commit_id2,
187 source_ref_type="branch", source_ref=commit_id2,
202 target_ref_type="branch", target_ref=commit_id1,
188 target_ref_type="branch", target_ref=commit_id1,
203 params=dict(merge='1', target_repo=repo2.repo_name),
189 params=dict(merge='1', target_repo=repo2.repo_name),
204 ))
190 ))
205
191
206 response.mustcontain('%s@%s' % (repo1.repo_name, commit_id2))
192 response.mustcontain(f'{repo1.repo_name}@{commit_id2}')
207 response.mustcontain('%s@%s' % (repo2.repo_name, commit_id1))
193 response.mustcontain(f'{repo2.repo_name}@{commit_id1}')
208
194
209 compare_page = ComparePage(response)
195 compare_page = ComparePage(response)
210 compare_page.contains_change_summary(1, 2, 0)
196 compare_page.contains_change_summary(1, 2, 0)
211 compare_page.contains_commits([commit1, commit2])
197 compare_page.contains_commits([commit1, commit2])
212 anchor = 'a_c-{}-826e8142e6ba'.format(commit0.short_id)
198 anchor = f'a_c-{commit0.short_id}-826e8142e6ba'
213 compare_page.contains_file_links_and_anchors([('file1', anchor), ])
199 compare_page.contains_file_links_and_anchors([('file1', anchor), ])
214
200
215 # Swap is removed when comparing branches since it's a PR feature and
201 # Swap is removed when comparing branches since it's a PR feature and
216 # it is then a preview mode
202 # it is then a preview mode
217 compare_page.swap_is_hidden()
203 compare_page.swap_is_hidden()
218 compare_page.target_source_are_disabled()
204 compare_page.target_source_are_disabled()
219
205
220 @pytest.mark.xfail_backends("svn")
206 @pytest.mark.xfail_backends("svn")
221 # TODO(marcink): no svn support for compare two seperate repos
207 # TODO(marcink): no svn support for compare two seperate repos
222 def test_compare_of_unrelated_forks(self, backend):
208 def test_compare_of_unrelated_forks(self, backend):
223 orig = backend.create_repo(number_of_commits=1)
209 orig = backend.create_repo(number_of_commits=1)
224 fork = backend.create_repo(number_of_commits=1)
210 fork = backend.create_repo(number_of_commits=1)
225
211
226 response = self.app.get(
212 response = self.app.get(
227 route_path('repo_compare',
213 route_path('repo_compare',
228 repo_name=orig.repo_name,
214 repo_name=orig.repo_name,
229 source_ref_type="rev", source_ref="tip",
215 source_ref_type="rev", source_ref="tip",
230 target_ref_type="rev", target_ref="tip",
216 target_ref_type="rev", target_ref="tip",
231 params=dict(merge='1', target_repo=fork.repo_name),
217 params=dict(merge='1', target_repo=fork.repo_name),
232 ),
218 ),
233 status=302)
219 status=302)
234 response = response.follow()
220 response = response.follow()
235 response.mustcontain("Repositories unrelated.")
221 response.mustcontain("Repositories unrelated.")
236
222
237 @pytest.mark.xfail_backends("svn")
223 @pytest.mark.xfail_backends("svn")
238 def test_compare_cherry_pick_commits_from_bottom(self, backend):
224 def test_compare_cherry_pick_commits_from_bottom(self, backend):
239
225
240 # repo1:
226 # repo1:
241 # commit0:
227 # commit0:
242 # commit1:
228 # commit1:
243 # repo1-fork- in which we will cherry pick bottom commits
229 # repo1-fork- in which we will cherry pick bottom commits
244 # commit0:
230 # commit0:
245 # commit1:
231 # commit1:
246 # commit2: x
232 # commit2: x
247 # commit3: x
233 # commit3: x
248 # commit4: x
234 # commit4: x
249 # commit5:
235 # commit5:
250 # make repo1, and commit1+commit2
236 # make repo1, and commit1+commit2
251
237
252 repo1 = backend.create_repo()
238 repo1 = backend.create_repo()
253
239
254 # commit something !
240 # commit something !
255 commit0 = commit_change(
241 commit0 = commit_change(
256 repo1.repo_name, filename=b'file1', content=b'line1\n',
242 repo1.repo_name, filename=b'file1', content=b'line1\n',
257 message='commit1', vcs_type=backend.alias, parent=None,
243 message='commit1', vcs_type=backend.alias, parent=None,
258 newfile=True)
244 newfile=True)
259 commit1 = commit_change(
245 commit1 = commit_change(
260 repo1.repo_name, filename=b'file1', content=b'line1\nline2\n',
246 repo1.repo_name, filename=b'file1', content=b'line1\nline2\n',
261 message='commit2', vcs_type=backend.alias, parent=commit0)
247 message='commit2', vcs_type=backend.alias, parent=commit0)
262
248
263 # fork this repo
249 # fork this repo
264 repo2 = backend.create_fork()
250 repo2 = backend.create_fork()
265
251
266 # now make commit3-6
252 # now make commit3-6
267 commit2 = commit_change(
253 commit2 = commit_change(
268 repo1.repo_name, filename=b'file1', content=b'line1\nline2\nline3\n',
254 repo1.repo_name, filename=b'file1', content=b'line1\nline2\nline3\n',
269 message='commit3', vcs_type=backend.alias, parent=commit1)
255 message='commit3', vcs_type=backend.alias, parent=commit1)
270 commit3 = commit_change(
256 commit3 = commit_change(
271 repo1.repo_name, filename=b'file1',content=b'line1\nline2\nline3\nline4\n',
257 repo1.repo_name, filename=b'file1',content=b'line1\nline2\nline3\nline4\n',
272 message='commit4', vcs_type=backend.alias, parent=commit2)
258 message='commit4', vcs_type=backend.alias, parent=commit2)
273 commit4 = commit_change(
259 commit4 = commit_change(
274 repo1.repo_name, filename=b'file1', content=b'line1\nline2\nline3\nline4\nline5\n',
260 repo1.repo_name, filename=b'file1', content=b'line1\nline2\nline3\nline4\nline5\n',
275 message='commit5', vcs_type=backend.alias, parent=commit3)
261 message='commit5', vcs_type=backend.alias, parent=commit3)
276 commit_change( # commit 5
262 commit_change( # commit 5
277 repo1.repo_name, filename=b'file1', content=b'line1\nline2\nline3\nline4\nline5\nline6\n',
263 repo1.repo_name, filename=b'file1', content=b'line1\nline2\nline3\nline4\nline5\nline6\n',
278 message='commit6', vcs_type=backend.alias, parent=commit4)
264 message='commit6', vcs_type=backend.alias, parent=commit4)
279
265
280 response = self.app.get(
266 response = self.app.get(
281 route_path('repo_compare',
267 route_path('repo_compare',
282 repo_name=repo2.repo_name,
268 repo_name=repo2.repo_name,
283 # parent of commit2, in target repo2
269 # parent of commit2, in target repo2
284 source_ref_type="rev", source_ref=commit1.raw_id,
270 source_ref_type="rev", source_ref=commit1.raw_id,
285 target_ref_type="rev", target_ref=commit4.raw_id,
271 target_ref_type="rev", target_ref=commit4.raw_id,
286 params=dict(merge='1', target_repo=repo1.repo_name),
272 params=dict(merge='1', target_repo=repo1.repo_name),
287 ))
273 ))
288 response.mustcontain('%s@%s' % (repo2.repo_name, commit1.short_id))
274 response.mustcontain('%s@%s' % (repo2.repo_name, commit1.short_id))
289 response.mustcontain('%s@%s' % (repo1.repo_name, commit4.short_id))
275 response.mustcontain('%s@%s' % (repo1.repo_name, commit4.short_id))
290
276
291 # files
277 # files
292 compare_page = ComparePage(response)
278 compare_page = ComparePage(response)
293 compare_page.contains_change_summary(1, 3, 0)
279 compare_page.contains_change_summary(1, 3, 0)
294 compare_page.contains_commits([commit2, commit3, commit4])
280 compare_page.contains_commits([commit2, commit3, commit4])
295 anchor = 'a_c-{}-826e8142e6ba'.format(commit1.short_id)
281 anchor = 'a_c-{}-826e8142e6ba'.format(commit1.short_id)
296 compare_page.contains_file_links_and_anchors([('file1', anchor),])
282 compare_page.contains_file_links_and_anchors([('file1', anchor),])
297
283
298 @pytest.mark.xfail_backends("svn")
284 @pytest.mark.xfail_backends("svn")
299 def test_compare_cherry_pick_commits_from_top(self, backend):
285 def test_compare_cherry_pick_commits_from_top(self, backend):
300 # repo1:
286 # repo1:
301 # commit0:
287 # commit0:
302 # commit1:
288 # commit1:
303 # repo1-fork- in which we will cherry pick bottom commits
289 # repo1-fork- in which we will cherry pick bottom commits
304 # commit0:
290 # commit0:
305 # commit1:
291 # commit1:
306 # commit2:
292 # commit2:
307 # commit3: x
293 # commit3: x
308 # commit4: x
294 # commit4: x
309 # commit5: x
295 # commit5: x
310
296
311 # make repo1, and commit1+commit2
297 # make repo1, and commit1+commit2
312 repo1 = backend.create_repo()
298 repo1 = backend.create_repo()
313
299
314 # commit something !
300 # commit something !
315 commit0 = commit_change(
301 commit0 = commit_change(
316 repo1.repo_name, filename=b'file1', content=b'line1\n',
302 repo1.repo_name, filename=b'file1', content=b'line1\n',
317 message='commit1', vcs_type=backend.alias, parent=None,
303 message='commit1', vcs_type=backend.alias, parent=None,
318 newfile=True)
304 newfile=True)
319 commit1 = commit_change(
305 commit1 = commit_change(
320 repo1.repo_name, filename=b'file1', content=b'line1\nline2\n',
306 repo1.repo_name, filename=b'file1', content=b'line1\nline2\n',
321 message='commit2', vcs_type=backend.alias, parent=commit0)
307 message='commit2', vcs_type=backend.alias, parent=commit0)
322
308
323 # fork this repo
309 # fork this repo
324 backend.create_fork()
310 backend.create_fork()
325
311
326 # now make commit3-6
312 # now make commit3-6
327 commit2 = commit_change(
313 commit2 = commit_change(
328 repo1.repo_name, filename=b'file1', content=b'line1\nline2\nline3\n',
314 repo1.repo_name, filename=b'file1', content=b'line1\nline2\nline3\n',
329 message='commit3', vcs_type=backend.alias, parent=commit1)
315 message='commit3', vcs_type=backend.alias, parent=commit1)
330 commit3 = commit_change(
316 commit3 = commit_change(
331 repo1.repo_name, filename=b'file1',
317 repo1.repo_name, filename=b'file1',
332 content=b'line1\nline2\nline3\nline4\n', message='commit4',
318 content=b'line1\nline2\nline3\nline4\n', message='commit4',
333 vcs_type=backend.alias, parent=commit2)
319 vcs_type=backend.alias, parent=commit2)
334 commit4 = commit_change(
320 commit4 = commit_change(
335 repo1.repo_name, filename=b'file1',
321 repo1.repo_name, filename=b'file1',
336 content=b'line1\nline2\nline3\nline4\nline5\n', message='commit5',
322 content=b'line1\nline2\nline3\nline4\nline5\n', message='commit5',
337 vcs_type=backend.alias, parent=commit3)
323 vcs_type=backend.alias, parent=commit3)
338 commit5 = commit_change(
324 commit5 = commit_change(
339 repo1.repo_name, filename=b'file1',
325 repo1.repo_name, filename=b'file1',
340 content=b'line1\nline2\nline3\nline4\nline5\nline6\n',
326 content=b'line1\nline2\nline3\nline4\nline5\nline6\n',
341 message='commit6', vcs_type=backend.alias, parent=commit4)
327 message='commit6', vcs_type=backend.alias, parent=commit4)
342
328
343 response = self.app.get(
329 response = self.app.get(
344 route_path('repo_compare',
330 route_path('repo_compare',
345 repo_name=repo1.repo_name,
331 repo_name=repo1.repo_name,
346 # parent of commit3, not in source repo2
332 # parent of commit3, not in source repo2
347 source_ref_type="rev", source_ref=commit2.raw_id,
333 source_ref_type="rev", source_ref=commit2.raw_id,
348 target_ref_type="rev", target_ref=commit5.raw_id,
334 target_ref_type="rev", target_ref=commit5.raw_id,
349 params=dict(merge='1'),))
335 params=dict(merge='1'),))
350
336
351 response.mustcontain('%s@%s' % (repo1.repo_name, commit2.short_id))
337 response.mustcontain('%s@%s' % (repo1.repo_name, commit2.short_id))
352 response.mustcontain('%s@%s' % (repo1.repo_name, commit5.short_id))
338 response.mustcontain('%s@%s' % (repo1.repo_name, commit5.short_id))
353
339
354 compare_page = ComparePage(response)
340 compare_page = ComparePage(response)
355 compare_page.contains_change_summary(1, 3, 0)
341 compare_page.contains_change_summary(1, 3, 0)
356 compare_page.contains_commits([commit3, commit4, commit5])
342 compare_page.contains_commits([commit3, commit4, commit5])
357
343
358 # files
344 # files
359 anchor = 'a_c-{}-826e8142e6ba'.format(commit2.short_id)
345 anchor = 'a_c-{}-826e8142e6ba'.format(commit2.short_id)
360 compare_page.contains_file_links_and_anchors([('file1', anchor),])
346 compare_page.contains_file_links_and_anchors([('file1', anchor),])
361
347
362 @pytest.mark.xfail_backends("svn")
348 @pytest.mark.xfail_backends("svn")
363 def test_compare_remote_branches(self, backend):
349 def test_compare_remote_branches(self, backend):
364 repo1 = backend.repo
350 repo1 = backend.repo
365 repo2 = backend.create_fork()
351 repo2 = backend.create_fork()
366
352
367 commit_id1 = repo1.get_commit(commit_idx=3).raw_id
353 commit_id1 = repo1.get_commit(commit_idx=3).raw_id
368 commit_id1_short = repo1.get_commit(commit_idx=3).short_id
354 commit_id1_short = repo1.get_commit(commit_idx=3).short_id
369 commit_id2 = repo1.get_commit(commit_idx=6).raw_id
355 commit_id2 = repo1.get_commit(commit_idx=6).raw_id
370 commit_id2_short = repo1.get_commit(commit_idx=6).short_id
356 commit_id2_short = repo1.get_commit(commit_idx=6).short_id
371
357
372 response = self.app.get(
358 response = self.app.get(
373 route_path('repo_compare',
359 route_path('repo_compare',
374 repo_name=repo1.repo_name,
360 repo_name=repo1.repo_name,
375 source_ref_type="rev", source_ref=commit_id1,
361 source_ref_type="rev", source_ref=commit_id1,
376 target_ref_type="rev", target_ref=commit_id2,
362 target_ref_type="rev", target_ref=commit_id2,
377 params=dict(merge='1', target_repo=repo2.repo_name),
363 params=dict(merge='1', target_repo=repo2.repo_name),
378 ))
364 ))
379
365
380 response.mustcontain('%s@%s' % (repo1.repo_name, commit_id1))
366 response.mustcontain('%s@%s' % (repo1.repo_name, commit_id1))
381 response.mustcontain('%s@%s' % (repo2.repo_name, commit_id2))
367 response.mustcontain('%s@%s' % (repo2.repo_name, commit_id2))
382
368
383 compare_page = ComparePage(response)
369 compare_page = ComparePage(response)
384
370
385 # outgoing commits between those commits
371 # outgoing commits between those commits
386 compare_page.contains_commits(
372 compare_page.contains_commits(
387 [repo2.get_commit(commit_idx=x) for x in [4, 5, 6]])
373 [repo2.get_commit(commit_idx=x) for x in [4, 5, 6]])
388
374
389 # files
375 # files
390 compare_page.contains_file_links_and_anchors([
376 compare_page.contains_file_links_and_anchors([
391 ('vcs/backends/hg.py', 'a_c-{}-9c390eb52cd6'.format(commit_id2_short)),
377 ('vcs/backends/hg.py', 'a_c-{}-9c390eb52cd6'.format(commit_id2_short)),
392 ('vcs/backends/__init__.py', 'a_c-{}-41b41c1f2796'.format(commit_id1_short)),
378 ('vcs/backends/__init__.py', 'a_c-{}-41b41c1f2796'.format(commit_id1_short)),
393 ('vcs/backends/base.py', 'a_c-{}-2f574d260608'.format(commit_id1_short)),
379 ('vcs/backends/base.py', 'a_c-{}-2f574d260608'.format(commit_id1_short)),
394 ])
380 ])
395
381
396 @pytest.mark.xfail_backends("svn")
382 @pytest.mark.xfail_backends("svn")
397 def test_source_repo_new_commits_after_forking_simple_diff(self, backend):
383 def test_source_repo_new_commits_after_forking_simple_diff(self, backend):
398 repo1 = backend.create_repo()
384 repo1 = backend.create_repo()
399 r1_name = repo1.repo_name
385 r1_name = repo1.repo_name
400
386
401 commit0 = commit_change(
387 commit0 = commit_change(
402 repo=r1_name, filename=b'file1',
388 repo=r1_name, filename=b'file1',
403 content=b'line1', message='commit1', vcs_type=backend.alias,
389 content=b'line1', message='commit1', vcs_type=backend.alias,
404 newfile=True)
390 newfile=True)
405 assert repo1.scm_instance().commit_ids == [commit0.raw_id]
391 assert repo1.scm_instance().commit_ids == [commit0.raw_id]
406
392
407 # fork the repo1
393 # fork the repo1
408 repo2 = backend.create_fork()
394 repo2 = backend.create_fork()
409 assert repo2.scm_instance().commit_ids == [commit0.raw_id]
395 assert repo2.scm_instance().commit_ids == [commit0.raw_id]
410
396
411 self.r2_id = repo2.repo_id
397 self.r2_id = repo2.repo_id
412 r2_name = repo2.repo_name
398 r2_name = repo2.repo_name
413
399
414 commit1 = commit_change(
400 commit1 = commit_change(
415 repo=r2_name, filename=b'file1-fork',
401 repo=r2_name, filename=b'file1-fork',
416 content=b'file1-line1-from-fork', message='commit1-fork',
402 content=b'file1-line1-from-fork', message='commit1-fork',
417 vcs_type=backend.alias, parent=repo2.scm_instance()[-1],
403 vcs_type=backend.alias, parent=repo2.scm_instance()[-1],
418 newfile=True)
404 newfile=True)
419
405
420 commit2 = commit_change(
406 commit2 = commit_change(
421 repo=r2_name, filename=b'file2-fork',
407 repo=r2_name, filename=b'file2-fork',
422 content=b'file2-line1-from-fork', message='commit2-fork',
408 content=b'file2-line1-from-fork', message='commit2-fork',
423 vcs_type=backend.alias, parent=commit1,
409 vcs_type=backend.alias, parent=commit1,
424 newfile=True)
410 newfile=True)
425
411
426 commit_change( # commit 3
412 commit_change( # commit 3
427 repo=r2_name, filename=b'file3-fork',
413 repo=r2_name, filename=b'file3-fork',
428 content=b'file3-line1-from-fork', message='commit3-fork',
414 content=b'file3-line1-from-fork', message='commit3-fork',
429 vcs_type=backend.alias, parent=commit2, newfile=True)
415 vcs_type=backend.alias, parent=commit2, newfile=True)
430
416
431 # compare !
417 # compare !
432 commit_id1 = repo1.scm_instance().DEFAULT_BRANCH_NAME
418 commit_id1 = repo1.scm_instance().DEFAULT_BRANCH_NAME
433 commit_id2 = repo2.scm_instance().DEFAULT_BRANCH_NAME
419 commit_id2 = repo2.scm_instance().DEFAULT_BRANCH_NAME
434
420
435 response = self.app.get(
421 response = self.app.get(
436 route_path('repo_compare',
422 route_path('repo_compare',
437 repo_name=r2_name,
423 repo_name=r2_name,
438 source_ref_type="branch", source_ref=commit_id1,
424 source_ref_type="branch", source_ref=commit_id1,
439 target_ref_type="branch", target_ref=commit_id2,
425 target_ref_type="branch", target_ref=commit_id2,
440 params=dict(merge='1', target_repo=r1_name),
426 params=dict(merge='1', target_repo=r1_name),
441 ))
427 ))
442
428
443 response.mustcontain('%s@%s' % (r2_name, commit_id1))
429 response.mustcontain('%s@%s' % (r2_name, commit_id1))
444 response.mustcontain('%s@%s' % (r1_name, commit_id2))
430 response.mustcontain('%s@%s' % (r1_name, commit_id2))
445 response.mustcontain('No files')
431 response.mustcontain('No files')
446 response.mustcontain('No commits in this compare')
432 response.mustcontain('No commits in this compare')
447
433
448 commit0 = commit_change(
434 commit0 = commit_change(
449 repo=r1_name, filename=b'file2',
435 repo=r1_name, filename=b'file2',
450 content=b'line1-added-after-fork', message='commit2-parent',
436 content=b'line1-added-after-fork', message='commit2-parent',
451 vcs_type=backend.alias, parent=None, newfile=True)
437 vcs_type=backend.alias, parent=None, newfile=True)
452
438
453 # compare !
439 # compare !
454 response = self.app.get(
440 response = self.app.get(
455 route_path('repo_compare',
441 route_path('repo_compare',
456 repo_name=r2_name,
442 repo_name=r2_name,
457 source_ref_type="branch", source_ref=commit_id1,
443 source_ref_type="branch", source_ref=commit_id1,
458 target_ref_type="branch", target_ref=commit_id2,
444 target_ref_type="branch", target_ref=commit_id2,
459 params=dict(merge='1', target_repo=r1_name),
445 params=dict(merge='1', target_repo=r1_name),
460 ))
446 ))
461
447
462 response.mustcontain('%s@%s' % (r2_name, commit_id1))
448 response.mustcontain('%s@%s' % (r2_name, commit_id1))
463 response.mustcontain('%s@%s' % (r1_name, commit_id2))
449 response.mustcontain('%s@%s' % (r1_name, commit_id2))
464
450
465 response.mustcontain("""commit2-parent""")
451 response.mustcontain("""commit2-parent""")
466 response.mustcontain("""line1-added-after-fork""")
452 response.mustcontain("""line1-added-after-fork""")
467 compare_page = ComparePage(response)
453 compare_page = ComparePage(response)
468 compare_page.contains_change_summary(1, 1, 0)
454 compare_page.contains_change_summary(1, 1, 0)
469
455
470 @pytest.mark.xfail_backends("svn")
456 @pytest.mark.xfail_backends("svn")
471 def test_compare_commits(self, backend, xhr_header):
457 def test_compare_commits(self, backend, xhr_header):
472 commit0 = backend.repo.get_commit(commit_idx=0)
458 commit0 = backend.repo.get_commit(commit_idx=0)
473 commit1 = backend.repo.get_commit(commit_idx=1)
459 commit1 = backend.repo.get_commit(commit_idx=1)
474
460
475 response = self.app.get(
461 response = self.app.get(
476 route_path('repo_compare',
462 route_path('repo_compare',
477 repo_name=backend.repo_name,
463 repo_name=backend.repo_name,
478 source_ref_type="rev", source_ref=commit0.raw_id,
464 source_ref_type="rev", source_ref=commit0.raw_id,
479 target_ref_type="rev", target_ref=commit1.raw_id,
465 target_ref_type="rev", target_ref=commit1.raw_id,
480 params=dict(merge='1')
466 params=dict(merge='1')
481 ),
467 ),
482 extra_environ=xhr_header, )
468 extra_environ=xhr_header, )
483
469
484 # outgoing commits between those commits
470 # outgoing commits between those commits
485 compare_page = ComparePage(response)
471 compare_page = ComparePage(response)
486 compare_page.contains_commits(commits=[commit1])
472 compare_page.contains_commits(commits=[commit1])
487
473
488 def test_errors_when_comparing_unknown_source_repo(self, backend):
474 def test_errors_when_comparing_unknown_source_repo(self, backend):
489 repo = backend.repo
475 repo = backend.repo
490
476
491 self.app.get(
477 self.app.get(
492 route_path('repo_compare',
478 route_path('repo_compare',
493 repo_name='badrepo',
479 repo_name='badrepo',
494 source_ref_type="rev", source_ref='tip',
480 source_ref_type="rev", source_ref='tip',
495 target_ref_type="rev", target_ref='tip',
481 target_ref_type="rev", target_ref='tip',
496 params=dict(merge='1', target_repo=repo.repo_name)
482 params=dict(merge='1', target_repo=repo.repo_name)
497 ),
483 ),
498 status=404)
484 status=404)
499
485
500 def test_errors_when_comparing_unknown_target_repo(self, backend):
486 def test_errors_when_comparing_unknown_target_repo(self, backend):
501 repo = backend.repo
487 repo = backend.repo
502 badrepo = 'badrepo'
488 badrepo = 'badrepo'
503
489
504 response = self.app.get(
490 response = self.app.get(
505 route_path('repo_compare',
491 route_path('repo_compare',
506 repo_name=repo.repo_name,
492 repo_name=repo.repo_name,
507 source_ref_type="rev", source_ref='tip',
493 source_ref_type="rev", source_ref='tip',
508 target_ref_type="rev", target_ref='tip',
494 target_ref_type="rev", target_ref='tip',
509 params=dict(merge='1', target_repo=badrepo),
495 params=dict(merge='1', target_repo=badrepo),
510 ),
496 ),
511 status=302)
497 status=302)
512 redirected = response.follow()
498 redirected = response.follow()
513 redirected.mustcontain(
499 redirected.mustcontain(
514 'Could not find the target repo: `{}`'.format(badrepo))
500 'Could not find the target repo: `{}`'.format(badrepo))
515
501
516 def test_compare_not_in_preview_mode(self, backend_stub):
502 def test_compare_not_in_preview_mode(self, backend_stub):
517 commit0 = backend_stub.repo.get_commit(commit_idx=0)
503 commit0 = backend_stub.repo.get_commit(commit_idx=0)
518 commit1 = backend_stub.repo.get_commit(commit_idx=1)
504 commit1 = backend_stub.repo.get_commit(commit_idx=1)
519
505
520 response = self.app.get(
506 response = self.app.get(
521 route_path('repo_compare',
507 route_path('repo_compare',
522 repo_name=backend_stub.repo_name,
508 repo_name=backend_stub.repo_name,
523 source_ref_type="rev", source_ref=commit0.raw_id,
509 source_ref_type="rev", source_ref=commit0.raw_id,
524 target_ref_type="rev", target_ref=commit1.raw_id,
510 target_ref_type="rev", target_ref=commit1.raw_id,
525 ))
511 ))
526
512
527 # outgoing commits between those commits
513 # outgoing commits between those commits
528 compare_page = ComparePage(response)
514 compare_page = ComparePage(response)
529 compare_page.swap_is_visible()
515 compare_page.swap_is_visible()
530 compare_page.target_source_are_enabled()
516 compare_page.target_source_are_enabled()
531
517
532 def test_compare_of_fork_with_largefiles(self, backend_hg, settings_util):
518 def test_compare_of_fork_with_largefiles(self, backend_hg, settings_util):
533 orig = backend_hg.create_repo(number_of_commits=1)
519 orig = backend_hg.create_repo(number_of_commits=1)
534 fork = backend_hg.create_fork()
520 fork = backend_hg.create_fork()
535
521
536 settings_util.create_repo_rhodecode_ui(
522 settings_util.create_repo_rhodecode_ui(
537 orig, 'extensions', value='', key='largefiles', active=False)
523 orig, 'extensions', value='', key='largefiles', active=False)
538 settings_util.create_repo_rhodecode_ui(
524 settings_util.create_repo_rhodecode_ui(
539 fork, 'extensions', value='', key='largefiles', active=True)
525 fork, 'extensions', value='', key='largefiles', active=True)
540
526
541 compare_module = ('rhodecode.lib.vcs.backends.hg.repository.'
527 compare_module = ('rhodecode.lib.vcs.backends.hg.repository.'
542 'MercurialRepository.compare')
528 'MercurialRepository.compare')
543 with mock.patch(compare_module) as compare_mock:
529 with mock.patch(compare_module) as compare_mock:
544 compare_mock.side_effect = RepositoryRequirementError()
530 compare_mock.side_effect = RepositoryRequirementError()
545
531
546 response = self.app.get(
532 response = self.app.get(
547 route_path('repo_compare',
533 route_path('repo_compare',
548 repo_name=orig.repo_name,
534 repo_name=orig.repo_name,
549 source_ref_type="rev", source_ref="tip",
535 source_ref_type="rev", source_ref="tip",
550 target_ref_type="rev", target_ref="tip",
536 target_ref_type="rev", target_ref="tip",
551 params=dict(merge='1', target_repo=fork.repo_name),
537 params=dict(merge='1', target_repo=fork.repo_name),
552 ),
538 ),
553 status=302)
539 status=302)
554
540
555 assert_session_flash(
541 assert_session_flash(
556 response,
542 response,
557 'Could not compare repos with different large file settings')
543 'Could not compare repos with different large file settings')
558
544
559
545
560 @pytest.mark.usefixtures("autologin_user")
546 @pytest.mark.usefixtures("autologin_user")
561 class TestCompareControllerSvn(object):
547 class TestCompareControllerSvn(object):
562
548
563 def test_supports_references_with_path(self, app, backend_svn):
549 def test_supports_references_with_path(self, app, backend_svn):
564 repo = backend_svn['svn-simple-layout']
550 repo = backend_svn['svn-simple-layout']
565 commit_id = repo.get_commit(commit_idx=-1).raw_id
551 commit_id = repo.get_commit(commit_idx=-1).raw_id
566 response = app.get(
552 response = app.get(
567 route_path('repo_compare',
553 route_path('repo_compare',
568 repo_name=repo.repo_name,
554 repo_name=repo.repo_name,
569 source_ref_type="tag",
555 source_ref_type="tag",
570 source_ref="%s@%s" % ('tags/v0.1', commit_id),
556 source_ref="%s@%s" % ('tags/v0.1', commit_id),
571 target_ref_type="tag",
557 target_ref_type="tag",
572 target_ref="%s@%s" % ('tags/v0.2', commit_id),
558 target_ref="%s@%s" % ('tags/v0.2', commit_id),
573 params=dict(merge='1'),
559 params=dict(merge='1'),
574 ),
560 ),
575 status=200)
561 status=200)
576
562
577 # Expecting no commits, since both paths are at the same revision
563 # Expecting no commits, since both paths are at the same revision
578 response.mustcontain('No commits in this compare')
564 response.mustcontain('No commits in this compare')
579
565
580 # Should find only one file changed when comparing those two tags
566 # Should find only one file changed when comparing those two tags
581 response.mustcontain('example.py')
567 response.mustcontain('example.py')
582 compare_page = ComparePage(response)
568 compare_page = ComparePage(response)
583 compare_page.contains_change_summary(1, 5, 1)
569 compare_page.contains_change_summary(1, 5, 1)
584
570
585 def test_shows_commits_if_different_ids(self, app, backend_svn):
571 def test_shows_commits_if_different_ids(self, app, backend_svn):
586 repo = backend_svn['svn-simple-layout']
572 repo = backend_svn['svn-simple-layout']
587 source_id = repo.get_commit(commit_idx=-6).raw_id
573 source_id = repo.get_commit(commit_idx=-6).raw_id
588 target_id = repo.get_commit(commit_idx=-1).raw_id
574 target_id = repo.get_commit(commit_idx=-1).raw_id
589 response = app.get(
575 response = app.get(
590 route_path('repo_compare',
576 route_path('repo_compare',
591 repo_name=repo.repo_name,
577 repo_name=repo.repo_name,
592 source_ref_type="tag",
578 source_ref_type="tag",
593 source_ref="%s@%s" % ('tags/v0.1', source_id),
579 source_ref="%s@%s" % ('tags/v0.1', source_id),
594 target_ref_type="tag",
580 target_ref_type="tag",
595 target_ref="%s@%s" % ('tags/v0.2', target_id),
581 target_ref="%s@%s" % ('tags/v0.2', target_id),
596 params=dict(merge='1')
582 params=dict(merge='1')
597 ),
583 ),
598 status=200)
584 status=200)
599
585
600 # It should show commits
586 # It should show commits
601 assert 'No commits in this compare' not in response.text
587 assert 'No commits in this compare' not in response.text
602
588
603 # Should find only one file changed when comparing those two tags
589 # Should find only one file changed when comparing those two tags
604 response.mustcontain('example.py')
590 response.mustcontain('example.py')
605 compare_page = ComparePage(response)
591 compare_page = ComparePage(response)
606 compare_page.contains_change_summary(1, 5, 1)
592 compare_page.contains_change_summary(1, 5, 1)
607
593
608
594
609 class ComparePage(AssertResponse):
595 class ComparePage(AssertResponse):
610 """
596 """
611 Abstracts the page template from the tests
597 Abstracts the page template from the tests
612 """
598 """
613
599
614 def contains_file_links_and_anchors(self, files):
600 def contains_file_links_and_anchors(self, files):
615 doc = lxml.html.fromstring(self.response.body)
601 doc = lxml.html.fromstring(self.response.body)
616 for filename, file_id in files:
602 for filename, file_id in files:
617 self.contains_one_anchor(file_id)
603 self.contains_one_anchor(file_id)
618 diffblock = doc.cssselect('[data-f-path="%s"]' % filename)
604 diffblock = doc.cssselect('[data-f-path="%s"]' % filename)
619 assert len(diffblock) == 2
605 assert len(diffblock) == 2
620 for lnk in diffblock[0].cssselect('a'):
606 for lnk in diffblock[0].cssselect('a'):
621 if 'permalink' in lnk.text:
607 if 'permalink' in lnk.text:
622 assert '#{}'.format(file_id) in lnk.attrib['href']
608 assert '#{}'.format(file_id) in lnk.attrib['href']
623 break
609 break
624 else:
610 else:
625 pytest.fail('Unable to find permalink')
611 pytest.fail('Unable to find permalink')
626
612
627 def contains_change_summary(self, files_changed, inserted, deleted):
613 def contains_change_summary(self, files_changed, inserted, deleted):
628 template = (
614 template = (
629 '{files_changed} file{plural} changed: '
615 '{files_changed} file{plural} changed: '
630 '<span class="op-added">{inserted} inserted</span>, <span class="op-deleted">{deleted} deleted</span>')
616 '<span class="op-added">{inserted} inserted</span>, <span class="op-deleted">{deleted} deleted</span>')
631 self.response.mustcontain(template.format(
617 self.response.mustcontain(template.format(
632 files_changed=files_changed,
618 files_changed=files_changed,
633 plural="s" if files_changed > 1 else "",
619 plural="s" if files_changed > 1 else "",
634 inserted=inserted,
620 inserted=inserted,
635 deleted=deleted))
621 deleted=deleted))
636
622
637 def contains_commits(self, commits, ancestors=None):
623 def contains_commits(self, commits, ancestors=None):
638 response = self.response
624 response = self.response
639
625
640 for commit in commits:
626 for commit in commits:
641 # Expecting to see the commit message in an element which
627 # Expecting to see the commit message in an element which
642 # has the ID "c-{commit.raw_id}"
628 # has the ID "c-{commit.raw_id}"
643 self.element_contains('#c-' + commit.raw_id, commit.message)
629 self.element_contains('#c-' + commit.raw_id, commit.message)
644 self.contains_one_link(
630 self.contains_one_link(
645 'r%s:%s' % (commit.idx, commit.short_id),
631 'r%s:%s' % (commit.idx, commit.short_id),
646 self._commit_url(commit))
632 self._commit_url(commit))
647
633
648 if ancestors:
634 if ancestors:
649 response.mustcontain('Ancestor')
635 response.mustcontain('Ancestor')
650 for ancestor in ancestors:
636 for ancestor in ancestors:
651 self.contains_one_link(
637 self.contains_one_link(
652 ancestor.short_id, self._commit_url(ancestor))
638 ancestor.short_id, self._commit_url(ancestor))
653
639
654 def _commit_url(self, commit):
640 def _commit_url(self, commit):
655 return '/%s/changeset/%s' % (commit.repository.name, commit.raw_id)
641 return '/%s/changeset/%s' % (commit.repository.name, commit.raw_id)
656
642
657 def swap_is_hidden(self):
643 def swap_is_hidden(self):
658 assert '<a id="btn-swap"' not in self.response.text
644 assert '<a id="btn-swap"' not in self.response.text
659
645
660 def swap_is_visible(self):
646 def swap_is_visible(self):
661 assert '<a id="btn-swap"' in self.response.text
647 assert '<a id="btn-swap"' in self.response.text
662
648
663 def target_source_are_disabled(self):
649 def target_source_are_disabled(self):
664 response = self.response
650 response = self.response
665 response.mustcontain("var enable_fields = false;")
651 response.mustcontain("var enable_fields = false;")
666 response.mustcontain('.select2("enable", enable_fields)')
652 response.mustcontain('.select2("enable", enable_fields)')
667
653
668 def target_source_are_enabled(self):
654 def target_source_are_enabled(self):
669 response = self.response
655 response = self.response
670 response.mustcontain("var enable_fields = true;")
656 response.mustcontain("var enable_fields = true;")
@@ -1,168 +1,154 b''
1
1
2 # Copyright (C) 2010-2023 RhodeCode GmbH
2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 #
3 #
4 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
7 #
7 #
8 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
11 # GNU General Public License for more details.
12 #
12 #
13 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
15 #
16 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19
19
20 import pytest
20 import pytest
21
21
22 from .test_repo_compare import ComparePage
22 from .test_repo_compare import ComparePage
23
23 from rhodecode.tests.routes import route_path
24
25 def route_path(name, params=None, **kwargs):
26 import urllib.request
27 import urllib.parse
28 import urllib.error
29
30 base_url = {
31 'repo_compare_select': '/{repo_name}/compare',
32 'repo_compare': '/{repo_name}/compare/{source_ref_type}@{source_ref}...{target_ref_type}@{target_ref}',
33 }[name].format(**kwargs)
34
35 if params:
36 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
37 return base_url
38
24
39
25
40 @pytest.mark.usefixtures("autologin_user", "app")
26 @pytest.mark.usefixtures("autologin_user", "app")
41 class TestCompareView(object):
27 class TestCompareView(object):
42
28
43 @pytest.mark.xfail_backends("svn", msg="Depends on branch and tag support")
29 @pytest.mark.xfail_backends("svn", msg="Depends on branch and tag support")
44 def test_compare_tag(self, backend):
30 def test_compare_tag(self, backend):
45 tag1 = 'v0.1.2'
31 tag1 = 'v0.1.2'
46 tag2 = 'v0.1.3'
32 tag2 = 'v0.1.3'
47 response = self.app.get(
33 response = self.app.get(
48 route_path(
34 route_path(
49 'repo_compare',
35 'repo_compare',
50 repo_name=backend.repo_name,
36 repo_name=backend.repo_name,
51 source_ref_type="tag", source_ref=tag1,
37 source_ref_type="tag", source_ref=tag1,
52 target_ref_type="tag", target_ref=tag2),
38 target_ref_type="tag", target_ref=tag2),
53 status=200)
39 status=200)
54
40
55 response.mustcontain('%s@%s' % (backend.repo_name, tag1))
41 response.mustcontain('%s@%s' % (backend.repo_name, tag1))
56 response.mustcontain('%s@%s' % (backend.repo_name, tag2))
42 response.mustcontain('%s@%s' % (backend.repo_name, tag2))
57
43
58 # outgoing commits between tags
44 # outgoing commits between tags
59 commit_indexes = {
45 commit_indexes = {
60 'git': [113] + list(range(115, 121)),
46 'git': [113] + list(range(115, 121)),
61 'hg': [112] + list(range(115, 121)),
47 'hg': [112] + list(range(115, 121)),
62 }
48 }
63 repo = backend.repo
49 repo = backend.repo
64 commits = (repo.get_commit(commit_idx=idx)
50 commits = (repo.get_commit(commit_idx=idx)
65 for idx in commit_indexes[backend.alias])
51 for idx in commit_indexes[backend.alias])
66 compare_page = ComparePage(response)
52 compare_page = ComparePage(response)
67 compare_page.contains_change_summary(11, 94, 64)
53 compare_page.contains_change_summary(11, 94, 64)
68 compare_page.contains_commits(commits)
54 compare_page.contains_commits(commits)
69
55
70 # files diff
56 # files diff
71 short_id = short_id_new = ''
57 short_id = short_id_new = ''
72 if backend.alias == 'git':
58 if backend.alias == 'git':
73 short_id = '5a3a8fb00555'
59 short_id = '5a3a8fb00555'
74 short_id_new = '0ba5f8a46600'
60 short_id_new = '0ba5f8a46600'
75 if backend.alias == 'hg':
61 if backend.alias == 'hg':
76 short_id = '17544fbfcd33'
62 short_id = '17544fbfcd33'
77 short_id_new = 'a7e60bff65d5'
63 short_id_new = 'a7e60bff65d5'
78
64
79 compare_page.contains_file_links_and_anchors([
65 compare_page.contains_file_links_and_anchors([
80 # modified
66 # modified
81 ('docs/api/utils/index.rst', 'a_c-{}-1c5cf9e91c12'.format(short_id)),
67 ('docs/api/utils/index.rst', 'a_c-{}-1c5cf9e91c12'.format(short_id)),
82 ('test_and_report.sh', 'a_c-{}-e3305437df55'.format(short_id)),
68 ('test_and_report.sh', 'a_c-{}-e3305437df55'.format(short_id)),
83 # added
69 # added
84 ('.hgignore', 'a_c-{}-c8e92ef85cd1'.format(short_id_new)),
70 ('.hgignore', 'a_c-{}-c8e92ef85cd1'.format(short_id_new)),
85 ('.hgtags', 'a_c-{}-6e08b694d687'.format(short_id_new)),
71 ('.hgtags', 'a_c-{}-6e08b694d687'.format(short_id_new)),
86 ('docs/api/index.rst', 'a_c-{}-2c14b00f3393'.format(short_id_new)),
72 ('docs/api/index.rst', 'a_c-{}-2c14b00f3393'.format(short_id_new)),
87 ('vcs/__init__.py', 'a_c-{}-430ccbc82bdf'.format(short_id_new)),
73 ('vcs/__init__.py', 'a_c-{}-430ccbc82bdf'.format(short_id_new)),
88 ('vcs/backends/hg.py', 'a_c-{}-9c390eb52cd6'.format(short_id_new)),
74 ('vcs/backends/hg.py', 'a_c-{}-9c390eb52cd6'.format(short_id_new)),
89 ('vcs/utils/__init__.py', 'a_c-{}-ebb592c595c0'.format(short_id_new)),
75 ('vcs/utils/__init__.py', 'a_c-{}-ebb592c595c0'.format(short_id_new)),
90 ('vcs/utils/annotate.py', 'a_c-{}-7abc741b5052'.format(short_id_new)),
76 ('vcs/utils/annotate.py', 'a_c-{}-7abc741b5052'.format(short_id_new)),
91 ('vcs/utils/diffs.py', 'a_c-{}-2ef0ef106c56'.format(short_id_new)),
77 ('vcs/utils/diffs.py', 'a_c-{}-2ef0ef106c56'.format(short_id_new)),
92 ('vcs/utils/lazy.py', 'a_c-{}-3150cb87d4b7'.format(short_id_new)),
78 ('vcs/utils/lazy.py', 'a_c-{}-3150cb87d4b7'.format(short_id_new)),
93 ])
79 ])
94
80
95 @pytest.mark.xfail_backends("svn", msg="Depends on branch and tag support")
81 @pytest.mark.xfail_backends("svn", msg="Depends on branch and tag support")
96 def test_compare_tag_branch(self, backend):
82 def test_compare_tag_branch(self, backend):
97 revisions = {
83 revisions = {
98 'hg': {
84 'hg': {
99 'tag': 'v0.2.0',
85 'tag': 'v0.2.0',
100 'branch': 'default',
86 'branch': 'default',
101 'response': (147, 5701, 10177)
87 'response': (147, 5701, 10177)
102 },
88 },
103 'git': {
89 'git': {
104 'tag': 'v0.2.2',
90 'tag': 'v0.2.2',
105 'branch': 'master',
91 'branch': 'master',
106 'response': (70, 1855, 3002)
92 'response': (70, 1855, 3002)
107 },
93 },
108 }
94 }
109
95
110 # Backend specific data, depends on the test repository for
96 # Backend specific data, depends on the test repository for
111 # functional tests.
97 # functional tests.
112 data = revisions[backend.alias]
98 data = revisions[backend.alias]
113
99
114 response = self.app.get(
100 response = self.app.get(
115 route_path(
101 route_path(
116 'repo_compare',
102 'repo_compare',
117 repo_name=backend.repo_name,
103 repo_name=backend.repo_name,
118 source_ref_type='branch', source_ref=data['branch'],
104 source_ref_type='branch', source_ref=data['branch'],
119 target_ref_type="tag", target_ref=data['tag'],
105 target_ref_type="tag", target_ref=data['tag'],
120 ))
106 ))
121
107
122 response.mustcontain('%s@%s' % (backend.repo_name, data['branch']))
108 response.mustcontain('%s@%s' % (backend.repo_name, data['branch']))
123 response.mustcontain('%s@%s' % (backend.repo_name, data['tag']))
109 response.mustcontain('%s@%s' % (backend.repo_name, data['tag']))
124 compare_page = ComparePage(response)
110 compare_page = ComparePage(response)
125 compare_page.contains_change_summary(*data['response'])
111 compare_page.contains_change_summary(*data['response'])
126
112
127 def test_index_branch(self, backend):
113 def test_index_branch(self, backend):
128 head_id = backend.default_head_id
114 head_id = backend.default_head_id
129 response = self.app.get(
115 response = self.app.get(
130 route_path(
116 route_path(
131 'repo_compare',
117 'repo_compare',
132 repo_name=backend.repo_name,
118 repo_name=backend.repo_name,
133 source_ref_type="branch", source_ref=head_id,
119 source_ref_type="branch", source_ref=head_id,
134 target_ref_type="branch", target_ref=head_id,
120 target_ref_type="branch", target_ref=head_id,
135 ))
121 ))
136
122
137 response.mustcontain('%s@%s' % (backend.repo_name, head_id))
123 response.mustcontain('%s@%s' % (backend.repo_name, head_id))
138
124
139 # branches are equal
125 # branches are equal
140 response.mustcontain('No files')
126 response.mustcontain('No files')
141 response.mustcontain('No commits in this compare')
127 response.mustcontain('No commits in this compare')
142
128
143 def test_compare_commits(self, backend):
129 def test_compare_commits(self, backend):
144 repo = backend.repo
130 repo = backend.repo
145 commit1 = repo.get_commit(commit_idx=0)
131 commit1 = repo.get_commit(commit_idx=0)
146 commit1_short_id = commit1.short_id
132 commit1_short_id = commit1.short_id
147 commit2 = repo.get_commit(commit_idx=1)
133 commit2 = repo.get_commit(commit_idx=1)
148 commit2_short_id = commit2.short_id
134 commit2_short_id = commit2.short_id
149
135
150 response = self.app.get(
136 response = self.app.get(
151 route_path(
137 route_path(
152 'repo_compare',
138 'repo_compare',
153 repo_name=backend.repo_name,
139 repo_name=backend.repo_name,
154 source_ref_type="rev", source_ref=commit1.raw_id,
140 source_ref_type="rev", source_ref=commit1.raw_id,
155 target_ref_type="rev", target_ref=commit2.raw_id,
141 target_ref_type="rev", target_ref=commit2.raw_id,
156 ))
142 ))
157 response.mustcontain('%s@%s' % (backend.repo_name, commit1.raw_id))
143 response.mustcontain('%s@%s' % (backend.repo_name, commit1.raw_id))
158 response.mustcontain('%s@%s' % (backend.repo_name, commit2.raw_id))
144 response.mustcontain('%s@%s' % (backend.repo_name, commit2.raw_id))
159 compare_page = ComparePage(response)
145 compare_page = ComparePage(response)
160
146
161 # files
147 # files
162 compare_page.contains_change_summary(1, 7, 0)
148 compare_page.contains_change_summary(1, 7, 0)
163
149
164 # outgoing commits between those commits
150 # outgoing commits between those commits
165 compare_page.contains_commits([commit2])
151 compare_page.contains_commits([commit2])
166 anchor = 'a_c-{}-c8e92ef85cd1'.format(commit2_short_id)
152 anchor = 'a_c-{}-c8e92ef85cd1'.format(commit2_short_id)
167 response.mustcontain(anchor)
153 response.mustcontain(anchor)
168 compare_page.contains_file_links_and_anchors([('.hgignore', anchor),])
154 compare_page.contains_file_links_and_anchors([('.hgignore', anchor),])
@@ -1,292 +1,279 b''
1
1
2 # Copyright (C) 2010-2023 RhodeCode GmbH
2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 #
3 #
4 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
7 #
7 #
8 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
11 # GNU General Public License for more details.
12 #
12 #
13 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
15 #
16 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19
19
20 import pytest
20 import pytest
21
21
22 from rhodecode.apps.repository.tests.test_repo_compare import ComparePage
22 from rhodecode.apps.repository.tests.test_repo_compare import ComparePage
23 from rhodecode.lib.vcs import nodes
23 from rhodecode.lib.vcs import nodes
24 from rhodecode.lib.vcs.backends.base import EmptyCommit
24 from rhodecode.lib.vcs.backends.base import EmptyCommit
25 from rhodecode.tests.fixture import Fixture
25 from rhodecode.tests.fixture import Fixture
26 from rhodecode.tests.utils import commit_change
26 from rhodecode.tests.utils import commit_change
27
27 from rhodecode.tests.routes import route_path
28 fixture = Fixture()
29
28
30
29
31 def route_path(name, params=None, **kwargs):
30 fixture = Fixture()
32 import urllib.request
33 import urllib.parse
34 import urllib.error
35
36 base_url = {
37 'repo_compare_select': '/{repo_name}/compare',
38 'repo_compare': '/{repo_name}/compare/{source_ref_type}@{source_ref}...{target_ref_type}@{target_ref}',
39 }[name].format(**kwargs)
40
41 if params:
42 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
43 return base_url
44
31
45
32
46 @pytest.mark.usefixtures("autologin_user", "app")
33 @pytest.mark.usefixtures("autologin_user", "app")
47 class TestSideBySideDiff(object):
34 class TestSideBySideDiff(object):
48
35
49 def test_diff_sidebyside_single_commit(self, app, backend):
36 def test_diff_sidebyside_single_commit(self, app, backend):
50 commit_id_range = {
37 commit_id_range = {
51 'hg': {
38 'hg': {
52 'commits': ['25d7e49c18b159446cadfa506a5cf8ad1cb04067',
39 'commits': ['25d7e49c18b159446cadfa506a5cf8ad1cb04067',
53 '603d6c72c46d953420c89d36372f08d9f305f5dd'],
40 '603d6c72c46d953420c89d36372f08d9f305f5dd'],
54 'changes': (21, 943, 288),
41 'changes': (21, 943, 288),
55 },
42 },
56 'git': {
43 'git': {
57 'commits': ['6fc9270775aaf5544c1deb014f4ddd60c952fcbb',
44 'commits': ['6fc9270775aaf5544c1deb014f4ddd60c952fcbb',
58 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'],
45 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'],
59 'changes': (20, 941, 286),
46 'changes': (20, 941, 286),
60 },
47 },
61
48
62 'svn': {
49 'svn': {
63 'commits': ['336',
50 'commits': ['336',
64 '337'],
51 '337'],
65 'changes': (21, 943, 288),
52 'changes': (21, 943, 288),
66 },
53 },
67 }
54 }
68
55
69 commit_info = commit_id_range[backend.alias]
56 commit_info = commit_id_range[backend.alias]
70 commit2, commit1 = commit_info['commits']
57 commit2, commit1 = commit_info['commits']
71 file_changes = commit_info['changes']
58 file_changes = commit_info['changes']
72
59
73 response = self.app.get(route_path(
60 response = self.app.get(route_path(
74 'repo_compare',
61 'repo_compare',
75 repo_name=backend.repo_name,
62 repo_name=backend.repo_name,
76 source_ref_type='rev',
63 source_ref_type='rev',
77 source_ref=commit2,
64 source_ref=commit2,
78 target_repo=backend.repo_name,
65 target_repo=backend.repo_name,
79 target_ref_type='rev',
66 target_ref_type='rev',
80 target_ref=commit1,
67 target_ref=commit1,
81 params=dict(target_repo=backend.repo_name, diffmode='sidebyside')
68 params=dict(target_repo=backend.repo_name, diffmode='sidebyside')
82 ))
69 ))
83
70
84 compare_page = ComparePage(response)
71 compare_page = ComparePage(response)
85 compare_page.contains_change_summary(*file_changes)
72 compare_page.contains_change_summary(*file_changes)
86 response.mustcontain('Collapse 1 commit')
73 response.mustcontain('Collapse 1 commit')
87
74
88 def test_diff_sidebyside_two_commits(self, app, backend):
75 def test_diff_sidebyside_two_commits(self, app, backend):
89 commit_id_range = {
76 commit_id_range = {
90 'hg': {
77 'hg': {
91 'commits': ['4fdd71e9427417b2e904e0464c634fdee85ec5a7',
78 'commits': ['4fdd71e9427417b2e904e0464c634fdee85ec5a7',
92 '603d6c72c46d953420c89d36372f08d9f305f5dd'],
79 '603d6c72c46d953420c89d36372f08d9f305f5dd'],
93 'changes': (32, 1165, 308),
80 'changes': (32, 1165, 308),
94 },
81 },
95 'git': {
82 'git': {
96 'commits': ['f5fbf9cfd5f1f1be146f6d3b38bcd791a7480c13',
83 'commits': ['f5fbf9cfd5f1f1be146f6d3b38bcd791a7480c13',
97 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'],
84 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'],
98 'changes': (31, 1163, 306),
85 'changes': (31, 1163, 306),
99 },
86 },
100
87
101 'svn': {
88 'svn': {
102 'commits': ['335',
89 'commits': ['335',
103 '337'],
90 '337'],
104 'changes': (32, 1179, 310),
91 'changes': (32, 1179, 310),
105 },
92 },
106 }
93 }
107
94
108 commit_info = commit_id_range[backend.alias]
95 commit_info = commit_id_range[backend.alias]
109 commit2, commit1 = commit_info['commits']
96 commit2, commit1 = commit_info['commits']
110 file_changes = commit_info['changes']
97 file_changes = commit_info['changes']
111
98
112 response = self.app.get(route_path(
99 response = self.app.get(route_path(
113 'repo_compare',
100 'repo_compare',
114 repo_name=backend.repo_name,
101 repo_name=backend.repo_name,
115 source_ref_type='rev',
102 source_ref_type='rev',
116 source_ref=commit2,
103 source_ref=commit2,
117 target_repo=backend.repo_name,
104 target_repo=backend.repo_name,
118 target_ref_type='rev',
105 target_ref_type='rev',
119 target_ref=commit1,
106 target_ref=commit1,
120 params=dict(target_repo=backend.repo_name, diffmode='sidebyside')
107 params=dict(target_repo=backend.repo_name, diffmode='sidebyside')
121 ))
108 ))
122
109
123 compare_page = ComparePage(response)
110 compare_page = ComparePage(response)
124 compare_page.contains_change_summary(*file_changes)
111 compare_page.contains_change_summary(*file_changes)
125
112
126 response.mustcontain('Collapse 2 commits')
113 response.mustcontain('Collapse 2 commits')
127
114
128 def test_diff_sidebyside_collapsed_commits(self, app, backend_svn):
115 def test_diff_sidebyside_collapsed_commits(self, app, backend_svn):
129 commit_id_range = {
116 commit_id_range = {
130
117
131 'svn': {
118 'svn': {
132 'commits': ['330',
119 'commits': ['330',
133 '337'],
120 '337'],
134
121
135 },
122 },
136 }
123 }
137
124
138 commit_info = commit_id_range['svn']
125 commit_info = commit_id_range['svn']
139 commit2, commit1 = commit_info['commits']
126 commit2, commit1 = commit_info['commits']
140
127
141 response = self.app.get(route_path(
128 response = self.app.get(route_path(
142 'repo_compare',
129 'repo_compare',
143 repo_name=backend_svn.repo_name,
130 repo_name=backend_svn.repo_name,
144 source_ref_type='rev',
131 source_ref_type='rev',
145 source_ref=commit2,
132 source_ref=commit2,
146 target_repo=backend_svn.repo_name,
133 target_repo=backend_svn.repo_name,
147 target_ref_type='rev',
134 target_ref_type='rev',
148 target_ref=commit1,
135 target_ref=commit1,
149 params=dict(target_repo=backend_svn.repo_name, diffmode='sidebyside')
136 params=dict(target_repo=backend_svn.repo_name, diffmode='sidebyside')
150 ))
137 ))
151
138
152 response.mustcontain('Expand 7 commits')
139 response.mustcontain('Expand 7 commits')
153
140
154 @pytest.mark.xfail(reason='GIT does not handle empty commit compare correct (missing 1 commit)')
141 @pytest.mark.xfail(reason='GIT does not handle empty commit compare correct (missing 1 commit)')
155 def test_diff_side_by_side_from_0_commit(self, app, backend, backend_stub):
142 def test_diff_side_by_side_from_0_commit(self, app, backend, backend_stub):
156 f_path = b'test_sidebyside_file.py'
143 f_path = b'test_sidebyside_file.py'
157 commit1_content = b'content-25d7e49c18b159446c\n'
144 commit1_content = b'content-25d7e49c18b159446c\n'
158 commit2_content = b'content-603d6c72c46d953420\n'
145 commit2_content = b'content-603d6c72c46d953420\n'
159 repo = backend.create_repo()
146 repo = backend.create_repo()
160
147
161 commit1 = commit_change(
148 commit1 = commit_change(
162 repo.repo_name, filename=f_path, content=commit1_content,
149 repo.repo_name, filename=f_path, content=commit1_content,
163 message='A', vcs_type=backend.alias, parent=None, newfile=True)
150 message='A', vcs_type=backend.alias, parent=None, newfile=True)
164
151
165 commit2 = commit_change(
152 commit2 = commit_change(
166 repo.repo_name, filename=f_path, content=commit2_content,
153 repo.repo_name, filename=f_path, content=commit2_content,
167 message='B, child of A', vcs_type=backend.alias, parent=commit1)
154 message='B, child of A', vcs_type=backend.alias, parent=commit1)
168
155
169 response = self.app.get(route_path(
156 response = self.app.get(route_path(
170 'repo_compare',
157 'repo_compare',
171 repo_name=repo.repo_name,
158 repo_name=repo.repo_name,
172 source_ref_type='rev',
159 source_ref_type='rev',
173 source_ref=EmptyCommit().raw_id,
160 source_ref=EmptyCommit().raw_id,
174 target_ref_type='rev',
161 target_ref_type='rev',
175 target_ref=commit2.raw_id,
162 target_ref=commit2.raw_id,
176 params=dict(diffmode='sidebyside')
163 params=dict(diffmode='sidebyside')
177 ))
164 ))
178
165
179 response.mustcontain('Collapse 2 commits')
166 response.mustcontain('Collapse 2 commits')
180 response.mustcontain('123 file changed')
167 response.mustcontain('123 file changed')
181
168
182 response.mustcontain(
169 response.mustcontain(
183 'r%s:%s...r%s:%s' % (
170 'r%s:%s...r%s:%s' % (
184 commit1.idx, commit1.short_id, commit2.idx, commit2.short_id))
171 commit1.idx, commit1.short_id, commit2.idx, commit2.short_id))
185
172
186 response.mustcontain(f_path)
173 response.mustcontain(f_path)
187
174
188 @pytest.mark.xfail(reason='GIT does not handle empty commit compare correct (missing 1 commit)')
175 @pytest.mark.xfail(reason='GIT does not handle empty commit compare correct (missing 1 commit)')
189 def test_diff_side_by_side_from_0_commit_with_file_filter(self, app, backend, backend_stub):
176 def test_diff_side_by_side_from_0_commit_with_file_filter(self, app, backend, backend_stub):
190 f_path = b'test_sidebyside_file.py'
177 f_path = b'test_sidebyside_file.py'
191 commit1_content = b'content-25d7e49c18b159446c\n'
178 commit1_content = b'content-25d7e49c18b159446c\n'
192 commit2_content = b'content-603d6c72c46d953420\n'
179 commit2_content = b'content-603d6c72c46d953420\n'
193 repo = backend.create_repo()
180 repo = backend.create_repo()
194
181
195 commit1 = commit_change(
182 commit1 = commit_change(
196 repo.repo_name, filename=f_path, content=commit1_content,
183 repo.repo_name, filename=f_path, content=commit1_content,
197 message='A', vcs_type=backend.alias, parent=None, newfile=True)
184 message='A', vcs_type=backend.alias, parent=None, newfile=True)
198
185
199 commit2 = commit_change(
186 commit2 = commit_change(
200 repo.repo_name, filename=f_path, content=commit2_content,
187 repo.repo_name, filename=f_path, content=commit2_content,
201 message='B, child of A', vcs_type=backend.alias, parent=commit1)
188 message='B, child of A', vcs_type=backend.alias, parent=commit1)
202
189
203 response = self.app.get(route_path(
190 response = self.app.get(route_path(
204 'repo_compare',
191 'repo_compare',
205 repo_name=repo.repo_name,
192 repo_name=repo.repo_name,
206 source_ref_type='rev',
193 source_ref_type='rev',
207 source_ref=EmptyCommit().raw_id,
194 source_ref=EmptyCommit().raw_id,
208 target_ref_type='rev',
195 target_ref_type='rev',
209 target_ref=commit2.raw_id,
196 target_ref=commit2.raw_id,
210 params=dict(f_path=f_path, target_repo=repo.repo_name, diffmode='sidebyside')
197 params=dict(f_path=f_path, target_repo=repo.repo_name, diffmode='sidebyside')
211 ))
198 ))
212
199
213 response.mustcontain('Collapse 2 commits')
200 response.mustcontain('Collapse 2 commits')
214 response.mustcontain('1 file changed')
201 response.mustcontain('1 file changed')
215
202
216 response.mustcontain(
203 response.mustcontain(
217 'r%s:%s...r%s:%s' % (
204 'r%s:%s...r%s:%s' % (
218 commit1.idx, commit1.short_id, commit2.idx, commit2.short_id))
205 commit1.idx, commit1.short_id, commit2.idx, commit2.short_id))
219
206
220 response.mustcontain(f_path)
207 response.mustcontain(f_path)
221
208
222 def test_diff_side_by_side_with_empty_file(self, app, backend, backend_stub):
209 def test_diff_side_by_side_with_empty_file(self, app, backend, backend_stub):
223 commits = [
210 commits = [
224 {'message': 'First commit'},
211 {'message': 'First commit'},
225 {'message': 'Second commit'},
212 {'message': 'Second commit'},
226 {'message': 'Commit with binary',
213 {'message': 'Commit with binary',
227 'added': [nodes.FileNode(b'file.empty', content=b'')]},
214 'added': [nodes.FileNode(b'file.empty', content=b'')]},
228 ]
215 ]
229 f_path = 'file.empty'
216 f_path = 'file.empty'
230 repo = backend.create_repo(commits=commits)
217 repo = backend.create_repo(commits=commits)
231 commit1 = repo.get_commit(commit_idx=0)
218 commit1 = repo.get_commit(commit_idx=0)
232 commit2 = repo.get_commit(commit_idx=1)
219 commit2 = repo.get_commit(commit_idx=1)
233 commit3 = repo.get_commit(commit_idx=2)
220 commit3 = repo.get_commit(commit_idx=2)
234
221
235 response = self.app.get(route_path(
222 response = self.app.get(route_path(
236 'repo_compare',
223 'repo_compare',
237 repo_name=repo.repo_name,
224 repo_name=repo.repo_name,
238 source_ref_type='rev',
225 source_ref_type='rev',
239 source_ref=commit1.raw_id,
226 source_ref=commit1.raw_id,
240 target_ref_type='rev',
227 target_ref_type='rev',
241 target_ref=commit3.raw_id,
228 target_ref=commit3.raw_id,
242 params=dict(f_path=f_path, target_repo=repo.repo_name, diffmode='sidebyside')
229 params=dict(f_path=f_path, target_repo=repo.repo_name, diffmode='sidebyside')
243 ))
230 ))
244
231
245 response.mustcontain('Collapse 2 commits')
232 response.mustcontain('Collapse 2 commits')
246 response.mustcontain('1 file changed')
233 response.mustcontain('1 file changed')
247
234
248 response.mustcontain(
235 response.mustcontain(
249 'r%s:%s...r%s:%s' % (
236 'r%s:%s...r%s:%s' % (
250 commit2.idx, commit2.short_id, commit3.idx, commit3.short_id))
237 commit2.idx, commit2.short_id, commit3.idx, commit3.short_id))
251
238
252 response.mustcontain(f_path)
239 response.mustcontain(f_path)
253
240
254 def test_diff_sidebyside_two_commits_with_file_filter(self, app, backend):
241 def test_diff_sidebyside_two_commits_with_file_filter(self, app, backend):
255 commit_id_range = {
242 commit_id_range = {
256 'hg': {
243 'hg': {
257 'commits': ['4fdd71e9427417b2e904e0464c634fdee85ec5a7',
244 'commits': ['4fdd71e9427417b2e904e0464c634fdee85ec5a7',
258 '603d6c72c46d953420c89d36372f08d9f305f5dd'],
245 '603d6c72c46d953420c89d36372f08d9f305f5dd'],
259 'changes': (1, 3, 3)
246 'changes': (1, 3, 3)
260 },
247 },
261 'git': {
248 'git': {
262 'commits': ['f5fbf9cfd5f1f1be146f6d3b38bcd791a7480c13',
249 'commits': ['f5fbf9cfd5f1f1be146f6d3b38bcd791a7480c13',
263 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'],
250 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'],
264 'changes': (1, 3, 3)
251 'changes': (1, 3, 3)
265 },
252 },
266
253
267 'svn': {
254 'svn': {
268 'commits': ['335',
255 'commits': ['335',
269 '337'],
256 '337'],
270 'changes': (1, 3, 3)
257 'changes': (1, 3, 3)
271 },
258 },
272 }
259 }
273 f_path = 'docs/conf.py'
260 f_path = 'docs/conf.py'
274
261
275 commit_info = commit_id_range[backend.alias]
262 commit_info = commit_id_range[backend.alias]
276 commit2, commit1 = commit_info['commits']
263 commit2, commit1 = commit_info['commits']
277 file_changes = commit_info['changes']
264 file_changes = commit_info['changes']
278
265
279 response = self.app.get(route_path(
266 response = self.app.get(route_path(
280 'repo_compare',
267 'repo_compare',
281 repo_name=backend.repo_name,
268 repo_name=backend.repo_name,
282 source_ref_type='rev',
269 source_ref_type='rev',
283 source_ref=commit2,
270 source_ref=commit2,
284 target_ref_type='rev',
271 target_ref_type='rev',
285 target_ref=commit1,
272 target_ref=commit1,
286 params=dict(f_path=f_path, target_repo=backend.repo_name, diffmode='sidebyside')
273 params=dict(f_path=f_path, target_repo=backend.repo_name, diffmode='sidebyside')
287 ))
274 ))
288
275
289 response.mustcontain('Collapse 2 commits')
276 response.mustcontain('Collapse 2 commits')
290
277
291 compare_page = ComparePage(response)
278 compare_page = ComparePage(response)
292 compare_page.contains_change_summary(*file_changes)
279 compare_page.contains_change_summary(*file_changes)
@@ -1,138 +1,122 b''
1
1
2 # Copyright (C) 2010-2023 RhodeCode GmbH
2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 #
3 #
4 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
7 #
7 #
8 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
11 # GNU General Public License for more details.
12 #
12 #
13 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
15 #
16 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19
19
20 import pytest
20 import pytest
21 from rhodecode.model.auth_token import AuthTokenModel
21 from rhodecode.model.auth_token import AuthTokenModel
22 from rhodecode.tests import TestController
22 from rhodecode.tests import TestController
23
23 from rhodecode.tests.routes import route_path
24
25 def route_path(name, params=None, **kwargs):
26 import urllib.request
27 import urllib.parse
28 import urllib.error
29
30 base_url = {
31 'rss_feed_home': '/{repo_name}/feed-rss',
32 'atom_feed_home': '/{repo_name}/feed-atom',
33 'rss_feed_home_old': '/{repo_name}/feed/rss',
34 'atom_feed_home_old': '/{repo_name}/feed/atom',
35 }[name].format(**kwargs)
36
37 if params:
38 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
39 return base_url
40
24
41
25
42 class TestFeedView(TestController):
26 class TestFeedView(TestController):
43
27
44 @pytest.mark.parametrize("feed_type,response_types,content_type",[
28 @pytest.mark.parametrize("feed_type,response_types,content_type",[
45 ('rss', ['<rss version="2.0"'],
29 ('rss', ['<rss version="2.0"'],
46 "application/rss+xml"),
30 "application/rss+xml"),
47 ('atom', ['xmlns="http://www.w3.org/2005/Atom"', 'xml:lang="en-us"'],
31 ('atom', ['xmlns="http://www.w3.org/2005/Atom"', 'xml:lang="en-us"'],
48 "application/atom+xml"),
32 "application/atom+xml"),
49 ])
33 ])
50 def test_feed(self, backend, feed_type, response_types, content_type):
34 def test_feed(self, backend, feed_type, response_types, content_type):
51 self.log_user()
35 self.log_user()
52 response = self.app.get(
36 response = self.app.get(
53 route_path('{}_feed_home'.format(feed_type),
37 route_path('{}_feed_home'.format(feed_type),
54 repo_name=backend.repo_name))
38 repo_name=backend.repo_name))
55
39
56 for content in response_types:
40 for content in response_types:
57 response.mustcontain(content)
41 response.mustcontain(content)
58
42
59 assert response.content_type == content_type
43 assert response.content_type == content_type
60
44
61 @pytest.mark.parametrize("feed_type, content_type", [
45 @pytest.mark.parametrize("feed_type, content_type", [
62 ('rss', "application/rss+xml"),
46 ('rss', "application/rss+xml"),
63 ('atom', "application/atom+xml")
47 ('atom', "application/atom+xml")
64 ])
48 ])
65 def test_feed_with_auth_token(
49 def test_feed_with_auth_token(
66 self, backend, user_admin, feed_type, content_type):
50 self, backend, user_admin, feed_type, content_type):
67 auth_token = user_admin.feed_token
51 auth_token = user_admin.feed_token
68 assert auth_token != ''
52 assert auth_token != ''
69
53
70 response = self.app.get(
54 response = self.app.get(
71 route_path(
55 route_path(
72 '{}_feed_home'.format(feed_type),
56 '{}_feed_home'.format(feed_type),
73 repo_name=backend.repo_name,
57 repo_name=backend.repo_name,
74 params=dict(auth_token=auth_token)),
58 params=dict(auth_token=auth_token)),
75 status=200)
59 status=200)
76
60
77 assert response.content_type == content_type
61 assert response.content_type == content_type
78
62
79 @pytest.mark.parametrize("feed_type, content_type", [
63 @pytest.mark.parametrize("feed_type, content_type", [
80 ('rss', "application/rss+xml"),
64 ('rss', "application/rss+xml"),
81 ('atom', "application/atom+xml")
65 ('atom', "application/atom+xml")
82 ])
66 ])
83 def test_feed_with_auth_token_by_uid(
67 def test_feed_with_auth_token_by_uid(
84 self, backend, user_admin, feed_type, content_type):
68 self, backend, user_admin, feed_type, content_type):
85 auth_token = user_admin.feed_token
69 auth_token = user_admin.feed_token
86 assert auth_token != ''
70 assert auth_token != ''
87
71
88 response = self.app.get(
72 response = self.app.get(
89 route_path(
73 route_path(
90 '{}_feed_home'.format(feed_type),
74 '{}_feed_home'.format(feed_type),
91 repo_name='_{}'.format(backend.repo.repo_id),
75 repo_name='_{}'.format(backend.repo.repo_id),
92 params=dict(auth_token=auth_token)),
76 params=dict(auth_token=auth_token)),
93 status=200)
77 status=200)
94
78
95 assert response.content_type == content_type
79 assert response.content_type == content_type
96
80
97 @pytest.mark.parametrize("feed_type, content_type", [
81 @pytest.mark.parametrize("feed_type, content_type", [
98 ('rss', "application/rss+xml"),
82 ('rss', "application/rss+xml"),
99 ('atom', "application/atom+xml")
83 ('atom', "application/atom+xml")
100 ])
84 ])
101 def test_feed_old_urls_with_auth_token(
85 def test_feed_old_urls_with_auth_token(
102 self, backend, user_admin, feed_type, content_type):
86 self, backend, user_admin, feed_type, content_type):
103 auth_token = user_admin.feed_token
87 auth_token = user_admin.feed_token
104 assert auth_token != ''
88 assert auth_token != ''
105
89
106 response = self.app.get(
90 response = self.app.get(
107 route_path(
91 route_path(
108 '{}_feed_home_old'.format(feed_type),
92 '{}_feed_home_old'.format(feed_type),
109 repo_name=backend.repo_name,
93 repo_name=backend.repo_name,
110 params=dict(auth_token=auth_token)),
94 params=dict(auth_token=auth_token)),
111 status=200)
95 status=200)
112
96
113 assert response.content_type == content_type
97 assert response.content_type == content_type
114
98
115 @pytest.mark.parametrize("feed_type", ['rss', 'atom'])
99 @pytest.mark.parametrize("feed_type", ['rss', 'atom'])
116 def test_feed_with_auth_token_of_wrong_type(
100 def test_feed_with_auth_token_of_wrong_type(
117 self, backend, user_util, feed_type):
101 self, backend, user_util, feed_type):
118 user = user_util.create_user()
102 user = user_util.create_user()
119 auth_token = AuthTokenModel().create(
103 auth_token = AuthTokenModel().create(
120 user.user_id, u'test-token', -1, AuthTokenModel.cls.ROLE_API)
104 user.user_id, u'test-token', -1, AuthTokenModel.cls.ROLE_API)
121 auth_token = auth_token.api_key
105 auth_token = auth_token.api_key
122
106
123 self.app.get(
107 self.app.get(
124 route_path(
108 route_path(
125 '{}_feed_home'.format(feed_type),
109 '{}_feed_home'.format(feed_type),
126 repo_name=backend.repo_name,
110 repo_name=backend.repo_name,
127 params=dict(auth_token=auth_token)),
111 params=dict(auth_token=auth_token)),
128 status=302)
112 status=302)
129
113
130 auth_token = AuthTokenModel().create(
114 auth_token = AuthTokenModel().create(
131 user.user_id, u'test-token', -1, AuthTokenModel.cls.ROLE_FEED)
115 user.user_id, u'test-token', -1, AuthTokenModel.cls.ROLE_FEED)
132 auth_token = auth_token.api_key
116 auth_token = auth_token.api_key
133 self.app.get(
117 self.app.get(
134 route_path(
118 route_path(
135 '{}_feed_home'.format(feed_type),
119 '{}_feed_home'.format(feed_type),
136 repo_name=backend.repo_name,
120 repo_name=backend.repo_name,
137 params=dict(auth_token=auth_token)),
121 params=dict(auth_token=auth_token)),
138 status=200)
122 status=200)
@@ -1,1125 +1,1090 b''
1
1
2 # Copyright (C) 2010-2023 RhodeCode GmbH
2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 #
3 #
4 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
7 #
7 #
8 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
11 # GNU General Public License for more details.
12 #
12 #
13 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
15 #
16 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19
19
20 import os
20 import os
21
21
22 import mock
22 import mock
23 import pytest
23 import pytest
24 from collections import OrderedDict
24 from collections import OrderedDict
25
25
26 from rhodecode.apps.repository.tests.test_repo_compare import ComparePage
26 from rhodecode.apps.repository.tests.test_repo_compare import ComparePage
27 from rhodecode.apps.repository.views.repo_files import RepoFilesView, get_archive_name, get_path_sha
27 from rhodecode.apps.repository.views.repo_files import RepoFilesView, get_archive_name, get_path_sha
28 from rhodecode.lib import helpers as h
28 from rhodecode.lib import helpers as h
29 from rhodecode.lib.ext_json import json
29 from rhodecode.lib.ext_json import json
30 from rhodecode.lib.str_utils import safe_str
30 from rhodecode.lib.str_utils import safe_str
31 from rhodecode.lib.vcs import nodes
31 from rhodecode.lib.vcs import nodes
32 from rhodecode.lib.vcs.conf import settings
32 from rhodecode.lib.vcs.conf import settings
33 from rhodecode.model.db import Session, Repository
33 from rhodecode.model.db import Session, Repository
34
34
35 from rhodecode.tests import assert_session_flash
35 from rhodecode.tests import assert_session_flash
36 from rhodecode.tests.fixture import Fixture
36 from rhodecode.tests.fixture import Fixture
37 from rhodecode.tests.routes import route_path
38
37
39
38 fixture = Fixture()
40 fixture = Fixture()
39
41
40
42
41 def get_node_history(backend_type):
43 def get_node_history(backend_type):
42 return {
44 return {
43 'hg': json.loads(fixture.load_resource('hg_node_history_response.json')),
45 'hg': json.loads(fixture.load_resource('hg_node_history_response.json')),
44 'git': json.loads(fixture.load_resource('git_node_history_response.json')),
46 'git': json.loads(fixture.load_resource('git_node_history_response.json')),
45 'svn': json.loads(fixture.load_resource('svn_node_history_response.json')),
47 'svn': json.loads(fixture.load_resource('svn_node_history_response.json')),
46 }[backend_type]
48 }[backend_type]
47
49
48
50
49 def route_path(name, params=None, **kwargs):
50 import urllib.request
51 import urllib.parse
52 import urllib.error
53
54 base_url = {
55 'repo_summary': '/{repo_name}',
56 'repo_archivefile': '/{repo_name}/archive/{fname}',
57 'repo_files_diff': '/{repo_name}/diff/{f_path}',
58 'repo_files_diff_2way_redirect': '/{repo_name}/diff-2way/{f_path}',
59 'repo_files': '/{repo_name}/files/{commit_id}/{f_path}',
60 'repo_files:default_path': '/{repo_name}/files/{commit_id}/',
61 'repo_files:default_commit': '/{repo_name}/files',
62 'repo_files:rendered': '/{repo_name}/render/{commit_id}/{f_path}',
63 'repo_files:annotated': '/{repo_name}/annotate/{commit_id}/{f_path}',
64 'repo_files:annotated_previous': '/{repo_name}/annotate-previous/{commit_id}/{f_path}',
65 'repo_files_nodelist': '/{repo_name}/nodelist/{commit_id}/{f_path}',
66 'repo_file_raw': '/{repo_name}/raw/{commit_id}/{f_path}',
67 'repo_file_download': '/{repo_name}/download/{commit_id}/{f_path}',
68 'repo_file_history': '/{repo_name}/history/{commit_id}/{f_path}',
69 'repo_file_authors': '/{repo_name}/authors/{commit_id}/{f_path}',
70 'repo_files_remove_file': '/{repo_name}/remove_file/{commit_id}/{f_path}',
71 'repo_files_delete_file': '/{repo_name}/delete_file/{commit_id}/{f_path}',
72 'repo_files_edit_file': '/{repo_name}/edit_file/{commit_id}/{f_path}',
73 'repo_files_update_file': '/{repo_name}/update_file/{commit_id}/{f_path}',
74 'repo_files_add_file': '/{repo_name}/add_file/{commit_id}/{f_path}',
75 'repo_files_upload_file': '/{repo_name}/upload_file/{commit_id}/{f_path}',
76 'repo_files_create_file': '/{repo_name}/create_file/{commit_id}/{f_path}',
77 'repo_nodetree_full': '/{repo_name}/nodetree_full/{commit_id}/{f_path}',
78 'repo_nodetree_full:default_path': '/{repo_name}/nodetree_full/{commit_id}/',
79 }[name].format(**kwargs)
80
81 if params:
82 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
83 return base_url
84
85
86 def assert_files_in_response(response, files, params):
51 def assert_files_in_response(response, files, params):
87 template = (
52 template = (
88 'href="/%(repo_name)s/files/%(commit_id)s/%(name)s"')
53 'href="/%(repo_name)s/files/%(commit_id)s/%(name)s"')
89 _assert_items_in_response(response, files, template, params)
54 _assert_items_in_response(response, files, template, params)
90
55
91
56
92 def assert_dirs_in_response(response, dirs, params):
57 def assert_dirs_in_response(response, dirs, params):
93 template = (
58 template = (
94 'href="/%(repo_name)s/files/%(commit_id)s/%(name)s"')
59 'href="/%(repo_name)s/files/%(commit_id)s/%(name)s"')
95 _assert_items_in_response(response, dirs, template, params)
60 _assert_items_in_response(response, dirs, template, params)
96
61
97
62
98 def _assert_items_in_response(response, items, template, params):
63 def _assert_items_in_response(response, items, template, params):
99 for item in items:
64 for item in items:
100 item_params = {'name': item}
65 item_params = {'name': item}
101 item_params.update(params)
66 item_params.update(params)
102 response.mustcontain(template % item_params)
67 response.mustcontain(template % item_params)
103
68
104
69
105 def assert_timeago_in_response(response, items, params):
70 def assert_timeago_in_response(response, items, params):
106 for item in items:
71 for item in items:
107 response.mustcontain(h.age_component(params['date']))
72 response.mustcontain(h.age_component(params['date']))
108
73
109
74
110 @pytest.mark.usefixtures("app")
75 @pytest.mark.usefixtures("app")
111 class TestFilesViews(object):
76 class TestFilesViews(object):
112
77
113 def test_show_files(self, backend):
78 def test_show_files(self, backend):
114 response = self.app.get(
79 response = self.app.get(
115 route_path('repo_files',
80 route_path('repo_files',
116 repo_name=backend.repo_name,
81 repo_name=backend.repo_name,
117 commit_id='tip', f_path='/'))
82 commit_id='tip', f_path='/'))
118 commit = backend.repo.get_commit()
83 commit = backend.repo.get_commit()
119
84
120 params = {
85 params = {
121 'repo_name': backend.repo_name,
86 'repo_name': backend.repo_name,
122 'commit_id': commit.raw_id,
87 'commit_id': commit.raw_id,
123 'date': commit.date
88 'date': commit.date
124 }
89 }
125 assert_dirs_in_response(response, ['docs', 'vcs'], params)
90 assert_dirs_in_response(response, ['docs', 'vcs'], params)
126 files = [
91 files = [
127 '.gitignore',
92 '.gitignore',
128 '.hgignore',
93 '.hgignore',
129 '.hgtags',
94 '.hgtags',
130 # TODO: missing in Git
95 # TODO: missing in Git
131 # '.travis.yml',
96 # '.travis.yml',
132 'MANIFEST.in',
97 'MANIFEST.in',
133 'README.rst',
98 'README.rst',
134 # TODO: File is missing in svn repository
99 # TODO: File is missing in svn repository
135 # 'run_test_and_report.sh',
100 # 'run_test_and_report.sh',
136 'setup.cfg',
101 'setup.cfg',
137 'setup.py',
102 'setup.py',
138 'test_and_report.sh',
103 'test_and_report.sh',
139 'tox.ini',
104 'tox.ini',
140 ]
105 ]
141 assert_files_in_response(response, files, params)
106 assert_files_in_response(response, files, params)
142 assert_timeago_in_response(response, files, params)
107 assert_timeago_in_response(response, files, params)
143
108
144 def test_show_files_links_submodules_with_absolute_url(self, backend_hg):
109 def test_show_files_links_submodules_with_absolute_url(self, backend_hg):
145 repo = backend_hg['subrepos']
110 repo = backend_hg['subrepos']
146 response = self.app.get(
111 response = self.app.get(
147 route_path('repo_files',
112 route_path('repo_files',
148 repo_name=repo.repo_name,
113 repo_name=repo.repo_name,
149 commit_id='tip', f_path='/'))
114 commit_id='tip', f_path='/'))
150 assert_response = response.assert_response()
115 assert_response = response.assert_response()
151 assert_response.contains_one_link(
116 assert_response.contains_one_link(
152 'absolute-path @ 000000000000', 'http://example.com/absolute-path')
117 'absolute-path @ 000000000000', 'http://example.com/absolute-path')
153
118
154 def test_show_files_links_submodules_with_absolute_url_subpaths(
119 def test_show_files_links_submodules_with_absolute_url_subpaths(
155 self, backend_hg):
120 self, backend_hg):
156 repo = backend_hg['subrepos']
121 repo = backend_hg['subrepos']
157 response = self.app.get(
122 response = self.app.get(
158 route_path('repo_files',
123 route_path('repo_files',
159 repo_name=repo.repo_name,
124 repo_name=repo.repo_name,
160 commit_id='tip', f_path='/'))
125 commit_id='tip', f_path='/'))
161 assert_response = response.assert_response()
126 assert_response = response.assert_response()
162 assert_response.contains_one_link(
127 assert_response.contains_one_link(
163 'subpaths-path @ 000000000000',
128 'subpaths-path @ 000000000000',
164 'http://sub-base.example.com/subpaths-path')
129 'http://sub-base.example.com/subpaths-path')
165
130
166 @pytest.mark.xfail_backends("svn", reason="Depends on branch support")
131 @pytest.mark.xfail_backends("svn", reason="Depends on branch support")
167 def test_files_menu(self, backend):
132 def test_files_menu(self, backend):
168 new_branch = "temp_branch_name"
133 new_branch = "temp_branch_name"
169 commits = [
134 commits = [
170 {'message': 'a'},
135 {'message': 'a'},
171 {'message': 'b', 'branch': new_branch}
136 {'message': 'b', 'branch': new_branch}
172 ]
137 ]
173 backend.create_repo(commits)
138 backend.create_repo(commits)
174 backend.repo.landing_rev = f"branch:{new_branch}"
139 backend.repo.landing_rev = f"branch:{new_branch}"
175 Session().commit()
140 Session().commit()
176
141
177 # get response based on tip and not new commit
142 # get response based on tip and not new commit
178 response = self.app.get(
143 response = self.app.get(
179 route_path('repo_files',
144 route_path('repo_files',
180 repo_name=backend.repo_name,
145 repo_name=backend.repo_name,
181 commit_id='tip', f_path='/'))
146 commit_id='tip', f_path='/'))
182
147
183 # make sure Files menu url is not tip but new commit
148 # make sure Files menu url is not tip but new commit
184 landing_rev = backend.repo.landing_ref_name
149 landing_rev = backend.repo.landing_ref_name
185 files_url = route_path('repo_files:default_path',
150 files_url = route_path('repo_files:default_path',
186 repo_name=backend.repo_name,
151 repo_name=backend.repo_name,
187 commit_id=landing_rev, params={'at': landing_rev})
152 commit_id=landing_rev, params={'at': landing_rev})
188
153
189 assert landing_rev != 'tip'
154 assert landing_rev != 'tip'
190 response.mustcontain(f'<li class="active"><a class="menulink" href="{files_url}">')
155 response.mustcontain(f'<li class="active"><a class="menulink" href="{files_url}">')
191
156
192 def test_show_files_commit(self, backend):
157 def test_show_files_commit(self, backend):
193 commit = backend.repo.get_commit(commit_idx=32)
158 commit = backend.repo.get_commit(commit_idx=32)
194
159
195 response = self.app.get(
160 response = self.app.get(
196 route_path('repo_files',
161 route_path('repo_files',
197 repo_name=backend.repo_name,
162 repo_name=backend.repo_name,
198 commit_id=commit.raw_id, f_path='/'))
163 commit_id=commit.raw_id, f_path='/'))
199
164
200 dirs = ['docs', 'tests']
165 dirs = ['docs', 'tests']
201 files = ['README.rst']
166 files = ['README.rst']
202 params = {
167 params = {
203 'repo_name': backend.repo_name,
168 'repo_name': backend.repo_name,
204 'commit_id': commit.raw_id,
169 'commit_id': commit.raw_id,
205 }
170 }
206 assert_dirs_in_response(response, dirs, params)
171 assert_dirs_in_response(response, dirs, params)
207 assert_files_in_response(response, files, params)
172 assert_files_in_response(response, files, params)
208
173
209 def test_show_files_different_branch(self, backend):
174 def test_show_files_different_branch(self, backend):
210 branches = dict(
175 branches = dict(
211 hg=(150, ['git']),
176 hg=(150, ['git']),
212 # TODO: Git test repository does not contain other branches
177 # TODO: Git test repository does not contain other branches
213 git=(633, ['master']),
178 git=(633, ['master']),
214 # TODO: Branch support in Subversion
179 # TODO: Branch support in Subversion
215 svn=(150, [])
180 svn=(150, [])
216 )
181 )
217 idx, branches = branches[backend.alias]
182 idx, branches = branches[backend.alias]
218 commit = backend.repo.get_commit(commit_idx=idx)
183 commit = backend.repo.get_commit(commit_idx=idx)
219 response = self.app.get(
184 response = self.app.get(
220 route_path('repo_files',
185 route_path('repo_files',
221 repo_name=backend.repo_name,
186 repo_name=backend.repo_name,
222 commit_id=commit.raw_id, f_path='/'))
187 commit_id=commit.raw_id, f_path='/'))
223
188
224 assert_response = response.assert_response()
189 assert_response = response.assert_response()
225 for branch in branches:
190 for branch in branches:
226 assert_response.element_contains('.tags .branchtag', branch)
191 assert_response.element_contains('.tags .branchtag', branch)
227
192
228 def test_show_files_paging(self, backend):
193 def test_show_files_paging(self, backend):
229 repo = backend.repo
194 repo = backend.repo
230 indexes = [73, 92, 109, 1, 0]
195 indexes = [73, 92, 109, 1, 0]
231 idx_map = [(rev, repo.get_commit(commit_idx=rev).raw_id)
196 idx_map = [(rev, repo.get_commit(commit_idx=rev).raw_id)
232 for rev in indexes]
197 for rev in indexes]
233
198
234 for idx in idx_map:
199 for idx in idx_map:
235 response = self.app.get(
200 response = self.app.get(
236 route_path('repo_files',
201 route_path('repo_files',
237 repo_name=backend.repo_name,
202 repo_name=backend.repo_name,
238 commit_id=idx[1], f_path='/'))
203 commit_id=idx[1], f_path='/'))
239
204
240 response.mustcontain("""r%s:%s""" % (idx[0], idx[1][:8]))
205 response.mustcontain("""r%s:%s""" % (idx[0], idx[1][:8]))
241
206
242 def test_file_source(self, backend):
207 def test_file_source(self, backend):
243 commit = backend.repo.get_commit(commit_idx=167)
208 commit = backend.repo.get_commit(commit_idx=167)
244 response = self.app.get(
209 response = self.app.get(
245 route_path('repo_files',
210 route_path('repo_files',
246 repo_name=backend.repo_name,
211 repo_name=backend.repo_name,
247 commit_id=commit.raw_id, f_path='vcs/nodes.py'))
212 commit_id=commit.raw_id, f_path='vcs/nodes.py'))
248
213
249 msgbox = """<div class="commit">%s</div>"""
214 msgbox = """<div class="commit">%s</div>"""
250 response.mustcontain(msgbox % (commit.message, ))
215 response.mustcontain(msgbox % (commit.message, ))
251
216
252 assert_response = response.assert_response()
217 assert_response = response.assert_response()
253 if commit.branch:
218 if commit.branch:
254 assert_response.element_contains(
219 assert_response.element_contains(
255 '.tags.tags-main .branchtag', commit.branch)
220 '.tags.tags-main .branchtag', commit.branch)
256 if commit.tags:
221 if commit.tags:
257 for tag in commit.tags:
222 for tag in commit.tags:
258 assert_response.element_contains('.tags.tags-main .tagtag', tag)
223 assert_response.element_contains('.tags.tags-main .tagtag', tag)
259
224
260 def test_file_source_annotated(self, backend):
225 def test_file_source_annotated(self, backend):
261 response = self.app.get(
226 response = self.app.get(
262 route_path('repo_files:annotated',
227 route_path('repo_files:annotated',
263 repo_name=backend.repo_name,
228 repo_name=backend.repo_name,
264 commit_id='tip', f_path='vcs/nodes.py'))
229 commit_id='tip', f_path='vcs/nodes.py'))
265 expected_commits = {
230 expected_commits = {
266 'hg': 'r356',
231 'hg': 'r356',
267 'git': 'r345',
232 'git': 'r345',
268 'svn': 'r208',
233 'svn': 'r208',
269 }
234 }
270 response.mustcontain(expected_commits[backend.alias])
235 response.mustcontain(expected_commits[backend.alias])
271
236
272 def test_file_source_authors(self, backend):
237 def test_file_source_authors(self, backend):
273 response = self.app.get(
238 response = self.app.get(
274 route_path('repo_file_authors',
239 route_path('repo_file_authors',
275 repo_name=backend.repo_name,
240 repo_name=backend.repo_name,
276 commit_id='tip', f_path='vcs/nodes.py'))
241 commit_id='tip', f_path='vcs/nodes.py'))
277 expected_authors = {
242 expected_authors = {
278 'hg': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
243 'hg': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
279 'git': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
244 'git': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
280 'svn': ('marcin', 'lukasz'),
245 'svn': ('marcin', 'lukasz'),
281 }
246 }
282
247
283 for author in expected_authors[backend.alias]:
248 for author in expected_authors[backend.alias]:
284 response.mustcontain(author)
249 response.mustcontain(author)
285
250
286 def test_file_source_authors_with_annotation(self, backend):
251 def test_file_source_authors_with_annotation(self, backend):
287 response = self.app.get(
252 response = self.app.get(
288 route_path('repo_file_authors',
253 route_path('repo_file_authors',
289 repo_name=backend.repo_name,
254 repo_name=backend.repo_name,
290 commit_id='tip', f_path='vcs/nodes.py',
255 commit_id='tip', f_path='vcs/nodes.py',
291 params=dict(annotate=1)))
256 params=dict(annotate=1)))
292 expected_authors = {
257 expected_authors = {
293 'hg': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
258 'hg': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
294 'git': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
259 'git': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
295 'svn': ('marcin', 'lukasz'),
260 'svn': ('marcin', 'lukasz'),
296 }
261 }
297
262
298 for author in expected_authors[backend.alias]:
263 for author in expected_authors[backend.alias]:
299 response.mustcontain(author)
264 response.mustcontain(author)
300
265
301 def test_file_source_history(self, backend, xhr_header):
266 def test_file_source_history(self, backend, xhr_header):
302 response = self.app.get(
267 response = self.app.get(
303 route_path('repo_file_history',
268 route_path('repo_file_history',
304 repo_name=backend.repo_name,
269 repo_name=backend.repo_name,
305 commit_id='tip', f_path='vcs/nodes.py'),
270 commit_id='tip', f_path='vcs/nodes.py'),
306 extra_environ=xhr_header)
271 extra_environ=xhr_header)
307 assert get_node_history(backend.alias) == json.loads(response.body)
272 assert get_node_history(backend.alias) == json.loads(response.body)
308
273
309 def test_file_source_history_svn(self, backend_svn, xhr_header):
274 def test_file_source_history_svn(self, backend_svn, xhr_header):
310 simple_repo = backend_svn['svn-simple-layout']
275 simple_repo = backend_svn['svn-simple-layout']
311 response = self.app.get(
276 response = self.app.get(
312 route_path('repo_file_history',
277 route_path('repo_file_history',
313 repo_name=simple_repo.repo_name,
278 repo_name=simple_repo.repo_name,
314 commit_id='tip', f_path='trunk/example.py'),
279 commit_id='tip', f_path='trunk/example.py'),
315 extra_environ=xhr_header)
280 extra_environ=xhr_header)
316
281
317 expected_data = json.loads(
282 expected_data = json.loads(
318 fixture.load_resource('svn_node_history_branches.json'))
283 fixture.load_resource('svn_node_history_branches.json'))
319
284
320 assert expected_data == response.json
285 assert expected_data == response.json
321
286
322 def test_file_source_history_with_annotation(self, backend, xhr_header):
287 def test_file_source_history_with_annotation(self, backend, xhr_header):
323 response = self.app.get(
288 response = self.app.get(
324 route_path('repo_file_history',
289 route_path('repo_file_history',
325 repo_name=backend.repo_name,
290 repo_name=backend.repo_name,
326 commit_id='tip', f_path='vcs/nodes.py',
291 commit_id='tip', f_path='vcs/nodes.py',
327 params=dict(annotate=1)),
292 params=dict(annotate=1)),
328
293
329 extra_environ=xhr_header)
294 extra_environ=xhr_header)
330 assert get_node_history(backend.alias) == json.loads(response.body)
295 assert get_node_history(backend.alias) == json.loads(response.body)
331
296
332 def test_tree_search_top_level(self, backend, xhr_header):
297 def test_tree_search_top_level(self, backend, xhr_header):
333 commit = backend.repo.get_commit(commit_idx=173)
298 commit = backend.repo.get_commit(commit_idx=173)
334 response = self.app.get(
299 response = self.app.get(
335 route_path('repo_files_nodelist',
300 route_path('repo_files_nodelist',
336 repo_name=backend.repo_name,
301 repo_name=backend.repo_name,
337 commit_id=commit.raw_id, f_path='/'),
302 commit_id=commit.raw_id, f_path='/'),
338 extra_environ=xhr_header)
303 extra_environ=xhr_header)
339 assert 'nodes' in response.json
304 assert 'nodes' in response.json
340 assert {'name': 'docs', 'type': 'dir'} in response.json['nodes']
305 assert {'name': 'docs', 'type': 'dir'} in response.json['nodes']
341
306
342 def test_tree_search_missing_xhr(self, backend):
307 def test_tree_search_missing_xhr(self, backend):
343 self.app.get(
308 self.app.get(
344 route_path('repo_files_nodelist',
309 route_path('repo_files_nodelist',
345 repo_name=backend.repo_name,
310 repo_name=backend.repo_name,
346 commit_id='tip', f_path='/'),
311 commit_id='tip', f_path='/'),
347 status=404)
312 status=404)
348
313
349 def test_tree_search_at_path(self, backend, xhr_header):
314 def test_tree_search_at_path(self, backend, xhr_header):
350 commit = backend.repo.get_commit(commit_idx=173)
315 commit = backend.repo.get_commit(commit_idx=173)
351 response = self.app.get(
316 response = self.app.get(
352 route_path('repo_files_nodelist',
317 route_path('repo_files_nodelist',
353 repo_name=backend.repo_name,
318 repo_name=backend.repo_name,
354 commit_id=commit.raw_id, f_path='/docs'),
319 commit_id=commit.raw_id, f_path='/docs'),
355 extra_environ=xhr_header)
320 extra_environ=xhr_header)
356 assert 'nodes' in response.json
321 assert 'nodes' in response.json
357 nodes = response.json['nodes']
322 nodes = response.json['nodes']
358 assert {'name': 'docs/api', 'type': 'dir'} in nodes
323 assert {'name': 'docs/api', 'type': 'dir'} in nodes
359 assert {'name': 'docs/index.rst', 'type': 'file'} in nodes
324 assert {'name': 'docs/index.rst', 'type': 'file'} in nodes
360
325
361 def test_tree_search_at_path_2nd_level(self, backend, xhr_header):
326 def test_tree_search_at_path_2nd_level(self, backend, xhr_header):
362 commit = backend.repo.get_commit(commit_idx=173)
327 commit = backend.repo.get_commit(commit_idx=173)
363 response = self.app.get(
328 response = self.app.get(
364 route_path('repo_files_nodelist',
329 route_path('repo_files_nodelist',
365 repo_name=backend.repo_name,
330 repo_name=backend.repo_name,
366 commit_id=commit.raw_id, f_path='/docs/api'),
331 commit_id=commit.raw_id, f_path='/docs/api'),
367 extra_environ=xhr_header)
332 extra_environ=xhr_header)
368 assert 'nodes' in response.json
333 assert 'nodes' in response.json
369 nodes = response.json['nodes']
334 nodes = response.json['nodes']
370 assert {'name': 'docs/api/index.rst', 'type': 'file'} in nodes
335 assert {'name': 'docs/api/index.rst', 'type': 'file'} in nodes
371
336
372 def test_tree_search_at_path_missing_xhr(self, backend):
337 def test_tree_search_at_path_missing_xhr(self, backend):
373 self.app.get(
338 self.app.get(
374 route_path('repo_files_nodelist',
339 route_path('repo_files_nodelist',
375 repo_name=backend.repo_name,
340 repo_name=backend.repo_name,
376 commit_id='tip', f_path='/docs'),
341 commit_id='tip', f_path='/docs'),
377 status=404)
342 status=404)
378
343
379 def test_nodetree(self, backend, xhr_header):
344 def test_nodetree(self, backend, xhr_header):
380 commit = backend.repo.get_commit(commit_idx=173)
345 commit = backend.repo.get_commit(commit_idx=173)
381 response = self.app.get(
346 response = self.app.get(
382 route_path('repo_nodetree_full',
347 route_path('repo_nodetree_full',
383 repo_name=backend.repo_name,
348 repo_name=backend.repo_name,
384 commit_id=commit.raw_id, f_path='/'),
349 commit_id=commit.raw_id, f_path='/'),
385 extra_environ=xhr_header)
350 extra_environ=xhr_header)
386
351
387 assert_response = response.assert_response()
352 assert_response = response.assert_response()
388
353
389 for attr in ['data-commit-id', 'data-date', 'data-author']:
354 for attr in ['data-commit-id', 'data-date', 'data-author']:
390 elements = assert_response.get_elements('[{}]'.format(attr))
355 elements = assert_response.get_elements('[{}]'.format(attr))
391 assert len(elements) > 1
356 assert len(elements) > 1
392
357
393 for element in elements:
358 for element in elements:
394 assert element.get(attr)
359 assert element.get(attr)
395
360
396 def test_nodetree_if_file(self, backend, xhr_header):
361 def test_nodetree_if_file(self, backend, xhr_header):
397 commit = backend.repo.get_commit(commit_idx=173)
362 commit = backend.repo.get_commit(commit_idx=173)
398 response = self.app.get(
363 response = self.app.get(
399 route_path('repo_nodetree_full',
364 route_path('repo_nodetree_full',
400 repo_name=backend.repo_name,
365 repo_name=backend.repo_name,
401 commit_id=commit.raw_id, f_path='README.rst'),
366 commit_id=commit.raw_id, f_path='README.rst'),
402 extra_environ=xhr_header)
367 extra_environ=xhr_header)
403 assert response.text == ''
368 assert response.text == ''
404
369
405 def test_nodetree_wrong_path(self, backend, xhr_header):
370 def test_nodetree_wrong_path(self, backend, xhr_header):
406 commit = backend.repo.get_commit(commit_idx=173)
371 commit = backend.repo.get_commit(commit_idx=173)
407 response = self.app.get(
372 response = self.app.get(
408 route_path('repo_nodetree_full',
373 route_path('repo_nodetree_full',
409 repo_name=backend.repo_name,
374 repo_name=backend.repo_name,
410 commit_id=commit.raw_id, f_path='/dont-exist'),
375 commit_id=commit.raw_id, f_path='/dont-exist'),
411 extra_environ=xhr_header)
376 extra_environ=xhr_header)
412
377
413 err = 'error: There is no file nor ' \
378 err = 'error: There is no file nor ' \
414 'directory at the given path'
379 'directory at the given path'
415 assert err in response.text
380 assert err in response.text
416
381
417 def test_nodetree_missing_xhr(self, backend):
382 def test_nodetree_missing_xhr(self, backend):
418 self.app.get(
383 self.app.get(
419 route_path('repo_nodetree_full',
384 route_path('repo_nodetree_full',
420 repo_name=backend.repo_name,
385 repo_name=backend.repo_name,
421 commit_id='tip', f_path='/'),
386 commit_id='tip', f_path='/'),
422 status=404)
387 status=404)
423
388
424
389
425 @pytest.mark.usefixtures("app", "autologin_user")
390 @pytest.mark.usefixtures("app", "autologin_user")
426 class TestRawFileHandling(object):
391 class TestRawFileHandling(object):
427
392
428 def test_download_file(self, backend):
393 def test_download_file(self, backend):
429 commit = backend.repo.get_commit(commit_idx=173)
394 commit = backend.repo.get_commit(commit_idx=173)
430 response = self.app.get(
395 response = self.app.get(
431 route_path('repo_file_download',
396 route_path('repo_file_download',
432 repo_name=backend.repo_name,
397 repo_name=backend.repo_name,
433 commit_id=commit.raw_id, f_path='vcs/nodes.py'),)
398 commit_id=commit.raw_id, f_path='vcs/nodes.py'),)
434
399
435 assert response.content_disposition == 'attachment; filename="nodes.py"; filename*=UTF-8\'\'nodes.py'
400 assert response.content_disposition == 'attachment; filename="nodes.py"; filename*=UTF-8\'\'nodes.py'
436 assert response.content_type == "text/x-python"
401 assert response.content_type == "text/x-python"
437
402
438 def test_download_file_wrong_cs(self, backend):
403 def test_download_file_wrong_cs(self, backend):
439 raw_id = u'ERRORce30c96924232dffcd24178a07ffeb5dfc'
404 raw_id = u'ERRORce30c96924232dffcd24178a07ffeb5dfc'
440
405
441 response = self.app.get(
406 response = self.app.get(
442 route_path('repo_file_download',
407 route_path('repo_file_download',
443 repo_name=backend.repo_name,
408 repo_name=backend.repo_name,
444 commit_id=raw_id, f_path='vcs/nodes.svg'),
409 commit_id=raw_id, f_path='vcs/nodes.svg'),
445 status=404)
410 status=404)
446
411
447 msg = """No such commit exists for this repository"""
412 msg = """No such commit exists for this repository"""
448 response.mustcontain(msg)
413 response.mustcontain(msg)
449
414
450 def test_download_file_wrong_f_path(self, backend):
415 def test_download_file_wrong_f_path(self, backend):
451 commit = backend.repo.get_commit(commit_idx=173)
416 commit = backend.repo.get_commit(commit_idx=173)
452 f_path = 'vcs/ERRORnodes.py'
417 f_path = 'vcs/ERRORnodes.py'
453
418
454 response = self.app.get(
419 response = self.app.get(
455 route_path('repo_file_download',
420 route_path('repo_file_download',
456 repo_name=backend.repo_name,
421 repo_name=backend.repo_name,
457 commit_id=commit.raw_id, f_path=f_path),
422 commit_id=commit.raw_id, f_path=f_path),
458 status=404)
423 status=404)
459
424
460 msg = (
425 msg = (
461 "There is no file nor directory at the given path: "
426 "There is no file nor directory at the given path: "
462 "`%s` at commit %s" % (f_path, commit.short_id))
427 "`%s` at commit %s" % (f_path, commit.short_id))
463 response.mustcontain(msg)
428 response.mustcontain(msg)
464
429
465 def test_file_raw(self, backend):
430 def test_file_raw(self, backend):
466 commit = backend.repo.get_commit(commit_idx=173)
431 commit = backend.repo.get_commit(commit_idx=173)
467 response = self.app.get(
432 response = self.app.get(
468 route_path('repo_file_raw',
433 route_path('repo_file_raw',
469 repo_name=backend.repo_name,
434 repo_name=backend.repo_name,
470 commit_id=commit.raw_id, f_path='vcs/nodes.py'),)
435 commit_id=commit.raw_id, f_path='vcs/nodes.py'),)
471
436
472 assert response.content_type == "text/plain"
437 assert response.content_type == "text/plain"
473
438
474 def test_file_raw_binary(self, backend):
439 def test_file_raw_binary(self, backend):
475 commit = backend.repo.get_commit()
440 commit = backend.repo.get_commit()
476 response = self.app.get(
441 response = self.app.get(
477 route_path('repo_file_raw',
442 route_path('repo_file_raw',
478 repo_name=backend.repo_name,
443 repo_name=backend.repo_name,
479 commit_id=commit.raw_id,
444 commit_id=commit.raw_id,
480 f_path='docs/theme/ADC/static/breadcrumb_background.png'),)
445 f_path='docs/theme/ADC/static/breadcrumb_background.png'),)
481
446
482 assert response.content_disposition == 'inline'
447 assert response.content_disposition == 'inline'
483
448
484 def test_raw_file_wrong_cs(self, backend):
449 def test_raw_file_wrong_cs(self, backend):
485 raw_id = u'ERRORcce30c96924232dffcd24178a07ffeb5dfc'
450 raw_id = u'ERRORcce30c96924232dffcd24178a07ffeb5dfc'
486
451
487 response = self.app.get(
452 response = self.app.get(
488 route_path('repo_file_raw',
453 route_path('repo_file_raw',
489 repo_name=backend.repo_name,
454 repo_name=backend.repo_name,
490 commit_id=raw_id, f_path='vcs/nodes.svg'),
455 commit_id=raw_id, f_path='vcs/nodes.svg'),
491 status=404)
456 status=404)
492
457
493 msg = """No such commit exists for this repository"""
458 msg = """No such commit exists for this repository"""
494 response.mustcontain(msg)
459 response.mustcontain(msg)
495
460
496 def test_raw_wrong_f_path(self, backend):
461 def test_raw_wrong_f_path(self, backend):
497 commit = backend.repo.get_commit(commit_idx=173)
462 commit = backend.repo.get_commit(commit_idx=173)
498 f_path = 'vcs/ERRORnodes.py'
463 f_path = 'vcs/ERRORnodes.py'
499 response = self.app.get(
464 response = self.app.get(
500 route_path('repo_file_raw',
465 route_path('repo_file_raw',
501 repo_name=backend.repo_name,
466 repo_name=backend.repo_name,
502 commit_id=commit.raw_id, f_path=f_path),
467 commit_id=commit.raw_id, f_path=f_path),
503 status=404)
468 status=404)
504
469
505 msg = (
470 msg = (
506 "There is no file nor directory at the given path: "
471 "There is no file nor directory at the given path: "
507 "`%s` at commit %s" % (f_path, commit.short_id))
472 "`%s` at commit %s" % (f_path, commit.short_id))
508 response.mustcontain(msg)
473 response.mustcontain(msg)
509
474
510 def test_raw_svg_should_not_be_rendered(self, backend):
475 def test_raw_svg_should_not_be_rendered(self, backend):
511 backend.create_repo()
476 backend.create_repo()
512 backend.ensure_file(b"xss.svg")
477 backend.ensure_file(b"xss.svg")
513 response = self.app.get(
478 response = self.app.get(
514 route_path('repo_file_raw',
479 route_path('repo_file_raw',
515 repo_name=backend.repo_name,
480 repo_name=backend.repo_name,
516 commit_id='tip', f_path='xss.svg'),)
481 commit_id='tip', f_path='xss.svg'),)
517 # If the content type is image/svg+xml then it allows to render HTML
482 # If the content type is image/svg+xml then it allows to render HTML
518 # and malicious SVG.
483 # and malicious SVG.
519 assert response.content_type == "text/plain"
484 assert response.content_type == "text/plain"
520
485
521
486
522 @pytest.mark.usefixtures("app")
487 @pytest.mark.usefixtures("app")
523 class TestRepositoryArchival(object):
488 class TestRepositoryArchival(object):
524
489
525 def test_archival(self, backend):
490 def test_archival(self, backend):
526 backend.enable_downloads()
491 backend.enable_downloads()
527 commit = backend.repo.get_commit(commit_idx=173)
492 commit = backend.repo.get_commit(commit_idx=173)
528
493
529 for a_type, content_type, extension in settings.ARCHIVE_SPECS:
494 for a_type, content_type, extension in settings.ARCHIVE_SPECS:
530 path_sha = get_path_sha('/')
495 path_sha = get_path_sha('/')
531 filename = get_archive_name(backend.repo_id, backend.repo_name, commit_sha=commit.short_id, ext=extension, path_sha=path_sha)
496 filename = get_archive_name(backend.repo_id, backend.repo_name, commit_sha=commit.short_id, ext=extension, path_sha=path_sha)
532
497
533 fname = commit.raw_id + extension
498 fname = commit.raw_id + extension
534 response = self.app.get(
499 response = self.app.get(
535 route_path('repo_archivefile',
500 route_path('repo_archivefile',
536 repo_name=backend.repo_name,
501 repo_name=backend.repo_name,
537 fname=fname))
502 fname=fname))
538
503
539 assert response.status == '200 OK'
504 assert response.status == '200 OK'
540 headers = [
505 headers = [
541 ('Content-Disposition', f'attachment; filename={filename}'),
506 ('Content-Disposition', f'attachment; filename={filename}'),
542 ('Content-Type', content_type),
507 ('Content-Type', content_type),
543 ]
508 ]
544
509
545 for header in headers:
510 for header in headers:
546 assert header in list(response.headers.items())
511 assert header in list(response.headers.items())
547
512
548 def test_archival_no_hash(self, backend):
513 def test_archival_no_hash(self, backend):
549 backend.enable_downloads()
514 backend.enable_downloads()
550 commit = backend.repo.get_commit(commit_idx=173)
515 commit = backend.repo.get_commit(commit_idx=173)
551 for a_type, content_type, extension in settings.ARCHIVE_SPECS:
516 for a_type, content_type, extension in settings.ARCHIVE_SPECS:
552 path_sha = get_path_sha('/')
517 path_sha = get_path_sha('/')
553 filename = get_archive_name(backend.repo_id, backend.repo_name, commit_sha=commit.short_id, ext=extension, path_sha=path_sha, with_hash=False)
518 filename = get_archive_name(backend.repo_id, backend.repo_name, commit_sha=commit.short_id, ext=extension, path_sha=path_sha, with_hash=False)
554
519
555 fname = commit.raw_id + extension
520 fname = commit.raw_id + extension
556 response = self.app.get(
521 response = self.app.get(
557 route_path('repo_archivefile',
522 route_path('repo_archivefile',
558 repo_name=backend.repo_name,
523 repo_name=backend.repo_name,
559 fname=fname, params={'with_hash': 0}))
524 fname=fname, params={'with_hash': 0}))
560
525
561 assert response.status == '200 OK'
526 assert response.status == '200 OK'
562 headers = [
527 headers = [
563 ('Content-Disposition', f'attachment; filename={filename}'),
528 ('Content-Disposition', f'attachment; filename={filename}'),
564 ('Content-Type', content_type),
529 ('Content-Type', content_type),
565 ]
530 ]
566
531
567 for header in headers:
532 for header in headers:
568 assert header in list(response.headers.items())
533 assert header in list(response.headers.items())
569
534
570 def test_archival_at_path(self, backend):
535 def test_archival_at_path(self, backend):
571 backend.enable_downloads()
536 backend.enable_downloads()
572 commit = backend.repo.get_commit(commit_idx=190)
537 commit = backend.repo.get_commit(commit_idx=190)
573 at_path = 'vcs'
538 at_path = 'vcs'
574
539
575 for a_type, content_type, extension in settings.ARCHIVE_SPECS:
540 for a_type, content_type, extension in settings.ARCHIVE_SPECS:
576 path_sha = get_path_sha(at_path)
541 path_sha = get_path_sha(at_path)
577 filename = get_archive_name(backend.repo_id, backend.repo_name, commit_sha=commit.short_id, ext=extension, path_sha=path_sha)
542 filename = get_archive_name(backend.repo_id, backend.repo_name, commit_sha=commit.short_id, ext=extension, path_sha=path_sha)
578
543
579 fname = commit.raw_id + extension
544 fname = commit.raw_id + extension
580 response = self.app.get(
545 response = self.app.get(
581 route_path('repo_archivefile',
546 route_path('repo_archivefile',
582 repo_name=backend.repo_name,
547 repo_name=backend.repo_name,
583 fname=fname, params={'at_path': at_path}))
548 fname=fname, params={'at_path': at_path}))
584
549
585 assert response.status == '200 OK'
550 assert response.status == '200 OK'
586 headers = [
551 headers = [
587 ('Content-Disposition', f'attachment; filename={filename}'),
552 ('Content-Disposition', f'attachment; filename={filename}'),
588 ('Content-Type', content_type),
553 ('Content-Type', content_type),
589 ]
554 ]
590
555
591 for header in headers:
556 for header in headers:
592 assert header in list(response.headers.items())
557 assert header in list(response.headers.items())
593
558
594 @pytest.mark.parametrize('arch_ext',[
559 @pytest.mark.parametrize('arch_ext',[
595 'tar', 'rar', 'x', '..ax', '.zipz', 'tar.gz.tar'])
560 'tar', 'rar', 'x', '..ax', '.zipz', 'tar.gz.tar'])
596 def test_archival_wrong_ext(self, backend, arch_ext):
561 def test_archival_wrong_ext(self, backend, arch_ext):
597 backend.enable_downloads()
562 backend.enable_downloads()
598 commit = backend.repo.get_commit(commit_idx=173)
563 commit = backend.repo.get_commit(commit_idx=173)
599
564
600 fname = commit.raw_id + '.' + arch_ext
565 fname = commit.raw_id + '.' + arch_ext
601
566
602 response = self.app.get(
567 response = self.app.get(
603 route_path('repo_archivefile',
568 route_path('repo_archivefile',
604 repo_name=backend.repo_name,
569 repo_name=backend.repo_name,
605 fname=fname))
570 fname=fname))
606 response.mustcontain(
571 response.mustcontain(
607 'Unknown archive type for: `{}`'.format(fname))
572 'Unknown archive type for: `{}`'.format(fname))
608
573
609 @pytest.mark.parametrize('commit_id', [
574 @pytest.mark.parametrize('commit_id', [
610 '00x000000', 'tar', 'wrong', '@$@$42413232', '232dffcd'])
575 '00x000000', 'tar', 'wrong', '@$@$42413232', '232dffcd'])
611 def test_archival_wrong_commit_id(self, backend, commit_id):
576 def test_archival_wrong_commit_id(self, backend, commit_id):
612 backend.enable_downloads()
577 backend.enable_downloads()
613 fname = f'{commit_id}.zip'
578 fname = f'{commit_id}.zip'
614
579
615 response = self.app.get(
580 response = self.app.get(
616 route_path('repo_archivefile',
581 route_path('repo_archivefile',
617 repo_name=backend.repo_name,
582 repo_name=backend.repo_name,
618 fname=fname))
583 fname=fname))
619 response.mustcontain('Unknown commit_id')
584 response.mustcontain('Unknown commit_id')
620
585
621
586
622 @pytest.mark.usefixtures("app")
587 @pytest.mark.usefixtures("app")
623 class TestFilesDiff(object):
588 class TestFilesDiff(object):
624
589
625 @pytest.mark.parametrize("diff", ['diff', 'download', 'raw'])
590 @pytest.mark.parametrize("diff", ['diff', 'download', 'raw'])
626 def test_file_full_diff(self, backend, diff):
591 def test_file_full_diff(self, backend, diff):
627 commit1 = backend.repo.get_commit(commit_idx=-1)
592 commit1 = backend.repo.get_commit(commit_idx=-1)
628 commit2 = backend.repo.get_commit(commit_idx=-2)
593 commit2 = backend.repo.get_commit(commit_idx=-2)
629
594
630 response = self.app.get(
595 response = self.app.get(
631 route_path('repo_files_diff',
596 route_path('repo_files_diff',
632 repo_name=backend.repo_name,
597 repo_name=backend.repo_name,
633 f_path='README'),
598 f_path='README'),
634 params={
599 params={
635 'diff1': commit2.raw_id,
600 'diff1': commit2.raw_id,
636 'diff2': commit1.raw_id,
601 'diff2': commit1.raw_id,
637 'fulldiff': '1',
602 'fulldiff': '1',
638 'diff': diff,
603 'diff': diff,
639 })
604 })
640
605
641 if diff == 'diff':
606 if diff == 'diff':
642 # use redirect since this is OLD view redirecting to compare page
607 # use redirect since this is OLD view redirecting to compare page
643 response = response.follow()
608 response = response.follow()
644
609
645 # It's a symlink to README.rst
610 # It's a symlink to README.rst
646 response.mustcontain('README.rst')
611 response.mustcontain('README.rst')
647 response.mustcontain('No newline at end of file')
612 response.mustcontain('No newline at end of file')
648
613
649 def test_file_binary_diff(self, backend):
614 def test_file_binary_diff(self, backend):
650 commits = [
615 commits = [
651 {'message': 'First commit'},
616 {'message': 'First commit'},
652 {'message': 'Commit with binary',
617 {'message': 'Commit with binary',
653 'added': [nodes.FileNode(b'file.bin', content='\0BINARY\0')]},
618 'added': [nodes.FileNode(b'file.bin', content='\0BINARY\0')]},
654 ]
619 ]
655 repo = backend.create_repo(commits=commits)
620 repo = backend.create_repo(commits=commits)
656
621
657 response = self.app.get(
622 response = self.app.get(
658 route_path('repo_files_diff',
623 route_path('repo_files_diff',
659 repo_name=backend.repo_name,
624 repo_name=backend.repo_name,
660 f_path='file.bin'),
625 f_path='file.bin'),
661 params={
626 params={
662 'diff1': repo.get_commit(commit_idx=0).raw_id,
627 'diff1': repo.get_commit(commit_idx=0).raw_id,
663 'diff2': repo.get_commit(commit_idx=1).raw_id,
628 'diff2': repo.get_commit(commit_idx=1).raw_id,
664 'fulldiff': '1',
629 'fulldiff': '1',
665 'diff': 'diff',
630 'diff': 'diff',
666 })
631 })
667 # use redirect since this is OLD view redirecting to compare page
632 # use redirect since this is OLD view redirecting to compare page
668 response = response.follow()
633 response = response.follow()
669 response.mustcontain('Collapse 1 commit')
634 response.mustcontain('Collapse 1 commit')
670 file_changes = (1, 0, 0)
635 file_changes = (1, 0, 0)
671
636
672 compare_page = ComparePage(response)
637 compare_page = ComparePage(response)
673 compare_page.contains_change_summary(*file_changes)
638 compare_page.contains_change_summary(*file_changes)
674
639
675 if backend.alias == 'svn':
640 if backend.alias == 'svn':
676 response.mustcontain('new file 10644')
641 response.mustcontain('new file 10644')
677 # TODO(marcink): SVN doesn't yet detect binary changes
642 # TODO(marcink): SVN doesn't yet detect binary changes
678 else:
643 else:
679 response.mustcontain('new file 100644')
644 response.mustcontain('new file 100644')
680 response.mustcontain('binary diff hidden')
645 response.mustcontain('binary diff hidden')
681
646
682 def test_diff_2way(self, backend):
647 def test_diff_2way(self, backend):
683 commit1 = backend.repo.get_commit(commit_idx=-1)
648 commit1 = backend.repo.get_commit(commit_idx=-1)
684 commit2 = backend.repo.get_commit(commit_idx=-2)
649 commit2 = backend.repo.get_commit(commit_idx=-2)
685 response = self.app.get(
650 response = self.app.get(
686 route_path('repo_files_diff_2way_redirect',
651 route_path('repo_files_diff_2way_redirect',
687 repo_name=backend.repo_name,
652 repo_name=backend.repo_name,
688 f_path='README'),
653 f_path='README'),
689 params={
654 params={
690 'diff1': commit2.raw_id,
655 'diff1': commit2.raw_id,
691 'diff2': commit1.raw_id,
656 'diff2': commit1.raw_id,
692 })
657 })
693 # use redirect since this is OLD view redirecting to compare page
658 # use redirect since this is OLD view redirecting to compare page
694 response = response.follow()
659 response = response.follow()
695
660
696 # It's a symlink to README.rst
661 # It's a symlink to README.rst
697 response.mustcontain('README.rst')
662 response.mustcontain('README.rst')
698 response.mustcontain('No newline at end of file')
663 response.mustcontain('No newline at end of file')
699
664
700 def test_requires_one_commit_id(self, backend, autologin_user):
665 def test_requires_one_commit_id(self, backend, autologin_user):
701 response = self.app.get(
666 response = self.app.get(
702 route_path('repo_files_diff',
667 route_path('repo_files_diff',
703 repo_name=backend.repo_name,
668 repo_name=backend.repo_name,
704 f_path='README.rst'),
669 f_path='README.rst'),
705 status=400)
670 status=400)
706 response.mustcontain(
671 response.mustcontain(
707 'Need query parameter', 'diff1', 'diff2', 'to generate a diff.')
672 'Need query parameter', 'diff1', 'diff2', 'to generate a diff.')
708
673
709 def test_returns_no_files_if_file_does_not_exist(self, vcsbackend):
674 def test_returns_no_files_if_file_does_not_exist(self, vcsbackend):
710 repo = vcsbackend.repo
675 repo = vcsbackend.repo
711 response = self.app.get(
676 response = self.app.get(
712 route_path('repo_files_diff',
677 route_path('repo_files_diff',
713 repo_name=repo.name,
678 repo_name=repo.name,
714 f_path='does-not-exist-in-any-commit'),
679 f_path='does-not-exist-in-any-commit'),
715 params={
680 params={
716 'diff1': repo[0].raw_id,
681 'diff1': repo[0].raw_id,
717 'diff2': repo[1].raw_id
682 'diff2': repo[1].raw_id
718 })
683 })
719
684
720 response = response.follow()
685 response = response.follow()
721 response.mustcontain('No files')
686 response.mustcontain('No files')
722
687
723 def test_returns_redirect_if_file_not_changed(self, backend):
688 def test_returns_redirect_if_file_not_changed(self, backend):
724 commit = backend.repo.get_commit(commit_idx=-1)
689 commit = backend.repo.get_commit(commit_idx=-1)
725 response = self.app.get(
690 response = self.app.get(
726 route_path('repo_files_diff_2way_redirect',
691 route_path('repo_files_diff_2way_redirect',
727 repo_name=backend.repo_name,
692 repo_name=backend.repo_name,
728 f_path='README'),
693 f_path='README'),
729 params={
694 params={
730 'diff1': commit.raw_id,
695 'diff1': commit.raw_id,
731 'diff2': commit.raw_id,
696 'diff2': commit.raw_id,
732 })
697 })
733
698
734 response = response.follow()
699 response = response.follow()
735 response.mustcontain('No files')
700 response.mustcontain('No files')
736 response.mustcontain('No commits in this compare')
701 response.mustcontain('No commits in this compare')
737
702
738 def test_supports_diff_to_different_path_svn(self, backend_svn):
703 def test_supports_diff_to_different_path_svn(self, backend_svn):
739 #TODO: check this case
704 #TODO: check this case
740 return
705 return
741
706
742 repo = backend_svn['svn-simple-layout'].scm_instance()
707 repo = backend_svn['svn-simple-layout'].scm_instance()
743 commit_id_1 = '24'
708 commit_id_1 = '24'
744 commit_id_2 = '26'
709 commit_id_2 = '26'
745
710
746 response = self.app.get(
711 response = self.app.get(
747 route_path('repo_files_diff',
712 route_path('repo_files_diff',
748 repo_name=backend_svn.repo_name,
713 repo_name=backend_svn.repo_name,
749 f_path='trunk/example.py'),
714 f_path='trunk/example.py'),
750 params={
715 params={
751 'diff1': 'tags/v0.2/example.py@' + commit_id_1,
716 'diff1': 'tags/v0.2/example.py@' + commit_id_1,
752 'diff2': commit_id_2,
717 'diff2': commit_id_2,
753 })
718 })
754
719
755 response = response.follow()
720 response = response.follow()
756 response.mustcontain(
721 response.mustcontain(
757 # diff contains this
722 # diff contains this
758 "Will print out a useful message on invocation.")
723 "Will print out a useful message on invocation.")
759
724
760 # Note: Expecting that we indicate the user what's being compared
725 # Note: Expecting that we indicate the user what's being compared
761 response.mustcontain("trunk/example.py")
726 response.mustcontain("trunk/example.py")
762 response.mustcontain("tags/v0.2/example.py")
727 response.mustcontain("tags/v0.2/example.py")
763
728
764 def test_show_rev_redirects_to_svn_path(self, backend_svn):
729 def test_show_rev_redirects_to_svn_path(self, backend_svn):
765 #TODO: check this case
730 #TODO: check this case
766 return
731 return
767
732
768 repo = backend_svn['svn-simple-layout'].scm_instance()
733 repo = backend_svn['svn-simple-layout'].scm_instance()
769 commit_id = repo[-1].raw_id
734 commit_id = repo[-1].raw_id
770
735
771 response = self.app.get(
736 response = self.app.get(
772 route_path('repo_files_diff',
737 route_path('repo_files_diff',
773 repo_name=backend_svn.repo_name,
738 repo_name=backend_svn.repo_name,
774 f_path='trunk/example.py'),
739 f_path='trunk/example.py'),
775 params={
740 params={
776 'diff1': 'branches/argparse/example.py@' + commit_id,
741 'diff1': 'branches/argparse/example.py@' + commit_id,
777 'diff2': commit_id,
742 'diff2': commit_id,
778 },
743 },
779 status=302)
744 status=302)
780 response = response.follow()
745 response = response.follow()
781 assert response.headers['Location'].endswith(
746 assert response.headers['Location'].endswith(
782 'svn-svn-simple-layout/files/26/branches/argparse/example.py')
747 'svn-svn-simple-layout/files/26/branches/argparse/example.py')
783
748
784 def test_show_rev_and_annotate_redirects_to_svn_path(self, backend_svn):
749 def test_show_rev_and_annotate_redirects_to_svn_path(self, backend_svn):
785 #TODO: check this case
750 #TODO: check this case
786 return
751 return
787
752
788 repo = backend_svn['svn-simple-layout'].scm_instance()
753 repo = backend_svn['svn-simple-layout'].scm_instance()
789 commit_id = repo[-1].raw_id
754 commit_id = repo[-1].raw_id
790 response = self.app.get(
755 response = self.app.get(
791 route_path('repo_files_diff',
756 route_path('repo_files_diff',
792 repo_name=backend_svn.repo_name,
757 repo_name=backend_svn.repo_name,
793 f_path='trunk/example.py'),
758 f_path='trunk/example.py'),
794 params={
759 params={
795 'diff1': 'branches/argparse/example.py@' + commit_id,
760 'diff1': 'branches/argparse/example.py@' + commit_id,
796 'diff2': commit_id,
761 'diff2': commit_id,
797 'show_rev': 'Show at Revision',
762 'show_rev': 'Show at Revision',
798 'annotate': 'true',
763 'annotate': 'true',
799 },
764 },
800 status=302)
765 status=302)
801 response = response.follow()
766 response = response.follow()
802 assert response.headers['Location'].endswith(
767 assert response.headers['Location'].endswith(
803 'svn-svn-simple-layout/annotate/26/branches/argparse/example.py')
768 'svn-svn-simple-layout/annotate/26/branches/argparse/example.py')
804
769
805
770
806 @pytest.mark.usefixtures("app", "autologin_user")
771 @pytest.mark.usefixtures("app", "autologin_user")
807 class TestModifyFilesWithWebInterface(object):
772 class TestModifyFilesWithWebInterface(object):
808
773
809 def test_add_file_view(self, backend):
774 def test_add_file_view(self, backend):
810 self.app.get(
775 self.app.get(
811 route_path('repo_files_add_file',
776 route_path('repo_files_add_file',
812 repo_name=backend.repo_name,
777 repo_name=backend.repo_name,
813 commit_id='tip', f_path='/')
778 commit_id='tip', f_path='/')
814 )
779 )
815
780
816 @pytest.mark.xfail_backends("svn", reason="Depends on online editing")
781 @pytest.mark.xfail_backends("svn", reason="Depends on online editing")
817 def test_add_file_into_repo_missing_content(self, backend, csrf_token):
782 def test_add_file_into_repo_missing_content(self, backend, csrf_token):
818 backend.create_repo()
783 backend.create_repo()
819 filename = 'init.py'
784 filename = 'init.py'
820 response = self.app.post(
785 response = self.app.post(
821 route_path('repo_files_create_file',
786 route_path('repo_files_create_file',
822 repo_name=backend.repo_name,
787 repo_name=backend.repo_name,
823 commit_id='tip', f_path='/'),
788 commit_id='tip', f_path='/'),
824 params={
789 params={
825 'content': "",
790 'content': "",
826 'filename': filename,
791 'filename': filename,
827 'csrf_token': csrf_token,
792 'csrf_token': csrf_token,
828 },
793 },
829 status=302)
794 status=302)
830 expected_msg = 'Successfully committed new file `{}`'.format(os.path.join(filename))
795 expected_msg = 'Successfully committed new file `{}`'.format(os.path.join(filename))
831 assert_session_flash(response, expected_msg)
796 assert_session_flash(response, expected_msg)
832
797
833 def test_add_file_into_repo_missing_filename(self, backend, csrf_token):
798 def test_add_file_into_repo_missing_filename(self, backend, csrf_token):
834 commit_id = backend.repo.get_commit().raw_id
799 commit_id = backend.repo.get_commit().raw_id
835 response = self.app.post(
800 response = self.app.post(
836 route_path('repo_files_create_file',
801 route_path('repo_files_create_file',
837 repo_name=backend.repo_name,
802 repo_name=backend.repo_name,
838 commit_id=commit_id, f_path='/'),
803 commit_id=commit_id, f_path='/'),
839 params={
804 params={
840 'content': "foo",
805 'content': "foo",
841 'csrf_token': csrf_token,
806 'csrf_token': csrf_token,
842 },
807 },
843 status=302)
808 status=302)
844
809
845 assert_session_flash(response, 'No filename specified')
810 assert_session_flash(response, 'No filename specified')
846
811
847 def test_add_file_into_repo_errors_and_no_commits(
812 def test_add_file_into_repo_errors_and_no_commits(
848 self, backend, csrf_token):
813 self, backend, csrf_token):
849 repo = backend.create_repo()
814 repo = backend.create_repo()
850 # Create a file with no filename, it will display an error but
815 # Create a file with no filename, it will display an error but
851 # the repo has no commits yet
816 # the repo has no commits yet
852 response = self.app.post(
817 response = self.app.post(
853 route_path('repo_files_create_file',
818 route_path('repo_files_create_file',
854 repo_name=repo.repo_name,
819 repo_name=repo.repo_name,
855 commit_id='tip', f_path='/'),
820 commit_id='tip', f_path='/'),
856 params={
821 params={
857 'content': "foo",
822 'content': "foo",
858 'csrf_token': csrf_token,
823 'csrf_token': csrf_token,
859 },
824 },
860 status=302)
825 status=302)
861
826
862 assert_session_flash(response, 'No filename specified')
827 assert_session_flash(response, 'No filename specified')
863
828
864 # Not allowed, redirect to the summary
829 # Not allowed, redirect to the summary
865 redirected = response.follow()
830 redirected = response.follow()
866 summary_url = h.route_path('repo_summary', repo_name=repo.repo_name)
831 summary_url = h.route_path('repo_summary', repo_name=repo.repo_name)
867
832
868 # As there are no commits, displays the summary page with the error of
833 # As there are no commits, displays the summary page with the error of
869 # creating a file with no filename
834 # creating a file with no filename
870
835
871 assert redirected.request.path == summary_url
836 assert redirected.request.path == summary_url
872
837
873 @pytest.mark.parametrize("filename, clean_filename", [
838 @pytest.mark.parametrize("filename, clean_filename", [
874 ('/abs/foo', 'abs/foo'),
839 ('/abs/foo', 'abs/foo'),
875 ('../rel/foo', 'rel/foo'),
840 ('../rel/foo', 'rel/foo'),
876 ('file/../foo/foo', 'file/foo/foo'),
841 ('file/../foo/foo', 'file/foo/foo'),
877 ])
842 ])
878 def test_add_file_into_repo_bad_filenames(self, filename, clean_filename, backend, csrf_token):
843 def test_add_file_into_repo_bad_filenames(self, filename, clean_filename, backend, csrf_token):
879 repo = backend.create_repo()
844 repo = backend.create_repo()
880 commit_id = repo.get_commit().raw_id
845 commit_id = repo.get_commit().raw_id
881
846
882 response = self.app.post(
847 response = self.app.post(
883 route_path('repo_files_create_file',
848 route_path('repo_files_create_file',
884 repo_name=repo.repo_name,
849 repo_name=repo.repo_name,
885 commit_id=commit_id, f_path='/'),
850 commit_id=commit_id, f_path='/'),
886 params={
851 params={
887 'content': "foo",
852 'content': "foo",
888 'filename': filename,
853 'filename': filename,
889 'csrf_token': csrf_token,
854 'csrf_token': csrf_token,
890 },
855 },
891 status=302)
856 status=302)
892
857
893 expected_msg = 'Successfully committed new file `{}`'.format(clean_filename)
858 expected_msg = 'Successfully committed new file `{}`'.format(clean_filename)
894 assert_session_flash(response, expected_msg)
859 assert_session_flash(response, expected_msg)
895
860
896 @pytest.mark.parametrize("cnt, filename, content", [
861 @pytest.mark.parametrize("cnt, filename, content", [
897 (1, 'foo.txt', "Content"),
862 (1, 'foo.txt', "Content"),
898 (2, 'dir/foo.rst', "Content"),
863 (2, 'dir/foo.rst', "Content"),
899 (3, 'dir/foo-second.rst', "Content"),
864 (3, 'dir/foo-second.rst', "Content"),
900 (4, 'rel/dir/foo.bar', "Content"),
865 (4, 'rel/dir/foo.bar', "Content"),
901 ])
866 ])
902 def test_add_file_into_empty_repo(self, cnt, filename, content, backend, csrf_token):
867 def test_add_file_into_empty_repo(self, cnt, filename, content, backend, csrf_token):
903 repo = backend.create_repo()
868 repo = backend.create_repo()
904 commit_id = repo.get_commit().raw_id
869 commit_id = repo.get_commit().raw_id
905 response = self.app.post(
870 response = self.app.post(
906 route_path('repo_files_create_file',
871 route_path('repo_files_create_file',
907 repo_name=repo.repo_name,
872 repo_name=repo.repo_name,
908 commit_id=commit_id, f_path='/'),
873 commit_id=commit_id, f_path='/'),
909 params={
874 params={
910 'content': content,
875 'content': content,
911 'filename': filename,
876 'filename': filename,
912 'csrf_token': csrf_token,
877 'csrf_token': csrf_token,
913 },
878 },
914 status=302)
879 status=302)
915
880
916 expected_msg = 'Successfully committed new file `{}`'.format(filename)
881 expected_msg = 'Successfully committed new file `{}`'.format(filename)
917 assert_session_flash(response, expected_msg)
882 assert_session_flash(response, expected_msg)
918
883
919 def test_edit_file_view(self, backend):
884 def test_edit_file_view(self, backend):
920 response = self.app.get(
885 response = self.app.get(
921 route_path('repo_files_edit_file',
886 route_path('repo_files_edit_file',
922 repo_name=backend.repo_name,
887 repo_name=backend.repo_name,
923 commit_id=backend.default_head_id,
888 commit_id=backend.default_head_id,
924 f_path='vcs/nodes.py'),
889 f_path='vcs/nodes.py'),
925 status=200)
890 status=200)
926 response.mustcontain("Module holding everything related to vcs nodes.")
891 response.mustcontain("Module holding everything related to vcs nodes.")
927
892
928 def test_edit_file_view_not_on_branch(self, backend):
893 def test_edit_file_view_not_on_branch(self, backend):
929 repo = backend.create_repo()
894 repo = backend.create_repo()
930 backend.ensure_file(b"vcs/nodes.py")
895 backend.ensure_file(b"vcs/nodes.py")
931
896
932 response = self.app.get(
897 response = self.app.get(
933 route_path('repo_files_edit_file',
898 route_path('repo_files_edit_file',
934 repo_name=repo.repo_name,
899 repo_name=repo.repo_name,
935 commit_id='tip',
900 commit_id='tip',
936 f_path='vcs/nodes.py'),
901 f_path='vcs/nodes.py'),
937 status=302)
902 status=302)
938 assert_session_flash(
903 assert_session_flash(
939 response, 'Cannot modify file. Given commit `tip` is not head of a branch.')
904 response, 'Cannot modify file. Given commit `tip` is not head of a branch.')
940
905
941 def test_edit_file_view_commit_changes(self, backend, csrf_token):
906 def test_edit_file_view_commit_changes(self, backend, csrf_token):
942 repo = backend.create_repo()
907 repo = backend.create_repo()
943 backend.ensure_file(b"vcs/nodes.py", content=b"print 'hello'")
908 backend.ensure_file(b"vcs/nodes.py", content=b"print 'hello'")
944
909
945 response = self.app.post(
910 response = self.app.post(
946 route_path('repo_files_update_file',
911 route_path('repo_files_update_file',
947 repo_name=repo.repo_name,
912 repo_name=repo.repo_name,
948 commit_id=backend.default_head_id,
913 commit_id=backend.default_head_id,
949 f_path='vcs/nodes.py'),
914 f_path='vcs/nodes.py'),
950 params={
915 params={
951 'content': "print 'hello world'",
916 'content': "print 'hello world'",
952 'message': 'I committed',
917 'message': 'I committed',
953 'filename': "vcs/nodes.py",
918 'filename': "vcs/nodes.py",
954 'csrf_token': csrf_token,
919 'csrf_token': csrf_token,
955 },
920 },
956 status=302)
921 status=302)
957 assert_session_flash(
922 assert_session_flash(
958 response, 'Successfully committed changes to file `vcs/nodes.py`')
923 response, 'Successfully committed changes to file `vcs/nodes.py`')
959 tip = repo.get_commit(commit_idx=-1)
924 tip = repo.get_commit(commit_idx=-1)
960 assert tip.message == 'I committed'
925 assert tip.message == 'I committed'
961
926
962 def test_edit_file_view_commit_changes_default_message(self, backend,
927 def test_edit_file_view_commit_changes_default_message(self, backend,
963 csrf_token):
928 csrf_token):
964 repo = backend.create_repo()
929 repo = backend.create_repo()
965 backend.ensure_file(b"vcs/nodes.py", content=b"print 'hello'")
930 backend.ensure_file(b"vcs/nodes.py", content=b"print 'hello'")
966
931
967 commit_id = (
932 commit_id = (
968 backend.default_branch_name or
933 backend.default_branch_name or
969 backend.repo.scm_instance().commit_ids[-1])
934 backend.repo.scm_instance().commit_ids[-1])
970
935
971 response = self.app.post(
936 response = self.app.post(
972 route_path('repo_files_update_file',
937 route_path('repo_files_update_file',
973 repo_name=repo.repo_name,
938 repo_name=repo.repo_name,
974 commit_id=commit_id,
939 commit_id=commit_id,
975 f_path='vcs/nodes.py'),
940 f_path='vcs/nodes.py'),
976 params={
941 params={
977 'content': "print 'hello world'",
942 'content': "print 'hello world'",
978 'message': '',
943 'message': '',
979 'filename': "vcs/nodes.py",
944 'filename': "vcs/nodes.py",
980 'csrf_token': csrf_token,
945 'csrf_token': csrf_token,
981 },
946 },
982 status=302)
947 status=302)
983 assert_session_flash(
948 assert_session_flash(
984 response, 'Successfully committed changes to file `vcs/nodes.py`')
949 response, 'Successfully committed changes to file `vcs/nodes.py`')
985 tip = repo.get_commit(commit_idx=-1)
950 tip = repo.get_commit(commit_idx=-1)
986 assert tip.message == 'Edited file vcs/nodes.py via RhodeCode Enterprise'
951 assert tip.message == 'Edited file vcs/nodes.py via RhodeCode Enterprise'
987
952
988 def test_delete_file_view(self, backend):
953 def test_delete_file_view(self, backend):
989 self.app.get(
954 self.app.get(
990 route_path('repo_files_remove_file',
955 route_path('repo_files_remove_file',
991 repo_name=backend.repo_name,
956 repo_name=backend.repo_name,
992 commit_id=backend.default_head_id,
957 commit_id=backend.default_head_id,
993 f_path='vcs/nodes.py'),
958 f_path='vcs/nodes.py'),
994 status=200)
959 status=200)
995
960
996 def test_delete_file_view_not_on_branch(self, backend):
961 def test_delete_file_view_not_on_branch(self, backend):
997 repo = backend.create_repo()
962 repo = backend.create_repo()
998 backend.ensure_file(b'vcs/nodes.py')
963 backend.ensure_file(b'vcs/nodes.py')
999
964
1000 response = self.app.get(
965 response = self.app.get(
1001 route_path('repo_files_remove_file',
966 route_path('repo_files_remove_file',
1002 repo_name=repo.repo_name,
967 repo_name=repo.repo_name,
1003 commit_id='tip',
968 commit_id='tip',
1004 f_path='vcs/nodes.py'),
969 f_path='vcs/nodes.py'),
1005 status=302)
970 status=302)
1006 assert_session_flash(
971 assert_session_flash(
1007 response, 'Cannot modify file. Given commit `tip` is not head of a branch.')
972 response, 'Cannot modify file. Given commit `tip` is not head of a branch.')
1008
973
1009 def test_delete_file_view_commit_changes(self, backend, csrf_token):
974 def test_delete_file_view_commit_changes(self, backend, csrf_token):
1010 repo = backend.create_repo()
975 repo = backend.create_repo()
1011 backend.ensure_file(b"vcs/nodes.py")
976 backend.ensure_file(b"vcs/nodes.py")
1012
977
1013 response = self.app.post(
978 response = self.app.post(
1014 route_path('repo_files_delete_file',
979 route_path('repo_files_delete_file',
1015 repo_name=repo.repo_name,
980 repo_name=repo.repo_name,
1016 commit_id=backend.default_head_id,
981 commit_id=backend.default_head_id,
1017 f_path='vcs/nodes.py'),
982 f_path='vcs/nodes.py'),
1018 params={
983 params={
1019 'message': 'i committed',
984 'message': 'i committed',
1020 'csrf_token': csrf_token,
985 'csrf_token': csrf_token,
1021 },
986 },
1022 status=302)
987 status=302)
1023 assert_session_flash(
988 assert_session_flash(
1024 response, 'Successfully deleted file `vcs/nodes.py`')
989 response, 'Successfully deleted file `vcs/nodes.py`')
1025
990
1026
991
1027 @pytest.mark.usefixtures("app")
992 @pytest.mark.usefixtures("app")
1028 class TestFilesViewOtherCases(object):
993 class TestFilesViewOtherCases(object):
1029
994
1030 def test_access_empty_repo_redirect_to_summary_with_alert_write_perms(
995 def test_access_empty_repo_redirect_to_summary_with_alert_write_perms(
1031 self, backend_stub, autologin_regular_user, user_regular,
996 self, backend_stub, autologin_regular_user, user_regular,
1032 user_util):
997 user_util):
1033
998
1034 repo = backend_stub.create_repo()
999 repo = backend_stub.create_repo()
1035 user_util.grant_user_permission_to_repo(
1000 user_util.grant_user_permission_to_repo(
1036 repo, user_regular, 'repository.write')
1001 repo, user_regular, 'repository.write')
1037 response = self.app.get(
1002 response = self.app.get(
1038 route_path('repo_files',
1003 route_path('repo_files',
1039 repo_name=repo.repo_name,
1004 repo_name=repo.repo_name,
1040 commit_id='tip', f_path='/'))
1005 commit_id='tip', f_path='/'))
1041
1006
1042 repo_file_add_url = route_path(
1007 repo_file_add_url = route_path(
1043 'repo_files_add_file',
1008 'repo_files_add_file',
1044 repo_name=repo.repo_name,
1009 repo_name=repo.repo_name,
1045 commit_id=0, f_path='')
1010 commit_id=0, f_path='')
1046 add_new = f'<a class="alert-link" href="{repo_file_add_url}">add a new file</a>'
1011 add_new = f'<a class="alert-link" href="{repo_file_add_url}">add a new file</a>'
1047
1012
1048 repo_file_upload_url = route_path(
1013 repo_file_upload_url = route_path(
1049 'repo_files_upload_file',
1014 'repo_files_upload_file',
1050 repo_name=repo.repo_name,
1015 repo_name=repo.repo_name,
1051 commit_id=0, f_path='')
1016 commit_id=0, f_path='')
1052 upload_new = f'<a class="alert-link" href="{repo_file_upload_url}">upload a new file</a>'
1017 upload_new = f'<a class="alert-link" href="{repo_file_upload_url}">upload a new file</a>'
1053
1018
1054 assert_session_flash(
1019 assert_session_flash(
1055 response,
1020 response,
1056 'There are no files yet. Click here to %s or %s.' % (add_new, upload_new)
1021 'There are no files yet. Click here to %s or %s.' % (add_new, upload_new)
1057 )
1022 )
1058
1023
1059 def test_access_empty_repo_redirect_to_summary_with_alert_no_write_perms(
1024 def test_access_empty_repo_redirect_to_summary_with_alert_no_write_perms(
1060 self, backend_stub, autologin_regular_user):
1025 self, backend_stub, autologin_regular_user):
1061 repo = backend_stub.create_repo()
1026 repo = backend_stub.create_repo()
1062 # init session for anon user
1027 # init session for anon user
1063 route_path('repo_summary', repo_name=repo.repo_name)
1028 route_path('repo_summary', repo_name=repo.repo_name)
1064
1029
1065 repo_file_add_url = route_path(
1030 repo_file_add_url = route_path(
1066 'repo_files_add_file',
1031 'repo_files_add_file',
1067 repo_name=repo.repo_name,
1032 repo_name=repo.repo_name,
1068 commit_id=0, f_path='')
1033 commit_id=0, f_path='')
1069
1034
1070 response = self.app.get(
1035 response = self.app.get(
1071 route_path('repo_files',
1036 route_path('repo_files',
1072 repo_name=repo.repo_name,
1037 repo_name=repo.repo_name,
1073 commit_id='tip', f_path='/'))
1038 commit_id='tip', f_path='/'))
1074
1039
1075 assert_session_flash(response, no_=repo_file_add_url)
1040 assert_session_flash(response, no_=repo_file_add_url)
1076
1041
1077 @pytest.mark.parametrize('file_node', [
1042 @pytest.mark.parametrize('file_node', [
1078 b'archive/file.zip',
1043 b'archive/file.zip',
1079 b'diff/my-file.txt',
1044 b'diff/my-file.txt',
1080 b'render.py',
1045 b'render.py',
1081 b'render',
1046 b'render',
1082 b'remove_file',
1047 b'remove_file',
1083 b'remove_file/to-delete.txt',
1048 b'remove_file/to-delete.txt',
1084 ])
1049 ])
1085 def test_file_names_equal_to_routes_parts(self, backend, file_node):
1050 def test_file_names_equal_to_routes_parts(self, backend, file_node):
1086 backend.create_repo()
1051 backend.create_repo()
1087 backend.ensure_file(file_node)
1052 backend.ensure_file(file_node)
1088
1053
1089 self.app.get(
1054 self.app.get(
1090 route_path('repo_files',
1055 route_path('repo_files',
1091 repo_name=backend.repo_name,
1056 repo_name=backend.repo_name,
1092 commit_id='tip', f_path=safe_str(file_node)),
1057 commit_id='tip', f_path=safe_str(file_node)),
1093 status=200)
1058 status=200)
1094
1059
1095
1060
1096 class TestAdjustFilePathForSvn(object):
1061 class TestAdjustFilePathForSvn(object):
1097 """
1062 """
1098 SVN specific adjustments of node history in RepoFilesView.
1063 SVN specific adjustments of node history in RepoFilesView.
1099 """
1064 """
1100
1065
1101 def test_returns_path_relative_to_matched_reference(self):
1066 def test_returns_path_relative_to_matched_reference(self):
1102 repo = self._repo(branches=['trunk'])
1067 repo = self._repo(branches=['trunk'])
1103 self.assert_file_adjustment('trunk/file', 'file', repo)
1068 self.assert_file_adjustment('trunk/file', 'file', repo)
1104
1069
1105 def test_does_not_modify_file_if_no_reference_matches(self):
1070 def test_does_not_modify_file_if_no_reference_matches(self):
1106 repo = self._repo(branches=['trunk'])
1071 repo = self._repo(branches=['trunk'])
1107 self.assert_file_adjustment('notes/file', 'notes/file', repo)
1072 self.assert_file_adjustment('notes/file', 'notes/file', repo)
1108
1073
1109 def test_does_not_adjust_partial_directory_names(self):
1074 def test_does_not_adjust_partial_directory_names(self):
1110 repo = self._repo(branches=['trun'])
1075 repo = self._repo(branches=['trun'])
1111 self.assert_file_adjustment('trunk/file', 'trunk/file', repo)
1076 self.assert_file_adjustment('trunk/file', 'trunk/file', repo)
1112
1077
1113 def test_is_robust_to_patterns_which_prefix_other_patterns(self):
1078 def test_is_robust_to_patterns_which_prefix_other_patterns(self):
1114 repo = self._repo(branches=['trunk', 'trunk/new', 'trunk/old'])
1079 repo = self._repo(branches=['trunk', 'trunk/new', 'trunk/old'])
1115 self.assert_file_adjustment('trunk/new/file', 'file', repo)
1080 self.assert_file_adjustment('trunk/new/file', 'file', repo)
1116
1081
1117 def assert_file_adjustment(self, f_path, expected, repo):
1082 def assert_file_adjustment(self, f_path, expected, repo):
1118 result = RepoFilesView.adjust_file_path_for_svn(f_path, repo)
1083 result = RepoFilesView.adjust_file_path_for_svn(f_path, repo)
1119 assert result == expected
1084 assert result == expected
1120
1085
1121 def _repo(self, branches=None):
1086 def _repo(self, branches=None):
1122 repo = mock.Mock()
1087 repo = mock.Mock()
1123 repo.branches = OrderedDict((name, '0') for name in branches or [])
1088 repo.branches = OrderedDict((name, '0') for name in branches or [])
1124 repo.tags = {}
1089 repo.tags = {}
1125 return repo
1090 return repo
@@ -1,334 +1,317 b''
1
1
2 # Copyright (C) 2010-2023 RhodeCode GmbH
2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 #
3 #
4 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
7 #
7 #
8 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
11 # GNU General Public License for more details.
12 #
12 #
13 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
15 #
16 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19
19
20 import pytest
20 import pytest
21
21
22 from rhodecode.tests import TestController, assert_session_flash, HG_FORK, GIT_FORK
22 from rhodecode.tests import TestController, assert_session_flash, HG_FORK, GIT_FORK
23
23
24 from rhodecode.tests.fixture import Fixture
24 from rhodecode.tests.fixture import Fixture
25 from rhodecode.lib import helpers as h
25 from rhodecode.lib import helpers as h
26
26
27 from rhodecode.model.db import Repository
27 from rhodecode.model.db import Repository
28 from rhodecode.model.repo import RepoModel
28 from rhodecode.model.repo import RepoModel
29 from rhodecode.model.user import UserModel
29 from rhodecode.model.user import UserModel
30 from rhodecode.model.meta import Session
30 from rhodecode.model.meta import Session
31
31 from rhodecode.tests.routes import route_path
32 fixture = Fixture()
33
32
34
33
35 def route_path(name, params=None, **kwargs):
34 fixture = Fixture()
36 import urllib.request
37 import urllib.parse
38 import urllib.error
39
40 base_url = {
41 'repo_summary': '/{repo_name}',
42 'repo_creating_check': '/{repo_name}/repo_creating_check',
43 'repo_fork_new': '/{repo_name}/fork',
44 'repo_fork_create': '/{repo_name}/fork/create',
45 'repo_forks_show_all': '/{repo_name}/forks',
46 'repo_forks_data': '/{repo_name}/forks/data',
47 }[name].format(**kwargs)
48
49 if params:
50 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
51 return base_url
52
35
53
36
54 FORK_NAME = {
37 FORK_NAME = {
55 'hg': HG_FORK,
38 'hg': HG_FORK,
56 'git': GIT_FORK
39 'git': GIT_FORK
57 }
40 }
58
41
59
42
60 @pytest.mark.skip_backends('svn')
43 @pytest.mark.skip_backends('svn')
61 class TestRepoForkViewTests(TestController):
44 class TestRepoForkViewTests(TestController):
62
45
63 def test_show_forks(self, backend, xhr_header):
46 def test_show_forks(self, backend, xhr_header):
64 self.log_user()
47 self.log_user()
65 response = self.app.get(
48 response = self.app.get(
66 route_path('repo_forks_data', repo_name=backend.repo_name),
49 route_path('repo_forks_data', repo_name=backend.repo_name),
67 extra_environ=xhr_header)
50 extra_environ=xhr_header)
68
51
69 assert response.json == {u'data': [], u'draw': None,
52 assert response.json == {u'data': [], u'draw': None,
70 u'recordsFiltered': 0, u'recordsTotal': 0}
53 u'recordsFiltered': 0, u'recordsTotal': 0}
71
54
72 def test_no_permissions_to_fork_page(self, backend, user_util):
55 def test_no_permissions_to_fork_page(self, backend, user_util):
73 user = user_util.create_user(password='qweqwe')
56 user = user_util.create_user(password='qweqwe')
74 user_id = user.user_id
57 user_id = user.user_id
75 self.log_user(user.username, 'qweqwe')
58 self.log_user(user.username, 'qweqwe')
76
59
77 user_model = UserModel()
60 user_model = UserModel()
78 user_model.revoke_perm(user_id, 'hg.fork.repository')
61 user_model.revoke_perm(user_id, 'hg.fork.repository')
79 user_model.grant_perm(user_id, 'hg.fork.none')
62 user_model.grant_perm(user_id, 'hg.fork.none')
80 u = UserModel().get(user_id)
63 u = UserModel().get(user_id)
81 u.inherit_default_permissions = False
64 u.inherit_default_permissions = False
82 Session().commit()
65 Session().commit()
83 # try create a fork
66 # try create a fork
84 self.app.get(
67 self.app.get(
85 route_path('repo_fork_new', repo_name=backend.repo_name),
68 route_path('repo_fork_new', repo_name=backend.repo_name),
86 status=404)
69 status=404)
87
70
88 def test_no_permissions_to_fork_submit(self, backend, csrf_token, user_util):
71 def test_no_permissions_to_fork_submit(self, backend, csrf_token, user_util):
89 user = user_util.create_user(password='qweqwe')
72 user = user_util.create_user(password='qweqwe')
90 user_id = user.user_id
73 user_id = user.user_id
91 self.log_user(user.username, 'qweqwe')
74 self.log_user(user.username, 'qweqwe')
92
75
93 user_model = UserModel()
76 user_model = UserModel()
94 user_model.revoke_perm(user_id, 'hg.fork.repository')
77 user_model.revoke_perm(user_id, 'hg.fork.repository')
95 user_model.grant_perm(user_id, 'hg.fork.none')
78 user_model.grant_perm(user_id, 'hg.fork.none')
96 u = UserModel().get(user_id)
79 u = UserModel().get(user_id)
97 u.inherit_default_permissions = False
80 u.inherit_default_permissions = False
98 Session().commit()
81 Session().commit()
99 # try create a fork
82 # try create a fork
100 self.app.post(
83 self.app.post(
101 route_path('repo_fork_create', repo_name=backend.repo_name),
84 route_path('repo_fork_create', repo_name=backend.repo_name),
102 {'csrf_token': csrf_token},
85 {'csrf_token': csrf_token},
103 status=404)
86 status=404)
104
87
105 def test_fork_missing_data(self, autologin_user, backend, csrf_token):
88 def test_fork_missing_data(self, autologin_user, backend, csrf_token):
106 # try create a fork
89 # try create a fork
107 response = self.app.post(
90 response = self.app.post(
108 route_path('repo_fork_create', repo_name=backend.repo_name),
91 route_path('repo_fork_create', repo_name=backend.repo_name),
109 {'csrf_token': csrf_token},
92 {'csrf_token': csrf_token},
110 status=200)
93 status=200)
111 # test if html fill works fine
94 # test if html fill works fine
112 response.mustcontain('Missing value')
95 response.mustcontain('Missing value')
113
96
114 def test_create_fork_page(self, autologin_user, backend):
97 def test_create_fork_page(self, autologin_user, backend):
115 self.app.get(
98 self.app.get(
116 route_path('repo_fork_new', repo_name=backend.repo_name),
99 route_path('repo_fork_new', repo_name=backend.repo_name),
117 status=200)
100 status=200)
118
101
119 def test_create_and_show_fork(
102 def test_create_and_show_fork(
120 self, autologin_user, backend, csrf_token, xhr_header):
103 self, autologin_user, backend, csrf_token, xhr_header):
121
104
122 # create a fork
105 # create a fork
123 fork_name = FORK_NAME[backend.alias]
106 fork_name = FORK_NAME[backend.alias]
124 description = 'fork of vcs test'
107 description = 'fork of vcs test'
125 repo_name = backend.repo_name
108 repo_name = backend.repo_name
126 source_repo = Repository.get_by_repo_name(repo_name)
109 source_repo = Repository.get_by_repo_name(repo_name)
127 creation_args = {
110 creation_args = {
128 'repo_name': fork_name,
111 'repo_name': fork_name,
129 'repo_group': '',
112 'repo_group': '',
130 'fork_parent_id': source_repo.repo_id,
113 'fork_parent_id': source_repo.repo_id,
131 'repo_type': backend.alias,
114 'repo_type': backend.alias,
132 'description': description,
115 'description': description,
133 'private': 'False',
116 'private': 'False',
134 'csrf_token': csrf_token,
117 'csrf_token': csrf_token,
135 }
118 }
136
119
137 self.app.post(
120 self.app.post(
138 route_path('repo_fork_create', repo_name=repo_name), creation_args)
121 route_path('repo_fork_create', repo_name=repo_name), creation_args)
139
122
140 response = self.app.get(
123 response = self.app.get(
141 route_path('repo_forks_data', repo_name=repo_name),
124 route_path('repo_forks_data', repo_name=repo_name),
142 extra_environ=xhr_header)
125 extra_environ=xhr_header)
143
126
144 assert response.json['data'][0]['fork_name'] == \
127 assert response.json['data'][0]['fork_name'] == \
145 """<a href="/%s">%s</a>""" % (fork_name, fork_name)
128 """<a href="/%s">%s</a>""" % (fork_name, fork_name)
146
129
147 # remove this fork
130 # remove this fork
148 fixture.destroy_repo(fork_name)
131 fixture.destroy_repo(fork_name)
149
132
150 def test_fork_create(self, autologin_user, backend, csrf_token):
133 def test_fork_create(self, autologin_user, backend, csrf_token):
151 fork_name = FORK_NAME[backend.alias]
134 fork_name = FORK_NAME[backend.alias]
152 description = 'fork of vcs test'
135 description = 'fork of vcs test'
153 repo_name = backend.repo_name
136 repo_name = backend.repo_name
154 source_repo = Repository.get_by_repo_name(repo_name)
137 source_repo = Repository.get_by_repo_name(repo_name)
155 creation_args = {
138 creation_args = {
156 'repo_name': fork_name,
139 'repo_name': fork_name,
157 'repo_group': '',
140 'repo_group': '',
158 'fork_parent_id': source_repo.repo_id,
141 'fork_parent_id': source_repo.repo_id,
159 'repo_type': backend.alias,
142 'repo_type': backend.alias,
160 'description': description,
143 'description': description,
161 'private': 'False',
144 'private': 'False',
162 'csrf_token': csrf_token,
145 'csrf_token': csrf_token,
163 }
146 }
164 self.app.post(
147 self.app.post(
165 route_path('repo_fork_create', repo_name=repo_name), creation_args)
148 route_path('repo_fork_create', repo_name=repo_name), creation_args)
166 repo = Repository.get_by_repo_name(FORK_NAME[backend.alias])
149 repo = Repository.get_by_repo_name(FORK_NAME[backend.alias])
167 assert repo.fork.repo_name == backend.repo_name
150 assert repo.fork.repo_name == backend.repo_name
168
151
169 # run the check page that triggers the flash message
152 # run the check page that triggers the flash message
170 response = self.app.get(
153 response = self.app.get(
171 route_path('repo_creating_check', repo_name=fork_name))
154 route_path('repo_creating_check', repo_name=fork_name))
172 # test if we have a message that fork is ok
155 # test if we have a message that fork is ok
173 assert_session_flash(response,
156 assert_session_flash(response,
174 'Forked repository %s as <a href="/%s">%s</a>' % (
157 'Forked repository %s as <a href="/%s">%s</a>' % (
175 repo_name, fork_name, fork_name))
158 repo_name, fork_name, fork_name))
176
159
177 # test if the fork was created in the database
160 # test if the fork was created in the database
178 fork_repo = Session().query(Repository)\
161 fork_repo = Session().query(Repository)\
179 .filter(Repository.repo_name == fork_name).one()
162 .filter(Repository.repo_name == fork_name).one()
180
163
181 assert fork_repo.repo_name == fork_name
164 assert fork_repo.repo_name == fork_name
182 assert fork_repo.fork.repo_name == repo_name
165 assert fork_repo.fork.repo_name == repo_name
183
166
184 # test if the repository is visible in the list ?
167 # test if the repository is visible in the list ?
185 response = self.app.get(
168 response = self.app.get(
186 h.route_path('repo_summary', repo_name=fork_name))
169 h.route_path('repo_summary', repo_name=fork_name))
187 response.mustcontain(fork_name)
170 response.mustcontain(fork_name)
188 response.mustcontain(backend.alias)
171 response.mustcontain(backend.alias)
189 response.mustcontain('Fork of')
172 response.mustcontain('Fork of')
190 response.mustcontain('<a href="/%s">%s</a>' % (repo_name, repo_name))
173 response.mustcontain('<a href="/%s">%s</a>' % (repo_name, repo_name))
191
174
192 def test_fork_create_into_group(self, autologin_user, backend, csrf_token):
175 def test_fork_create_into_group(self, autologin_user, backend, csrf_token):
193 group = fixture.create_repo_group('vc')
176 group = fixture.create_repo_group('vc')
194 group_id = group.group_id
177 group_id = group.group_id
195 fork_name = FORK_NAME[backend.alias]
178 fork_name = FORK_NAME[backend.alias]
196 fork_name_full = 'vc/%s' % fork_name
179 fork_name_full = 'vc/%s' % fork_name
197 description = 'fork of vcs test'
180 description = 'fork of vcs test'
198 repo_name = backend.repo_name
181 repo_name = backend.repo_name
199 source_repo = Repository.get_by_repo_name(repo_name)
182 source_repo = Repository.get_by_repo_name(repo_name)
200 creation_args = {
183 creation_args = {
201 'repo_name': fork_name,
184 'repo_name': fork_name,
202 'repo_group': group_id,
185 'repo_group': group_id,
203 'fork_parent_id': source_repo.repo_id,
186 'fork_parent_id': source_repo.repo_id,
204 'repo_type': backend.alias,
187 'repo_type': backend.alias,
205 'description': description,
188 'description': description,
206 'private': 'False',
189 'private': 'False',
207 'csrf_token': csrf_token,
190 'csrf_token': csrf_token,
208 }
191 }
209 self.app.post(
192 self.app.post(
210 route_path('repo_fork_create', repo_name=repo_name), creation_args)
193 route_path('repo_fork_create', repo_name=repo_name), creation_args)
211 repo = Repository.get_by_repo_name(fork_name_full)
194 repo = Repository.get_by_repo_name(fork_name_full)
212 assert repo.fork.repo_name == backend.repo_name
195 assert repo.fork.repo_name == backend.repo_name
213
196
214 # run the check page that triggers the flash message
197 # run the check page that triggers the flash message
215 response = self.app.get(
198 response = self.app.get(
216 route_path('repo_creating_check', repo_name=fork_name_full))
199 route_path('repo_creating_check', repo_name=fork_name_full))
217 # test if we have a message that fork is ok
200 # test if we have a message that fork is ok
218 assert_session_flash(response,
201 assert_session_flash(response,
219 'Forked repository %s as <a href="/%s">%s</a>' % (
202 'Forked repository %s as <a href="/%s">%s</a>' % (
220 repo_name, fork_name_full, fork_name_full))
203 repo_name, fork_name_full, fork_name_full))
221
204
222 # test if the fork was created in the database
205 # test if the fork was created in the database
223 fork_repo = Session().query(Repository)\
206 fork_repo = Session().query(Repository)\
224 .filter(Repository.repo_name == fork_name_full).one()
207 .filter(Repository.repo_name == fork_name_full).one()
225
208
226 assert fork_repo.repo_name == fork_name_full
209 assert fork_repo.repo_name == fork_name_full
227 assert fork_repo.fork.repo_name == repo_name
210 assert fork_repo.fork.repo_name == repo_name
228
211
229 # test if the repository is visible in the list ?
212 # test if the repository is visible in the list ?
230 response = self.app.get(
213 response = self.app.get(
231 h.route_path('repo_summary', repo_name=fork_name_full))
214 h.route_path('repo_summary', repo_name=fork_name_full))
232 response.mustcontain(fork_name_full)
215 response.mustcontain(fork_name_full)
233 response.mustcontain(backend.alias)
216 response.mustcontain(backend.alias)
234
217
235 response.mustcontain('Fork of')
218 response.mustcontain('Fork of')
236 response.mustcontain('<a href="/%s">%s</a>' % (repo_name, repo_name))
219 response.mustcontain('<a href="/%s">%s</a>' % (repo_name, repo_name))
237
220
238 fixture.destroy_repo(fork_name_full)
221 fixture.destroy_repo(fork_name_full)
239 fixture.destroy_repo_group(group_id)
222 fixture.destroy_repo_group(group_id)
240
223
241 def test_fork_read_permission(self, backend, xhr_header, user_util):
224 def test_fork_read_permission(self, backend, xhr_header, user_util):
242 user = user_util.create_user(password='qweqwe')
225 user = user_util.create_user(password='qweqwe')
243 user_id = user.user_id
226 user_id = user.user_id
244 self.log_user(user.username, 'qweqwe')
227 self.log_user(user.username, 'qweqwe')
245
228
246 # create a fake fork
229 # create a fake fork
247 fork = user_util.create_repo(repo_type=backend.alias)
230 fork = user_util.create_repo(repo_type=backend.alias)
248 source = user_util.create_repo(repo_type=backend.alias)
231 source = user_util.create_repo(repo_type=backend.alias)
249 repo_name = source.repo_name
232 repo_name = source.repo_name
250
233
251 fork.fork_id = source.repo_id
234 fork.fork_id = source.repo_id
252 fork_name = fork.repo_name
235 fork_name = fork.repo_name
253 Session().commit()
236 Session().commit()
254
237
255 forks = Repository.query()\
238 forks = Repository.query()\
256 .filter(Repository.repo_type == backend.alias)\
239 .filter(Repository.repo_type == backend.alias)\
257 .filter(Repository.fork_id == source.repo_id).all()
240 .filter(Repository.fork_id == source.repo_id).all()
258 assert 1 == len(forks)
241 assert 1 == len(forks)
259
242
260 # set read permissions for this
243 # set read permissions for this
261 RepoModel().grant_user_permission(
244 RepoModel().grant_user_permission(
262 repo=forks[0], user=user_id, perm='repository.read')
245 repo=forks[0], user=user_id, perm='repository.read')
263 Session().commit()
246 Session().commit()
264
247
265 response = self.app.get(
248 response = self.app.get(
266 route_path('repo_forks_data', repo_name=repo_name),
249 route_path('repo_forks_data', repo_name=repo_name),
267 extra_environ=xhr_header)
250 extra_environ=xhr_header)
268
251
269 assert response.json['data'][0]['fork_name'] == \
252 assert response.json['data'][0]['fork_name'] == \
270 """<a href="/%s">%s</a>""" % (fork_name, fork_name)
253 """<a href="/%s">%s</a>""" % (fork_name, fork_name)
271
254
272 def test_fork_none_permission(self, backend, xhr_header, user_util):
255 def test_fork_none_permission(self, backend, xhr_header, user_util):
273 user = user_util.create_user(password='qweqwe')
256 user = user_util.create_user(password='qweqwe')
274 user_id = user.user_id
257 user_id = user.user_id
275 self.log_user(user.username, 'qweqwe')
258 self.log_user(user.username, 'qweqwe')
276
259
277 # create a fake fork
260 # create a fake fork
278 fork = user_util.create_repo(repo_type=backend.alias)
261 fork = user_util.create_repo(repo_type=backend.alias)
279 source = user_util.create_repo(repo_type=backend.alias)
262 source = user_util.create_repo(repo_type=backend.alias)
280 repo_name = source.repo_name
263 repo_name = source.repo_name
281
264
282 fork.fork_id = source.repo_id
265 fork.fork_id = source.repo_id
283
266
284 Session().commit()
267 Session().commit()
285
268
286 forks = Repository.query()\
269 forks = Repository.query()\
287 .filter(Repository.repo_type == backend.alias)\
270 .filter(Repository.repo_type == backend.alias)\
288 .filter(Repository.fork_id == source.repo_id).all()
271 .filter(Repository.fork_id == source.repo_id).all()
289 assert 1 == len(forks)
272 assert 1 == len(forks)
290
273
291 # set none
274 # set none
292 RepoModel().grant_user_permission(
275 RepoModel().grant_user_permission(
293 repo=forks[0], user=user_id, perm='repository.none')
276 repo=forks[0], user=user_id, perm='repository.none')
294 Session().commit()
277 Session().commit()
295
278
296 # fork shouldn't be there
279 # fork shouldn't be there
297 response = self.app.get(
280 response = self.app.get(
298 route_path('repo_forks_data', repo_name=repo_name),
281 route_path('repo_forks_data', repo_name=repo_name),
299 extra_environ=xhr_header)
282 extra_environ=xhr_header)
300
283
301 assert response.json == {u'data': [], u'draw': None,
284 assert response.json == {u'data': [], u'draw': None,
302 u'recordsFiltered': 0, u'recordsTotal': 0}
285 u'recordsFiltered': 0, u'recordsTotal': 0}
303
286
304 @pytest.mark.parametrize('url_type', [
287 @pytest.mark.parametrize('url_type', [
305 'repo_fork_new',
288 'repo_fork_new',
306 'repo_fork_create'
289 'repo_fork_create'
307 ])
290 ])
308 def test_fork_is_forbidden_on_archived_repo(self, backend, xhr_header, user_util, url_type):
291 def test_fork_is_forbidden_on_archived_repo(self, backend, xhr_header, user_util, url_type):
309 user = user_util.create_user(password='qweqwe')
292 user = user_util.create_user(password='qweqwe')
310 self.log_user(user.username, 'qweqwe')
293 self.log_user(user.username, 'qweqwe')
311
294
312 # create a temporary repo
295 # create a temporary repo
313 source = user_util.create_repo(repo_type=backend.alias)
296 source = user_util.create_repo(repo_type=backend.alias)
314 repo_name = source.repo_name
297 repo_name = source.repo_name
315 repo = Repository.get_by_repo_name(repo_name)
298 repo = Repository.get_by_repo_name(repo_name)
316 repo.archived = True
299 repo.archived = True
317 Session().commit()
300 Session().commit()
318
301
319 response = self.app.get(
302 response = self.app.get(
320 route_path(url_type, repo_name=repo_name), status=302)
303 route_path(url_type, repo_name=repo_name), status=302)
321
304
322 msg = 'Action not supported for archived repository.'
305 msg = 'Action not supported for archived repository.'
323 assert_session_flash(response, msg)
306 assert_session_flash(response, msg)
324
307
325
308
326 class TestSVNFork(TestController):
309 class TestSVNFork(TestController):
327 @pytest.mark.parametrize('route_name', [
310 @pytest.mark.parametrize('route_name', [
328 'repo_fork_create', 'repo_fork_new'
311 'repo_fork_create', 'repo_fork_new'
329 ])
312 ])
330 def test_fork_redirects(self, autologin_user, backend_svn, route_name):
313 def test_fork_redirects(self, autologin_user, backend_svn, route_name):
331
314
332 self.app.get(route_path(
315 self.app.get(route_path(
333 route_name, repo_name=backend_svn.repo_name),
316 route_name, repo_name=backend_svn.repo_name),
334 status=404)
317 status=404)
@@ -1,150 +1,134 b''
1
1
2 # Copyright (C) 2010-2023 RhodeCode GmbH
2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 #
3 #
4 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
7 #
7 #
8 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
11 # GNU General Public License for more details.
12 #
12 #
13 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
15 #
16 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19
19
20 import pytest
20 import pytest
21
21
22 from rhodecode.lib.hash_utils import md5_safe
22 from rhodecode.lib.hash_utils import md5_safe
23 from rhodecode.model.db import Repository
23 from rhodecode.model.db import Repository
24 from rhodecode.model.meta import Session
24 from rhodecode.model.meta import Session
25 from rhodecode.model.settings import SettingsModel, IssueTrackerSettingsModel
25 from rhodecode.model.settings import SettingsModel, IssueTrackerSettingsModel
26
26 from rhodecode.tests.routes import route_path
27
28 def route_path(name, params=None, **kwargs):
29 import urllib.request
30 import urllib.parse
31 import urllib.error
32
27
33 base_url = {
34 'repo_summary': '/{repo_name}',
35 'edit_repo_issuetracker': '/{repo_name}/settings/issue_trackers',
36 'edit_repo_issuetracker_test': '/{repo_name}/settings/issue_trackers/test',
37 'edit_repo_issuetracker_delete': '/{repo_name}/settings/issue_trackers/delete',
38 'edit_repo_issuetracker_update': '/{repo_name}/settings/issue_trackers/update',
39 }[name].format(**kwargs)
40
41 if params:
42 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
43 return base_url
44
28
45
29
46 @pytest.mark.usefixtures("app")
30 @pytest.mark.usefixtures("app")
47 class TestRepoIssueTracker(object):
31 class TestRepoIssueTracker(object):
48 def test_issuetracker_index(self, autologin_user, backend):
32 def test_issuetracker_index(self, autologin_user, backend):
49 repo = backend.create_repo()
33 repo = backend.create_repo()
50 response = self.app.get(route_path('edit_repo_issuetracker',
34 response = self.app.get(route_path('edit_repo_issuetracker',
51 repo_name=repo.repo_name))
35 repo_name=repo.repo_name))
52 assert response.status_code == 200
36 assert response.status_code == 200
53
37
54 def test_add_and_test_issuetracker_patterns(
38 def test_add_and_test_issuetracker_patterns(
55 self, autologin_user, backend, csrf_token, request, xhr_header):
39 self, autologin_user, backend, csrf_token, request, xhr_header):
56 pattern = 'issuetracker_pat'
40 pattern = 'issuetracker_pat'
57 another_pattern = pattern+'1'
41 another_pattern = pattern+'1'
58 post_url = route_path(
42 post_url = route_path(
59 'edit_repo_issuetracker_update', repo_name=backend.repo.repo_name)
43 'edit_repo_issuetracker_update', repo_name=backend.repo.repo_name)
60 post_data = {
44 post_data = {
61 'new_pattern_pattern_0': pattern,
45 'new_pattern_pattern_0': pattern,
62 'new_pattern_url_0': 'http://url',
46 'new_pattern_url_0': 'http://url',
63 'new_pattern_prefix_0': 'prefix',
47 'new_pattern_prefix_0': 'prefix',
64 'new_pattern_description_0': 'description',
48 'new_pattern_description_0': 'description',
65 'new_pattern_pattern_1': another_pattern,
49 'new_pattern_pattern_1': another_pattern,
66 'new_pattern_url_1': '/url1',
50 'new_pattern_url_1': '/url1',
67 'new_pattern_prefix_1': 'prefix1',
51 'new_pattern_prefix_1': 'prefix1',
68 'new_pattern_description_1': 'description1',
52 'new_pattern_description_1': 'description1',
69 'csrf_token': csrf_token
53 'csrf_token': csrf_token
70 }
54 }
71 self.app.post(post_url, post_data, status=302)
55 self.app.post(post_url, post_data, status=302)
72 self.settings_model = IssueTrackerSettingsModel(repo=backend.repo)
56 self.settings_model = IssueTrackerSettingsModel(repo=backend.repo)
73 settings = self.settings_model.get_repo_settings()
57 settings = self.settings_model.get_repo_settings()
74 self.uid = md5_safe(pattern)
58 self.uid = md5_safe(pattern)
75 assert settings[self.uid]['pat'] == pattern
59 assert settings[self.uid]['pat'] == pattern
76 self.another_uid = md5_safe(another_pattern)
60 self.another_uid = md5_safe(another_pattern)
77 assert settings[self.another_uid]['pat'] == another_pattern
61 assert settings[self.another_uid]['pat'] == another_pattern
78
62
79 # test pattern
63 # test pattern
80 data = {'test_text': 'example of issuetracker_pat replacement',
64 data = {'test_text': 'example of issuetracker_pat replacement',
81 'csrf_token': csrf_token}
65 'csrf_token': csrf_token}
82 response = self.app.post(
66 response = self.app.post(
83 route_path('edit_repo_issuetracker_test',
67 route_path('edit_repo_issuetracker_test',
84 repo_name=backend.repo.repo_name),
68 repo_name=backend.repo.repo_name),
85 extra_environ=xhr_header, params=data)
69 extra_environ=xhr_header, params=data)
86
70
87 assert response.text == \
71 assert response.text == \
88 'example of <a class="tooltip issue-tracker-link" href="http://url" title="description">prefix</a> replacement'
72 'example of <a class="tooltip issue-tracker-link" href="http://url" title="description">prefix</a> replacement'
89
73
90 @request.addfinalizer
74 @request.addfinalizer
91 def cleanup():
75 def cleanup():
92 self.settings_model.delete_entries(self.uid)
76 self.settings_model.delete_entries(self.uid)
93 self.settings_model.delete_entries(self.another_uid)
77 self.settings_model.delete_entries(self.another_uid)
94
78
95 def test_edit_issuetracker_pattern(
79 def test_edit_issuetracker_pattern(
96 self, autologin_user, backend, csrf_token, request):
80 self, autologin_user, backend, csrf_token, request):
97 entry_key = 'issuetracker_pat_'
81 entry_key = 'issuetracker_pat_'
98 pattern = 'issuetracker_pat2'
82 pattern = 'issuetracker_pat2'
99 old_pattern = 'issuetracker_pat'
83 old_pattern = 'issuetracker_pat'
100 old_uid = md5_safe(old_pattern)
84 old_uid = md5_safe(old_pattern)
101
85
102 sett = SettingsModel(repo=backend.repo).create_or_update_setting(
86 sett = SettingsModel(repo=backend.repo).create_or_update_setting(
103 entry_key+old_uid, old_pattern, 'unicode')
87 entry_key+old_uid, old_pattern, 'unicode')
104 Session().add(sett)
88 Session().add(sett)
105 Session().commit()
89 Session().commit()
106 post_url = route_path(
90 post_url = route_path(
107 'edit_repo_issuetracker_update', repo_name=backend.repo.repo_name)
91 'edit_repo_issuetracker_update', repo_name=backend.repo.repo_name)
108 post_data = {
92 post_data = {
109 'new_pattern_pattern_0': pattern,
93 'new_pattern_pattern_0': pattern,
110 'new_pattern_url_0': '/url',
94 'new_pattern_url_0': '/url',
111 'new_pattern_prefix_0': 'prefix',
95 'new_pattern_prefix_0': 'prefix',
112 'new_pattern_description_0': 'description',
96 'new_pattern_description_0': 'description',
113 'uid': old_uid,
97 'uid': old_uid,
114 'csrf_token': csrf_token
98 'csrf_token': csrf_token
115 }
99 }
116 self.app.post(post_url, post_data, status=302)
100 self.app.post(post_url, post_data, status=302)
117 self.settings_model = IssueTrackerSettingsModel(repo=backend.repo)
101 self.settings_model = IssueTrackerSettingsModel(repo=backend.repo)
118 settings = self.settings_model.get_repo_settings()
102 settings = self.settings_model.get_repo_settings()
119 self.uid = md5_safe(pattern)
103 self.uid = md5_safe(pattern)
120 assert settings[self.uid]['pat'] == pattern
104 assert settings[self.uid]['pat'] == pattern
121 with pytest.raises(KeyError):
105 with pytest.raises(KeyError):
122 key = settings[old_uid]
106 key = settings[old_uid]
123
107
124 @request.addfinalizer
108 @request.addfinalizer
125 def cleanup():
109 def cleanup():
126 self.settings_model.delete_entries(self.uid)
110 self.settings_model.delete_entries(self.uid)
127
111
128 def test_delete_issuetracker_pattern(
112 def test_delete_issuetracker_pattern(
129 self, autologin_user, backend, csrf_token, settings_util, xhr_header):
113 self, autologin_user, backend, csrf_token, settings_util, xhr_header):
130 repo = backend.create_repo()
114 repo = backend.create_repo()
131 repo_name = repo.repo_name
115 repo_name = repo.repo_name
132 entry_key = 'issuetracker_pat_'
116 entry_key = 'issuetracker_pat_'
133 pattern = 'issuetracker_pat3'
117 pattern = 'issuetracker_pat3'
134 uid = md5_safe(pattern)
118 uid = md5_safe(pattern)
135 settings_util.create_repo_rhodecode_setting(
119 settings_util.create_repo_rhodecode_setting(
136 repo=backend.repo, name=entry_key+uid,
120 repo=backend.repo, name=entry_key+uid,
137 value=entry_key, type_='unicode', cleanup=False)
121 value=entry_key, type_='unicode', cleanup=False)
138
122
139 self.app.post(
123 self.app.post(
140 route_path(
124 route_path(
141 'edit_repo_issuetracker_delete',
125 'edit_repo_issuetracker_delete',
142 repo_name=backend.repo.repo_name),
126 repo_name=backend.repo.repo_name),
143 {
127 {
144 'uid': uid,
128 'uid': uid,
145 'csrf_token': csrf_token,
129 'csrf_token': csrf_token,
146 '': ''
130 '': ''
147 }, extra_environ=xhr_header, status=200)
131 }, extra_environ=xhr_header, status=200)
148 settings = IssueTrackerSettingsModel(
132 settings = IssueTrackerSettingsModel(
149 repo=Repository.get_by_repo_name(repo_name)).get_repo_settings()
133 repo=Repository.get_by_repo_name(repo_name)).get_repo_settings()
150 assert 'rhodecode_%s%s' % (entry_key, uid) not in settings
134 assert 'rhodecode_%s%s' % (entry_key, uid) not in settings
@@ -1,75 +1,55 b''
1
1
2 # Copyright (C) 2010-2023 RhodeCode GmbH
2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 #
3 #
4 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
7 #
7 #
8 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
11 # GNU General Public License for more details.
12 #
12 #
13 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
15 #
16 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19
19
20 import mock
21 import pytest
20 import pytest
22
21
23 from rhodecode.lib.utils2 import str2bool
24 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
25 from rhodecode.model.db import Repository, UserRepoToPerm, Permission, User
22 from rhodecode.model.db import Repository, UserRepoToPerm, Permission, User
26 from rhodecode.model.meta import Session
23
27 from rhodecode.tests import (
28 TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN, assert_session_flash)
29 from rhodecode.tests.fixture import Fixture
24 from rhodecode.tests.fixture import Fixture
25 from rhodecode.tests.routes import route_path
30
26
31 fixture = Fixture()
27 fixture = Fixture()
32
28
33
29
34 def route_path(name, params=None, **kwargs):
35 import urllib.request
36 import urllib.parse
37 import urllib.error
38
39 base_url = {
40 'edit_repo_maintenance': '/{repo_name}/settings/maintenance',
41 'edit_repo_maintenance_execute': '/{repo_name}/settings/maintenance/execute',
42
43 }[name].format(**kwargs)
44
45 if params:
46 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
47 return base_url
48
49
50 def _get_permission_for_user(user, repo):
30 def _get_permission_for_user(user, repo):
51 perm = UserRepoToPerm.query()\
31 perm = UserRepoToPerm.query()\
52 .filter(UserRepoToPerm.repository ==
32 .filter(UserRepoToPerm.repository ==
53 Repository.get_by_repo_name(repo))\
33 Repository.get_by_repo_name(repo))\
54 .filter(UserRepoToPerm.user == User.get_by_username(user))\
34 .filter(UserRepoToPerm.user == User.get_by_username(user))\
55 .all()
35 .all()
56 return perm
36 return perm
57
37
58
38
59 @pytest.mark.usefixtures('autologin_user', 'app')
39 @pytest.mark.usefixtures('autologin_user', 'app')
60 class TestAdminRepoMaintenance(object):
40 class TestAdminRepoMaintenance(object):
61 @pytest.mark.parametrize('urlname', [
41 @pytest.mark.parametrize('urlname', [
62 'edit_repo_maintenance',
42 'edit_repo_maintenance',
63 ])
43 ])
64 def test_show_page(self, urlname, app, backend):
44 def test_show_page(self, urlname, app, backend):
65 app.get(route_path(urlname, repo_name=backend.repo_name), status=200)
45 app.get(route_path(urlname, repo_name=backend.repo_name), status=200)
66
46
67 def test_execute_maintenance_for_repo_hg(self, app, backend_hg, autologin_user, xhr_header):
47 def test_execute_maintenance_for_repo_hg(self, app, backend_hg, autologin_user, xhr_header):
68 repo_name = backend_hg.repo_name
48 repo_name = backend_hg.repo_name
69
49
70 response = app.get(
50 response = app.get(
71 route_path('edit_repo_maintenance_execute',
51 route_path('edit_repo_maintenance_execute',
72 repo_name=repo_name,),
52 repo_name=repo_name,),
73 extra_environ=xhr_header)
53 extra_environ=xhr_header)
74
54
75 assert "HG Verify repo" in ''.join(response.json)
55 assert "HG Verify repo" in ''.join(response.json)
@@ -1,78 +1,64 b''
1
1
2 # Copyright (C) 2010-2023 RhodeCode GmbH
2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 #
3 #
4 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
7 #
7 #
8 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
11 # GNU General Public License for more details.
12 #
12 #
13 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
15 #
16 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19
19
20 import pytest
20 import pytest
21
21
22 from rhodecode.tests.utils import permission_update_data_generator
22 from rhodecode.tests.utils import permission_update_data_generator
23
23 from rhodecode.tests.routes import route_path
24
25 def route_path(name, params=None, **kwargs):
26 import urllib.request
27 import urllib.parse
28 import urllib.error
29
30 base_url = {
31 'edit_repo_perms': '/{repo_name}/settings/permissions'
32 # update is the same url
33 }[name].format(**kwargs)
34
35 if params:
36 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
37 return base_url
38
24
39
25
40 @pytest.mark.usefixtures("app")
26 @pytest.mark.usefixtures("app")
41 class TestRepoPermissionsView(object):
27 class TestRepoPermissionsView(object):
42
28
43 def test_edit_perms_view(self, user_util, autologin_user):
29 def test_edit_perms_view(self, user_util, autologin_user):
44 repo = user_util.create_repo()
30 repo = user_util.create_repo()
45 self.app.get(
31 self.app.get(
46 route_path('edit_repo_perms',
32 route_path('edit_repo_perms',
47 repo_name=repo.repo_name), status=200)
33 repo_name=repo.repo_name), status=200)
48
34
49 def test_update_permissions(self, csrf_token, user_util):
35 def test_update_permissions(self, csrf_token, user_util):
50 repo = user_util.create_repo()
36 repo = user_util.create_repo()
51 repo_name = repo.repo_name
37 repo_name = repo.repo_name
52 user = user_util.create_user()
38 user = user_util.create_user()
53 user_id = user.user_id
39 user_id = user.user_id
54 username = user.username
40 username = user.username
55
41
56 # grant new
42 # grant new
57 form_data = permission_update_data_generator(
43 form_data = permission_update_data_generator(
58 csrf_token,
44 csrf_token,
59 default='repository.write',
45 default='repository.write',
60 grant=[(user_id, 'repository.write', username, 'user')])
46 grant=[(user_id, 'repository.write', username, 'user')])
61
47
62 response = self.app.post(
48 response = self.app.post(
63 route_path('edit_repo_perms',
49 route_path('edit_repo_perms',
64 repo_name=repo_name), form_data).follow()
50 repo_name=repo_name), form_data).follow()
65
51
66 assert 'Repository access permissions updated' in response
52 assert 'Repository access permissions updated' in response
67
53
68 # revoke given
54 # revoke given
69 form_data = permission_update_data_generator(
55 form_data = permission_update_data_generator(
70 csrf_token,
56 csrf_token,
71 default='repository.read',
57 default='repository.read',
72 revoke=[(user_id, 'user')])
58 revoke=[(user_id, 'user')])
73
59
74 response = self.app.post(
60 response = self.app.post(
75 route_path('edit_repo_perms',
61 route_path('edit_repo_perms',
76 repo_name=repo_name), form_data).follow()
62 repo_name=repo_name), form_data).follow()
77
63
78 assert 'Repository access permissions updated' in response
64 assert 'Repository access permissions updated' in response
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
General Comments 0
You need to be logged in to leave comments. Login now