##// 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
The requested commit or file is too big and content was truncated. Show full diff
@@ -1,574 +1,573 b''
1 1 # Copyright (C) 2011-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import itertools
20 20 import logging
21 21 import sys
22 22 import fnmatch
23 23
24 24 import decorator
25 25 import typing
26 26 import venusian
27 27 from collections import OrderedDict
28 28
29 29 from pyramid.exceptions import ConfigurationError
30 30 from pyramid.renderers import render
31 31 from pyramid.response import Response
32 32 from pyramid.httpexceptions import HTTPNotFound
33 33
34 34 from rhodecode.api.exc import (
35 35 JSONRPCBaseError, JSONRPCError, JSONRPCForbidden, JSONRPCValidationError)
36 36 from rhodecode.apps._base import TemplateArgs
37 37 from rhodecode.lib.auth import AuthUser
38 38 from rhodecode.lib.base import get_ip_addr, attach_context_attributes
39 39 from rhodecode.lib.exc_tracking import store_exception
40 40 from rhodecode.lib import ext_json
41 41 from rhodecode.lib.utils2 import safe_str
42 42 from rhodecode.lib.plugins.utils import get_plugin_settings
43 43 from rhodecode.model.db import User, UserApiKeys
44 44
45 45 log = logging.getLogger(__name__)
46 46
47 47 DEFAULT_RENDERER = 'jsonrpc_renderer'
48 48 DEFAULT_URL = '/_admin/apiv2'
49 49
50 50
51 51 def find_methods(jsonrpc_methods, pattern):
52 52 matches = OrderedDict()
53 53 if not isinstance(pattern, (list, tuple)):
54 54 pattern = [pattern]
55 55
56 56 for single_pattern in pattern:
57 57 for method_name, method in jsonrpc_methods.items():
58 58 if fnmatch.fnmatch(method_name, single_pattern):
59 59 matches[method_name] = method
60 60 return matches
61 61
62 62
63 63 class ExtJsonRenderer(object):
64 64 """
65 65 Custom renderer that makes use of our ext_json lib
66 66
67 67 """
68 68
69 69 def __init__(self):
70 70 self.serializer = ext_json.formatted_json
71 71
72 72 def __call__(self, info):
73 73 """ Returns a plain JSON-encoded string with content-type
74 74 ``application/json``. The content-type may be overridden by
75 75 setting ``request.response.content_type``."""
76 76
77 77 def _render(value, system):
78 78 request = system.get('request')
79 79 if request is not None:
80 80 response = request.response
81 81 ct = response.content_type
82 82 if ct == response.default_content_type:
83 83 response.content_type = 'application/json'
84 84
85 85 return self.serializer(value)
86 86
87 87 return _render
88 88
89 89
90 90 def jsonrpc_response(request, result):
91 91 rpc_id = getattr(request, 'rpc_id', None)
92 92
93 93 ret_value = ''
94 94 if rpc_id:
95 95 ret_value = {'id': rpc_id, 'result': result, 'error': None}
96 96
97 97 # fetch deprecation warnings, and store it inside results
98 98 deprecation = getattr(request, 'rpc_deprecation', None)
99 99 if deprecation:
100 100 ret_value['DEPRECATION_WARNING'] = deprecation
101 101
102 102 raw_body = render(DEFAULT_RENDERER, ret_value, request=request)
103 103 content_type = 'application/json'
104 104 content_type_header = 'Content-Type'
105 105 headers = {
106 106 content_type_header: content_type
107 107 }
108 108 return Response(
109 109 body=raw_body,
110 110 content_type=content_type,
111 111 headerlist=[(k, v) for k, v in headers.items()]
112 112 )
113 113
114 114
115 115 def jsonrpc_error(request, message, retid=None, code: int | None = None, headers: dict | None = None):
116 116 """
117 117 Generate a Response object with a JSON-RPC error body
118 118 """
119 119 headers = headers or {}
120 120 content_type = 'application/json'
121 121 content_type_header = 'Content-Type'
122 122 if content_type_header not in headers:
123 123 headers[content_type_header] = content_type
124 124
125 125 err_dict = {'id': retid, 'result': None, 'error': message}
126 126 raw_body = render(DEFAULT_RENDERER, err_dict, request=request)
127 127
128 128 return Response(
129 129 body=raw_body,
130 130 status=code,
131 131 content_type=content_type,
132 132 headerlist=[(k, v) for k, v in headers.items()]
133 133 )
134 134
135 135
136 136 def exception_view(exc, request):
137 137 rpc_id = getattr(request, 'rpc_id', None)
138 138
139 139 if isinstance(exc, JSONRPCError):
140 140 fault_message = safe_str(exc)
141 141 log.debug('json-rpc error rpc_id:%s "%s"', rpc_id, fault_message)
142 142 elif isinstance(exc, JSONRPCValidationError):
143 143 colander_exc = exc.colander_exception
144 144 # TODO(marcink): think maybe of nicer way to serialize errors ?
145 145 fault_message = colander_exc.asdict()
146 146 log.debug('json-rpc colander error rpc_id:%s "%s"', rpc_id, fault_message)
147 147 elif isinstance(exc, JSONRPCForbidden):
148 148 fault_message = 'Access was denied to this resource.'
149 149 log.warning('json-rpc forbidden call rpc_id:%s "%s"', rpc_id, fault_message)
150 150 elif isinstance(exc, HTTPNotFound):
151 151 method = request.rpc_method
152 152 log.debug('json-rpc method `%s` not found in list of '
153 153 'api calls: %s, rpc_id:%s',
154 154 method, list(request.registry.jsonrpc_methods.keys()), rpc_id)
155 155
156 156 similar = 'none'
157 157 try:
158 158 similar_paterns = [f'*{x}*' for x in method.split('_')]
159 159 similar_found = find_methods(
160 160 request.registry.jsonrpc_methods, similar_paterns)
161 161 similar = ', '.join(similar_found.keys()) or similar
162 162 except Exception:
163 163 # make the whole above block safe
164 164 pass
165 165
166 fault_message = "No such method: {}. Similar methods: {}".format(
167 method, similar)
166 fault_message = f"No such method: {method}. Similar methods: {similar}"
168 167 else:
169 168 fault_message = 'undefined error'
170 169 exc_info = exc.exc_info()
171 170 store_exception(id(exc_info), exc_info, prefix='rhodecode-api')
172 171
173 172 statsd = request.registry.statsd
174 173 if statsd:
175 174 exc_type = f"{exc.__class__.__module__}.{exc.__class__.__name__}"
176 175 statsd.incr('rhodecode_exception_total',
177 176 tags=["exc_source:api", f"type:{exc_type}"])
178 177
179 178 return jsonrpc_error(request, fault_message, rpc_id)
180 179
181 180
182 181 def request_view(request):
183 182 """
184 183 Main request handling method. It handles all logic to call a specific
185 184 exposed method
186 185 """
187 186 # cython compatible inspect
188 187 from rhodecode.config.patches import inspect_getargspec
189 188 inspect = inspect_getargspec()
190 189
191 190 # check if we can find this session using api_key, get_by_auth_token
192 191 # search not expired tokens only
193 192 try:
194 193 api_user = User.get_by_auth_token(request.rpc_api_key)
195 194
196 195 if api_user is None:
197 196 return jsonrpc_error(
198 197 request, retid=request.rpc_id, message='Invalid API KEY')
199 198
200 199 if not api_user.active:
201 200 return jsonrpc_error(
202 201 request, retid=request.rpc_id,
203 202 message='Request from this user not allowed')
204 203
205 204 # check if we are allowed to use this IP
206 205 auth_u = AuthUser(
207 206 api_user.user_id, request.rpc_api_key, ip_addr=request.rpc_ip_addr)
208 207 if not auth_u.ip_allowed:
209 208 return jsonrpc_error(
210 209 request, retid=request.rpc_id,
211 210 message='Request from IP:{} not allowed'.format(
212 211 request.rpc_ip_addr))
213 212 else:
214 213 log.info('Access for IP:%s allowed', request.rpc_ip_addr)
215 214
216 215 # register our auth-user
217 216 request.rpc_user = auth_u
218 217 request.environ['rc_auth_user_id'] = str(auth_u.user_id)
219 218
220 219 # now check if token is valid for API
221 220 auth_token = request.rpc_api_key
222 221 token_match = api_user.authenticate_by_token(
223 222 auth_token, roles=[UserApiKeys.ROLE_API])
224 223 invalid_token = not token_match
225 224
226 225 log.debug('Checking if API KEY is valid with proper role')
227 226 if invalid_token:
228 227 return jsonrpc_error(
229 228 request, retid=request.rpc_id,
230 229 message='API KEY invalid or, has bad role for an API call')
231 230
232 231 except Exception:
233 232 log.exception('Error on API AUTH')
234 233 return jsonrpc_error(
235 234 request, retid=request.rpc_id, message='Invalid API KEY')
236 235
237 236 method = request.rpc_method
238 237 func = request.registry.jsonrpc_methods[method]
239 238
240 239 # now that we have a method, add request._req_params to
241 240 # self.kargs and dispatch control to WGIController
242 241
243 242 argspec = inspect.getargspec(func)
244 243 arglist = argspec[0]
245 244 defs = argspec[3] or []
246 245 defaults = [type(a) for a in defs]
247 246 default_empty = type(NotImplemented)
248 247
249 248 # kw arguments required by this method
250 249 func_kwargs = dict(itertools.zip_longest(
251 250 reversed(arglist), reversed(defaults), fillvalue=default_empty))
252 251
253 252 # This attribute will need to be first param of a method that uses
254 253 # api_key, which is translated to instance of user at that name
255 254 user_var = 'apiuser'
256 255 request_var = 'request'
257 256
258 257 for arg in [user_var, request_var]:
259 258 if arg not in arglist:
260 259 return jsonrpc_error(
261 260 request,
262 261 retid=request.rpc_id,
263 262 message='This method [%s] does not support '
264 263 'required parameter `%s`' % (func.__name__, arg))
265 264
266 265 # get our arglist and check if we provided them as args
267 266 for arg, default in func_kwargs.items():
268 267 if arg in [user_var, request_var]:
269 268 # user_var and request_var are pre-hardcoded parameters and we
270 269 # don't need to do any translation
271 270 continue
272 271
273 272 # skip the required param check if it's default value is
274 273 # NotImplementedType (default_empty)
275 274 if default == default_empty and arg not in request.rpc_params:
276 275 return jsonrpc_error(
277 276 request,
278 277 retid=request.rpc_id,
279 278 message=('Missing non optional `%s` arg in JSON DATA' % arg)
280 279 )
281 280
282 281 # sanitize extra passed arguments
283 282 for k in list(request.rpc_params.keys()):
284 283 if k not in func_kwargs:
285 284 del request.rpc_params[k]
286 285
287 286 call_params = request.rpc_params
288 287 call_params.update({
289 288 'request': request,
290 289 'apiuser': auth_u
291 290 })
292 291
293 292 # register some common functions for usage
294 293 attach_context_attributes(TemplateArgs(), request, request.rpc_user.user_id)
295 294
296 295 statsd = request.registry.statsd
297 296
298 297 try:
299 298 ret_value = func(**call_params)
300 299 resp = jsonrpc_response(request, ret_value)
301 300 if statsd:
302 301 statsd.incr('rhodecode_api_call_success_total')
303 302 return resp
304 303 except JSONRPCBaseError:
305 304 raise
306 305 except Exception:
307 306 log.exception('Unhandled exception occurred on api call: %s', func)
308 307 exc_info = sys.exc_info()
309 308 exc_id, exc_type_name = store_exception(
310 309 id(exc_info), exc_info, prefix='rhodecode-api')
311 310 error_headers = {
312 311 'RhodeCode-Exception-Id': str(exc_id),
313 312 'RhodeCode-Exception-Type': str(exc_type_name)
314 313 }
315 314 err_resp = jsonrpc_error(
316 315 request, retid=request.rpc_id, message='Internal server error',
317 316 headers=error_headers)
318 317 if statsd:
319 318 statsd.incr('rhodecode_api_call_fail_total')
320 319 return err_resp
321 320
322 321
323 322 def setup_request(request):
324 323 """
325 324 Parse a JSON-RPC request body. It's used inside the predicates method
326 325 to validate and bootstrap requests for usage in rpc calls.
327 326
328 327 We need to raise JSONRPCError here if we want to return some errors back to
329 328 user.
330 329 """
331 330
332 331 log.debug('Executing setup request: %r', request)
333 332 request.rpc_ip_addr = get_ip_addr(request.environ)
334 333 # TODO(marcink): deprecate GET at some point
335 334 if request.method not in ['POST', 'GET']:
336 335 log.debug('unsupported request method "%s"', request.method)
337 336 raise JSONRPCError(
338 337 'unsupported request method "%s". Please use POST' % request.method)
339 338
340 339 if 'CONTENT_LENGTH' not in request.environ:
341 340 log.debug("No Content-Length")
342 341 raise JSONRPCError("Empty body, No Content-Length in request")
343 342
344 343 else:
345 344 length = request.environ['CONTENT_LENGTH']
346 345 log.debug('Content-Length: %s', length)
347 346
348 347 if length == 0:
349 348 log.debug("Content-Length is 0")
350 349 raise JSONRPCError("Content-Length is 0")
351 350
352 351 raw_body = request.body
353 352 log.debug("Loading JSON body now")
354 353 try:
355 354 json_body = ext_json.json.loads(raw_body)
356 355 except ValueError as e:
357 356 # catch JSON errors Here
358 357 raise JSONRPCError(f"JSON parse error ERR:{e} RAW:{raw_body!r}")
359 358
360 359 request.rpc_id = json_body.get('id')
361 360 request.rpc_method = json_body.get('method')
362 361
363 362 # check required base parameters
364 363 try:
365 364 api_key = json_body.get('api_key')
366 365 if not api_key:
367 366 api_key = json_body.get('auth_token')
368 367
369 368 if not api_key:
370 369 raise KeyError('api_key or auth_token')
371 370
372 371 # TODO(marcink): support passing in token in request header
373 372
374 373 request.rpc_api_key = api_key
375 374 request.rpc_id = json_body['id']
376 375 request.rpc_method = json_body['method']
377 376 request.rpc_params = json_body['args'] \
378 377 if isinstance(json_body['args'], dict) else {}
379 378
380 379 log.debug('method: %s, params: %.10240r', request.rpc_method, request.rpc_params)
381 380 except KeyError as e:
382 381 raise JSONRPCError(f'Incorrect JSON data. Missing {e}')
383 382
384 383 log.debug('setup complete, now handling method:%s rpcid:%s',
385 384 request.rpc_method, request.rpc_id, )
386 385
387 386
388 387 class RoutePredicate(object):
389 388 def __init__(self, val, config):
390 389 self.val = val
391 390
392 391 def text(self):
393 392 return f'jsonrpc route = {self.val}'
394 393
395 394 phash = text
396 395
397 396 def __call__(self, info, request):
398 397 if self.val:
399 398 # potentially setup and bootstrap our call
400 399 setup_request(request)
401 400
402 401 # Always return True so that even if it isn't a valid RPC it
403 402 # will fall through to the underlaying handlers like notfound_view
404 403 return True
405 404
406 405
407 406 class NotFoundPredicate(object):
408 407 def __init__(self, val, config):
409 408 self.val = val
410 409 self.methods = config.registry.jsonrpc_methods
411 410
412 411 def text(self):
413 412 return f'jsonrpc method not found = {self.val}'
414 413
415 414 phash = text
416 415
417 416 def __call__(self, info, request):
418 417 return hasattr(request, 'rpc_method')
419 418
420 419
421 420 class MethodPredicate(object):
422 421 def __init__(self, val, config):
423 422 self.method = val
424 423
425 424 def text(self):
426 425 return f'jsonrpc method = {self.method}'
427 426
428 427 phash = text
429 428
430 429 def __call__(self, context, request):
431 430 # we need to explicitly return False here, so pyramid doesn't try to
432 431 # execute our view directly. We need our main handler to execute things
433 432 return getattr(request, 'rpc_method') == self.method
434 433
435 434
436 435 def add_jsonrpc_method(config, view, **kwargs):
437 436 # pop the method name
438 437 method = kwargs.pop('method', None)
439 438
440 439 if method is None:
441 440 raise ConfigurationError(
442 441 'Cannot register a JSON-RPC method without specifying the "method"')
443 442
444 443 # we define custom predicate, to enable to detect conflicting methods,
445 444 # those predicates are kind of "translation" from the decorator variables
446 445 # to internal predicates names
447 446
448 447 kwargs['jsonrpc_method'] = method
449 448
450 449 # register our view into global view store for validation
451 450 config.registry.jsonrpc_methods[method] = view
452 451
453 452 # we're using our main request_view handler, here, so each method
454 453 # has a unified handler for itself
455 454 config.add_view(request_view, route_name='apiv2', **kwargs)
456 455
457 456
458 457 class jsonrpc_method(object):
459 458 """
460 459 decorator that works similar to @add_view_config decorator,
461 460 but tailored for our JSON RPC
462 461 """
463 462
464 463 venusian = venusian # for testing injection
465 464
466 465 def __init__(self, method=None, **kwargs):
467 466 self.method = method
468 467 self.kwargs = kwargs
469 468
470 469 def __call__(self, wrapped):
471 470 kwargs = self.kwargs.copy()
472 471 kwargs['method'] = self.method or wrapped.__name__
473 472 depth = kwargs.pop('_depth', 0)
474 473
475 474 def callback(context, name, ob):
476 475 config = context.config.with_package(info.module)
477 476 config.add_jsonrpc_method(view=ob, **kwargs)
478 477
479 478 info = venusian.attach(wrapped, callback, category='pyramid',
480 479 depth=depth + 1)
481 480 if info.scope == 'class':
482 481 # ensure that attr is set if decorating a class method
483 482 kwargs.setdefault('attr', wrapped.__name__)
484 483
485 484 kwargs['_info'] = info.codeinfo # fbo action_method
486 485 return wrapped
487 486
488 487
489 488 class jsonrpc_deprecated_method(object):
490 489 """
491 490 Marks method as deprecated, adds log.warning, and inject special key to
492 491 the request variable to mark method as deprecated.
493 492 Also injects special docstring that extract_docs will catch to mark
494 493 method as deprecated.
495 494
496 495 :param use_method: specify which method should be used instead of
497 496 the decorated one
498 497
499 498 Use like::
500 499
501 500 @jsonrpc_method()
502 501 @jsonrpc_deprecated_method(use_method='new_func', deprecated_at_version='3.0.0')
503 502 def old_func(request, apiuser, arg1, arg2):
504 503 ...
505 504 """
506 505
507 506 def __init__(self, use_method, deprecated_at_version):
508 507 self.use_method = use_method
509 508 self.deprecated_at_version = deprecated_at_version
510 509 self.deprecated_msg = ''
511 510
512 511 def __call__(self, func):
513 512 self.deprecated_msg = 'Please use method `{method}` instead.'.format(
514 513 method=self.use_method)
515 514
516 515 docstring = """\n
517 516 .. deprecated:: {version}
518 517
519 518 {deprecation_message}
520 519
521 520 {original_docstring}
522 521 """
523 522 func.__doc__ = docstring.format(
524 523 version=self.deprecated_at_version,
525 524 deprecation_message=self.deprecated_msg,
526 525 original_docstring=func.__doc__)
527 526 return decorator.decorator(self.__wrapper, func)
528 527
529 528 def __wrapper(self, func, *fargs, **fkwargs):
530 529 log.warning('DEPRECATED API CALL on function %s, please '
531 530 'use `%s` instead', func, self.use_method)
532 531 # alter function docstring to mark as deprecated, this is picked up
533 532 # via fabric file that generates API DOC.
534 533 result = func(*fargs, **fkwargs)
535 534
536 535 request = fargs[0]
537 536 request.rpc_deprecation = 'DEPRECATED METHOD ' + self.deprecated_msg
538 537 return result
539 538
540 539
541 540 def add_api_methods(config):
542 541 from rhodecode.api.views import (
543 542 deprecated_api, gist_api, pull_request_api, repo_api, repo_group_api,
544 543 server_api, search_api, testing_api, user_api, user_group_api)
545 544
546 545 config.scan('rhodecode.api.views')
547 546
548 547
549 548 def includeme(config):
550 549 plugin_module = 'rhodecode.api'
551 550 plugin_settings = get_plugin_settings(
552 551 plugin_module, config.registry.settings)
553 552
554 553 if not hasattr(config.registry, 'jsonrpc_methods'):
555 554 config.registry.jsonrpc_methods = OrderedDict()
556 555
557 556 # match filter by given method only
558 557 config.add_view_predicate('jsonrpc_method', MethodPredicate)
559 558 config.add_view_predicate('jsonrpc_method_not_found', NotFoundPredicate)
560 559
561 560 config.add_renderer(DEFAULT_RENDERER, ExtJsonRenderer())
562 561 config.add_directive('add_jsonrpc_method', add_jsonrpc_method)
563 562
564 563 config.add_route_predicate(
565 564 'jsonrpc_call', RoutePredicate)
566 565
567 566 config.add_route(
568 567 'apiv2', plugin_settings.get('url', DEFAULT_URL), jsonrpc_call=True)
569 568
570 569 # register some exception handling view
571 570 config.add_view(exception_view, context=JSONRPCBaseError)
572 571 config.add_notfound_view(exception_view, jsonrpc_method_not_found=True)
573 572
574 573 add_api_methods(config)
@@ -1,172 +1,156 b''
1 1
2 2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19
20 20 import os
21 21 import csv
22 22 import datetime
23 23
24 24 import pytest
25 25
26 26 from rhodecode.lib.str_utils import safe_str
27 27 from rhodecode.tests import *
28 from rhodecode.tests.routes import route_path
28 29 from rhodecode.tests.fixture import FIXTURES
29 30 from rhodecode.model.db import UserLog
30 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 34 @pytest.mark.usefixtures('app')
51 35 class TestAdminController(object):
52 36
53 37 @pytest.fixture(scope='class', autouse=True)
54 38 def prepare(self, request, baseapp):
55 39 UserLog.query().delete()
56 40 Session().commit()
57 41
58 42 def strptime(val):
59 43 fmt = '%Y-%m-%d %H:%M:%S'
60 44 if '.' not in val:
61 45 return datetime.datetime.strptime(val, fmt)
62 46
63 47 nofrag, frag = val.split(".")
64 48 date = datetime.datetime.strptime(nofrag, fmt)
65 49
66 50 frag = frag[:6] # truncate to microseconds
67 51 frag += (6 - len(frag)) * '0' # add 0s
68 52 return date.replace(microsecond=int(frag))
69 53
70 54 with open(os.path.join(FIXTURES, 'journal_dump.csv')) as f:
71 55 for row in csv.DictReader(f):
72 56 ul = UserLog()
73 57 for k, v in row.items():
74 58 v = safe_str(v)
75 59 if k == 'action_date':
76 60 v = strptime(v)
77 61 if k in ['user_id', 'repository_id']:
78 62 # nullable due to FK problems
79 63 v = None
80 64 setattr(ul, k, v)
81 65 Session().add(ul)
82 66 Session().commit()
83 67
84 68 @request.addfinalizer
85 69 def cleanup():
86 70 UserLog.query().delete()
87 71 Session().commit()
88 72
89 73 def test_index(self, autologin_user):
90 74 response = self.app.get(route_path('admin_audit_logs'))
91 75 response.mustcontain('Admin audit logs')
92 76
93 77 def test_filter_all_entries(self, autologin_user):
94 78 response = self.app.get(route_path('admin_audit_logs'))
95 79 all_count = UserLog.query().count()
96 80 response.mustcontain('%s entries' % all_count)
97 81
98 82 def test_filter_journal_filter_exact_match_on_repository(self, autologin_user):
99 83 response = self.app.get(route_path('admin_audit_logs',
100 84 params=dict(filter='repository:rhodecode')))
101 85 response.mustcontain('3 entries')
102 86
103 87 def test_filter_journal_filter_exact_match_on_repository_CamelCase(self, autologin_user):
104 88 response = self.app.get(route_path('admin_audit_logs',
105 89 params=dict(filter='repository:RhodeCode')))
106 90 response.mustcontain('3 entries')
107 91
108 92 def test_filter_journal_filter_wildcard_on_repository(self, autologin_user):
109 93 response = self.app.get(route_path('admin_audit_logs',
110 94 params=dict(filter='repository:*test*')))
111 95 response.mustcontain('862 entries')
112 96
113 97 def test_filter_journal_filter_prefix_on_repository(self, autologin_user):
114 98 response = self.app.get(route_path('admin_audit_logs',
115 99 params=dict(filter='repository:test*')))
116 100 response.mustcontain('257 entries')
117 101
118 102 def test_filter_journal_filter_prefix_on_repository_CamelCase(self, autologin_user):
119 103 response = self.app.get(route_path('admin_audit_logs',
120 104 params=dict(filter='repository:Test*')))
121 105 response.mustcontain('257 entries')
122 106
123 107 def test_filter_journal_filter_prefix_on_repository_and_user(self, autologin_user):
124 108 response = self.app.get(route_path('admin_audit_logs',
125 109 params=dict(filter='repository:test* AND username:demo')))
126 110 response.mustcontain('130 entries')
127 111
128 112 def test_filter_journal_filter_prefix_on_repository_or_target_repo(self, autologin_user):
129 113 response = self.app.get(route_path('admin_audit_logs',
130 114 params=dict(filter='repository:test* OR repository:rhodecode')))
131 115 response.mustcontain('260 entries') # 257 + 3
132 116
133 117 def test_filter_journal_filter_exact_match_on_username(self, autologin_user):
134 118 response = self.app.get(route_path('admin_audit_logs',
135 119 params=dict(filter='username:demo')))
136 120 response.mustcontain('1087 entries')
137 121
138 122 def test_filter_journal_filter_exact_match_on_username_camelCase(self, autologin_user):
139 123 response = self.app.get(route_path('admin_audit_logs',
140 124 params=dict(filter='username:DemO')))
141 125 response.mustcontain('1087 entries')
142 126
143 127 def test_filter_journal_filter_wildcard_on_username(self, autologin_user):
144 128 response = self.app.get(route_path('admin_audit_logs',
145 129 params=dict(filter='username:*test*')))
146 130 entries_count = UserLog.query().filter(UserLog.username.ilike('%test%')).count()
147 131 response.mustcontain('{} entries'.format(entries_count))
148 132
149 133 def test_filter_journal_filter_prefix_on_username(self, autologin_user):
150 134 response = self.app.get(route_path('admin_audit_logs',
151 135 params=dict(filter='username:demo*')))
152 136 response.mustcontain('1101 entries')
153 137
154 138 def test_filter_journal_filter_prefix_on_user_or_other_user(self, autologin_user):
155 139 response = self.app.get(route_path('admin_audit_logs',
156 140 params=dict(filter='username:demo OR username:volcan')))
157 141 response.mustcontain('1095 entries') # 1087 + 8
158 142
159 143 def test_filter_journal_filter_wildcard_on_action(self, autologin_user):
160 144 response = self.app.get(route_path('admin_audit_logs',
161 145 params=dict(filter='action:*pull_request*')))
162 146 response.mustcontain('187 entries')
163 147
164 148 def test_filter_journal_filter_on_date(self, autologin_user):
165 149 response = self.app.get(route_path('admin_audit_logs',
166 150 params=dict(filter='date:20121010')))
167 151 response.mustcontain('47 entries')
168 152
169 153 def test_filter_journal_filter_on_date_2(self, autologin_user):
170 154 response = self.app.get(route_path('admin_audit_logs',
171 155 params=dict(filter='date:20121020')))
172 156 response.mustcontain('17 entries')
@@ -1,86 +1,69 b''
1 1
2 2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19
20 20 import pytest
21 21
22 22 from rhodecode.tests import assert_session_flash
23 from rhodecode.tests.routes import route_path
23 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 27 @pytest.mark.usefixtures("app")
45 28 class TestDefaultsView(object):
46 29
47 30 def test_index(self, autologin_user):
48 31 response = self.app.get(route_path('admin_defaults_repositories'))
49 32 response.mustcontain('default_repo_private')
50 33 response.mustcontain('default_repo_enable_statistics')
51 34 response.mustcontain('default_repo_enable_downloads')
52 35 response.mustcontain('default_repo_enable_locking')
53 36
54 37 def test_update_params_true_hg(self, autologin_user, csrf_token):
55 38 params = {
56 39 'default_repo_enable_locking': True,
57 40 'default_repo_enable_downloads': True,
58 41 'default_repo_enable_statistics': True,
59 42 'default_repo_private': True,
60 43 'default_repo_type': 'hg',
61 44 'csrf_token': csrf_token,
62 45 }
63 46 response = self.app.post(
64 47 route_path('admin_defaults_repositories_update'), params=params)
65 48 assert_session_flash(response, 'Default settings updated successfully')
66 49
67 50 defs = SettingsModel().get_default_repo_settings()
68 51 del params['csrf_token']
69 52 assert params == defs
70 53
71 54 def test_update_params_false_git(self, autologin_user, csrf_token):
72 55 params = {
73 56 'default_repo_enable_locking': False,
74 57 'default_repo_enable_downloads': False,
75 58 'default_repo_enable_statistics': False,
76 59 'default_repo_private': False,
77 60 'default_repo_type': 'git',
78 61 'csrf_token': csrf_token,
79 62 }
80 63 response = self.app.post(
81 64 route_path('admin_defaults_repositories_update'), params=params)
82 65 assert_session_flash(response, 'Default settings updated successfully')
83 66
84 67 defs = SettingsModel().get_default_repo_settings()
85 68 del params['csrf_token']
86 69 assert params == defs
@@ -1,86 +1,67 b''
1 1
2 2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19
20 20 import pytest
21 21
22 22 from rhodecode.tests import TestController
23 23 from rhodecode.tests.fixture import Fixture
24 from rhodecode.tests.routes import route_path
24 25
25 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 29 class TestAdminMainView(TestController):
49 30
50 31 def test_access_admin_home(self):
51 32 self.log_user()
52 33 response = self.app.get(route_path('admin_home'), status=200)
53 34 response.mustcontain("Administration area")
54 35
55 36 @pytest.mark.parametrize('view', [
56 37 'pull_requests_global',
57 38 ])
58 39 def test_redirect_pull_request_view_global(self, view):
59 40 self.log_user()
60 41 self.app.get(
61 42 route_path(view, pull_request_id='xxxx'),
62 43 status=404)
63 44
64 45 @pytest.mark.backends("git", "hg")
65 46 @pytest.mark.parametrize('view', [
66 47 'pull_requests_global',
67 48 'pull_requests_global_0',
68 49 'pull_requests_global_1',
69 50 ])
70 51 def test_redirect_pull_request_view(self, view, pr_util):
71 52 self.log_user()
72 53 pull_request = pr_util.create_pull_request()
73 54 pull_request_id = pull_request.pull_request_id
74 55 repo_name = pull_request.target_repo.repo_name
75 56
76 57 response = self.app.get(
77 58 route_path(view, pull_request_id=pull_request_id),
78 59 status=302)
79 60 assert response.location.endswith(
80 61 'pull-request/{}'.format(pull_request_id))
81 62
82 63 redirect_url = route_path(
83 64 'pullrequest_show', repo_name=repo_name,
84 65 pull_request_id=pull_request_id)
85 66
86 67 assert redirect_url in response.location
@@ -1,300 +1,253 b''
1 1
2 2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19
20 20 import mock
21 21 import pytest
22 22 from rhodecode.model.db import User, UserIpMap
23 23 from rhodecode.model.meta import Session
24 24 from rhodecode.model.permission import PermissionModel
25 25 from rhodecode.model.ssh_key import SshKeyModel
26 26 from rhodecode.tests import (
27 27 TestController, clear_cache_regions, assert_session_flash)
28
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
28 from rhodecode.tests.routes import route_path
76 29
77 30
78 31 class TestAdminPermissionsController(TestController):
79 32
80 33 @pytest.fixture(scope='class', autouse=True)
81 34 def prepare(self, request):
82 35 # cleanup and reset to default permissions after
83 36 @request.addfinalizer
84 37 def cleanup():
85 38 PermissionModel().create_default_user_permissions(
86 39 User.get_default_user(), force=True)
87 40
88 41 def test_index_application(self):
89 42 self.log_user()
90 43 self.app.get(route_path('admin_permissions_application'))
91 44
92 45 @pytest.mark.parametrize(
93 46 'anonymous, default_register, default_register_message, default_password_reset,'
94 47 'default_extern_activate, expect_error, expect_form_error', [
95 48 (True, 'hg.register.none', '', 'hg.password_reset.enabled', 'hg.extern_activate.manual',
96 49 False, False),
97 50 (True, 'hg.register.manual_activate', '', 'hg.password_reset.enabled', 'hg.extern_activate.auto',
98 51 False, False),
99 52 (True, 'hg.register.auto_activate', '', 'hg.password_reset.enabled', 'hg.extern_activate.manual',
100 53 False, False),
101 54 (True, 'hg.register.auto_activate', '', 'hg.password_reset.enabled', 'hg.extern_activate.manual',
102 55 False, False),
103 56 (True, 'hg.register.XXX', '', 'hg.password_reset.enabled', 'hg.extern_activate.manual',
104 57 False, True),
105 58 (True, '', '', 'hg.password_reset.enabled', '', True, False),
106 59 ])
107 60 def test_update_application_permissions(
108 61 self, anonymous, default_register, default_register_message, default_password_reset,
109 62 default_extern_activate, expect_error, expect_form_error):
110 63
111 64 self.log_user()
112 65
113 66 # TODO: anonymous access set here to False, breaks some other tests
114 67 params = {
115 68 'csrf_token': self.csrf_token,
116 69 'anonymous': anonymous,
117 70 'default_register': default_register,
118 71 'default_register_message': default_register_message,
119 72 'default_password_reset': default_password_reset,
120 73 'default_extern_activate': default_extern_activate,
121 74 }
122 75 response = self.app.post(route_path('admin_permissions_application_update'),
123 76 params=params)
124 77 if expect_form_error:
125 78 assert response.status_int == 200
126 79 response.mustcontain('Value must be one of')
127 80 else:
128 81 if expect_error:
129 82 msg = 'Error occurred during update of permissions'
130 83 else:
131 84 msg = 'Application permissions updated successfully'
132 85 assert_session_flash(response, msg)
133 86
134 87 def test_index_object(self):
135 88 self.log_user()
136 89 self.app.get(route_path('admin_permissions_object'))
137 90
138 91 @pytest.mark.parametrize(
139 92 'repo, repo_group, user_group, expect_error, expect_form_error', [
140 93 ('repository.none', 'group.none', 'usergroup.none', False, False),
141 94 ('repository.read', 'group.read', 'usergroup.read', False, False),
142 95 ('repository.write', 'group.write', 'usergroup.write',
143 96 False, False),
144 97 ('repository.admin', 'group.admin', 'usergroup.admin',
145 98 False, False),
146 99 ('repository.XXX', 'group.admin', 'usergroup.admin', False, True),
147 100 ('', '', '', True, False),
148 101 ])
149 102 def test_update_object_permissions(self, repo, repo_group, user_group,
150 103 expect_error, expect_form_error):
151 104 self.log_user()
152 105
153 106 params = {
154 107 'csrf_token': self.csrf_token,
155 108 'default_repo_perm': repo,
156 109 'overwrite_default_repo': False,
157 110 'default_group_perm': repo_group,
158 111 'overwrite_default_group': False,
159 112 'default_user_group_perm': user_group,
160 113 'overwrite_default_user_group': False,
161 114 }
162 115 response = self.app.post(route_path('admin_permissions_object_update'),
163 116 params=params)
164 117 if expect_form_error:
165 118 assert response.status_int == 200
166 119 response.mustcontain('Value must be one of')
167 120 else:
168 121 if expect_error:
169 122 msg = 'Error occurred during update of permissions'
170 123 else:
171 124 msg = 'Object permissions updated successfully'
172 125 assert_session_flash(response, msg)
173 126
174 127 def test_index_global(self):
175 128 self.log_user()
176 129 self.app.get(route_path('admin_permissions_global'))
177 130
178 131 @pytest.mark.parametrize(
179 132 'repo_create, repo_create_write, user_group_create, repo_group_create,'
180 133 'fork_create, inherit_default_permissions, expect_error,'
181 134 'expect_form_error', [
182 135 ('hg.create.none', 'hg.create.write_on_repogroup.false',
183 136 'hg.usergroup.create.false', 'hg.repogroup.create.false',
184 137 'hg.fork.none', 'hg.inherit_default_perms.false', False, False),
185 138 ('hg.create.repository', 'hg.create.write_on_repogroup.true',
186 139 'hg.usergroup.create.true', 'hg.repogroup.create.true',
187 140 'hg.fork.repository', 'hg.inherit_default_perms.false',
188 141 False, False),
189 142 ('hg.create.XXX', 'hg.create.write_on_repogroup.true',
190 143 'hg.usergroup.create.true', 'hg.repogroup.create.true',
191 144 'hg.fork.repository', 'hg.inherit_default_perms.false',
192 145 False, True),
193 146 ('', '', '', '', '', '', True, False),
194 147 ])
195 148 def test_update_global_permissions(
196 149 self, repo_create, repo_create_write, user_group_create,
197 150 repo_group_create, fork_create, inherit_default_permissions,
198 151 expect_error, expect_form_error):
199 152 self.log_user()
200 153
201 154 params = {
202 155 'csrf_token': self.csrf_token,
203 156 'default_repo_create': repo_create,
204 157 'default_repo_create_on_write': repo_create_write,
205 158 'default_user_group_create': user_group_create,
206 159 'default_repo_group_create': repo_group_create,
207 160 'default_fork_create': fork_create,
208 161 'default_inherit_default_permissions': inherit_default_permissions
209 162 }
210 163 response = self.app.post(route_path('admin_permissions_global_update'),
211 164 params=params)
212 165 if expect_form_error:
213 166 assert response.status_int == 200
214 167 response.mustcontain('Value must be one of')
215 168 else:
216 169 if expect_error:
217 170 msg = 'Error occurred during update of permissions'
218 171 else:
219 172 msg = 'Global permissions updated successfully'
220 173 assert_session_flash(response, msg)
221 174
222 175 def test_index_ips(self):
223 176 self.log_user()
224 177 response = self.app.get(route_path('admin_permissions_ips'))
225 178 response.mustcontain('All IP addresses are allowed')
226 179
227 180 def test_add_delete_ips(self):
228 181 clear_cache_regions(['sql_cache_short'])
229 182 self.log_user()
230 183
231 184 # ADD
232 185 default_user_id = User.get_default_user_id()
233 186 self.app.post(
234 187 route_path('edit_user_ips_add', user_id=default_user_id),
235 188 params={'new_ip': '0.0.0.0/24', 'csrf_token': self.csrf_token})
236 189
237 190 response = self.app.get(route_path('admin_permissions_ips'))
238 191 response.mustcontain('0.0.0.0/24')
239 192 response.mustcontain('0.0.0.0 - 0.0.0.255')
240 193
241 194 # DELETE
242 195 default_user_id = User.get_default_user_id()
243 196 del_ip_id = UserIpMap.query().filter(UserIpMap.user_id ==
244 197 default_user_id).first().ip_id
245 198
246 199 response = self.app.post(
247 200 route_path('edit_user_ips_delete', user_id=default_user_id),
248 201 params={'del_ip_id': del_ip_id, 'csrf_token': self.csrf_token})
249 202
250 203 assert_session_flash(response, 'Removed ip address from user whitelist')
251 204
252 205 clear_cache_regions(['sql_cache_short'])
253 206 response = self.app.get(route_path('admin_permissions_ips'))
254 207 response.mustcontain('All IP addresses are allowed')
255 208 response.mustcontain(no=['0.0.0.0/24'])
256 209 response.mustcontain(no=['0.0.0.0 - 0.0.0.255'])
257 210
258 211 def test_index_overview(self):
259 212 self.log_user()
260 213 self.app.get(route_path('admin_permissions_overview'))
261 214
262 215 def test_ssh_keys(self):
263 216 self.log_user()
264 217 self.app.get(route_path('admin_permissions_ssh_keys'), status=200)
265 218
266 219 def test_ssh_keys_data(self, user_util, xhr_header):
267 220 self.log_user()
268 221 response = self.app.get(route_path('admin_permissions_ssh_keys_data'),
269 222 extra_environ=xhr_header)
270 223 assert response.json == {u'data': [], u'draw': None,
271 224 u'recordsFiltered': 0, u'recordsTotal': 0}
272 225
273 226 dummy_user = user_util.create_user()
274 227 SshKeyModel().create(dummy_user, 'ab:cd:ef', 'KEYKEY', 'test_key')
275 228 Session().commit()
276 229 response = self.app.get(route_path('admin_permissions_ssh_keys_data'),
277 230 extra_environ=xhr_header)
278 231 assert response.json['data'][0]['fingerprint'] == 'ab:cd:ef'
279 232
280 233 def test_ssh_keys_update(self):
281 234 self.log_user()
282 235 response = self.app.post(
283 236 route_path('admin_permissions_ssh_keys_update'),
284 237 dict(csrf_token=self.csrf_token), status=302)
285 238
286 239 assert_session_flash(
287 240 response, 'Updated SSH keys file')
288 241
289 242 def test_ssh_keys_update_disabled(self):
290 243 self.log_user()
291 244
292 245 from rhodecode.apps.admin.views.permissions import AdminPermissionsView
293 246 with mock.patch.object(AdminPermissionsView, 'ssh_enabled',
294 247 return_value=False):
295 248 response = self.app.post(
296 249 route_path('admin_permissions_ssh_keys_update'),
297 250 dict(csrf_token=self.csrf_token), status=302)
298 251
299 252 assert_session_flash(
300 253 response, 'SSH key support is disabled in .ini file') No newline at end of file
@@ -1,515 +1,497 b''
1 1
2 2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19
20 20 import urllib.request
21 21 import urllib.parse
22 22 import urllib.error
23 23
24 24 import mock
25 25 import pytest
26 26
27 27 from rhodecode.apps._base import ADMIN_PREFIX
28 28 from rhodecode.lib import auth
29 29 from rhodecode.lib.utils2 import safe_str
30 30 from rhodecode.lib import helpers as h
31 31 from rhodecode.model.db import (
32 32 Repository, RepoGroup, UserRepoToPerm, User, Permission)
33 33 from rhodecode.model.meta import Session
34 34 from rhodecode.model.repo import RepoModel
35 35 from rhodecode.model.repo_group import RepoGroupModel
36 36 from rhodecode.model.user import UserModel
37 37 from rhodecode.tests import (
38 38 login_user_session, assert_session_flash, TEST_USER_ADMIN_LOGIN,
39 39 TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
40 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 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 47 def _get_permission_for_user(user, repo):
66 48 perm = UserRepoToPerm.query()\
67 49 .filter(UserRepoToPerm.repository ==
68 50 Repository.get_by_repo_name(repo))\
69 51 .filter(UserRepoToPerm.user == User.get_by_username(user))\
70 52 .all()
71 53 return perm
72 54
73 55
74 56 @pytest.mark.usefixtures("app")
75 57 class TestAdminRepos(object):
76 58
77 59 def test_repo_list(self, autologin_user, user_util, xhr_header):
78 60 repo = user_util.create_repo()
79 61 repo_name = repo.repo_name
80 62 response = self.app.get(
81 63 route_path('repos_data'), status=200,
82 64 extra_environ=xhr_header)
83 65
84 66 response.mustcontain(repo_name)
85 67
86 68 def test_create_page_restricted_to_single_backend(self, autologin_user, backend):
87 69 with mock.patch('rhodecode.BACKENDS', {'git': 'git'}):
88 70 response = self.app.get(route_path('repo_new'), status=200)
89 71 assert_response = response.assert_response()
90 72 element = assert_response.get_element('[name=repo_type]')
91 73 assert element.get('value') == 'git'
92 74
93 75 def test_create_page_non_restricted_backends(self, autologin_user, backend):
94 76 response = self.app.get(route_path('repo_new'), status=200)
95 77 assert_response = response.assert_response()
96 78 assert ['hg', 'git', 'svn'] == [x.get('value') for x in assert_response.get_elements('[name=repo_type]')]
97 79
98 80 @pytest.mark.parametrize(
99 81 "suffix", ['', 'xxa'], ids=['', 'non-ascii'])
100 82 def test_create(self, autologin_user, backend, suffix, csrf_token):
101 83 repo_name_unicode = backend.new_repo_name(suffix=suffix)
102 84 repo_name = repo_name_unicode
103 85
104 86 description_unicode = 'description for newly created repo' + suffix
105 87 description = description_unicode
106 88
107 89 response = self.app.post(
108 90 route_path('repo_create'),
109 91 fixture._get_repo_create_params(
110 92 repo_private=False,
111 93 repo_name=repo_name,
112 94 repo_type=backend.alias,
113 95 repo_description=description,
114 96 csrf_token=csrf_token),
115 97 status=302)
116 98
117 99 self.assert_repository_is_created_correctly(
118 100 repo_name, description, backend)
119 101
120 102 def test_create_numeric_name(self, autologin_user, backend, csrf_token):
121 103 numeric_repo = '1234'
122 104 repo_name = numeric_repo
123 105 description = 'description for newly created repo' + numeric_repo
124 106 self.app.post(
125 107 route_path('repo_create'),
126 108 fixture._get_repo_create_params(
127 109 repo_private=False,
128 110 repo_name=repo_name,
129 111 repo_type=backend.alias,
130 112 repo_description=description,
131 113 csrf_token=csrf_token))
132 114
133 115 self.assert_repository_is_created_correctly(
134 116 repo_name, description, backend)
135 117
136 118 @pytest.mark.parametrize("suffix", ['', '_ąćę'], ids=['', 'non-ascii'])
137 119 def test_create_in_group(
138 120 self, autologin_user, backend, suffix, csrf_token):
139 121 # create GROUP
140 122 group_name = f'sometest_{backend.alias}'
141 123 gr = RepoGroupModel().create(group_name=group_name,
142 124 group_description='test',
143 125 owner=TEST_USER_ADMIN_LOGIN)
144 126 Session().commit()
145 127
146 128 repo_name = f'ingroup{suffix}'
147 129 repo_name_full = RepoGroup.url_sep().join([group_name, repo_name])
148 130 description = 'description for newly created repo'
149 131
150 132 self.app.post(
151 133 route_path('repo_create'),
152 134 fixture._get_repo_create_params(
153 135 repo_private=False,
154 136 repo_name=safe_str(repo_name),
155 137 repo_type=backend.alias,
156 138 repo_description=description,
157 139 repo_group=gr.group_id,
158 140 csrf_token=csrf_token))
159 141
160 142 # TODO: johbo: Cleanup work to fixture
161 143 try:
162 144 self.assert_repository_is_created_correctly(
163 145 repo_name_full, description, backend)
164 146
165 147 new_repo = RepoModel().get_by_repo_name(repo_name_full)
166 148 inherited_perms = UserRepoToPerm.query().filter(
167 149 UserRepoToPerm.repository_id == new_repo.repo_id).all()
168 150 assert len(inherited_perms) == 1
169 151 finally:
170 152 RepoModel().delete(repo_name_full)
171 153 RepoGroupModel().delete(group_name)
172 154 Session().commit()
173 155
174 156 def test_create_in_group_numeric_name(
175 157 self, autologin_user, backend, csrf_token):
176 158 # create GROUP
177 159 group_name = 'sometest_%s' % backend.alias
178 160 gr = RepoGroupModel().create(group_name=group_name,
179 161 group_description='test',
180 162 owner=TEST_USER_ADMIN_LOGIN)
181 163 Session().commit()
182 164
183 165 repo_name = '12345'
184 166 repo_name_full = RepoGroup.url_sep().join([group_name, repo_name])
185 167 description = 'description for newly created repo'
186 168 self.app.post(
187 169 route_path('repo_create'),
188 170 fixture._get_repo_create_params(
189 171 repo_private=False,
190 172 repo_name=repo_name,
191 173 repo_type=backend.alias,
192 174 repo_description=description,
193 175 repo_group=gr.group_id,
194 176 csrf_token=csrf_token))
195 177
196 178 # TODO: johbo: Cleanup work to fixture
197 179 try:
198 180 self.assert_repository_is_created_correctly(
199 181 repo_name_full, description, backend)
200 182
201 183 new_repo = RepoModel().get_by_repo_name(repo_name_full)
202 184 inherited_perms = UserRepoToPerm.query()\
203 185 .filter(UserRepoToPerm.repository_id == new_repo.repo_id).all()
204 186 assert len(inherited_perms) == 1
205 187 finally:
206 188 RepoModel().delete(repo_name_full)
207 189 RepoGroupModel().delete(group_name)
208 190 Session().commit()
209 191
210 192 def test_create_in_group_without_needed_permissions(self, backend):
211 193 session = login_user_session(
212 194 self.app, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
213 195 csrf_token = auth.get_csrf_token(session)
214 196 # revoke
215 197 user_model = UserModel()
216 198 # disable fork and create on default user
217 199 user_model.revoke_perm(User.DEFAULT_USER, 'hg.create.repository')
218 200 user_model.grant_perm(User.DEFAULT_USER, 'hg.create.none')
219 201 user_model.revoke_perm(User.DEFAULT_USER, 'hg.fork.repository')
220 202 user_model.grant_perm(User.DEFAULT_USER, 'hg.fork.none')
221 203
222 204 # disable on regular user
223 205 user_model.revoke_perm(TEST_USER_REGULAR_LOGIN, 'hg.create.repository')
224 206 user_model.grant_perm(TEST_USER_REGULAR_LOGIN, 'hg.create.none')
225 207 user_model.revoke_perm(TEST_USER_REGULAR_LOGIN, 'hg.fork.repository')
226 208 user_model.grant_perm(TEST_USER_REGULAR_LOGIN, 'hg.fork.none')
227 209 Session().commit()
228 210
229 211 # create GROUP
230 212 group_name = 'reg_sometest_%s' % backend.alias
231 213 gr = RepoGroupModel().create(group_name=group_name,
232 214 group_description='test',
233 215 owner=TEST_USER_ADMIN_LOGIN)
234 216 Session().commit()
235 217 repo_group_id = gr.group_id
236 218
237 219 group_name_allowed = 'reg_sometest_allowed_%s' % backend.alias
238 220 gr_allowed = RepoGroupModel().create(
239 221 group_name=group_name_allowed,
240 222 group_description='test',
241 223 owner=TEST_USER_REGULAR_LOGIN)
242 224 allowed_repo_group_id = gr_allowed.group_id
243 225 Session().commit()
244 226
245 227 repo_name = 'ingroup'
246 228 description = 'description for newly created repo'
247 229 response = self.app.post(
248 230 route_path('repo_create'),
249 231 fixture._get_repo_create_params(
250 232 repo_private=False,
251 233 repo_name=repo_name,
252 234 repo_type=backend.alias,
253 235 repo_description=description,
254 236 repo_group=repo_group_id,
255 237 csrf_token=csrf_token))
256 238
257 239 response.mustcontain('Invalid value')
258 240
259 241 # user is allowed to create in this group
260 242 repo_name = 'ingroup'
261 243 repo_name_full = RepoGroup.url_sep().join(
262 244 [group_name_allowed, repo_name])
263 245 description = 'description for newly created repo'
264 246 response = self.app.post(
265 247 route_path('repo_create'),
266 248 fixture._get_repo_create_params(
267 249 repo_private=False,
268 250 repo_name=repo_name,
269 251 repo_type=backend.alias,
270 252 repo_description=description,
271 253 repo_group=allowed_repo_group_id,
272 254 csrf_token=csrf_token))
273 255
274 256 # TODO: johbo: Cleanup in pytest fixture
275 257 try:
276 258 self.assert_repository_is_created_correctly(
277 259 repo_name_full, description, backend)
278 260
279 261 new_repo = RepoModel().get_by_repo_name(repo_name_full)
280 262 inherited_perms = UserRepoToPerm.query().filter(
281 263 UserRepoToPerm.repository_id == new_repo.repo_id).all()
282 264 assert len(inherited_perms) == 1
283 265
284 266 assert repo_on_filesystem(repo_name_full)
285 267 finally:
286 268 RepoModel().delete(repo_name_full)
287 269 RepoGroupModel().delete(group_name)
288 270 RepoGroupModel().delete(group_name_allowed)
289 271 Session().commit()
290 272
291 273 def test_create_in_group_inherit_permissions(self, autologin_user, backend,
292 274 csrf_token):
293 275 # create GROUP
294 276 group_name = 'sometest_%s' % backend.alias
295 277 gr = RepoGroupModel().create(group_name=group_name,
296 278 group_description='test',
297 279 owner=TEST_USER_ADMIN_LOGIN)
298 280 perm = Permission.get_by_key('repository.write')
299 281 RepoGroupModel().grant_user_permission(
300 282 gr, TEST_USER_REGULAR_LOGIN, perm)
301 283
302 284 # add repo permissions
303 285 Session().commit()
304 286 repo_group_id = gr.group_id
305 287 repo_name = 'ingroup_inherited_%s' % backend.alias
306 288 repo_name_full = RepoGroup.url_sep().join([group_name, repo_name])
307 289 description = 'description for newly created repo'
308 290 self.app.post(
309 291 route_path('repo_create'),
310 292 fixture._get_repo_create_params(
311 293 repo_private=False,
312 294 repo_name=repo_name,
313 295 repo_type=backend.alias,
314 296 repo_description=description,
315 297 repo_group=repo_group_id,
316 298 repo_copy_permissions=True,
317 299 csrf_token=csrf_token))
318 300
319 301 # TODO: johbo: Cleanup to pytest fixture
320 302 try:
321 303 self.assert_repository_is_created_correctly(
322 304 repo_name_full, description, backend)
323 305 except Exception:
324 306 RepoGroupModel().delete(group_name)
325 307 Session().commit()
326 308 raise
327 309
328 310 # check if inherited permissions are applied
329 311 new_repo = RepoModel().get_by_repo_name(repo_name_full)
330 312 inherited_perms = UserRepoToPerm.query().filter(
331 313 UserRepoToPerm.repository_id == new_repo.repo_id).all()
332 314 assert len(inherited_perms) == 2
333 315
334 316 assert TEST_USER_REGULAR_LOGIN in [
335 317 x.user.username for x in inherited_perms]
336 318 assert 'repository.write' in [
337 319 x.permission.permission_name for x in inherited_perms]
338 320
339 321 RepoModel().delete(repo_name_full)
340 322 RepoGroupModel().delete(group_name)
341 323 Session().commit()
342 324
343 325 @pytest.mark.xfail_backends(
344 326 "git", "hg", reason="Missing reposerver support")
345 327 def test_create_with_clone_uri(self, autologin_user, backend, reposerver,
346 328 csrf_token):
347 329 source_repo = backend.create_repo(number_of_commits=2)
348 330 source_repo_name = source_repo.repo_name
349 331 reposerver.serve(source_repo.scm_instance())
350 332
351 333 repo_name = backend.new_repo_name()
352 334 response = self.app.post(
353 335 route_path('repo_create'),
354 336 fixture._get_repo_create_params(
355 337 repo_private=False,
356 338 repo_name=repo_name,
357 339 repo_type=backend.alias,
358 340 repo_description='',
359 341 clone_uri=reposerver.url,
360 342 csrf_token=csrf_token),
361 343 status=302)
362 344
363 345 # Should be redirected to the creating page
364 346 response.mustcontain('repo_creating')
365 347
366 348 # Expecting that both repositories have same history
367 349 source_repo = RepoModel().get_by_repo_name(source_repo_name)
368 350 source_vcs = source_repo.scm_instance()
369 351 repo = RepoModel().get_by_repo_name(repo_name)
370 352 repo_vcs = repo.scm_instance()
371 353 assert source_vcs[0].message == repo_vcs[0].message
372 354 assert source_vcs.count() == repo_vcs.count()
373 355 assert source_vcs.commit_ids == repo_vcs.commit_ids
374 356
375 357 @pytest.mark.xfail_backends("svn", reason="Depends on import support")
376 358 def test_create_remote_repo_wrong_clone_uri(self, autologin_user, backend,
377 359 csrf_token):
378 360 repo_name = backend.new_repo_name()
379 361 description = 'description for newly created repo'
380 362 response = self.app.post(
381 363 route_path('repo_create'),
382 364 fixture._get_repo_create_params(
383 365 repo_private=False,
384 366 repo_name=repo_name,
385 367 repo_type=backend.alias,
386 368 repo_description=description,
387 369 clone_uri='http://repo.invalid/repo',
388 370 csrf_token=csrf_token))
389 371 response.mustcontain('invalid clone url')
390 372
391 373 @pytest.mark.xfail_backends("svn", reason="Depends on import support")
392 374 def test_create_remote_repo_wrong_clone_uri_hg_svn(
393 375 self, autologin_user, backend, csrf_token):
394 376 repo_name = backend.new_repo_name()
395 377 description = 'description for newly created repo'
396 378 response = self.app.post(
397 379 route_path('repo_create'),
398 380 fixture._get_repo_create_params(
399 381 repo_private=False,
400 382 repo_name=repo_name,
401 383 repo_type=backend.alias,
402 384 repo_description=description,
403 385 clone_uri='svn+http://svn.invalid/repo',
404 386 csrf_token=csrf_token))
405 387 response.mustcontain('invalid clone url')
406 388
407 389 def test_create_with_git_suffix(
408 390 self, autologin_user, backend, csrf_token):
409 391 repo_name = backend.new_repo_name() + ".git"
410 392 description = 'description for newly created repo'
411 393 response = self.app.post(
412 394 route_path('repo_create'),
413 395 fixture._get_repo_create_params(
414 396 repo_private=False,
415 397 repo_name=repo_name,
416 398 repo_type=backend.alias,
417 399 repo_description=description,
418 400 csrf_token=csrf_token))
419 401 response.mustcontain('Repository name cannot end with .git')
420 402
421 403 def test_default_user_cannot_access_private_repo_in_a_group(
422 404 self, autologin_user, user_util, backend):
423 405
424 406 group = user_util.create_repo_group()
425 407
426 408 repo = backend.create_repo(
427 409 repo_private=True, repo_group=group, repo_copy_permissions=True)
428 410
429 411 permissions = _get_permission_for_user(
430 412 user='default', repo=repo.repo_name)
431 413 assert len(permissions) == 1
432 414 assert permissions[0].permission.permission_name == 'repository.none'
433 415 assert permissions[0].repository.private is True
434 416
435 417 def test_create_on_top_level_without_permissions(self, backend):
436 418 session = login_user_session(
437 419 self.app, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
438 420 csrf_token = auth.get_csrf_token(session)
439 421
440 422 # revoke
441 423 user_model = UserModel()
442 424 # disable fork and create on default user
443 425 user_model.revoke_perm(User.DEFAULT_USER, 'hg.create.repository')
444 426 user_model.grant_perm(User.DEFAULT_USER, 'hg.create.none')
445 427 user_model.revoke_perm(User.DEFAULT_USER, 'hg.fork.repository')
446 428 user_model.grant_perm(User.DEFAULT_USER, 'hg.fork.none')
447 429
448 430 # disable on regular user
449 431 user_model.revoke_perm(TEST_USER_REGULAR_LOGIN, 'hg.create.repository')
450 432 user_model.grant_perm(TEST_USER_REGULAR_LOGIN, 'hg.create.none')
451 433 user_model.revoke_perm(TEST_USER_REGULAR_LOGIN, 'hg.fork.repository')
452 434 user_model.grant_perm(TEST_USER_REGULAR_LOGIN, 'hg.fork.none')
453 435 Session().commit()
454 436
455 437 repo_name = backend.new_repo_name()
456 438 description = 'description for newly created repo'
457 439 response = self.app.post(
458 440 route_path('repo_create'),
459 441 fixture._get_repo_create_params(
460 442 repo_private=False,
461 443 repo_name=repo_name,
462 444 repo_type=backend.alias,
463 445 repo_description=description,
464 446 csrf_token=csrf_token))
465 447
466 448 response.mustcontain(
467 449 u"You do not have the permission to store repositories in "
468 450 u"the root location.")
469 451
470 452 @mock.patch.object(RepoModel, '_create_filesystem_repo', error_function)
471 453 def test_create_repo_when_filesystem_op_fails(
472 454 self, autologin_user, backend, csrf_token):
473 455 repo_name = backend.new_repo_name()
474 456 description = 'description for newly created repo'
475 457
476 458 response = self.app.post(
477 459 route_path('repo_create'),
478 460 fixture._get_repo_create_params(
479 461 repo_private=False,
480 462 repo_name=repo_name,
481 463 repo_type=backend.alias,
482 464 repo_description=description,
483 465 csrf_token=csrf_token))
484 466
485 467 assert_session_flash(
486 468 response, 'Error creating repository %s' % repo_name)
487 469 # repo must not be in db
488 470 assert backend.repo is None
489 471 # repo must not be in filesystem !
490 472 assert not repo_on_filesystem(repo_name)
491 473
492 474 def assert_repository_is_created_correctly(self, repo_name, description, backend):
493 475 url_quoted_repo_name = urllib.parse.quote(repo_name)
494 476
495 477 # run the check page that triggers the flash message
496 478 response = self.app.get(
497 479 route_path('repo_creating_check', repo_name=repo_name))
498 480 assert response.json == {'result': True}
499 481
500 482 flash_msg = 'Created repository <a href="/{}">{}</a>'.format(url_quoted_repo_name, repo_name)
501 483 assert_session_flash(response, flash_msg)
502 484
503 485 # test if the repo was created in the database
504 486 new_repo = RepoModel().get_by_repo_name(repo_name)
505 487
506 488 assert new_repo.repo_name == repo_name
507 489 assert new_repo.description == description
508 490
509 491 # test if the repository is visible in the list ?
510 492 response = self.app.get(
511 493 h.route_path('repo_summary', repo_name=repo_name))
512 494 response.mustcontain(repo_name)
513 495 response.mustcontain(backend.alias)
514 496
515 497 assert repo_on_filesystem(repo_name)
@@ -1,195 +1,179 b''
1 1
2 2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19
20 20 import os
21 21 import pytest
22 22
23 23 from rhodecode.apps._base import ADMIN_PREFIX
24 24 from rhodecode.lib import helpers as h
25 25 from rhodecode.model.db import Repository, UserRepoToPerm, User, RepoGroup
26 26 from rhodecode.model.meta import Session
27 27 from rhodecode.model.repo_group import RepoGroupModel
28 28 from rhodecode.tests import (
29 29 assert_session_flash, TEST_USER_REGULAR_LOGIN, TESTS_TMP_PATH)
30 30 from rhodecode.tests.fixture import Fixture
31
32 fixture = Fixture()
31 from rhodecode.tests.routes import route_path
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
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
34 fixture = Fixture()
51 35
52 36
53 37 def _get_permission_for_user(user, repo):
54 38 perm = UserRepoToPerm.query()\
55 39 .filter(UserRepoToPerm.repository ==
56 40 Repository.get_by_repo_name(repo))\
57 41 .filter(UserRepoToPerm.user == User.get_by_username(user))\
58 42 .all()
59 43 return perm
60 44
61 45
62 46 @pytest.mark.usefixtures("app")
63 47 class TestAdminRepositoryGroups(object):
64 48
65 49 def test_show_repo_groups(self, autologin_user):
66 50 self.app.get(route_path('repo_groups'))
67 51
68 52 def test_show_repo_groups_data(self, autologin_user, xhr_header):
69 53 response = self.app.get(route_path(
70 54 'repo_groups_data'), extra_environ=xhr_header)
71 55
72 56 all_repo_groups = RepoGroup.query().count()
73 57 assert response.json['recordsTotal'] == all_repo_groups
74 58
75 59 def test_show_repo_groups_data_filtered(self, autologin_user, xhr_header):
76 60 response = self.app.get(route_path(
77 61 'repo_groups_data', params={'search[value]': 'empty_search'}),
78 62 extra_environ=xhr_header)
79 63
80 64 all_repo_groups = RepoGroup.query().count()
81 65 assert response.json['recordsTotal'] == all_repo_groups
82 66 assert response.json['recordsFiltered'] == 0
83 67
84 68 def test_show_repo_groups_after_creating_group(self, autologin_user, xhr_header):
85 69 fixture.create_repo_group('test_repo_group')
86 70 response = self.app.get(route_path(
87 71 'repo_groups_data'), extra_environ=xhr_header)
88 72 response.mustcontain('<a href=\\"/{}/_edit\\" title=\\"Edit\\">Edit</a>'.format('test_repo_group'))
89 73 fixture.destroy_repo_group('test_repo_group')
90 74
91 75 def test_new(self, autologin_user):
92 76 self.app.get(route_path('repo_group_new'))
93 77
94 78 def test_new_with_parent_group(self, autologin_user, user_util):
95 79 gr = user_util.create_repo_group()
96 80
97 81 self.app.get(route_path('repo_group_new'),
98 82 params=dict(parent_group=gr.group_name))
99 83
100 84 def test_new_by_regular_user_no_permission(self, autologin_regular_user):
101 85 self.app.get(route_path('repo_group_new'), status=403)
102 86
103 87 @pytest.mark.parametrize('repo_group_name', [
104 88 'git_repo',
105 89 'git_repo_ąć',
106 90 'hg_repo',
107 91 '12345',
108 92 'hg_repo_ąć',
109 93 ])
110 94 def test_create(self, autologin_user, repo_group_name, csrf_token):
111 95 repo_group_name_non_ascii = repo_group_name
112 96 description = 'description for newly created repo group'
113 97
114 98 response = self.app.post(
115 99 route_path('repo_group_create'),
116 100 fixture._get_group_create_params(
117 101 group_name=repo_group_name,
118 102 group_description=description,
119 103 csrf_token=csrf_token))
120 104
121 105 # run the check page that triggers the flash message
122 106 repo_gr_url = h.route_path(
123 107 'repo_group_home', repo_group_name=repo_group_name)
124 108
125 109 assert_session_flash(
126 110 response,
127 111 'Created repository group <a href="%s">%s</a>' % (
128 112 repo_gr_url, repo_group_name_non_ascii))
129 113
130 114 # # test if the repo group was created in the database
131 115 new_repo_group = RepoGroupModel()._get_repo_group(
132 116 repo_group_name_non_ascii)
133 117 assert new_repo_group is not None
134 118
135 119 assert new_repo_group.group_name == repo_group_name_non_ascii
136 120 assert new_repo_group.group_description == description
137 121
138 122 # test if the repository is visible in the list ?
139 123 response = self.app.get(repo_gr_url)
140 124 response.mustcontain(repo_group_name)
141 125
142 126 # test if the repository group was created on filesystem
143 127 is_on_filesystem = os.path.isdir(
144 128 os.path.join(TESTS_TMP_PATH, repo_group_name))
145 129 if not is_on_filesystem:
146 130 self.fail('no repo group %s in filesystem' % repo_group_name)
147 131
148 132 RepoGroupModel().delete(repo_group_name_non_ascii)
149 133 Session().commit()
150 134
151 135 @pytest.mark.parametrize('repo_group_name', [
152 136 'git_repo',
153 137 'git_repo_ąć',
154 138 'hg_repo',
155 139 '12345',
156 140 'hg_repo_ąć',
157 141 ])
158 142 def test_create_subgroup(self, autologin_user, user_util, repo_group_name, csrf_token):
159 143 parent_group = user_util.create_repo_group()
160 144 parent_group_name = parent_group.group_name
161 145
162 146 expected_group_name = '{}/{}'.format(
163 147 parent_group_name, repo_group_name)
164 148 expected_group_name_non_ascii = expected_group_name
165 149
166 150 try:
167 151 response = self.app.post(
168 152 route_path('repo_group_create'),
169 153 fixture._get_group_create_params(
170 154 group_name=repo_group_name,
171 155 group_parent_id=parent_group.group_id,
172 156 group_description='Test desciption',
173 157 csrf_token=csrf_token))
174 158
175 159 assert_session_flash(
176 160 response,
177 161 u'Created repository group <a href="%s">%s</a>' % (
178 162 h.route_path('repo_group_home',
179 163 repo_group_name=expected_group_name),
180 164 expected_group_name_non_ascii))
181 165 finally:
182 166 RepoGroupModel().delete(expected_group_name_non_ascii)
183 167 Session().commit()
184 168
185 169 def test_user_with_creation_permissions_cannot_create_subgroups(
186 170 self, autologin_regular_user, user_util):
187 171
188 172 user_util.grant_user_permission(
189 173 TEST_USER_REGULAR_LOGIN, 'hg.repogroup.create.true')
190 174 parent_group = user_util.create_repo_group()
191 175 parent_group_id = parent_group.group_id
192 176 self.app.get(
193 177 route_path('repo_group_new',
194 178 params=dict(parent_group=parent_group_id), ),
195 179 status=403)
@@ -1,768 +1,695 b''
1 1
2 2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19
20 20 import mock
21 21 import pytest
22 22
23 23 import rhodecode
24 24 from rhodecode.apps._base import ADMIN_PREFIX
25 25 from rhodecode.lib.hash_utils import md5_safe
26 26 from rhodecode.model.db import RhodeCodeUi
27 27 from rhodecode.model.meta import Session
28 28 from rhodecode.model.settings import SettingsModel, IssueTrackerSettingsModel
29 29 from rhodecode.tests import assert_session_flash
30 from rhodecode.tests.routes import route_path
30 31
31 32
32 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 36 @pytest.mark.usefixtures('autologin_user', 'app')
110 37 class TestAdminSettingsController(object):
111 38
112 39 @pytest.mark.parametrize('urlname', [
113 40 'admin_settings_vcs',
114 41 'admin_settings_mapping',
115 42 'admin_settings_global',
116 43 'admin_settings_visual',
117 44 'admin_settings_email',
118 45 'admin_settings_hooks',
119 46 'admin_settings_search',
120 47 ])
121 48 def test_simple_get(self, urlname):
122 49 self.app.get(route_path(urlname))
123 50
124 51 def test_create_custom_hook(self, csrf_token):
125 52 response = self.app.post(
126 53 route_path('admin_settings_hooks_update'),
127 54 params={
128 55 'new_hook_ui_key': 'test_hooks_1',
129 56 'new_hook_ui_value': 'cd /tmp',
130 57 'csrf_token': csrf_token})
131 58
132 59 response = response.follow()
133 60 response.mustcontain('test_hooks_1')
134 61 response.mustcontain('cd /tmp')
135 62
136 63 def test_create_custom_hook_delete(self, csrf_token):
137 64 response = self.app.post(
138 65 route_path('admin_settings_hooks_update'),
139 66 params={
140 67 'new_hook_ui_key': 'test_hooks_2',
141 68 'new_hook_ui_value': 'cd /tmp2',
142 69 'csrf_token': csrf_token})
143 70
144 71 response = response.follow()
145 72 response.mustcontain('test_hooks_2')
146 73 response.mustcontain('cd /tmp2')
147 74
148 75 hook_id = SettingsModel().get_ui_by_key('test_hooks_2').ui_id
149 76
150 77 # delete
151 78 self.app.post(
152 79 route_path('admin_settings_hooks_delete'),
153 80 params={'hook_id': hook_id, 'csrf_token': csrf_token})
154 81 response = self.app.get(route_path('admin_settings_hooks'))
155 82 response.mustcontain(no=['test_hooks_2'])
156 83 response.mustcontain(no=['cd /tmp2'])
157 84
158 85
159 86 @pytest.mark.usefixtures('autologin_user', 'app')
160 87 class TestAdminSettingsGlobal(object):
161 88
162 89 def test_pre_post_code_code_active(self, csrf_token):
163 90 pre_code = 'rc-pre-code-187652122'
164 91 post_code = 'rc-postcode-98165231'
165 92
166 93 response = self.post_and_verify_settings({
167 94 'rhodecode_pre_code': pre_code,
168 95 'rhodecode_post_code': post_code,
169 96 'csrf_token': csrf_token,
170 97 })
171 98
172 99 response = response.follow()
173 100 response.mustcontain(pre_code, post_code)
174 101
175 102 def test_pre_post_code_code_inactive(self, csrf_token):
176 103 pre_code = 'rc-pre-code-187652122'
177 104 post_code = 'rc-postcode-98165231'
178 105 response = self.post_and_verify_settings({
179 106 'rhodecode_pre_code': '',
180 107 'rhodecode_post_code': '',
181 108 'csrf_token': csrf_token,
182 109 })
183 110
184 111 response = response.follow()
185 112 response.mustcontain(no=[pre_code, post_code])
186 113
187 114 def test_captcha_activate(self, csrf_token):
188 115 self.post_and_verify_settings({
189 116 'rhodecode_captcha_private_key': '1234567890',
190 117 'rhodecode_captcha_public_key': '1234567890',
191 118 'csrf_token': csrf_token,
192 119 })
193 120
194 121 response = self.app.get(ADMIN_PREFIX + '/register')
195 122 response.mustcontain('captcha')
196 123
197 124 def test_captcha_deactivate(self, csrf_token):
198 125 self.post_and_verify_settings({
199 126 'rhodecode_captcha_private_key': '',
200 127 'rhodecode_captcha_public_key': '1234567890',
201 128 'csrf_token': csrf_token,
202 129 })
203 130
204 131 response = self.app.get(ADMIN_PREFIX + '/register')
205 132 response.mustcontain(no=['captcha'])
206 133
207 134 def test_title_change(self, csrf_token):
208 135 old_title = 'RhodeCode'
209 136
210 137 for new_title in ['Changed', 'Żółwik', old_title]:
211 138 response = self.post_and_verify_settings({
212 139 'rhodecode_title': new_title,
213 140 'csrf_token': csrf_token,
214 141 })
215 142
216 143 response = response.follow()
217 144 response.mustcontain(new_title)
218 145
219 146 def post_and_verify_settings(self, settings):
220 147 old_title = 'RhodeCode'
221 148 old_realm = 'RhodeCode authentication'
222 149 params = {
223 150 'rhodecode_title': old_title,
224 151 'rhodecode_realm': old_realm,
225 152 'rhodecode_pre_code': '',
226 153 'rhodecode_post_code': '',
227 154 'rhodecode_captcha_private_key': '',
228 155 'rhodecode_captcha_public_key': '',
229 156 'rhodecode_create_personal_repo_group': False,
230 157 'rhodecode_personal_repo_group_pattern': '${username}',
231 158 }
232 159 params.update(settings)
233 160 response = self.app.post(
234 161 route_path('admin_settings_global_update'), params=params)
235 162
236 163 assert_session_flash(response, 'Updated application settings')
237 164
238 165 app_settings = SettingsModel().get_all_settings()
239 166 del settings['csrf_token']
240 167 for key, value in settings.items():
241 168 assert app_settings[key] == value
242 169
243 170 return response
244 171
245 172
246 173 @pytest.mark.usefixtures('autologin_user', 'app')
247 174 class TestAdminSettingsVcs(object):
248 175
249 176 def test_contains_svn_default_patterns(self):
250 177 response = self.app.get(route_path('admin_settings_vcs'))
251 178 expected_patterns = [
252 179 '/trunk',
253 180 '/branches/*',
254 181 '/tags/*',
255 182 ]
256 183 for pattern in expected_patterns:
257 184 response.mustcontain(pattern)
258 185
259 186 def test_add_new_svn_branch_and_tag_pattern(
260 187 self, backend_svn, form_defaults, disable_sql_cache,
261 188 csrf_token):
262 189 form_defaults.update({
263 190 'new_svn_branch': '/exp/branches/*',
264 191 'new_svn_tag': '/important_tags/*',
265 192 'csrf_token': csrf_token,
266 193 })
267 194
268 195 response = self.app.post(
269 196 route_path('admin_settings_vcs_update'),
270 197 params=form_defaults, status=302)
271 198 response = response.follow()
272 199
273 200 # Expect to find the new values on the page
274 201 response.mustcontain('/exp/branches/*')
275 202 response.mustcontain('/important_tags/*')
276 203
277 204 # Expect that those patterns are used to match branches and tags now
278 205 repo = backend_svn['svn-simple-layout'].scm_instance()
279 206 assert 'exp/branches/exp-sphinx-docs' in repo.branches
280 207 assert 'important_tags/v0.5' in repo.tags
281 208
282 209 def test_add_same_svn_value_twice_shows_an_error_message(
283 210 self, form_defaults, csrf_token, settings_util):
284 211 settings_util.create_rhodecode_ui('vcs_svn_branch', '/test')
285 212 settings_util.create_rhodecode_ui('vcs_svn_tag', '/test')
286 213
287 214 response = self.app.post(
288 215 route_path('admin_settings_vcs_update'),
289 216 params={
290 217 'paths_root_path': form_defaults['paths_root_path'],
291 218 'new_svn_branch': '/test',
292 219 'new_svn_tag': '/test',
293 220 'csrf_token': csrf_token,
294 221 },
295 222 status=200)
296 223
297 224 response.mustcontain("Pattern already exists")
298 225 response.mustcontain("Some form inputs contain invalid data.")
299 226
300 227 @pytest.mark.parametrize('section', [
301 228 'vcs_svn_branch',
302 229 'vcs_svn_tag',
303 230 ])
304 231 def test_delete_svn_patterns(
305 232 self, section, csrf_token, settings_util):
306 233 setting = settings_util.create_rhodecode_ui(
307 234 section, '/test_delete', cleanup=False)
308 235
309 236 self.app.post(
310 237 route_path('admin_settings_vcs_svn_pattern_delete'),
311 238 params={
312 239 'delete_svn_pattern': setting.ui_id,
313 240 'csrf_token': csrf_token},
314 241 headers={'X-REQUESTED-WITH': 'XMLHttpRequest'})
315 242
316 243 @pytest.mark.parametrize('section', [
317 244 'vcs_svn_branch',
318 245 'vcs_svn_tag',
319 246 ])
320 247 def test_delete_svn_patterns_raises_404_when_no_xhr(
321 248 self, section, csrf_token, settings_util):
322 249 setting = settings_util.create_rhodecode_ui(section, '/test_delete')
323 250
324 251 self.app.post(
325 252 route_path('admin_settings_vcs_svn_pattern_delete'),
326 253 params={
327 254 'delete_svn_pattern': setting.ui_id,
328 255 'csrf_token': csrf_token},
329 256 status=404)
330 257
331 258 def test_extensions_hgsubversion(self, form_defaults, csrf_token):
332 259 form_defaults.update({
333 260 'csrf_token': csrf_token,
334 261 'extensions_hgsubversion': 'True',
335 262 })
336 263 response = self.app.post(
337 264 route_path('admin_settings_vcs_update'),
338 265 params=form_defaults,
339 266 status=302)
340 267
341 268 response = response.follow()
342 269 extensions_input = (
343 270 '<input id="extensions_hgsubversion" '
344 271 'name="extensions_hgsubversion" type="checkbox" '
345 272 'value="True" checked="checked" />')
346 273 response.mustcontain(extensions_input)
347 274
348 275 def test_extensions_hgevolve(self, form_defaults, csrf_token):
349 276 form_defaults.update({
350 277 'csrf_token': csrf_token,
351 278 'extensions_evolve': 'True',
352 279 })
353 280 response = self.app.post(
354 281 route_path('admin_settings_vcs_update'),
355 282 params=form_defaults,
356 283 status=302)
357 284
358 285 response = response.follow()
359 286 extensions_input = (
360 287 '<input id="extensions_evolve" '
361 288 'name="extensions_evolve" type="checkbox" '
362 289 'value="True" checked="checked" />')
363 290 response.mustcontain(extensions_input)
364 291
365 292 def test_has_a_section_for_pull_request_settings(self):
366 293 response = self.app.get(route_path('admin_settings_vcs'))
367 294 response.mustcontain('Pull Request Settings')
368 295
369 296 def test_has_an_input_for_invalidation_of_inline_comments(self):
370 297 response = self.app.get(route_path('admin_settings_vcs'))
371 298 assert_response = response.assert_response()
372 299 assert_response.one_element_exists(
373 300 '[name=rhodecode_use_outdated_comments]')
374 301
375 302 @pytest.mark.parametrize('new_value', [True, False])
376 303 def test_allows_to_change_invalidation_of_inline_comments(
377 304 self, form_defaults, csrf_token, new_value):
378 305 setting_key = 'use_outdated_comments'
379 306 setting = SettingsModel().create_or_update_setting(
380 307 setting_key, not new_value, 'bool')
381 308 Session().add(setting)
382 309 Session().commit()
383 310
384 311 form_defaults.update({
385 312 'csrf_token': csrf_token,
386 313 'rhodecode_use_outdated_comments': str(new_value),
387 314 })
388 315 response = self.app.post(
389 316 route_path('admin_settings_vcs_update'),
390 317 params=form_defaults,
391 318 status=302)
392 319 response = response.follow()
393 320 setting = SettingsModel().get_setting_by_name(setting_key)
394 321 assert setting.app_settings_value is new_value
395 322
396 323 @pytest.mark.parametrize('new_value', [True, False])
397 324 def test_allows_to_change_hg_rebase_merge_strategy(
398 325 self, form_defaults, csrf_token, new_value):
399 326 setting_key = 'hg_use_rebase_for_merging'
400 327
401 328 form_defaults.update({
402 329 'csrf_token': csrf_token,
403 330 'rhodecode_' + setting_key: str(new_value),
404 331 })
405 332
406 333 with mock.patch.dict(
407 334 rhodecode.CONFIG, {'labs_settings_active': 'true'}):
408 335 self.app.post(
409 336 route_path('admin_settings_vcs_update'),
410 337 params=form_defaults,
411 338 status=302)
412 339
413 340 setting = SettingsModel().get_setting_by_name(setting_key)
414 341 assert setting.app_settings_value is new_value
415 342
416 343 @pytest.fixture()
417 344 def disable_sql_cache(self, request):
418 345 # patch _do_orm_execute so it returns None similar like if we don't use a cached query
419 346 patcher = mock.patch(
420 347 'rhodecode.lib.caching_query.ORMCache._do_orm_execute', return_value=None)
421 348 request.addfinalizer(patcher.stop)
422 349 patcher.start()
423 350
424 351 @pytest.fixture()
425 352 def form_defaults(self):
426 353 from rhodecode.apps.admin.views.settings import AdminSettingsView
427 354 return AdminSettingsView._form_defaults()
428 355
429 356 # TODO: johbo: What we really want is to checkpoint before a test run and
430 357 # reset the session afterwards.
431 358 @pytest.fixture(scope='class', autouse=True)
432 359 def cleanup_settings(self, request, baseapp):
433 360 ui_id = RhodeCodeUi.ui_id
434 361 original_ids = [r.ui_id for r in RhodeCodeUi.query().with_entities(ui_id)]
435 362
436 363 @request.addfinalizer
437 364 def cleanup():
438 365 RhodeCodeUi.query().filter(
439 366 ui_id.notin_(original_ids)).delete(False)
440 367
441 368
442 369 @pytest.mark.usefixtures('autologin_user', 'app')
443 370 class TestLabsSettings(object):
444 371 def test_get_settings_page_disabled(self):
445 372 with mock.patch.dict(
446 373 rhodecode.CONFIG, {'labs_settings_active': 'false'}):
447 374
448 375 response = self.app.get(
449 376 route_path('admin_settings_labs'), status=302)
450 377
451 378 assert response.location.endswith(route_path('admin_settings'))
452 379
453 380 def test_get_settings_page_enabled(self):
454 381 from rhodecode.apps.admin.views import settings
455 382 lab_settings = [
456 383 settings.LabSetting(
457 384 key='rhodecode_bool',
458 385 type='bool',
459 386 group='bool group',
460 387 label='bool label',
461 388 help='bool help'
462 389 ),
463 390 settings.LabSetting(
464 391 key='rhodecode_text',
465 392 type='unicode',
466 393 group='text group',
467 394 label='text label',
468 395 help='text help'
469 396 ),
470 397 ]
471 398 with mock.patch.dict(rhodecode.CONFIG,
472 399 {'labs_settings_active': 'true'}):
473 400 with mock.patch.object(settings, '_LAB_SETTINGS', lab_settings):
474 401 response = self.app.get(route_path('admin_settings_labs'))
475 402
476 403 assert '<label>bool group:</label>' in response
477 404 assert '<label for="rhodecode_bool">bool label</label>' in response
478 405 assert '<p class="help-block">bool help</p>' in response
479 406 assert 'name="rhodecode_bool" type="checkbox"' in response
480 407
481 408 assert '<label>text group:</label>' in response
482 409 assert '<label for="rhodecode_text">text label</label>' in response
483 410 assert '<p class="help-block">text help</p>' in response
484 411 assert 'name="rhodecode_text" size="60" type="text"' in response
485 412
486 413
487 414 @pytest.mark.usefixtures('app')
488 415 class TestOpenSourceLicenses(object):
489 416
490 417 def test_records_are_displayed(self, autologin_user):
491 418 sample_licenses = [
492 419 {
493 420 "license": [
494 421 {
495 422 "fullName": "BSD 4-clause \"Original\" or \"Old\" License",
496 423 "shortName": "bsdOriginal",
497 424 "spdxId": "BSD-4-Clause",
498 425 "url": "http://spdx.org/licenses/BSD-4-Clause.html"
499 426 }
500 427 ],
501 428 "name": "python2.7-coverage-3.7.1"
502 429 },
503 430 {
504 431 "license": [
505 432 {
506 433 "fullName": "MIT License",
507 434 "shortName": "mit",
508 435 "spdxId": "MIT",
509 436 "url": "http://spdx.org/licenses/MIT.html"
510 437 }
511 438 ],
512 439 "name": "python2.7-bootstrapped-pip-9.0.1"
513 440 },
514 441 ]
515 442 read_licenses_patch = mock.patch(
516 443 'rhodecode.apps.admin.views.open_source_licenses.read_opensource_licenses',
517 444 return_value=sample_licenses)
518 445 with read_licenses_patch:
519 446 response = self.app.get(
520 447 route_path('admin_settings_open_source'), status=200)
521 448
522 449 assert_response = response.assert_response()
523 450 assert_response.element_contains(
524 451 '.panel-heading', 'Licenses of Third Party Packages')
525 452 for license_data in sample_licenses:
526 453 response.mustcontain(license_data["license"][0]["spdxId"])
527 454 assert_response.element_contains('.panel-body', license_data["name"])
528 455
529 456 def test_records_can_be_read(self, autologin_user):
530 457 response = self.app.get(
531 458 route_path('admin_settings_open_source'), status=200)
532 459 assert_response = response.assert_response()
533 460 assert_response.element_contains(
534 461 '.panel-heading', 'Licenses of Third Party Packages')
535 462
536 463 def test_forbidden_when_normal_user(self, autologin_regular_user):
537 464 self.app.get(
538 465 route_path('admin_settings_open_source'), status=404)
539 466
540 467
541 468 @pytest.mark.usefixtures('app')
542 469 class TestUserSessions(object):
543 470
544 471 def test_forbidden_when_normal_user(self, autologin_regular_user):
545 472 self.app.get(route_path('admin_settings_sessions'), status=404)
546 473
547 474 def test_show_sessions_page(self, autologin_user):
548 475 response = self.app.get(route_path('admin_settings_sessions'), status=200)
549 476 response.mustcontain('file')
550 477
551 478 def test_cleanup_old_sessions(self, autologin_user, csrf_token):
552 479
553 480 post_data = {
554 481 'csrf_token': csrf_token,
555 482 'expire_days': '60'
556 483 }
557 484 response = self.app.post(
558 485 route_path('admin_settings_sessions_cleanup'), params=post_data,
559 486 status=302)
560 487 assert_session_flash(response, 'Cleaned up old sessions')
561 488
562 489
563 490 @pytest.mark.usefixtures('app')
564 491 class TestAdminSystemInfo(object):
565 492
566 493 def test_forbidden_when_normal_user(self, autologin_regular_user):
567 494 self.app.get(route_path('admin_settings_system'), status=404)
568 495
569 496 def test_system_info_page(self, autologin_user):
570 497 response = self.app.get(route_path('admin_settings_system'))
571 498 response.mustcontain('RhodeCode Community Edition, version {}'.format(
572 499 rhodecode.__version__))
573 500
574 501 def test_system_update_new_version(self, autologin_user):
575 502 update_data = {
576 503 'versions': [
577 504 {
578 505 'version': '100.3.1415926535',
579 506 'general': 'The latest version we are ever going to ship'
580 507 },
581 508 {
582 509 'version': '0.0.0',
583 510 'general': 'The first version we ever shipped'
584 511 }
585 512 ]
586 513 }
587 514 with mock.patch(UPDATE_DATA_QUALNAME, return_value=update_data):
588 515 response = self.app.get(route_path('admin_settings_system_update'))
589 516 response.mustcontain('A <b>new version</b> is available')
590 517
591 518 def test_system_update_nothing_new(self, autologin_user):
592 519 update_data = {
593 520 'versions': [
594 521 {
595 522 'version': '0.0.0',
596 523 'general': 'The first version we ever shipped'
597 524 }
598 525 ]
599 526 }
600 527 with mock.patch(UPDATE_DATA_QUALNAME, return_value=update_data):
601 528 response = self.app.get(route_path('admin_settings_system_update'))
602 529 response.mustcontain(
603 530 'This instance is already running the <b>latest</b> stable version')
604 531
605 532 def test_system_update_bad_response(self, autologin_user):
606 533 with mock.patch(UPDATE_DATA_QUALNAME, side_effect=ValueError('foo')):
607 534 response = self.app.get(route_path('admin_settings_system_update'))
608 535 response.mustcontain(
609 536 'Bad data sent from update server')
610 537
611 538
612 539 @pytest.mark.usefixtures("app")
613 540 class TestAdminSettingsIssueTracker(object):
614 541 RC_PREFIX = 'rhodecode_'
615 542 SHORT_PATTERN_KEY = 'issuetracker_pat_'
616 543 PATTERN_KEY = RC_PREFIX + SHORT_PATTERN_KEY
617 544 DESC_KEY = RC_PREFIX + 'issuetracker_desc_'
618 545
619 546 def test_issuetracker_index(self, autologin_user):
620 547 response = self.app.get(route_path('admin_settings_issuetracker'))
621 548 assert response.status_code == 200
622 549
623 550 def test_add_empty_issuetracker_pattern(
624 551 self, request, autologin_user, csrf_token):
625 552 post_url = route_path('admin_settings_issuetracker_update')
626 553 post_data = {
627 554 'csrf_token': csrf_token
628 555 }
629 556 self.app.post(post_url, post_data, status=302)
630 557
631 558 def test_add_issuetracker_pattern(
632 559 self, request, autologin_user, csrf_token):
633 560 pattern = 'issuetracker_pat'
634 561 another_pattern = pattern+'1'
635 562 post_url = route_path('admin_settings_issuetracker_update')
636 563 post_data = {
637 564 'new_pattern_pattern_0': pattern,
638 565 'new_pattern_url_0': 'http://url',
639 566 'new_pattern_prefix_0': 'prefix',
640 567 'new_pattern_description_0': 'description',
641 568 'new_pattern_pattern_1': another_pattern,
642 569 'new_pattern_url_1': 'https://url1',
643 570 'new_pattern_prefix_1': 'prefix1',
644 571 'new_pattern_description_1': 'description1',
645 572 'csrf_token': csrf_token
646 573 }
647 574 self.app.post(post_url, post_data, status=302)
648 575 settings = SettingsModel().get_all_settings()
649 576 self.uid = md5_safe(pattern)
650 577 assert settings[self.PATTERN_KEY+self.uid] == pattern
651 578 self.another_uid = md5_safe(another_pattern)
652 579 assert settings[self.PATTERN_KEY+self.another_uid] == another_pattern
653 580
654 581 @request.addfinalizer
655 582 def cleanup():
656 583 defaults = SettingsModel().get_all_settings()
657 584
658 585 entries = [name for name in defaults if (
659 586 (self.uid in name) or (self.another_uid in name))]
660 587 start = len(self.RC_PREFIX)
661 588 for del_key in entries:
662 589 # TODO: anderson: get_by_name needs name without prefix
663 590 entry = SettingsModel().get_setting_by_name(del_key[start:])
664 591 Session().delete(entry)
665 592
666 593 Session().commit()
667 594
668 595 def test_edit_issuetracker_pattern(
669 596 self, autologin_user, backend, csrf_token, request):
670 597
671 598 old_pattern = 'issuetracker_pat1'
672 599 old_uid = md5_safe(old_pattern)
673 600
674 601 post_url = route_path('admin_settings_issuetracker_update')
675 602 post_data = {
676 603 'new_pattern_pattern_0': old_pattern,
677 604 'new_pattern_url_0': 'http://url',
678 605 'new_pattern_prefix_0': 'prefix',
679 606 'new_pattern_description_0': 'description',
680 607
681 608 'csrf_token': csrf_token
682 609 }
683 610 self.app.post(post_url, post_data, status=302)
684 611
685 612 new_pattern = 'issuetracker_pat1_edited'
686 613 self.new_uid = md5_safe(new_pattern)
687 614
688 615 post_url = route_path('admin_settings_issuetracker_update')
689 616 post_data = {
690 617 'new_pattern_pattern_{}'.format(old_uid): new_pattern,
691 618 'new_pattern_url_{}'.format(old_uid): 'https://url_edited',
692 619 'new_pattern_prefix_{}'.format(old_uid): 'prefix_edited',
693 620 'new_pattern_description_{}'.format(old_uid): 'description_edited',
694 621 'uid': old_uid,
695 622 'csrf_token': csrf_token
696 623 }
697 624 self.app.post(post_url, post_data, status=302)
698 625
699 626 settings = SettingsModel().get_all_settings()
700 627 assert settings[self.PATTERN_KEY+self.new_uid] == new_pattern
701 628 assert settings[self.DESC_KEY + self.new_uid] == 'description_edited'
702 629 assert self.PATTERN_KEY+old_uid not in settings
703 630
704 631 @request.addfinalizer
705 632 def cleanup():
706 633 IssueTrackerSettingsModel().delete_entries(old_uid)
707 634 IssueTrackerSettingsModel().delete_entries(self.new_uid)
708 635
709 636 def test_replace_issuetracker_pattern_description(
710 637 self, autologin_user, csrf_token, request, settings_util):
711 638 prefix = 'issuetracker'
712 639 pattern = 'issuetracker_pat'
713 640 self.uid = md5_safe(pattern)
714 641 pattern_key = '_'.join([prefix, 'pat', self.uid])
715 642 rc_pattern_key = '_'.join(['rhodecode', pattern_key])
716 643 desc_key = '_'.join([prefix, 'desc', self.uid])
717 644 rc_desc_key = '_'.join(['rhodecode', desc_key])
718 645 new_description = 'new_description'
719 646
720 647 settings_util.create_rhodecode_setting(
721 648 pattern_key, pattern, 'unicode', cleanup=False)
722 649 settings_util.create_rhodecode_setting(
723 650 desc_key, 'old description', 'unicode', cleanup=False)
724 651
725 652 post_url = route_path('admin_settings_issuetracker_update')
726 653 post_data = {
727 654 'new_pattern_pattern_0': pattern,
728 655 'new_pattern_url_0': 'https://url',
729 656 'new_pattern_prefix_0': 'prefix',
730 657 'new_pattern_description_0': new_description,
731 658 'uid': self.uid,
732 659 'csrf_token': csrf_token
733 660 }
734 661 self.app.post(post_url, post_data, status=302)
735 662 settings = SettingsModel().get_all_settings()
736 663 assert settings[rc_pattern_key] == pattern
737 664 assert settings[rc_desc_key] == new_description
738 665
739 666 @request.addfinalizer
740 667 def cleanup():
741 668 IssueTrackerSettingsModel().delete_entries(self.uid)
742 669
743 670 def test_delete_issuetracker_pattern(
744 671 self, autologin_user, backend, csrf_token, settings_util, xhr_header):
745 672
746 673 old_pattern = 'issuetracker_pat_deleted'
747 674 old_uid = md5_safe(old_pattern)
748 675
749 676 post_url = route_path('admin_settings_issuetracker_update')
750 677 post_data = {
751 678 'new_pattern_pattern_0': old_pattern,
752 679 'new_pattern_url_0': 'http://url',
753 680 'new_pattern_prefix_0': 'prefix',
754 681 'new_pattern_description_0': 'description',
755 682
756 683 'csrf_token': csrf_token
757 684 }
758 685 self.app.post(post_url, post_data, status=302)
759 686
760 687 post_url = route_path('admin_settings_issuetracker_delete')
761 688 post_data = {
762 689 'uid': old_uid,
763 690 'csrf_token': csrf_token
764 691 }
765 692 self.app.post(post_url, post_data, extra_environ=xhr_header, status=200)
766 693 settings = SettingsModel().get_all_settings()
767 694 assert self.PATTERN_KEY+old_uid not in settings
768 695 assert self.DESC_KEY + old_uid not in settings
@@ -1,171 +1,152 b''
1 1
2 2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19
20 20 import pytest
21 21
22 22 from rhodecode.model.db import UserGroup, User
23 23 from rhodecode.model.meta import Session
24 24
25 25 from rhodecode.tests import (
26 TestController, TEST_USER_REGULAR_LOGIN, assert_session_flash)
26 TestController, assert_session_flash)
27 27 from rhodecode.tests.fixture import Fixture
28 from rhodecode.tests.routes import route_path
28 29
29 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 33 class TestAdminUserGroupsView(TestController):
53 34
54 35 def test_show_users(self):
55 36 self.log_user()
56 37 self.app.get(route_path('user_groups'))
57 38
58 39 def test_show_user_groups_data(self, xhr_header):
59 40 self.log_user()
60 41 response = self.app.get(route_path(
61 42 'user_groups_data'), extra_environ=xhr_header)
62 43
63 44 all_user_groups = UserGroup.query().count()
64 45 assert response.json['recordsTotal'] == all_user_groups
65 46
66 47 def test_show_user_groups_data_filtered(self, xhr_header):
67 48 self.log_user()
68 49 response = self.app.get(route_path(
69 50 'user_groups_data', params={'search[value]': 'empty_search'}),
70 51 extra_environ=xhr_header)
71 52
72 53 all_user_groups = UserGroup.query().count()
73 54 assert response.json['recordsTotal'] == all_user_groups
74 55 assert response.json['recordsFiltered'] == 0
75 56
76 57 def test_usergroup_escape(self, user_util, xhr_header):
77 58 self.log_user()
78 59
79 60 xss_img = '<img src="/image1" onload="alert(\'Hello, World!\');">'
80 61 user = user_util.create_user()
81 62 user.name = xss_img
82 63 user.lastname = xss_img
83 64 Session().add(user)
84 65 Session().commit()
85 66
86 67 user_group = user_util.create_user_group()
87 68
88 69 user_group.users_group_name = xss_img
89 70 user_group.user_group_description = '<strong onload="alert();">DESC</strong>'
90 71
91 72 response = self.app.get(
92 73 route_path('user_groups_data'), extra_environ=xhr_header)
93 74
94 75 response.mustcontain(
95 76 '&lt;strong onload=&#34;alert();&#34;&gt;DESC&lt;/strong&gt;')
96 77 response.mustcontain(
97 78 '&lt;img src=&#34;/image1&#34; onload=&#34;'
98 79 'alert(&#39;Hello, World!&#39;);&#34;&gt;')
99 80
100 81 def test_edit_user_group_autocomplete_empty_members(self, xhr_header, user_util):
101 82 self.log_user()
102 83 ug = user_util.create_user_group()
103 84 response = self.app.get(
104 85 route_path('user_group_members_data', user_group_id=ug.users_group_id),
105 86 extra_environ=xhr_header)
106 87
107 88 assert response.json == {'members': []}
108 89
109 90 def test_edit_user_group_autocomplete_members(self, xhr_header, user_util):
110 91 self.log_user()
111 92 members = [u.user_id for u in User.get_all()]
112 93 ug = user_util.create_user_group(members=members)
113 94 response = self.app.get(
114 95 route_path('user_group_members_data',
115 96 user_group_id=ug.users_group_id),
116 97 extra_environ=xhr_header)
117 98
118 99 assert len(response.json['members']) == len(members)
119 100
120 101 def test_creation_page(self):
121 102 self.log_user()
122 103 self.app.get(route_path('user_groups_new'), status=200)
123 104
124 105 def test_create(self):
125 106 from rhodecode.lib import helpers as h
126 107
127 108 self.log_user()
128 109 users_group_name = 'test_user_group'
129 110 response = self.app.post(route_path('user_groups_create'), {
130 111 'users_group_name': users_group_name,
131 112 'user_group_description': 'DESC',
132 113 'active': True,
133 114 'csrf_token': self.csrf_token})
134 115
135 116 user_group_id = UserGroup.get_by_group_name(
136 117 users_group_name).users_group_id
137 118
138 119 user_group_link = h.link_to(
139 120 users_group_name,
140 121 route_path('edit_user_group', user_group_id=user_group_id))
141 122
142 123 assert_session_flash(
143 124 response,
144 125 'Created user group %s' % user_group_link)
145 126
146 127 fixture.destroy_user_group(users_group_name)
147 128
148 129 def test_create_with_empty_name(self):
149 130 self.log_user()
150 131
151 132 response = self.app.post(route_path('user_groups_create'), {
152 133 'users_group_name': '',
153 134 'user_group_description': 'DESC',
154 135 'active': True,
155 136 'csrf_token': self.csrf_token}, status=200)
156 137
157 138 response.mustcontain('Please enter a value')
158 139
159 140 def test_create_duplicate(self, user_util):
160 141 self.log_user()
161 142
162 143 user_group = user_util.create_user_group()
163 144 duplicate_name = user_group.users_group_name
164 145 response = self.app.post(route_path('user_groups_create'), {
165 146 'users_group_name': duplicate_name,
166 147 'user_group_description': 'DESC',
167 148 'active': True,
168 149 'csrf_token': self.csrf_token}, status=200)
169 150
170 151 response.mustcontain(
171 152 'User group `{}` already exists'.format(duplicate_name))
@@ -1,795 +1,727 b''
1 1
2 2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19
20 20 import pytest
21 21 from sqlalchemy.orm.exc import NoResultFound
22 22
23 23 from rhodecode.lib import auth
24 24 from rhodecode.lib import helpers as h
25 25 from rhodecode.model.db import User, UserApiKeys, UserEmailMap, Repository
26 26 from rhodecode.model.meta import Session
27 27 from rhodecode.model.user import UserModel
28 28
29 29 from rhodecode.tests import (
30 30 TestController, TEST_USER_REGULAR_LOGIN, assert_session_flash)
31 31 from rhodecode.tests.fixture import Fixture
32 from rhodecode.tests.routes import route_path
32 33
33 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 37 class TestAdminUsersView(TestController):
106 38
107 39 def test_show_users(self):
108 40 self.log_user()
109 41 self.app.get(route_path('users'))
110 42
111 43 def test_show_users_data(self, xhr_header):
112 44 self.log_user()
113 45 response = self.app.get(route_path(
114 46 'users_data'), extra_environ=xhr_header)
115 47
116 48 all_users = User.query().filter(
117 49 User.username != User.DEFAULT_USER).count()
118 50 assert response.json['recordsTotal'] == all_users
119 51
120 52 def test_show_users_data_filtered(self, xhr_header):
121 53 self.log_user()
122 54 response = self.app.get(route_path(
123 55 'users_data', params={'search[value]': 'empty_search'}),
124 56 extra_environ=xhr_header)
125 57
126 58 all_users = User.query().filter(
127 59 User.username != User.DEFAULT_USER).count()
128 60 assert response.json['recordsTotal'] == all_users
129 61 assert response.json['recordsFiltered'] == 0
130 62
131 63 def test_auth_tokens_default_user(self):
132 64 self.log_user()
133 65 user = User.get_default_user()
134 66 response = self.app.get(
135 67 route_path('edit_user_auth_tokens', user_id=user.user_id),
136 68 status=302)
137 69
138 70 def test_auth_tokens(self):
139 71 self.log_user()
140 72
141 73 user = User.get_by_username(TEST_USER_REGULAR_LOGIN)
142 74 user_id = user.user_id
143 75 auth_tokens = user.auth_tokens
144 76 response = self.app.get(
145 77 route_path('edit_user_auth_tokens', user_id=user_id))
146 78 for token in auth_tokens:
147 79 response.mustcontain(token[:4])
148 80 response.mustcontain('never')
149 81
150 82 @pytest.mark.parametrize("desc, lifetime", [
151 83 ('forever', -1),
152 84 ('5mins', 60*5),
153 85 ('30days', 60*60*24*30),
154 86 ])
155 87 def test_add_auth_token(self, desc, lifetime, user_util):
156 88 self.log_user()
157 89 user = user_util.create_user()
158 90 user_id = user.user_id
159 91
160 92 response = self.app.post(
161 93 route_path('edit_user_auth_tokens_add', user_id=user_id),
162 94 {'description': desc, 'lifetime': lifetime,
163 95 'csrf_token': self.csrf_token})
164 96 assert_session_flash(response, 'Auth token successfully created')
165 97
166 98 response = response.follow()
167 99 user = User.get(user_id)
168 100 for auth_token in user.auth_tokens:
169 101 response.mustcontain(auth_token[:4])
170 102
171 103 def test_delete_auth_token(self, user_util):
172 104 self.log_user()
173 105 user = user_util.create_user()
174 106 user_id = user.user_id
175 107 keys = user.auth_tokens
176 108 assert 2 == len(keys)
177 109
178 110 response = self.app.post(
179 111 route_path('edit_user_auth_tokens_add', user_id=user_id),
180 112 {'description': 'desc', 'lifetime': -1,
181 113 'csrf_token': self.csrf_token})
182 114 assert_session_flash(response, 'Auth token successfully created')
183 115 response.follow()
184 116
185 117 # now delete our key
186 118 keys = UserApiKeys.query().filter(UserApiKeys.user_id == user_id).all()
187 119 assert 3 == len(keys)
188 120
189 121 response = self.app.post(
190 122 route_path('edit_user_auth_tokens_delete', user_id=user_id),
191 123 {'del_auth_token': keys[0].user_api_key_id,
192 124 'csrf_token': self.csrf_token})
193 125
194 126 assert_session_flash(response, 'Auth token successfully deleted')
195 127 keys = UserApiKeys.query().filter(UserApiKeys.user_id == user_id).all()
196 128 assert 2 == len(keys)
197 129
198 130 def test_ips(self):
199 131 self.log_user()
200 132 user = User.get_by_username(TEST_USER_REGULAR_LOGIN)
201 133 response = self.app.get(route_path('edit_user_ips', user_id=user.user_id))
202 134 response.mustcontain('All IP addresses are allowed')
203 135
204 136 @pytest.mark.parametrize("test_name, ip, ip_range, failure", [
205 137 ('127/24', '127.0.0.1/24', '127.0.0.0 - 127.0.0.255', False),
206 138 ('10/32', '10.0.0.10/32', '10.0.0.10 - 10.0.0.10', False),
207 139 ('0/16', '0.0.0.0/16', '0.0.0.0 - 0.0.255.255', False),
208 140 ('0/8', '0.0.0.0/8', '0.0.0.0 - 0.255.255.255', False),
209 141 ('127_bad_mask', '127.0.0.1/99', '127.0.0.1 - 127.0.0.1', True),
210 142 ('127_bad_ip', 'foobar', 'foobar', True),
211 143 ])
212 144 def test_ips_add(self, user_util, test_name, ip, ip_range, failure):
213 145 self.log_user()
214 146 user = user_util.create_user(username=test_name)
215 147 user_id = user.user_id
216 148
217 149 response = self.app.post(
218 150 route_path('edit_user_ips_add', user_id=user_id),
219 151 params={'new_ip': ip, 'csrf_token': self.csrf_token})
220 152
221 153 if failure:
222 154 assert_session_flash(
223 155 response, 'Please enter a valid IPv4 or IpV6 address')
224 156 response = self.app.get(route_path('edit_user_ips', user_id=user_id))
225 157
226 158 response.mustcontain(no=[ip])
227 159 response.mustcontain(no=[ip_range])
228 160
229 161 else:
230 162 response = self.app.get(route_path('edit_user_ips', user_id=user_id))
231 163 response.mustcontain(ip)
232 164 response.mustcontain(ip_range)
233 165
234 166 def test_ips_delete(self, user_util):
235 167 self.log_user()
236 168 user = user_util.create_user()
237 169 user_id = user.user_id
238 170 ip = '127.0.0.1/32'
239 171 ip_range = '127.0.0.1 - 127.0.0.1'
240 172 new_ip = UserModel().add_extra_ip(user_id, ip)
241 173 Session().commit()
242 174 new_ip_id = new_ip.ip_id
243 175
244 176 response = self.app.get(route_path('edit_user_ips', user_id=user_id))
245 177 response.mustcontain(ip)
246 178 response.mustcontain(ip_range)
247 179
248 180 self.app.post(
249 181 route_path('edit_user_ips_delete', user_id=user_id),
250 182 params={'del_ip_id': new_ip_id, 'csrf_token': self.csrf_token})
251 183
252 184 response = self.app.get(route_path('edit_user_ips', user_id=user_id))
253 185 response.mustcontain('All IP addresses are allowed')
254 186 response.mustcontain(no=[ip])
255 187 response.mustcontain(no=[ip_range])
256 188
257 189 def test_emails(self):
258 190 self.log_user()
259 191 user = User.get_by_username(TEST_USER_REGULAR_LOGIN)
260 192 response = self.app.get(
261 193 route_path('edit_user_emails', user_id=user.user_id))
262 194 response.mustcontain('No additional emails specified')
263 195
264 196 def test_emails_add(self, user_util):
265 197 self.log_user()
266 198 user = user_util.create_user()
267 199 user_id = user.user_id
268 200
269 201 self.app.post(
270 202 route_path('edit_user_emails_add', user_id=user_id),
271 203 params={'new_email': 'example@rhodecode.com',
272 204 'csrf_token': self.csrf_token})
273 205
274 206 response = self.app.get(
275 207 route_path('edit_user_emails', user_id=user_id))
276 208 response.mustcontain('example@rhodecode.com')
277 209
278 210 def test_emails_add_existing_email(self, user_util, user_regular):
279 211 existing_email = user_regular.email
280 212
281 213 self.log_user()
282 214 user = user_util.create_user()
283 215 user_id = user.user_id
284 216
285 217 response = self.app.post(
286 218 route_path('edit_user_emails_add', user_id=user_id),
287 219 params={'new_email': existing_email,
288 220 'csrf_token': self.csrf_token})
289 221 assert_session_flash(
290 222 response, 'This e-mail address is already taken')
291 223
292 224 response = self.app.get(
293 225 route_path('edit_user_emails', user_id=user_id))
294 226 response.mustcontain(no=[existing_email])
295 227
296 228 def test_emails_delete(self, user_util):
297 229 self.log_user()
298 230 user = user_util.create_user()
299 231 user_id = user.user_id
300 232
301 233 self.app.post(
302 234 route_path('edit_user_emails_add', user_id=user_id),
303 235 params={'new_email': 'example@rhodecode.com',
304 236 'csrf_token': self.csrf_token})
305 237
306 238 response = self.app.get(
307 239 route_path('edit_user_emails', user_id=user_id))
308 240 response.mustcontain('example@rhodecode.com')
309 241
310 242 user_email = UserEmailMap.query()\
311 243 .filter(UserEmailMap.email == 'example@rhodecode.com') \
312 244 .filter(UserEmailMap.user_id == user_id)\
313 245 .one()
314 246
315 247 del_email_id = user_email.email_id
316 248 self.app.post(
317 249 route_path('edit_user_emails_delete', user_id=user_id),
318 250 params={'del_email_id': del_email_id,
319 251 'csrf_token': self.csrf_token})
320 252
321 253 response = self.app.get(
322 254 route_path('edit_user_emails', user_id=user_id))
323 255 response.mustcontain(no=['example@rhodecode.com'])
324 256
325 257 def test_create(self, request, xhr_header):
326 258 self.log_user()
327 259 username = 'newtestuser'
328 260 password = 'test12'
329 261 password_confirmation = password
330 262 name = 'name'
331 263 lastname = 'lastname'
332 264 email = 'mail@mail.com'
333 265
334 266 self.app.get(route_path('users_new'))
335 267
336 268 response = self.app.post(route_path('users_create'), params={
337 269 'username': username,
338 270 'password': password,
339 271 'description': 'mr CTO',
340 272 'password_confirmation': password_confirmation,
341 273 'firstname': name,
342 274 'active': True,
343 275 'lastname': lastname,
344 276 'extern_name': 'rhodecode',
345 277 'extern_type': 'rhodecode',
346 278 'email': email,
347 279 'csrf_token': self.csrf_token,
348 280 })
349 281 user_link = h.link_to(
350 282 username,
351 283 route_path(
352 284 'user_edit', user_id=User.get_by_username(username).user_id))
353 285 assert_session_flash(response, 'Created user %s' % (user_link,))
354 286
355 287 @request.addfinalizer
356 288 def cleanup():
357 289 fixture.destroy_user(username)
358 290 Session().commit()
359 291
360 292 new_user = User.query().filter(User.username == username).one()
361 293
362 294 assert new_user.username == username
363 295 assert auth.check_password(password, new_user.password)
364 296 assert new_user.name == name
365 297 assert new_user.lastname == lastname
366 298 assert new_user.email == email
367 299
368 300 response = self.app.get(route_path('users_data'),
369 301 extra_environ=xhr_header)
370 302 response.mustcontain(username)
371 303
372 304 def test_create_err(self):
373 305 self.log_user()
374 306 username = 'new_user'
375 307 password = ''
376 308 name = 'name'
377 309 lastname = 'lastname'
378 310 email = 'errmail.com'
379 311
380 312 self.app.get(route_path('users_new'))
381 313
382 314 response = self.app.post(route_path('users_create'), params={
383 315 'username': username,
384 316 'password': password,
385 317 'name': name,
386 318 'active': False,
387 319 'lastname': lastname,
388 320 'description': 'mr CTO',
389 321 'email': email,
390 322 'csrf_token': self.csrf_token,
391 323 })
392 324
393 325 msg = u'Username "%(username)s" is forbidden'
394 326 msg = h.html_escape(msg % {'username': 'new_user'})
395 327 response.mustcontain('<span class="error-message">%s</span>' % msg)
396 328 response.mustcontain(
397 329 '<span class="error-message">Please enter a value</span>')
398 330 response.mustcontain(
399 331 '<span class="error-message">An email address must contain a'
400 332 ' single @</span>')
401 333
402 334 def get_user():
403 335 Session().query(User).filter(User.username == username).one()
404 336
405 337 with pytest.raises(NoResultFound):
406 338 get_user()
407 339
408 340 def test_new(self):
409 341 self.log_user()
410 342 self.app.get(route_path('users_new'))
411 343
412 344 @pytest.mark.parametrize("name, attrs", [
413 345 ('firstname', {'firstname': 'new_username'}),
414 346 ('lastname', {'lastname': 'new_username'}),
415 347 ('admin', {'admin': True}),
416 348 ('admin', {'admin': False}),
417 349 ('extern_type', {'extern_type': 'ldap'}),
418 350 ('extern_type', {'extern_type': None}),
419 351 ('extern_name', {'extern_name': 'test'}),
420 352 ('extern_name', {'extern_name': None}),
421 353 ('active', {'active': False}),
422 354 ('active', {'active': True}),
423 355 ('email', {'email': 'some@email.com'}),
424 356 ('language', {'language': 'de'}),
425 357 ('language', {'language': 'en'}),
426 358 ('description', {'description': 'hello CTO'}),
427 359 # ('new_password', {'new_password': 'foobar123',
428 360 # 'password_confirmation': 'foobar123'})
429 361 ])
430 362 def test_update(self, name, attrs, user_util):
431 363 self.log_user()
432 364 usr = user_util.create_user(
433 365 password='qweqwe',
434 366 email='testme@rhodecode.org',
435 367 extern_type='rhodecode',
436 368 extern_name='xxx',
437 369 )
438 370 user_id = usr.user_id
439 371 Session().commit()
440 372
441 373 params = usr.get_api_data()
442 374 cur_lang = params['language'] or 'en'
443 375 params.update({
444 376 'password_confirmation': '',
445 377 'new_password': '',
446 378 'language': cur_lang,
447 379 'csrf_token': self.csrf_token,
448 380 })
449 381 params.update({'new_password': ''})
450 382 params.update(attrs)
451 383 if name == 'email':
452 384 params['emails'] = [attrs['email']]
453 385 elif name == 'extern_type':
454 386 # cannot update this via form, expected value is original one
455 387 params['extern_type'] = "rhodecode"
456 388 elif name == 'extern_name':
457 389 # cannot update this via form, expected value is original one
458 390 params['extern_name'] = 'xxx'
459 391 # special case since this user is not
460 392 # logged in yet his data is not filled
461 393 # so we use creation data
462 394
463 395 response = self.app.post(
464 396 route_path('user_update', user_id=usr.user_id), params)
465 397 assert response.status_int == 302
466 398 assert_session_flash(response, 'User updated successfully')
467 399
468 400 updated_user = User.get(user_id)
469 401 updated_params = updated_user.get_api_data()
470 402 updated_params.update({'password_confirmation': ''})
471 403 updated_params.update({'new_password': ''})
472 404
473 405 del params['csrf_token']
474 406 assert params == updated_params
475 407
476 408 def test_update_and_migrate_password(
477 409 self, autologin_user, real_crypto_backend, user_util):
478 410
479 411 user = user_util.create_user()
480 412 temp_user = user.username
481 413 user.password = auth._RhodeCodeCryptoSha256().hash_create(
482 414 b'test123')
483 415 Session().add(user)
484 416 Session().commit()
485 417
486 418 params = user.get_api_data()
487 419
488 420 params.update({
489 421 'password_confirmation': 'qweqwe123',
490 422 'new_password': 'qweqwe123',
491 423 'language': 'en',
492 424 'csrf_token': autologin_user.csrf_token,
493 425 })
494 426
495 427 response = self.app.post(
496 428 route_path('user_update', user_id=user.user_id), params)
497 429 assert response.status_int == 302
498 430 assert_session_flash(response, 'User updated successfully')
499 431
500 432 # new password should be bcrypted, after log-in and transfer
501 433 user = User.get_by_username(temp_user)
502 434 assert user.password.startswith('$')
503 435
504 436 updated_user = User.get_by_username(temp_user)
505 437 updated_params = updated_user.get_api_data()
506 438 updated_params.update({'password_confirmation': 'qweqwe123'})
507 439 updated_params.update({'new_password': 'qweqwe123'})
508 440
509 441 del params['csrf_token']
510 442 assert params == updated_params
511 443
512 444 def test_delete(self):
513 445 self.log_user()
514 446 username = 'newtestuserdeleteme'
515 447
516 448 fixture.create_user(name=username)
517 449
518 450 new_user = Session().query(User)\
519 451 .filter(User.username == username).one()
520 452 response = self.app.post(
521 453 route_path('user_delete', user_id=new_user.user_id),
522 454 params={'csrf_token': self.csrf_token})
523 455
524 456 assert_session_flash(response, 'Successfully deleted user `{}`'.format(username))
525 457
526 458 def test_delete_owner_of_repository(self, request, user_util):
527 459 self.log_user()
528 460 obj_name = 'test_repo'
529 461 usr = user_util.create_user()
530 462 username = usr.username
531 463 fixture.create_repo(obj_name, cur_user=usr.username)
532 464
533 465 new_user = Session().query(User)\
534 466 .filter(User.username == username).one()
535 467 response = self.app.post(
536 468 route_path('user_delete', user_id=new_user.user_id),
537 469 params={'csrf_token': self.csrf_token})
538 470
539 471 msg = 'user "%s" still owns 1 repositories and cannot be removed. ' \
540 472 'Switch owners or remove those repositories:%s' % (username, obj_name)
541 473 assert_session_flash(response, msg)
542 474 fixture.destroy_repo(obj_name)
543 475
544 476 def test_delete_owner_of_repository_detaching(self, request, user_util):
545 477 self.log_user()
546 478 obj_name = 'test_repo'
547 479 usr = user_util.create_user(auto_cleanup=False)
548 480 username = usr.username
549 481 fixture.create_repo(obj_name, cur_user=usr.username)
550 482 Session().commit()
551 483
552 484 new_user = Session().query(User)\
553 485 .filter(User.username == username).one()
554 486 response = self.app.post(
555 487 route_path('user_delete', user_id=new_user.user_id),
556 488 params={'user_repos': 'detach', 'csrf_token': self.csrf_token})
557 489
558 490 msg = 'Detached 1 repositories'
559 491 assert_session_flash(response, msg)
560 492 fixture.destroy_repo(obj_name)
561 493
562 494 def test_delete_owner_of_repository_deleting(self, request, user_util):
563 495 self.log_user()
564 496 obj_name = 'test_repo'
565 497 usr = user_util.create_user(auto_cleanup=False)
566 498 username = usr.username
567 499 fixture.create_repo(obj_name, cur_user=usr.username)
568 500
569 501 new_user = Session().query(User)\
570 502 .filter(User.username == username).one()
571 503 response = self.app.post(
572 504 route_path('user_delete', user_id=new_user.user_id),
573 505 params={'user_repos': 'delete', 'csrf_token': self.csrf_token})
574 506
575 507 msg = 'Deleted 1 repositories'
576 508 assert_session_flash(response, msg)
577 509
578 510 def test_delete_owner_of_repository_group(self, request, user_util):
579 511 self.log_user()
580 512 obj_name = 'test_group'
581 513 usr = user_util.create_user()
582 514 username = usr.username
583 515 fixture.create_repo_group(obj_name, cur_user=usr.username)
584 516
585 517 new_user = Session().query(User)\
586 518 .filter(User.username == username).one()
587 519 response = self.app.post(
588 520 route_path('user_delete', user_id=new_user.user_id),
589 521 params={'csrf_token': self.csrf_token})
590 522
591 523 msg = 'user "%s" still owns 1 repository groups and cannot be removed. ' \
592 524 'Switch owners or remove those repository groups:%s' % (username, obj_name)
593 525 assert_session_flash(response, msg)
594 526 fixture.destroy_repo_group(obj_name)
595 527
596 528 def test_delete_owner_of_repository_group_detaching(self, request, user_util):
597 529 self.log_user()
598 530 obj_name = 'test_group'
599 531 usr = user_util.create_user(auto_cleanup=False)
600 532 username = usr.username
601 533 fixture.create_repo_group(obj_name, cur_user=usr.username)
602 534
603 535 new_user = Session().query(User)\
604 536 .filter(User.username == username).one()
605 537 response = self.app.post(
606 538 route_path('user_delete', user_id=new_user.user_id),
607 539 params={'user_repo_groups': 'delete', 'csrf_token': self.csrf_token})
608 540
609 541 msg = 'Deleted 1 repository groups'
610 542 assert_session_flash(response, msg)
611 543
612 544 def test_delete_owner_of_repository_group_deleting(self, request, user_util):
613 545 self.log_user()
614 546 obj_name = 'test_group'
615 547 usr = user_util.create_user(auto_cleanup=False)
616 548 username = usr.username
617 549 fixture.create_repo_group(obj_name, cur_user=usr.username)
618 550
619 551 new_user = Session().query(User)\
620 552 .filter(User.username == username).one()
621 553 response = self.app.post(
622 554 route_path('user_delete', user_id=new_user.user_id),
623 555 params={'user_repo_groups': 'detach', 'csrf_token': self.csrf_token})
624 556
625 557 msg = 'Detached 1 repository groups'
626 558 assert_session_flash(response, msg)
627 559 fixture.destroy_repo_group(obj_name)
628 560
629 561 def test_delete_owner_of_user_group(self, request, user_util):
630 562 self.log_user()
631 563 obj_name = 'test_user_group'
632 564 usr = user_util.create_user()
633 565 username = usr.username
634 566 fixture.create_user_group(obj_name, cur_user=usr.username)
635 567
636 568 new_user = Session().query(User)\
637 569 .filter(User.username == username).one()
638 570 response = self.app.post(
639 571 route_path('user_delete', user_id=new_user.user_id),
640 572 params={'csrf_token': self.csrf_token})
641 573
642 574 msg = 'user "%s" still owns 1 user groups and cannot be removed. ' \
643 575 'Switch owners or remove those user groups:%s' % (username, obj_name)
644 576 assert_session_flash(response, msg)
645 577 fixture.destroy_user_group(obj_name)
646 578
647 579 def test_delete_owner_of_user_group_detaching(self, request, user_util):
648 580 self.log_user()
649 581 obj_name = 'test_user_group'
650 582 usr = user_util.create_user(auto_cleanup=False)
651 583 username = usr.username
652 584 fixture.create_user_group(obj_name, cur_user=usr.username)
653 585
654 586 new_user = Session().query(User)\
655 587 .filter(User.username == username).one()
656 588 try:
657 589 response = self.app.post(
658 590 route_path('user_delete', user_id=new_user.user_id),
659 591 params={'user_user_groups': 'detach',
660 592 'csrf_token': self.csrf_token})
661 593
662 594 msg = 'Detached 1 user groups'
663 595 assert_session_flash(response, msg)
664 596 finally:
665 597 fixture.destroy_user_group(obj_name)
666 598
667 599 def test_delete_owner_of_user_group_deleting(self, request, user_util):
668 600 self.log_user()
669 601 obj_name = 'test_user_group'
670 602 usr = user_util.create_user(auto_cleanup=False)
671 603 username = usr.username
672 604 fixture.create_user_group(obj_name, cur_user=usr.username)
673 605
674 606 new_user = Session().query(User)\
675 607 .filter(User.username == username).one()
676 608 response = self.app.post(
677 609 route_path('user_delete', user_id=new_user.user_id),
678 610 params={'user_user_groups': 'delete', 'csrf_token': self.csrf_token})
679 611
680 612 msg = 'Deleted 1 user groups'
681 613 assert_session_flash(response, msg)
682 614
683 615 def test_edit(self, user_util):
684 616 self.log_user()
685 617 user = user_util.create_user()
686 618 self.app.get(route_path('user_edit', user_id=user.user_id))
687 619
688 620 def test_edit_default_user_redirect(self):
689 621 self.log_user()
690 622 user = User.get_default_user()
691 623 self.app.get(route_path('user_edit', user_id=user.user_id), status=302)
692 624
693 625 @pytest.mark.parametrize(
694 626 'repo_create, repo_create_write, user_group_create, repo_group_create,'
695 627 'fork_create, inherit_default_permissions, expect_error,'
696 628 'expect_form_error', [
697 629 ('hg.create.none', 'hg.create.write_on_repogroup.false',
698 630 'hg.usergroup.create.false', 'hg.repogroup.create.false',
699 631 'hg.fork.none', 'hg.inherit_default_perms.false', False, False),
700 632 ('hg.create.repository', 'hg.create.write_on_repogroup.false',
701 633 'hg.usergroup.create.false', 'hg.repogroup.create.false',
702 634 'hg.fork.none', 'hg.inherit_default_perms.false', False, False),
703 635 ('hg.create.repository', 'hg.create.write_on_repogroup.true',
704 636 'hg.usergroup.create.true', 'hg.repogroup.create.true',
705 637 'hg.fork.repository', 'hg.inherit_default_perms.false', False,
706 638 False),
707 639 ('hg.create.XXX', 'hg.create.write_on_repogroup.true',
708 640 'hg.usergroup.create.true', 'hg.repogroup.create.true',
709 641 'hg.fork.repository', 'hg.inherit_default_perms.false', False,
710 642 True),
711 643 ('', '', '', '', '', '', True, False),
712 644 ])
713 645 def test_global_perms_on_user(
714 646 self, repo_create, repo_create_write, user_group_create,
715 647 repo_group_create, fork_create, expect_error, expect_form_error,
716 648 inherit_default_permissions, user_util):
717 649 self.log_user()
718 650 user = user_util.create_user()
719 651 uid = user.user_id
720 652
721 653 # ENABLE REPO CREATE ON A GROUP
722 654 perm_params = {
723 655 'inherit_default_permissions': False,
724 656 'default_repo_create': repo_create,
725 657 'default_repo_create_on_write': repo_create_write,
726 658 'default_user_group_create': user_group_create,
727 659 'default_repo_group_create': repo_group_create,
728 660 'default_fork_create': fork_create,
729 661 'default_inherit_default_permissions': inherit_default_permissions,
730 662 'csrf_token': self.csrf_token,
731 663 }
732 664 response = self.app.post(
733 665 route_path('user_edit_global_perms_update', user_id=uid),
734 666 params=perm_params)
735 667
736 668 if expect_form_error:
737 669 assert response.status_int == 200
738 670 response.mustcontain('Value must be one of')
739 671 else:
740 672 if expect_error:
741 673 msg = 'An error occurred during permissions saving'
742 674 else:
743 675 msg = 'User global permissions updated successfully'
744 676 ug = User.get(uid)
745 677 del perm_params['inherit_default_permissions']
746 678 del perm_params['csrf_token']
747 679 assert perm_params == ug.get_default_perms()
748 680 assert_session_flash(response, msg)
749 681
750 682 def test_global_permissions_initial_values(self, user_util):
751 683 self.log_user()
752 684 user = user_util.create_user()
753 685 uid = user.user_id
754 686 response = self.app.get(
755 687 route_path('user_edit_global_perms', user_id=uid))
756 688 default_user = User.get_default_user()
757 689 default_permissions = default_user.get_default_perms()
758 690 assert_response = response.assert_response()
759 691 expected_permissions = (
760 692 'default_repo_create', 'default_repo_create_on_write',
761 693 'default_fork_create', 'default_repo_group_create',
762 694 'default_user_group_create', 'default_inherit_default_permissions')
763 695 for permission in expected_permissions:
764 696 css_selector = '[name={}][checked=checked]'.format(permission)
765 697 element = assert_response.get_element(css_selector)
766 698 assert element.value == default_permissions[permission]
767 699
768 700 def test_perms_summary_page(self):
769 701 user = self.log_user()
770 702 response = self.app.get(
771 703 route_path('edit_user_perms_summary', user_id=user['user_id']))
772 704 for repo in Repository.query().all():
773 705 response.mustcontain(repo.repo_name)
774 706
775 707 def test_perms_summary_page_json(self):
776 708 user = self.log_user()
777 709 response = self.app.get(
778 710 route_path('edit_user_perms_summary_json', user_id=user['user_id']))
779 711 for repo in Repository.query().all():
780 712 response.mustcontain(repo.repo_name)
781 713
782 714 def test_audit_log_page(self):
783 715 user = self.log_user()
784 716 self.app.get(
785 717 route_path('edit_user_audit_logs', user_id=user['user_id']))
786 718
787 719 def test_audit_log_page_download(self):
788 720 user = self.log_user()
789 721 user_id = user['user_id']
790 722 response = self.app.get(
791 723 route_path('edit_user_audit_logs_download', user_id=user_id))
792 724
793 725 assert response.content_disposition == \
794 726 'attachment; filename=user_{}_audit_logs.json'.format(user_id)
795 727 assert response.content_type == "application/json"
@@ -1,177 +1,155 b''
1 1
2 2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19
20 20 import pytest
21 21
22 22 from rhodecode.model.db import User, UserSshKeys
23 23
24 24 from rhodecode.tests import TestController, assert_session_flash
25 25 from rhodecode.tests.fixture import Fixture
26 from rhodecode.tests.routes import route_path
26 27
27 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 31 class TestAdminUsersSshKeysView(TestController):
54 32 INVALID_KEY = """\
55 33 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDk+77sjDzVeB6vevJsuZds1iNU5
56 34 LANOa5CU5G/9JYIA6RYsWWMO7mbsR82IUckdqOHmxSykfR1D1TdluyIpQLrwgH5kb
57 35 n8FkVI8zBMCKakxowvN67B0R7b1BT4PPzW2JlOXei/m9W12ZY484VTow6/B+kf2Q8
58 36 cP8tmCJmKWZma5Em7OTUhvjyQVNz3v7HfeY5Hq0Ci4ECJ59hepFDabJvtAXg9XrI6
59 37 jvdphZTc30I4fG8+hBHzpeFxUGvSGNtXPUbwaAY8j/oHYrTpMgkj6pUEFsiKfC5zP
60 38 qPFR5HyKTCHW0nFUJnZsbyFT5hMiF/hZkJc9A0ZbdSvJwCRQ/g3bmdL
61 39 your_email@example.com
62 40 """
63 41 VALID_KEY = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDk+77sjDzVeB6vev' \
64 42 'JsuZds1iNU5LANOa5CU5G/9JYIA6RYsWWMO7mbsR82IUckdqOHmxSy' \
65 43 'kfR1D1TdluyIpQLrwgH5kbn8FkVI8zBMCKakxowvN67B0R7b1BT4PP' \
66 44 'zW2JlOXei/m9W12ZY484VTow6/B+kf2Q8cP8tmCJmKWZma5Em7OTUh' \
67 45 'vjyQVNz3v7HfeY5Hq0Ci4ECJ59hepFDabJvtAXg9XrI6jvdphZTc30' \
68 46 'I4fG8+hBHzpeFxUGvSGNtXPUbwaAY8j/oHYrTpMgkj6pUEFsiKfC5zPq' \
69 47 'PFR5HyKTCHW0nFUJnZsbyFT5hMiF/hZkJc9A0ZbdSvJwCRQ/g3bmdL ' \
70 48 'your_email@example.com'
71 49 FINGERPRINT = 'MD5:01:4f:ad:29:22:6e:01:37:c9:d2:52:26:52:b0:2d:93'
72 50
73 51 def test_ssh_keys_default_user(self):
74 52 self.log_user()
75 53 user = User.get_default_user()
76 54 self.app.get(
77 55 route_path('edit_user_ssh_keys', user_id=user.user_id),
78 56 status=302)
79 57
80 58 def test_add_ssh_key_error(self, user_util):
81 59 self.log_user()
82 60 user = user_util.create_user()
83 61 user_id = user.user_id
84 62
85 63 key_data = self.INVALID_KEY
86 64
87 65 desc = 'MY SSH KEY'
88 66 response = self.app.post(
89 67 route_path('edit_user_ssh_keys_add', user_id=user_id),
90 68 {'description': desc, 'key_data': key_data,
91 69 'csrf_token': self.csrf_token})
92 70 assert_session_flash(response, 'An error occurred during ssh '
93 71 'key saving: Unable to decode the key')
94 72
95 73 def test_ssh_key_duplicate(self, user_util):
96 74 self.log_user()
97 75 user = user_util.create_user()
98 76 user_id = user.user_id
99 77
100 78 key_data = self.VALID_KEY
101 79
102 80 desc = 'MY SSH KEY'
103 81 response = self.app.post(
104 82 route_path('edit_user_ssh_keys_add', user_id=user_id),
105 83 {'description': desc, 'key_data': key_data,
106 84 'csrf_token': self.csrf_token})
107 85 assert_session_flash(response, 'Ssh Key successfully created')
108 86 response.follow() # flush session flash
109 87
110 88 # add the same key AGAIN
111 89 desc = 'MY SSH KEY'
112 90 response = self.app.post(
113 91 route_path('edit_user_ssh_keys_add', user_id=user_id),
114 92 {'description': desc, 'key_data': key_data,
115 93 'csrf_token': self.csrf_token})
116 94
117 95 err = 'Such key with fingerprint `{}` already exists, ' \
118 96 'please use a different one'.format(self.FINGERPRINT)
119 97 assert_session_flash(response, 'An error occurred during ssh key '
120 98 'saving: {}'.format(err))
121 99
122 100 def test_add_ssh_key(self, user_util):
123 101 self.log_user()
124 102 user = user_util.create_user()
125 103 user_id = user.user_id
126 104
127 105 key_data = self.VALID_KEY
128 106
129 107 desc = 'MY SSH KEY'
130 108 response = self.app.post(
131 109 route_path('edit_user_ssh_keys_add', user_id=user_id),
132 110 {'description': desc, 'key_data': key_data,
133 111 'csrf_token': self.csrf_token})
134 112 assert_session_flash(response, 'Ssh Key successfully created')
135 113
136 114 response = response.follow()
137 115 response.mustcontain(desc)
138 116
139 117 def test_delete_ssh_key(self, user_util):
140 118 self.log_user()
141 119 user = user_util.create_user()
142 120 user_id = user.user_id
143 121
144 122 key_data = self.VALID_KEY
145 123
146 124 desc = 'MY SSH KEY'
147 125 response = self.app.post(
148 126 route_path('edit_user_ssh_keys_add', user_id=user_id),
149 127 {'description': desc, 'key_data': key_data,
150 128 'csrf_token': self.csrf_token})
151 129 assert_session_flash(response, 'Ssh Key successfully created')
152 130 response = response.follow() # flush the Session flash
153 131
154 132 # now delete our key
155 133 keys = UserSshKeys.query().filter(UserSshKeys.user_id == user_id).all()
156 134 assert 1 == len(keys)
157 135
158 136 response = self.app.post(
159 137 route_path('edit_user_ssh_keys_delete', user_id=user_id),
160 138 {'del_ssh_key': keys[0].ssh_key_id,
161 139 'csrf_token': self.csrf_token})
162 140
163 141 assert_session_flash(response, 'Ssh key successfully deleted')
164 142 keys = UserSshKeys.query().filter(UserSshKeys.user_id == user_id).all()
165 143 assert 0 == len(keys)
166 144
167 145 def test_generate_keypair(self, user_util):
168 146 self.log_user()
169 147 user = user_util.create_user()
170 148 user_id = user.user_id
171 149
172 150 response = self.app.get(
173 151 route_path('edit_user_ssh_keys_generate_keypair', user_id=user_id))
174 152
175 153 response.mustcontain('Private key')
176 154 response.mustcontain('Public key')
177 155 response.mustcontain('-----BEGIN PRIVATE KEY-----')
@@ -1,261 +1,246 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18 import os
19 19 import pytest
20 20
21 21 from rhodecode.lib.ext_json import json
22 22 from rhodecode.model.auth_token import AuthTokenModel
23 23 from rhodecode.model.db import Session, FileStore, Repository, User
24 from rhodecode.tests import TestController
25 24 from rhodecode.apps.file_store import utils, config_keys
26 25
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 '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
26 from rhodecode.tests import TestController
27 from rhodecode.tests.routes import route_path
43 28
44 29
45 30 class TestFileStoreViews(TestController):
46 31
47 32 @pytest.mark.parametrize("fid, content, exists", [
48 33 ('abcde-0.jpg', "xxxxx", True),
49 34 ('abcde-0.exe', "1234567", True),
50 35 ('abcde-0.jpg', "xxxxx", False),
51 36 ])
52 37 def test_get_files_from_store(self, fid, content, exists, tmpdir, user_util):
53 38 user = self.log_user()
54 39 user_id = user['user_id']
55 40 repo_id = user_util.create_repo().repo_id
56 41 store_path = self.app._pyramid_settings[config_keys.store_path]
57 42 store_uid = fid
58 43
59 44 if exists:
60 45 status = 200
61 46 store = utils.get_file_storage({config_keys.store_path: store_path})
62 47 filesystem_file = os.path.join(str(tmpdir), fid)
63 48 with open(filesystem_file, 'wt') as f:
64 49 f.write(content)
65 50
66 51 with open(filesystem_file, 'rb') as f:
67 52 store_uid, metadata = store.save_file(f, fid, extra_metadata={'filename': fid})
68 53
69 54 entry = FileStore.create(
70 55 file_uid=store_uid, filename=metadata["filename"],
71 56 file_hash=metadata["sha256"], file_size=metadata["size"],
72 57 file_display_name='file_display_name',
73 58 file_description='repo artifact `{}`'.format(metadata["filename"]),
74 59 check_acl=True, user_id=user_id,
75 60 scope_repo_id=repo_id
76 61 )
77 62 Session().add(entry)
78 63 Session().commit()
79 64
80 65 else:
81 66 status = 404
82 67
83 68 response = self.app.get(route_path('download_file', fid=store_uid), status=status)
84 69
85 70 if exists:
86 71 assert response.text == content
87 72 file_store_path = os.path.dirname(store.resolve_name(store_uid, store_path)[1])
88 73 metadata_file = os.path.join(file_store_path, store_uid + '.meta')
89 74 assert os.path.exists(metadata_file)
90 75 with open(metadata_file, 'rb') as f:
91 76 json_data = json.loads(f.read())
92 77
93 78 assert json_data
94 79 assert 'size' in json_data
95 80
96 81 def test_upload_files_without_content_to_store(self):
97 82 self.log_user()
98 83 response = self.app.post(
99 84 route_path('upload_file'),
100 85 params={'csrf_token': self.csrf_token},
101 86 status=200)
102 87
103 88 assert response.json == {
104 89 'error': 'store_file data field is missing',
105 90 'access_path': None,
106 91 'store_fid': None}
107 92
108 93 def test_upload_files_bogus_content_to_store(self):
109 94 self.log_user()
110 95 response = self.app.post(
111 96 route_path('upload_file'),
112 97 params={'csrf_token': self.csrf_token, 'store_file': 'bogus'},
113 98 status=200)
114 99
115 100 assert response.json == {
116 101 'error': 'filename cannot be read from the data field',
117 102 'access_path': None,
118 103 'store_fid': None}
119 104
120 105 def test_upload_content_to_store(self):
121 106 self.log_user()
122 107 response = self.app.post(
123 108 route_path('upload_file'),
124 109 upload_files=[('store_file', b'myfile.txt', b'SOME CONTENT')],
125 110 params={'csrf_token': self.csrf_token},
126 111 status=200)
127 112
128 113 assert response.json['store_fid']
129 114
130 115 @pytest.fixture()
131 116 def create_artifact_factory(self, tmpdir):
132 117 def factory(user_id, content):
133 118 store_path = self.app._pyramid_settings[config_keys.store_path]
134 119 store = utils.get_file_storage({config_keys.store_path: store_path})
135 120 fid = 'example.txt'
136 121
137 122 filesystem_file = os.path.join(str(tmpdir), fid)
138 123 with open(filesystem_file, 'wt') as f:
139 124 f.write(content)
140 125
141 126 with open(filesystem_file, 'rb') as f:
142 127 store_uid, metadata = store.save_file(f, fid, extra_metadata={'filename': fid})
143 128
144 129 entry = FileStore.create(
145 130 file_uid=store_uid, filename=metadata["filename"],
146 131 file_hash=metadata["sha256"], file_size=metadata["size"],
147 132 file_display_name='file_display_name',
148 133 file_description='repo artifact `{}`'.format(metadata["filename"]),
149 134 check_acl=True, user_id=user_id,
150 135 )
151 136 Session().add(entry)
152 137 Session().commit()
153 138 return entry
154 139 return factory
155 140
156 141 def test_download_file_non_scoped(self, user_util, create_artifact_factory):
157 142 user = self.log_user()
158 143 user_id = user['user_id']
159 144 content = 'HELLO MY NAME IS ARTIFACT !'
160 145
161 146 artifact = create_artifact_factory(user_id, content)
162 147 file_uid = artifact.file_uid
163 148 response = self.app.get(route_path('download_file', fid=file_uid), status=200)
164 149 assert response.text == content
165 150
166 151 # log-in to new user and test download again
167 152 user = user_util.create_user(password='qweqwe')
168 153 self.log_user(user.username, 'qweqwe')
169 154 response = self.app.get(route_path('download_file', fid=file_uid), status=200)
170 155 assert response.text == content
171 156
172 157 def test_download_file_scoped_to_repo(self, user_util, create_artifact_factory):
173 158 user = self.log_user()
174 159 user_id = user['user_id']
175 160 content = 'HELLO MY NAME IS ARTIFACT !'
176 161
177 162 artifact = create_artifact_factory(user_id, content)
178 163 # bind to repo
179 164 repo = user_util.create_repo()
180 165 repo_id = repo.repo_id
181 166 artifact.scope_repo_id = repo_id
182 167 Session().add(artifact)
183 168 Session().commit()
184 169
185 170 file_uid = artifact.file_uid
186 171 response = self.app.get(route_path('download_file', fid=file_uid), status=200)
187 172 assert response.text == content
188 173
189 174 # log-in to new user and test download again
190 175 user = user_util.create_user(password='qweqwe')
191 176 self.log_user(user.username, 'qweqwe')
192 177 response = self.app.get(route_path('download_file', fid=file_uid), status=200)
193 178 assert response.text == content
194 179
195 180 # forbid user the rights to repo
196 181 repo = Repository.get(repo_id)
197 182 user_util.grant_user_permission_to_repo(repo, user, 'repository.none')
198 183 self.app.get(route_path('download_file', fid=file_uid), status=404)
199 184
200 185 def test_download_file_scoped_to_user(self, user_util, create_artifact_factory):
201 186 user = self.log_user()
202 187 user_id = user['user_id']
203 188 content = 'HELLO MY NAME IS ARTIFACT !'
204 189
205 190 artifact = create_artifact_factory(user_id, content)
206 191 # bind to user
207 192 user = user_util.create_user(password='qweqwe')
208 193
209 194 artifact.scope_user_id = user.user_id
210 195 Session().add(artifact)
211 196 Session().commit()
212 197
213 198 # artifact creator doesn't have access since it's bind to another user
214 199 file_uid = artifact.file_uid
215 200 self.app.get(route_path('download_file', fid=file_uid), status=404)
216 201
217 202 # log-in to new user and test download again, should be ok since we're bind to this artifact
218 203 self.log_user(user.username, 'qweqwe')
219 204 response = self.app.get(route_path('download_file', fid=file_uid), status=200)
220 205 assert response.text == content
221 206
222 207 def test_download_file_scoped_to_repo_with_bad_token(self, user_util, create_artifact_factory):
223 208 user_id = User.get_first_super_admin().user_id
224 209 content = 'HELLO MY NAME IS ARTIFACT !'
225 210
226 211 artifact = create_artifact_factory(user_id, content)
227 212 # bind to repo
228 213 repo = user_util.create_repo()
229 214 repo_id = repo.repo_id
230 215 artifact.scope_repo_id = repo_id
231 216 Session().add(artifact)
232 217 Session().commit()
233 218
234 219 file_uid = artifact.file_uid
235 220 self.app.get(route_path('download_file_by_token',
236 221 _auth_token='bogus', fid=file_uid), status=302)
237 222
238 223 def test_download_file_scoped_to_repo_with_token(self, user_util, create_artifact_factory):
239 224 user = User.get_first_super_admin()
240 225 AuthTokenModel().create(user, 'test artifact token',
241 226 role=AuthTokenModel.cls.ROLE_ARTIFACT_DOWNLOAD)
242 227
243 228 user = User.get_first_super_admin()
244 229 artifact_token = user.artifact_token
245 230
246 231 user_id = User.get_first_super_admin().user_id
247 232 content = 'HELLO MY NAME IS ARTIFACT !'
248 233
249 234 artifact = create_artifact_factory(user_id, content)
250 235 # bind to repo
251 236 repo = user_util.create_repo()
252 237 repo_id = repo.repo_id
253 238 artifact.scope_repo_id = repo_id
254 239 Session().add(artifact)
255 240 Session().commit()
256 241
257 242 file_uid = artifact.file_uid
258 243 response = self.app.get(
259 244 route_path('download_file_by_token',
260 245 _auth_token=artifact_token, fid=file_uid), status=200)
261 246 assert response.text == content
@@ -1,389 +1,365 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import mock
20 20 import pytest
21 21
22 22 from rhodecode.lib import helpers as h
23 23 from rhodecode.model.db import User, Gist
24 24 from rhodecode.model.gist import GistModel
25 25 from rhodecode.model.meta import Session
26 26 from rhodecode.tests import (
27 27 TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS,
28 28 TestController, assert_session_flash)
29
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
29 from rhodecode.tests.routes import route_path
54 30
55 31
56 32 class GistUtility(object):
57 33
58 34 def __init__(self):
59 35 self._gist_ids = []
60 36
61 37 def __call__(
62 38 self, f_name: bytes, content: bytes = b'some gist', lifetime=-1,
63 39 description='gist-desc', gist_type='public',
64 40 acl_level=Gist.GIST_PUBLIC, owner=TEST_USER_ADMIN_LOGIN):
65 41 gist_mapping = {
66 42 f_name: {'content': content}
67 43 }
68 44 user = User.get_by_username(owner)
69 45 gist = GistModel().create(
70 46 description, owner=user, gist_mapping=gist_mapping,
71 47 gist_type=gist_type, lifetime=lifetime, gist_acl_level=acl_level)
72 48 Session().commit()
73 49 self._gist_ids.append(gist.gist_id)
74 50 return gist
75 51
76 52 def cleanup(self):
77 53 for gist_id in self._gist_ids:
78 54 gist = Gist.get(gist_id)
79 55 if gist:
80 56 Session().delete(gist)
81 57
82 58 Session().commit()
83 59
84 60
85 61 @pytest.fixture()
86 62 def create_gist(request):
87 63 gist_utility = GistUtility()
88 64 request.addfinalizer(gist_utility.cleanup)
89 65 return gist_utility
90 66
91 67
92 68 class TestGistsController(TestController):
93 69
94 70 def test_index_empty(self, create_gist):
95 71 self.log_user()
96 72 response = self.app.get(route_path('gists_show'))
97 73 response.mustcontain('var gist_data = [];')
98 74
99 75 def test_index(self, create_gist):
100 76 self.log_user()
101 77 g1 = create_gist(b'gist1')
102 78 g2 = create_gist(b'gist2', lifetime=1400)
103 79 g3 = create_gist(b'gist3', description='gist3-desc')
104 80 g4 = create_gist(b'gist4', gist_type='private').gist_access_id
105 81 response = self.app.get(route_path('gists_show'))
106 82
107 83 response.mustcontain(g1.gist_access_id)
108 84 response.mustcontain(g2.gist_access_id)
109 85 response.mustcontain(g3.gist_access_id)
110 86 response.mustcontain('gist3-desc')
111 87 response.mustcontain(no=[g4])
112 88
113 89 # Expiration information should be visible
114 90 expires_tag = str(h.age_component(h.time_to_utcdatetime(g2.gist_expires)))
115 91 response.mustcontain(expires_tag.replace('"', '\\"'))
116 92
117 93 def test_index_private_gists(self, create_gist):
118 94 self.log_user()
119 95 gist = create_gist(b'gist5', gist_type='private')
120 96 response = self.app.get(route_path('gists_show', params=dict(private=1)))
121 97
122 98 # and privates
123 99 response.mustcontain(gist.gist_access_id)
124 100
125 101 def test_index_show_all(self, create_gist):
126 102 self.log_user()
127 103 create_gist(b'gist1')
128 104 create_gist(b'gist2', lifetime=1400)
129 105 create_gist(b'gist3', description='gist3-desc')
130 106 create_gist(b'gist4', gist_type='private')
131 107
132 108 response = self.app.get(route_path('gists_show', params=dict(all=1)))
133 109
134 110 assert len(GistModel.get_all()) == 4
135 111 # and privates
136 112 for gist in GistModel.get_all():
137 113 response.mustcontain(gist.gist_access_id)
138 114
139 115 def test_index_show_all_hidden_from_regular(self, create_gist):
140 116 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
141 117 create_gist(b'gist2', gist_type='private')
142 118 create_gist(b'gist3', gist_type='private')
143 119 create_gist(b'gist4', gist_type='private')
144 120
145 121 response = self.app.get(route_path('gists_show', params=dict(all=1)))
146 122
147 123 assert len(GistModel.get_all()) == 3
148 124 # since we don't have access to private in this view, we
149 125 # should see nothing
150 126 for gist in GistModel.get_all():
151 127 response.mustcontain(no=[gist.gist_access_id])
152 128
153 129 def test_create(self):
154 130 self.log_user()
155 131 response = self.app.post(
156 132 route_path('gists_create'),
157 133 params={'lifetime': -1,
158 134 'content': 'gist test',
159 135 'filename': 'foo',
160 136 'gist_type': 'public',
161 137 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
162 138 'csrf_token': self.csrf_token},
163 139 status=302)
164 140 response = response.follow()
165 141 response.mustcontain('added file: foo')
166 142 response.mustcontain('gist test')
167 143
168 144 def test_create_with_path_with_dirs(self):
169 145 self.log_user()
170 146 response = self.app.post(
171 147 route_path('gists_create'),
172 148 params={'lifetime': -1,
173 149 'content': 'gist test',
174 150 'filename': '/home/foo',
175 151 'gist_type': 'public',
176 152 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
177 153 'csrf_token': self.csrf_token},
178 154 status=200)
179 155 response.mustcontain('Filename /home/foo cannot be inside a directory')
180 156
181 157 def test_access_expired_gist(self, create_gist):
182 158 self.log_user()
183 159 gist = create_gist(b'never-see-me')
184 160 gist.gist_expires = 0 # 1970
185 161 Session().add(gist)
186 162 Session().commit()
187 163
188 164 self.app.get(route_path('gist_show', gist_id=gist.gist_access_id),
189 165 status=404)
190 166
191 167 def test_create_private(self):
192 168 self.log_user()
193 169 response = self.app.post(
194 170 route_path('gists_create'),
195 171 params={'lifetime': -1,
196 172 'content': 'private gist test',
197 173 'filename': 'private-foo',
198 174 'gist_type': 'private',
199 175 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
200 176 'csrf_token': self.csrf_token},
201 177 status=302)
202 178 response = response.follow()
203 179 response.mustcontain('added file: private-foo<')
204 180 response.mustcontain('private gist test')
205 181 response.mustcontain('Private Gist')
206 182 # Make sure private gists are not indexed by robots
207 183 response.mustcontain(
208 184 '<meta name="robots" content="noindex, nofollow">')
209 185
210 186 def test_create_private_acl_private(self):
211 187 self.log_user()
212 188 response = self.app.post(
213 189 route_path('gists_create'),
214 190 params={'lifetime': -1,
215 191 'content': 'private gist test',
216 192 'filename': 'private-foo',
217 193 'gist_type': 'private',
218 194 'gist_acl_level': Gist.ACL_LEVEL_PRIVATE,
219 195 'csrf_token': self.csrf_token},
220 196 status=302)
221 197 response = response.follow()
222 198 response.mustcontain('added file: private-foo<')
223 199 response.mustcontain('private gist test')
224 200 response.mustcontain('Private Gist')
225 201 # Make sure private gists are not indexed by robots
226 202 response.mustcontain(
227 203 '<meta name="robots" content="noindex, nofollow">')
228 204
229 205 def test_create_with_description(self):
230 206 self.log_user()
231 207 response = self.app.post(
232 208 route_path('gists_create'),
233 209 params={'lifetime': -1,
234 210 'content': 'gist test',
235 211 'filename': 'foo-desc',
236 212 'description': 'gist-desc',
237 213 'gist_type': 'public',
238 214 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
239 215 'csrf_token': self.csrf_token},
240 216 status=302)
241 217 response = response.follow()
242 218 response.mustcontain('added file: foo-desc')
243 219 response.mustcontain('gist test')
244 220 response.mustcontain('gist-desc')
245 221
246 222 def test_create_public_with_anonymous_access(self):
247 223 self.log_user()
248 224 params = {
249 225 'lifetime': -1,
250 226 'content': 'gist test',
251 227 'filename': 'foo-desc',
252 228 'description': 'gist-desc',
253 229 'gist_type': 'public',
254 230 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
255 231 'csrf_token': self.csrf_token
256 232 }
257 233 response = self.app.post(
258 234 route_path('gists_create'), params=params, status=302)
259 235 self.logout_user()
260 236 response = response.follow()
261 237 response.mustcontain('added file: foo-desc')
262 238 response.mustcontain('gist test')
263 239 response.mustcontain('gist-desc')
264 240
265 241 def test_new(self):
266 242 self.log_user()
267 243 self.app.get(route_path('gists_new'))
268 244
269 245 def test_delete(self, create_gist):
270 246 self.log_user()
271 247 gist = create_gist(b'delete-me')
272 248 response = self.app.post(
273 249 route_path('gist_delete', gist_id=gist.gist_id),
274 250 params={'csrf_token': self.csrf_token})
275 251 assert_session_flash(response, 'Deleted gist %s' % gist.gist_id)
276 252
277 253 def test_delete_normal_user_his_gist(self, create_gist):
278 254 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
279 255 gist = create_gist(b'delete-me', owner=TEST_USER_REGULAR_LOGIN)
280 256
281 257 response = self.app.post(
282 258 route_path('gist_delete', gist_id=gist.gist_id),
283 259 params={'csrf_token': self.csrf_token})
284 260 assert_session_flash(response, 'Deleted gist %s' % gist.gist_id)
285 261
286 262 def test_delete_normal_user_not_his_own_gist(self, create_gist):
287 263 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
288 264 gist = create_gist(b'delete-me-2')
289 265
290 266 self.app.post(
291 267 route_path('gist_delete', gist_id=gist.gist_id),
292 268 params={'csrf_token': self.csrf_token}, status=404)
293 269
294 270 def test_show(self, create_gist):
295 271 gist = create_gist(b'gist-show-me')
296 272 response = self.app.get(route_path('gist_show', gist_id=gist.gist_access_id))
297 273
298 274 response.mustcontain('added file: gist-show-me<')
299 275
300 276 assert_response = response.assert_response()
301 277 assert_response.element_equals_to(
302 278 'div.rc-user span.user',
303 279 '<a href="/_profiles/test_admin">test_admin</a>')
304 280
305 281 response.mustcontain('gist-desc')
306 282
307 283 def test_show_without_hg(self, create_gist):
308 284 with mock.patch(
309 285 'rhodecode.lib.vcs.settings.ALIASES', ['git']):
310 286 gist = create_gist(b'gist-show-me-again')
311 287 self.app.get(
312 288 route_path('gist_show', gist_id=gist.gist_access_id), status=200)
313 289
314 290 def test_show_acl_private(self, create_gist):
315 291 gist = create_gist(b'gist-show-me-only-when-im-logged-in',
316 292 acl_level=Gist.ACL_LEVEL_PRIVATE)
317 293 self.app.get(
318 294 route_path('gist_show', gist_id=gist.gist_access_id), status=404)
319 295
320 296 # now we log-in we should see thi gist
321 297 self.log_user()
322 298 response = self.app.get(
323 299 route_path('gist_show', gist_id=gist.gist_access_id))
324 300 response.mustcontain('added file: gist-show-me-only-when-im-logged-in')
325 301
326 302 assert_response = response.assert_response()
327 303 assert_response.element_equals_to(
328 304 'div.rc-user span.user',
329 305 '<a href="/_profiles/test_admin">test_admin</a>')
330 306 response.mustcontain('gist-desc')
331 307
332 308 def test_show_as_raw(self, create_gist):
333 309 gist = create_gist(b'gist-show-me', content=b'GIST CONTENT')
334 310 response = self.app.get(
335 311 route_path('gist_show_formatted',
336 312 gist_id=gist.gist_access_id, revision='tip',
337 313 format='raw'))
338 314 assert response.text == 'GIST CONTENT'
339 315
340 316 def test_show_as_raw_individual_file(self, create_gist):
341 317 gist = create_gist(b'gist-show-me-raw', content=b'GIST BODY')
342 318 response = self.app.get(
343 319 route_path('gist_show_formatted_path',
344 320 gist_id=gist.gist_access_id, format='raw',
345 321 revision='tip', f_path='gist-show-me-raw'))
346 322 assert response.text == 'GIST BODY'
347 323
348 324 def test_edit_page(self, create_gist):
349 325 self.log_user()
350 326 gist = create_gist(b'gist-for-edit', content=b'GIST EDIT BODY')
351 327 response = self.app.get(route_path('gist_edit', gist_id=gist.gist_access_id))
352 328 response.mustcontain('GIST EDIT BODY')
353 329
354 330 def test_edit_page_non_logged_user(self, create_gist):
355 331 gist = create_gist(b'gist-for-edit', content=b'GIST EDIT BODY')
356 332 self.app.get(route_path('gist_edit', gist_id=gist.gist_access_id),
357 333 status=302)
358 334
359 335 def test_edit_normal_user_his_gist(self, create_gist):
360 336 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
361 337 gist = create_gist(b'gist-for-edit', owner=TEST_USER_REGULAR_LOGIN)
362 338 self.app.get(route_path('gist_edit', gist_id=gist.gist_access_id,
363 339 status=200))
364 340
365 341 def test_edit_normal_user_not_his_own_gist(self, create_gist):
366 342 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
367 343 gist = create_gist(b'delete-me')
368 344 self.app.get(route_path('gist_edit', gist_id=gist.gist_access_id),
369 345 status=404)
370 346
371 347 def test_user_first_name_is_escaped(self, user_util, create_gist):
372 348 xss_atack_string = '"><script>alert(\'First Name\')</script>'
373 349 xss_escaped_string = h.html_escape(h.escape(xss_atack_string))
374 350 password = 'test'
375 351 user = user_util.create_user(
376 352 firstname=xss_atack_string, password=password)
377 353 create_gist(b'gist', gist_type='public', owner=user.username)
378 354 response = self.app.get(route_path('gists_show'))
379 355 response.mustcontain(xss_escaped_string)
380 356
381 357 def test_user_last_name_is_escaped(self, user_util, create_gist):
382 358 xss_atack_string = '"><script>alert(\'Last Name\')</script>'
383 359 xss_escaped_string = h.html_escape(h.escape(xss_atack_string))
384 360 password = 'test'
385 361 user = user_util.create_user(
386 362 lastname=xss_atack_string, password=password)
387 363 create_gist(b'gist', gist_type='public', owner=user.username)
388 364 response = self.app.get(route_path('gists_show'))
389 365 response.mustcontain(xss_escaped_string)
@@ -1,179 +1,167 b''
1 1 # Copyright (C) 2016-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import pytest
20 20
21 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 23 from rhodecode.lib.utils import map_groups
26 24 from rhodecode.lib.ext_json import json
27 25 from rhodecode.model.repo import RepoModel
28 26 from rhodecode.model.repo_group import RepoGroupModel
29 27 from rhodecode.model.db import Session, Repository, RepoGroup
30 28
31 fixture = Fixture()
32
33
34 def route_path(name, params=None, **kwargs):
35 import urllib.request
36 import urllib.parse
37 import urllib.error
29 from rhodecode.tests import TestController, TEST_USER_ADMIN_LOGIN
30 from rhodecode.tests.fixture import Fixture
31 from rhodecode.tests.routes import route_path
38 32
39 base_url = {
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
33 fixture = Fixture()
46 34
47 35
48 36 class TestGotoSwitcherData(TestController):
49 37
50 38 required_repos_with_groups = [
51 39 'abc',
52 40 'abc-fork',
53 41 'forks/abcd',
54 42 'abcd',
55 43 'abcde',
56 44 'a/abc',
57 45 'aa/abc',
58 46 'aaa/abc',
59 47 'aaaa/abc',
60 48 'repos_abc/aaa/abc',
61 49 'abc_repos/abc',
62 50 'abc_repos/abcd',
63 51 'xxx/xyz',
64 52 'forked-abc/a/abc'
65 53 ]
66 54
67 55 @pytest.fixture(autouse=True, scope='class')
68 56 def prepare(self, request, baseapp):
69 57 for repo_and_group in self.required_repos_with_groups:
70 58 # create structure of groups and return the last group
71 59
72 60 repo_group = map_groups(repo_and_group)
73 61
74 62 RepoModel()._create_repo(
75 63 repo_and_group, 'hg', 'test-ac', TEST_USER_ADMIN_LOGIN,
76 64 repo_group=getattr(repo_group, 'group_id', None))
77 65
78 66 Session().commit()
79 67
80 68 request.addfinalizer(self.cleanup)
81 69
82 70 def cleanup(self):
83 71 # first delete all repos
84 72 for repo_and_groups in self.required_repos_with_groups:
85 73 repo = Repository.get_by_repo_name(repo_and_groups)
86 74 if repo:
87 75 RepoModel().delete(repo)
88 76 Session().commit()
89 77
90 78 # then delete all empty groups
91 79 for repo_and_groups in self.required_repos_with_groups:
92 80 if '/' in repo_and_groups:
93 81 r_group = repo_and_groups.rsplit('/', 1)[0]
94 82 repo_group = RepoGroup.get_by_group_name(r_group)
95 83 if not repo_group:
96 84 continue
97 85 parents = repo_group.parents
98 86 RepoGroupModel().delete(repo_group, force_delete=True)
99 87 Session().commit()
100 88
101 89 for el in reversed(parents):
102 90 RepoGroupModel().delete(el, force_delete=True)
103 91 Session().commit()
104 92
105 93 def test_empty_query(self, xhr_header):
106 94 self.log_user()
107 95
108 96 response = self.app.get(
109 97 route_path('goto_switcher_data'),
110 98 extra_environ=xhr_header, status=200)
111 99 result = json.loads(response.body)['suggestions']
112 100
113 101 assert result == []
114 102
115 103 def test_returns_list_of_repos_and_groups_filtered(self, xhr_header):
116 104 self.log_user()
117 105
118 106 response = self.app.get(
119 107 route_path('goto_switcher_data'),
120 108 params={'query': 'abc'},
121 109 extra_environ=xhr_header, status=200)
122 110 result = json.loads(response.body)['suggestions']
123 111
124 112 repos, groups, users, commits = assert_and_get_main_filter_content(result)
125 113
126 114 assert len(repos) == 13
127 115 assert len(groups) == 5
128 116 assert len(users) == 0
129 117 assert len(commits) == 0
130 118
131 119 def test_returns_list_of_users_filtered(self, xhr_header):
132 120 self.log_user()
133 121
134 122 response = self.app.get(
135 123 route_path('goto_switcher_data'),
136 124 params={'query': 'user:admin'},
137 125 extra_environ=xhr_header, status=200)
138 126 result = json.loads(response.body)['suggestions']
139 127
140 128 repos, groups, users, commits = assert_and_get_main_filter_content(result)
141 129
142 130 assert len(repos) == 0
143 131 assert len(groups) == 0
144 132 assert len(users) == 1
145 133 assert len(commits) == 0
146 134
147 135 def test_returns_list_of_commits_filtered(self, xhr_header):
148 136 self.log_user()
149 137
150 138 response = self.app.get(
151 139 route_path('goto_switcher_data'),
152 140 params={'query': 'commit:e8'},
153 141 extra_environ=xhr_header, status=200)
154 142 result = json.loads(response.body)['suggestions']
155 143
156 144 repos, groups, users, commits = assert_and_get_main_filter_content(result)
157 145
158 146 assert len(repos) == 0
159 147 assert len(groups) == 0
160 148 assert len(users) == 0
161 149 assert len(commits) == 5
162 150
163 151 def test_returns_list_of_properly_sorted_and_filtered(self, xhr_header):
164 152 self.log_user()
165 153
166 154 response = self.app.get(
167 155 route_path('goto_switcher_data'),
168 156 params={'query': 'abc'},
169 157 extra_environ=xhr_header, status=200)
170 158 result = json.loads(response.body)['suggestions']
171 159
172 160 repos, groups, users, commits = assert_and_get_main_filter_content(result)
173 161
174 162 test_repos = [x['value_display'] for x in repos[:4]]
175 163 assert ['abc', 'abcd', 'a/abc', 'abcde'] == test_repos
176 164
177 165 test_groups = [x['value_display'] for x in groups[:4]]
178 166 assert ['abc_repos', 'repos_abc',
179 167 'forked-abc', 'forked-abc/a'] == test_groups
@@ -1,95 +1,83 b''
1 1 # Copyright (C) 2016-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 from . import assert_and_get_repo_list_content
20 from rhodecode.tests import TestController
21 from rhodecode.tests.fixture import Fixture
20
22 21 from rhodecode.model.db import Repository
23 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 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 31 class TestRepoListData(TestController):
44 32
45 33 def test_returns_list_of_repos_and_groups(self, xhr_header):
46 34 self.log_user()
47 35
48 36 response = self.app.get(
49 37 route_path('repo_list_data'),
50 38 extra_environ=xhr_header, status=200)
51 39 result = json.loads(response.body)['results']
52 40
53 41 repos = assert_and_get_repo_list_content(result)
54 42
55 43 assert len(repos) == len(Repository.get_all())
56 44
57 45 def test_returns_list_of_repos_and_groups_filtered(self, xhr_header):
58 46 self.log_user()
59 47
60 48 response = self.app.get(
61 49 route_path('repo_list_data'),
62 50 params={'query': 'vcs_test_git'},
63 51 extra_environ=xhr_header, status=200)
64 52 result = json.loads(response.body)['results']
65 53
66 54 repos = assert_and_get_repo_list_content(result)
67 55
68 56 assert len(repos) == len(Repository.query().filter(
69 57 Repository.repo_name.ilike('%vcs_test_git%')).all())
70 58
71 59 def test_returns_list_of_repos_and_groups_filtered_with_type(self, xhr_header):
72 60 self.log_user()
73 61
74 62 response = self.app.get(
75 63 route_path('repo_list_data'),
76 64 params={'query': 'vcs_test_git', 'repo_type': 'git'},
77 65 extra_environ=xhr_header, status=200)
78 66 result = json.loads(response.body)['results']
79 67
80 68 repos = assert_and_get_repo_list_content(result)
81 69
82 70 assert len(repos) == len(Repository.query().filter(
83 71 Repository.repo_name.ilike('%vcs_test_git%')).all())
84 72
85 73 def test_returns_list_of_repos_non_ascii_query(self, xhr_header):
86 74 self.log_user()
87 75 response = self.app.get(
88 76 route_path('repo_list_data'),
89 77 params={'query': 'ć_vcs_test_ą', 'repo_type': 'git'},
90 78 extra_environ=xhr_header, status=200)
91 79 result = json.loads(response.body)['results']
92 80
93 81 repos = assert_and_get_repo_list_content(result)
94 82
95 83 assert len(repos) == 0
@@ -1,110 +1,97 b''
1 1 # Copyright (C) 2016-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18 import pytest
19 19
20 from rhodecode.lib.ext_json import json
21
20 22 from rhodecode.tests import TestController
21 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 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 29 class TestUserAutocompleteData(TestController):
43 30
44 31 def test_returns_list_of_users(self, user_util, xhr_header):
45 32 self.log_user()
46 33 user = user_util.create_user(active=True)
47 34 user_name = user.username
48 35 response = self.app.get(
49 36 route_path('user_autocomplete_data'),
50 37 extra_environ=xhr_header, status=200)
51 38 result = json.loads(response.body)
52 39 values = [suggestion['value'] for suggestion in result['suggestions']]
53 40 assert user_name in values
54 41
55 42 def test_returns_inactive_users_when_active_flag_sent(
56 43 self, user_util, xhr_header):
57 44 self.log_user()
58 45 user = user_util.create_user(active=False)
59 46 user_name = user.username
60 47
61 48 response = self.app.get(
62 49 route_path('user_autocomplete_data',
63 50 params=dict(user_groups='true', active='0')),
64 51 extra_environ=xhr_header, status=200)
65 52 result = json.loads(response.body)
66 53 values = [suggestion['value'] for suggestion in result['suggestions']]
67 54 assert user_name in values
68 55
69 56 response = self.app.get(
70 57 route_path('user_autocomplete_data',
71 58 params=dict(user_groups='true', active='1')),
72 59 extra_environ=xhr_header, status=200)
73 60 result = json.loads(response.body)
74 61 values = [suggestion['value'] for suggestion in result['suggestions']]
75 62 assert user_name not in values
76 63
77 64 def test_returns_groups_when_user_groups_flag_sent(
78 65 self, user_util, xhr_header):
79 66 self.log_user()
80 67 group = user_util.create_user_group(user_groups_active=True)
81 68 group_name = group.users_group_name
82 69 response = self.app.get(
83 70 route_path('user_autocomplete_data',
84 71 params=dict(user_groups='true')),
85 72 extra_environ=xhr_header, status=200)
86 73 result = json.loads(response.body)
87 74 values = [suggestion['value'] for suggestion in result['suggestions']]
88 75 assert group_name in values
89 76
90 77 @pytest.mark.parametrize('query, count', [
91 78 ('hello1', 0),
92 79 ('dev', 2),
93 80 ])
94 81 def test_result_is_limited_when_query_is_sent(self, user_util, xhr_header,
95 82 query, count):
96 83 self.log_user()
97 84
98 85 user_util._test_name = 'dev-test'
99 86 user_util.create_user()
100 87
101 88 user_util._test_name = 'dev-group-test'
102 89 user_util.create_user_group()
103 90
104 91 response = self.app.get(
105 92 route_path('user_autocomplete_data',
106 93 params=dict(user_groups='true', query=query)),
107 94 extra_environ=xhr_header, status=200)
108 95
109 96 result = json.loads(response.body)
110 97 assert len(result['suggestions']) == count
@@ -1,116 +1,102 b''
1 1 # Copyright (C) 2016-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19
20 20 # Copyright (C) 2016-2023 RhodeCode GmbH
21 21 #
22 22 # This program is free software: you can redistribute it and/or modify
23 23 # it under the terms of the GNU Affero General Public License, version 3
24 24 # (only), as published by the Free Software Foundation.
25 25 #
26 26 # This program is distributed in the hope that it will be useful,
27 27 # but WITHOUT ANY WARRANTY; without even the implied warranty of
28 28 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
29 29 # GNU General Public License for more details.
30 30 #
31 31 # You should have received a copy of the GNU Affero General Public License
32 32 # along with this program. If not, see <http://www.gnu.org/licenses/>.
33 33 #
34 34 # This program is dual-licensed. If you wish to learn more about the
35 35 # RhodeCode Enterprise Edition, including its added features, Support services,
36 36 # and proprietary license terms, please see https://rhodecode.com/licenses/
37 37
38 38 import pytest
39 39
40 from rhodecode.lib.ext_json import json
41
40 42 from rhodecode.tests import TestController
41 43 from rhodecode.tests.fixture import Fixture
42 from rhodecode.lib.ext_json import json
43
44 from rhodecode.tests.routes import route_path
44 45
45 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 49 class TestUserGroupAutocompleteData(TestController):
64 50
65 51 def test_returns_list_of_user_groups(self, user_util, xhr_header):
66 52 self.log_user()
67 53 user_group = user_util.create_user_group(active=True)
68 54 user_group_name = user_group.users_group_name
69 55 response = self.app.get(
70 56 route_path('user_group_autocomplete_data'),
71 57 extra_environ=xhr_header, status=200)
72 58 result = json.loads(response.body)
73 59 values = [suggestion['value'] for suggestion in result['suggestions']]
74 60 assert user_group_name in values
75 61
76 62 def test_returns_inactive_user_groups_when_active_flag_sent(
77 63 self, user_util, xhr_header):
78 64 self.log_user()
79 65 user_group = user_util.create_user_group(active=False)
80 66 user_group_name = user_group.users_group_name
81 67
82 68 response = self.app.get(
83 69 route_path('user_group_autocomplete_data',
84 70 params=dict(active='0')),
85 71 extra_environ=xhr_header, status=200)
86 72 result = json.loads(response.body)
87 73 values = [suggestion['value'] for suggestion in result['suggestions']]
88 74 assert user_group_name in values
89 75
90 76 response = self.app.get(
91 77 route_path('user_group_autocomplete_data',
92 78 params=dict(active='1')),
93 79 extra_environ=xhr_header, status=200)
94 80 result = json.loads(response.body)
95 81 values = [suggestion['value'] for suggestion in result['suggestions']]
96 82 assert user_group_name not in values
97 83
98 84 @pytest.mark.parametrize('query, count', [
99 85 ('hello1', 0),
100 86 ('dev', 1),
101 87 ])
102 88 def test_result_is_limited_when_query_is_sent(self, user_util, xhr_header, query, count):
103 89 self.log_user()
104 90
105 91 user_util._test_name = 'dev-test'
106 92 user_util.create_user_group()
107 93
108 94 response = self.app.get(
109 95 route_path('user_group_autocomplete_data',
110 96 params=dict(user_groups='true',
111 97 query=query)),
112 98 extra_environ=xhr_header, status=200)
113 99
114 100 result = json.loads(response.body)
115 101
116 102 assert len(result['suggestions']) == count
@@ -1,177 +1,167 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19
20 20 import pytest
21 21
22 22 import rhodecode
23 23 from rhodecode.model.db import Repository, RepoGroup, User
24 24 from rhodecode.model.meta import Session
25 from rhodecode.model.repo import RepoModel
26 from rhodecode.model.repo_group import RepoGroupModel
27 25 from rhodecode.model.settings import SettingsModel
28 26 from rhodecode.tests import TestController
29 27 from rhodecode.tests.fixture import Fixture
30 from rhodecode.lib import helpers as h
31
32 fixture = Fixture()
28 from rhodecode.tests.routes import route_path
33 29
34 30
35 def route_path(name, **kwargs):
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)
31 fixture = Fixture()
42 32
43 33
44 34 class TestHomeController(TestController):
45 35
46 36 def test_index(self):
47 37 self.log_user()
48 38 response = self.app.get(route_path('home'))
49 39 # if global permission is set
50 40 response.mustcontain('New Repository')
51 41
52 42 def test_index_grid_repos(self, xhr_header):
53 43 self.log_user()
54 44 response = self.app.get(route_path('main_page_repos_data'), extra_environ=xhr_header)
55 45 # search for objects inside the JavaScript JSON
56 46 for obj in Repository.getAll():
57 47 response.mustcontain('<a href=\\"/{}\\">'.format(obj.repo_name))
58 48
59 49 def test_index_grid_repo_groups(self, xhr_header):
60 50 self.log_user()
61 51 response = self.app.get(route_path('main_page_repo_groups_data'),
62 52 extra_environ=xhr_header,)
63 53
64 54 # search for objects inside the JavaScript JSON
65 55 for obj in RepoGroup.getAll():
66 56 response.mustcontain('<a href=\\"/{}\\">'.format(obj.group_name))
67 57
68 58 def test_index_grid_repo_groups_without_access(self, xhr_header, user_util):
69 59 user = user_util.create_user(password='qweqwe')
70 60 group_ok = user_util.create_repo_group(owner=user)
71 61 group_id_ok = group_ok.group_id
72 62
73 63 group_forbidden = user_util.create_repo_group(owner=User.get_first_super_admin())
74 64 group_id_forbidden = group_forbidden.group_id
75 65
76 66 user_util.grant_user_permission_to_repo_group(group_forbidden, user, 'group.none')
77 67 self.log_user(user.username, 'qweqwe')
78 68
79 69 self.app.get(route_path('main_page_repo_groups_data'),
80 70 extra_environ=xhr_header,
81 71 params={'repo_group_id': group_id_ok}, status=200)
82 72
83 73 self.app.get(route_path('main_page_repo_groups_data'),
84 74 extra_environ=xhr_header,
85 75 params={'repo_group_id': group_id_forbidden}, status=404)
86 76
87 77 def test_index_contains_statics_with_ver(self):
88 78 from rhodecode.lib.base import calculate_version_hash
89 79
90 80 self.log_user()
91 81 response = self.app.get(route_path('home'))
92 82
93 83 rhodecode_version_hash = calculate_version_hash(
94 84 {'beaker.session.secret': 'test-rc-uytcxaz'})
95 85 response.mustcontain('style.css?ver={0}'.format(rhodecode_version_hash))
96 86 response.mustcontain('scripts.min.js?ver={0}'.format(rhodecode_version_hash))
97 87
98 88 def test_index_contains_backend_specific_details(self, backend, xhr_header):
99 89 self.log_user()
100 90 response = self.app.get(route_path('main_page_repos_data'), extra_environ=xhr_header)
101 91 tip = backend.repo.get_commit().raw_id
102 92
103 93 # html in javascript variable:
104 94 response.mustcontain(r'<i class=\"icon-%s\"' % (backend.alias, ))
105 95 response.mustcontain(r'href=\"/%s\"' % (backend.repo_name, ))
106 96
107 97 response.mustcontain("""/%s/changeset/%s""" % (backend.repo_name, tip))
108 98 response.mustcontain("""Added a symlink""")
109 99
110 100 def test_index_with_anonymous_access_disabled(self):
111 101 with fixture.anon_access(False):
112 102 response = self.app.get(route_path('home'), status=302)
113 103 assert 'login' in response.location
114 104
115 105 def test_index_page_on_groups_with_wrong_group_id(self, autologin_user, xhr_header):
116 106 group_id = 918123
117 107 self.app.get(
118 108 route_path('main_page_repo_groups_data'),
119 109 params={'repo_group_id': group_id},
120 110 status=404, extra_environ=xhr_header)
121 111
122 112 def test_index_page_on_groups(self, autologin_user, user_util, xhr_header):
123 113 gr = user_util.create_repo_group()
124 114 repo = user_util.create_repo(parent=gr)
125 115 repo_name = repo.repo_name
126 116 group_id = gr.group_id
127 117
128 118 response = self.app.get(route_path(
129 119 'repo_group_home', repo_group_name=gr.group_name))
130 120 response.mustcontain('d.repo_group_id = {}'.format(group_id))
131 121
132 122 response = self.app.get(
133 123 route_path('main_page_repos_data'),
134 124 params={'repo_group_id': group_id},
135 125 extra_environ=xhr_header,)
136 126 response.mustcontain(repo_name)
137 127
138 128 def test_index_page_on_group_with_trailing_slash(self, autologin_user, user_util, xhr_header):
139 129 gr = user_util.create_repo_group()
140 130 repo = user_util.create_repo(parent=gr)
141 131 repo_name = repo.repo_name
142 132 group_id = gr.group_id
143 133
144 134 response = self.app.get(route_path(
145 135 'repo_group_home', repo_group_name=gr.group_name+'/'))
146 136 response.mustcontain('d.repo_group_id = {}'.format(group_id))
147 137
148 138 response = self.app.get(
149 139 route_path('main_page_repos_data'),
150 140 params={'repo_group_id': group_id},
151 141 extra_environ=xhr_header, )
152 142 response.mustcontain(repo_name)
153 143
154 144 @pytest.mark.parametrize("name, state", [
155 145 ('Disabled', False),
156 146 ('Enabled', True),
157 147 ])
158 148 def test_index_show_version(self, autologin_user, name, state):
159 149 version_string = 'RhodeCode %s' % rhodecode.__version__
160 150
161 151 sett = SettingsModel().create_or_update_setting(
162 152 'show_version', state, 'bool')
163 153 Session().add(sett)
164 154 Session().commit()
165 155 SettingsModel().invalidate_settings_cache(hard=True)
166 156
167 157 response = self.app.get(route_path('home'))
168 158 if state is True:
169 159 response.mustcontain(version_string)
170 160 if state is False:
171 161 response.mustcontain(no=[version_string])
172 162
173 163 def test_logout_form_contains_csrf(self, autologin_user, csrf_token):
174 164 response = self.app.get(route_path('home'))
175 165 assert_response = response.assert_response()
176 166 element = assert_response.get_element('.logout [name=csrf_token]')
177 167 assert element.value == csrf_token
@@ -1,107 +1,87 b''
1 1
2 2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19
20 20 import datetime
21 21
22 22 import pytest
23 23
24 24 from rhodecode.apps._base import ADMIN_PREFIX
25 from rhodecode.tests import TestController
26 25 from rhodecode.model.db import UserFollowing, Repository
27 26
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 '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
27 from rhodecode.tests import TestController
28 from rhodecode.tests.routes import route_path
49 29
50 30
51 31 class TestJournalViews(TestController):
52 32
53 33 def test_journal(self):
54 34 self.log_user()
55 35 response = self.app.get(route_path('journal'))
56 36 # response.mustcontain(
57 37 # """<div class="journal_day">%s</div>""" % datetime.date.today())
58 38
59 39 @pytest.mark.parametrize("feed_type, content_type", [
60 40 ('rss', "application/rss+xml"),
61 41 ('atom', "application/atom+xml")
62 42 ])
63 43 def test_journal_feed(self, feed_type, content_type):
64 44 self.log_user()
65 45 response = self.app.get(
66 46 route_path(
67 47 'journal_{}'.format(feed_type)),
68 48 status=200)
69 49
70 50 assert response.content_type == content_type
71 51
72 52 def test_toggle_following_repository(self, backend):
73 53 user = self.log_user()
74 54 repo = Repository.get_by_repo_name(backend.repo_name)
75 55 repo_id = repo.repo_id
76 56 self.app.post(
77 57 route_path('toggle_following'), {'follows_repo_id': repo_id,
78 58 'csrf_token': self.csrf_token})
79 59
80 60 followings = UserFollowing.query()\
81 61 .filter(UserFollowing.user_id == user['user_id'])\
82 62 .filter(UserFollowing.follows_repo_id == repo_id).all()
83 63
84 64 assert len(followings) == 0
85 65
86 66 self.app.post(
87 67 route_path('toggle_following'), {'follows_repo_id': repo_id,
88 68 'csrf_token': self.csrf_token})
89 69
90 70 followings = UserFollowing.query()\
91 71 .filter(UserFollowing.user_id == user['user_id'])\
92 72 .filter(UserFollowing.follows_repo_id == repo_id).all()
93 73
94 74 assert len(followings) == 1
95 75
96 76 @pytest.mark.parametrize("feed_type, content_type", [
97 77 ('rss', "application/rss+xml"),
98 78 ('atom', "application/atom+xml")
99 79 ])
100 80 def test_public_journal_feed(self, feed_type, content_type):
101 81 self.log_user()
102 82 response = self.app.get(
103 83 route_path(
104 84 'journal_public_{}'.format(feed_type)),
105 85 status=200)
106 86
107 87 assert response.content_type == content_type
@@ -1,607 +1,581 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import urllib.parse
20 20
21 21 import mock
22 22 import pytest
23 23
24 from rhodecode.tests import (
25 assert_session_flash, HG_REPO, TEST_USER_ADMIN_LOGIN,
26 no_newline_id_generator)
27 from rhodecode.tests.fixture import Fixture
24
28 25 from rhodecode.lib.auth import check_password
29 26 from rhodecode.lib import helpers as h
30 27 from rhodecode.model.auth_token import AuthTokenModel
31 28 from rhodecode.model.db import User, Notification, UserApiKeys
32 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 37 fixture = Fixture()
35 38
36 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 42 @pytest.mark.usefixtures('app')
69 43 class TestLoginController(object):
70 44 destroy_users = set()
71 45
72 46 @classmethod
73 47 def teardown_class(cls):
74 48 fixture.destroy_users(cls.destroy_users)
75 49
76 50 def teardown_method(self, method):
77 51 for n in Notification.query().all():
78 52 Session().delete(n)
79 53
80 54 Session().commit()
81 55 assert Notification.query().all() == []
82 56
83 57 def test_index(self):
84 58 response = self.app.get(route_path('login'))
85 59 assert response.status == '200 OK'
86 60 # Test response...
87 61
88 62 def test_login_admin_ok(self):
89 63 response = self.app.post(route_path('login'),
90 64 {'username': 'test_admin',
91 65 'password': 'test12'}, status=302)
92 66 response = response.follow()
93 67 session = response.get_session_from_response()
94 68 username = session['rhodecode_user'].get('username')
95 69 assert username == 'test_admin'
96 70 response.mustcontain('logout')
97 71
98 72 def test_login_regular_ok(self):
99 73 response = self.app.post(route_path('login'),
100 74 {'username': 'test_regular',
101 75 'password': 'test12'}, status=302)
102 76
103 77 response = response.follow()
104 78 session = response.get_session_from_response()
105 79 username = session['rhodecode_user'].get('username')
106 80 assert username == 'test_regular'
107 81 response.mustcontain('logout')
108 82
109 83 def test_login_regular_forbidden_when_super_admin_restriction(self):
110 84 from rhodecode.authentication.plugins.auth_rhodecode import RhodeCodeAuthPlugin
111 85 with fixture.auth_restriction(self.app._pyramid_registry,
112 86 RhodeCodeAuthPlugin.AUTH_RESTRICTION_SUPER_ADMIN):
113 87 response = self.app.post(route_path('login'),
114 88 {'username': 'test_regular',
115 89 'password': 'test12'})
116 90
117 91 response.mustcontain('invalid user name')
118 92 response.mustcontain('invalid password')
119 93
120 94 def test_login_regular_forbidden_when_scope_restriction(self):
121 95 from rhodecode.authentication.plugins.auth_rhodecode import RhodeCodeAuthPlugin
122 96 with fixture.scope_restriction(self.app._pyramid_registry,
123 97 RhodeCodeAuthPlugin.AUTH_RESTRICTION_SCOPE_VCS):
124 98 response = self.app.post(route_path('login'),
125 99 {'username': 'test_regular',
126 100 'password': 'test12'})
127 101
128 102 response.mustcontain('invalid user name')
129 103 response.mustcontain('invalid password')
130 104
131 105 def test_login_ok_came_from(self):
132 106 test_came_from = '/_admin/users?branch=stable'
133 107 _url = '{}?came_from={}'.format(route_path('login'), test_came_from)
134 108 response = self.app.post(
135 109 _url, {'username': 'test_admin', 'password': 'test12'}, status=302)
136 110
137 111 assert 'branch=stable' in response.location
138 112 response = response.follow()
139 113
140 114 assert response.status == '200 OK'
141 115 response.mustcontain('Users administration')
142 116
143 117 def test_redirect_to_login_with_get_args(self):
144 118 with fixture.anon_access(False):
145 119 kwargs = {'branch': 'stable'}
146 120 response = self.app.get(
147 121 h.route_path('repo_summary', repo_name=HG_REPO, _query=kwargs),
148 122 status=302)
149 123
150 124 response_query = urllib.parse.parse_qsl(response.location)
151 125 assert 'branch=stable' in response_query[0][1]
152 126
153 127 def test_login_form_with_get_args(self):
154 128 _url = '{}?came_from=/_admin/users,branch=stable'.format(route_path('login'))
155 129 response = self.app.get(_url)
156 130 assert 'branch%3Dstable' in response.form.action
157 131
158 132 @pytest.mark.parametrize("url_came_from", [
159 133 'data:text/html,<script>window.alert("xss")</script>',
160 134 'mailto:test@rhodecode.org',
161 135 'file:///etc/passwd',
162 136 'ftp://some.ftp.server',
163 137 'http://other.domain',
164 138 ], ids=no_newline_id_generator)
165 139 def test_login_bad_came_froms(self, url_came_from):
166 140 _url = '{}?came_from={}'.format(route_path('login'), url_came_from)
167 141 response = self.app.post(
168 142 _url, {'username': 'test_admin', 'password': 'test12'}, status=302)
169 143 assert response.status == '302 Found'
170 144 response = response.follow()
171 145 assert response.status == '200 OK'
172 146 assert response.request.path == '/'
173 147
174 148 @pytest.mark.xfail(reason="newline params changed behaviour in python3")
175 149 @pytest.mark.parametrize("url_came_from", [
176 150 '/\r\nX-Forwarded-Host: \rhttp://example.org',
177 151 ], ids=no_newline_id_generator)
178 152 def test_login_bad_came_froms_404(self, url_came_from):
179 153 _url = '{}?came_from={}'.format(route_path('login'), url_came_from)
180 154 response = self.app.post(
181 155 _url, {'username': 'test_admin', 'password': 'test12'}, status=302)
182 156
183 157 response = response.follow()
184 158 assert response.status == '404 Not Found'
185 159
186 160 def test_login_short_password(self):
187 161 response = self.app.post(route_path('login'),
188 162 {'username': 'test_admin',
189 163 'password': 'as'})
190 164 assert response.status == '200 OK'
191 165
192 166 response.mustcontain('Enter 3 characters or more')
193 167
194 168 def test_login_wrong_non_ascii_password(self, user_regular):
195 169 response = self.app.post(
196 170 route_path('login'),
197 171 {'username': user_regular.username,
198 172 'password': 'invalid-non-asci\xe4'.encode('utf8')})
199 173
200 174 response.mustcontain('invalid user name')
201 175 response.mustcontain('invalid password')
202 176
203 177 def test_login_with_non_ascii_password(self, user_util):
204 178 password = u'valid-non-ascii\xe4'
205 179 user = user_util.create_user(password=password)
206 180 response = self.app.post(
207 181 route_path('login'),
208 182 {'username': user.username,
209 183 'password': password})
210 184 assert response.status_code == 302
211 185
212 186 def test_login_wrong_username_password(self):
213 187 response = self.app.post(route_path('login'),
214 188 {'username': 'error',
215 189 'password': 'test12'})
216 190
217 191 response.mustcontain('invalid user name')
218 192 response.mustcontain('invalid password')
219 193
220 194 def test_login_admin_ok_password_migration(self, real_crypto_backend):
221 195 from rhodecode.lib import auth
222 196
223 197 # create new user, with sha256 password
224 198 temp_user = 'test_admin_sha256'
225 199 user = fixture.create_user(temp_user)
226 200 user.password = auth._RhodeCodeCryptoSha256().hash_create(
227 201 b'test123')
228 202 Session().add(user)
229 203 Session().commit()
230 204 self.destroy_users.add(temp_user)
231 205 response = self.app.post(route_path('login'),
232 206 {'username': temp_user,
233 207 'password': 'test123'}, status=302)
234 208
235 209 response = response.follow()
236 210 session = response.get_session_from_response()
237 211 username = session['rhodecode_user'].get('username')
238 212 assert username == temp_user
239 213 response.mustcontain('logout')
240 214
241 215 # new password should be bcrypted, after log-in and transfer
242 216 user = User.get_by_username(temp_user)
243 217 assert user.password.startswith('$')
244 218
245 219 # REGISTRATIONS
246 220 def test_register(self):
247 221 response = self.app.get(route_path('register'))
248 222 response.mustcontain('Create an Account')
249 223
250 224 def test_register_err_same_username(self):
251 225 uname = 'test_admin'
252 226 response = self.app.post(
253 227 route_path('register'),
254 228 {
255 229 'username': uname,
256 230 'password': 'test12',
257 231 'password_confirmation': 'test12',
258 232 'email': 'goodmail@domain.com',
259 233 'firstname': 'test',
260 234 'lastname': 'test'
261 235 }
262 236 )
263 237
264 238 assertr = response.assert_response()
265 239 msg = 'Username "%(username)s" already exists'
266 240 msg = msg % {'username': uname}
267 241 assertr.element_contains('#username+.error-message', msg)
268 242
269 243 def test_register_err_same_email(self):
270 244 response = self.app.post(
271 245 route_path('register'),
272 246 {
273 247 'username': 'test_admin_0',
274 248 'password': 'test12',
275 249 'password_confirmation': 'test12',
276 250 'email': 'test_admin@mail.com',
277 251 'firstname': 'test',
278 252 'lastname': 'test'
279 253 }
280 254 )
281 255
282 256 assertr = response.assert_response()
283 257 msg = u'This e-mail address is already taken'
284 258 assertr.element_contains('#email+.error-message', msg)
285 259
286 260 def test_register_err_same_email_case_sensitive(self):
287 261 response = self.app.post(
288 262 route_path('register'),
289 263 {
290 264 'username': 'test_admin_1',
291 265 'password': 'test12',
292 266 'password_confirmation': 'test12',
293 267 'email': 'TesT_Admin@mail.COM',
294 268 'firstname': 'test',
295 269 'lastname': 'test'
296 270 }
297 271 )
298 272 assertr = response.assert_response()
299 273 msg = u'This e-mail address is already taken'
300 274 assertr.element_contains('#email+.error-message', msg)
301 275
302 276 def test_register_err_wrong_data(self):
303 277 response = self.app.post(
304 278 route_path('register'),
305 279 {
306 280 'username': 'xs',
307 281 'password': 'test',
308 282 'password_confirmation': 'test',
309 283 'email': 'goodmailm',
310 284 'firstname': 'test',
311 285 'lastname': 'test'
312 286 }
313 287 )
314 288 assert response.status == '200 OK'
315 289 response.mustcontain('An email address must contain a single @')
316 290 response.mustcontain('Enter a value 6 characters long or more')
317 291
318 292 def test_register_err_username(self):
319 293 response = self.app.post(
320 294 route_path('register'),
321 295 {
322 296 'username': 'error user',
323 297 'password': 'test12',
324 298 'password_confirmation': 'test12',
325 299 'email': 'goodmailm',
326 300 'firstname': 'test',
327 301 'lastname': 'test'
328 302 }
329 303 )
330 304
331 305 response.mustcontain('An email address must contain a single @')
332 306 response.mustcontain(
333 307 'Username may only contain '
334 308 'alphanumeric characters underscores, '
335 309 'periods or dashes and must begin with '
336 310 'alphanumeric character')
337 311
338 312 def test_register_err_case_sensitive(self):
339 313 usr = 'Test_Admin'
340 314 response = self.app.post(
341 315 route_path('register'),
342 316 {
343 317 'username': usr,
344 318 'password': 'test12',
345 319 'password_confirmation': 'test12',
346 320 'email': 'goodmailm',
347 321 'firstname': 'test',
348 322 'lastname': 'test'
349 323 }
350 324 )
351 325
352 326 assertr = response.assert_response()
353 327 msg = u'Username "%(username)s" already exists'
354 328 msg = msg % {'username': usr}
355 329 assertr.element_contains('#username+.error-message', msg)
356 330
357 331 def test_register_special_chars(self):
358 332 response = self.app.post(
359 333 route_path('register'),
360 334 {
361 335 'username': 'xxxaxn',
362 336 'password': 'ąćźżąśśśś',
363 337 'password_confirmation': 'ąćźżąśśśś',
364 338 'email': 'goodmailm@test.plx',
365 339 'firstname': 'test',
366 340 'lastname': 'test'
367 341 }
368 342 )
369 343
370 344 msg = u'Invalid characters (non-ascii) in password'
371 345 response.mustcontain(msg)
372 346
373 347 def test_register_password_mismatch(self):
374 348 response = self.app.post(
375 349 route_path('register'),
376 350 {
377 351 'username': 'xs',
378 352 'password': '123qwe',
379 353 'password_confirmation': 'qwe123',
380 354 'email': 'goodmailm@test.plxa',
381 355 'firstname': 'test',
382 356 'lastname': 'test'
383 357 }
384 358 )
385 359 msg = u'Passwords do not match'
386 360 response.mustcontain(msg)
387 361
388 362 def test_register_ok(self):
389 363 username = 'test_regular4'
390 364 password = 'qweqwe'
391 365 email = 'marcin@test.com'
392 366 name = 'testname'
393 367 lastname = 'testlastname'
394 368
395 369 # this initializes a session
396 370 response = self.app.get(route_path('register'))
397 371 response.mustcontain('Create an Account')
398 372
399 373
400 374 response = self.app.post(
401 375 route_path('register'),
402 376 {
403 377 'username': username,
404 378 'password': password,
405 379 'password_confirmation': password,
406 380 'email': email,
407 381 'firstname': name,
408 382 'lastname': lastname,
409 383 'admin': True
410 384 },
411 385 status=302
412 386 ) # This should be overridden
413 387
414 388 assert_session_flash(
415 389 response, 'You have successfully registered with RhodeCode. You can log-in now.')
416 390
417 391 ret = Session().query(User).filter(
418 392 User.username == 'test_regular4').one()
419 393 assert ret.username == username
420 394 assert check_password(password, ret.password)
421 395 assert ret.email == email
422 396 assert ret.name == name
423 397 assert ret.lastname == lastname
424 398 assert ret.auth_tokens is not None
425 399 assert not ret.admin
426 400
427 401 def test_forgot_password_wrong_mail(self):
428 402 bad_email = 'marcin@wrongmail.org'
429 403 # this initializes a session
430 404 self.app.get(route_path('reset_password'))
431 405
432 406 response = self.app.post(
433 407 route_path('reset_password'), {'email': bad_email, }
434 408 )
435 409 assert_session_flash(response,
436 410 'If such email exists, a password reset link was sent to it.')
437 411
438 412 def test_forgot_password(self, user_util):
439 413 # this initializes a session
440 414 self.app.get(route_path('reset_password'))
441 415
442 416 user = user_util.create_user()
443 417 user_id = user.user_id
444 418 email = user.email
445 419
446 420 response = self.app.post(route_path('reset_password'), {'email': email, })
447 421
448 422 assert_session_flash(response,
449 423 'If such email exists, a password reset link was sent to it.')
450 424
451 425 # BAD KEY
452 426 confirm_url = '{}?key={}'.format(route_path('reset_password_confirmation'), 'badkey')
453 427 response = self.app.get(confirm_url, status=302)
454 428 assert response.location.endswith(route_path('reset_password'))
455 429 assert_session_flash(response, 'Given reset token is invalid')
456 430
457 431 response.follow() # cleanup flash
458 432
459 433 # GOOD KEY
460 434 key = UserApiKeys.query()\
461 435 .filter(UserApiKeys.user_id == user_id)\
462 436 .filter(UserApiKeys.role == UserApiKeys.ROLE_PASSWORD_RESET)\
463 437 .first()
464 438
465 439 assert key
466 440
467 441 confirm_url = '{}?key={}'.format(route_path('reset_password_confirmation'), key.api_key)
468 442 response = self.app.get(confirm_url)
469 443 assert response.status == '302 Found'
470 444 assert response.location.endswith(route_path('login'))
471 445
472 446 assert_session_flash(
473 447 response,
474 448 'Your password reset was successful, '
475 449 'a new password has been sent to your email')
476 450
477 451 response.follow()
478 452
479 453 def _get_api_whitelist(self, values=None):
480 454 config = {'api_access_controllers_whitelist': values or []}
481 455 return config
482 456
483 457 @pytest.mark.parametrize("test_name, auth_token", [
484 458 ('none', None),
485 459 ('empty_string', ''),
486 460 ('fake_number', '123456'),
487 461 ('proper_auth_token', None)
488 462 ])
489 463 def test_access_not_whitelisted_page_via_auth_token(
490 464 self, test_name, auth_token, user_admin):
491 465
492 466 whitelist = self._get_api_whitelist([])
493 467 with mock.patch.dict('rhodecode.CONFIG', whitelist):
494 468 assert [] == whitelist['api_access_controllers_whitelist']
495 469 if test_name == 'proper_auth_token':
496 470 # use builtin if api_key is None
497 471 auth_token = user_admin.api_key
498 472
499 473 with fixture.anon_access(False):
500 474 # webtest uses linter to check if response is bytes,
501 475 # and we use memoryview here as a wrapper, quick turn-off
502 476 self.app.lint = False
503 477
504 478 self.app.get(
505 479 route_path('repo_commit_raw',
506 480 repo_name=HG_REPO, commit_id='tip',
507 481 params=dict(api_key=auth_token)),
508 482 status=302)
509 483
510 484 @pytest.mark.parametrize("test_name, auth_token, code", [
511 485 ('none', None, 302),
512 486 ('empty_string', '', 302),
513 487 ('fake_number', '123456', 302),
514 488 ('proper_auth_token', None, 200)
515 489 ])
516 490 def test_access_whitelisted_page_via_auth_token(
517 491 self, test_name, auth_token, code, user_admin):
518 492
519 493 whitelist = self._get_api_whitelist(whitelist_view)
520 494
521 495 with mock.patch.dict('rhodecode.CONFIG', whitelist):
522 496 assert whitelist_view == whitelist['api_access_controllers_whitelist']
523 497
524 498 if test_name == 'proper_auth_token':
525 499 auth_token = user_admin.api_key
526 500 assert auth_token
527 501
528 502 with fixture.anon_access(False):
529 503 # webtest uses linter to check if response is bytes,
530 504 # and we use memoryview here as a wrapper, quick turn-off
531 505 self.app.lint = False
532 506 self.app.get(
533 507 route_path('repo_commit_raw',
534 508 repo_name=HG_REPO, commit_id='tip',
535 509 params=dict(api_key=auth_token)),
536 510 status=code)
537 511
538 512 @pytest.mark.parametrize("test_name, auth_token, code", [
539 513 ('proper_auth_token', None, 200),
540 514 ('wrong_auth_token', '123456', 302),
541 515 ])
542 516 def test_access_whitelisted_page_via_auth_token_bound_to_token(
543 517 self, test_name, auth_token, code, user_admin):
544 518
545 519 expected_token = auth_token
546 520 if test_name == 'proper_auth_token':
547 521 auth_token = user_admin.api_key
548 522 expected_token = auth_token
549 523 assert auth_token
550 524
551 525 whitelist = self._get_api_whitelist([
552 526 'RepoCommitsView:repo_commit_raw@{}'.format(expected_token)])
553 527
554 528 with mock.patch.dict('rhodecode.CONFIG', whitelist):
555 529
556 530 with fixture.anon_access(False):
557 531 # webtest uses linter to check if response is bytes,
558 532 # and we use memoryview here as a wrapper, quick turn-off
559 533 self.app.lint = False
560 534
561 535 self.app.get(
562 536 route_path('repo_commit_raw',
563 537 repo_name=HG_REPO, commit_id='tip',
564 538 params=dict(api_key=auth_token)),
565 539 status=code)
566 540
567 541 def test_access_page_via_extra_auth_token(self):
568 542 whitelist = self._get_api_whitelist(whitelist_view)
569 543 with mock.patch.dict('rhodecode.CONFIG', whitelist):
570 544 assert whitelist_view == \
571 545 whitelist['api_access_controllers_whitelist']
572 546
573 547 new_auth_token = AuthTokenModel().create(
574 548 TEST_USER_ADMIN_LOGIN, 'test')
575 549 Session().commit()
576 550 with fixture.anon_access(False):
577 551 # webtest uses linter to check if response is bytes,
578 552 # and we use memoryview here as a wrapper, quick turn-off
579 553 self.app.lint = False
580 554 self.app.get(
581 555 route_path('repo_commit_raw',
582 556 repo_name=HG_REPO, commit_id='tip',
583 557 params=dict(api_key=new_auth_token.api_key)),
584 558 status=200)
585 559
586 560 def test_access_page_via_expired_auth_token(self):
587 561 whitelist = self._get_api_whitelist(whitelist_view)
588 562 with mock.patch.dict('rhodecode.CONFIG', whitelist):
589 563 assert whitelist_view == \
590 564 whitelist['api_access_controllers_whitelist']
591 565
592 566 new_auth_token = AuthTokenModel().create(
593 567 TEST_USER_ADMIN_LOGIN, 'test')
594 568 Session().commit()
595 569 # patch the api key and make it expired
596 570 new_auth_token.expires = 0
597 571 Session().add(new_auth_token)
598 572 Session().commit()
599 573 with fixture.anon_access(False):
600 574 # webtest uses linter to check if response is bytes,
601 575 # and we use memoryview here as a wrapper, quick turn-off
602 576 self.app.lint = False
603 577 self.app.get(
604 578 route_path('repo_commit_raw',
605 579 repo_name=HG_REPO, commit_id='tip',
606 580 params=dict(api_key=new_auth_token.api_key)),
607 581 status=302)
@@ -1,118 +1,94 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import pytest
20 20
21 21 from rhodecode.lib import helpers as h
22 22 from rhodecode.tests import (
23 23 TestController, clear_cache_regions,
24 24 TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS)
25 25 from rhodecode.tests.fixture import Fixture
26 26 from rhodecode.tests.utils import AssertResponse
27
28 fixture = Fixture()
27 from rhodecode.tests.routes import route_path
29 28
30 29
31 def route_path(name, params=None, **kwargs):
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
30 fixture = Fixture()
55 31
56 32
57 33 class TestPasswordReset(TestController):
58 34
59 35 @pytest.mark.parametrize(
60 36 'pwd_reset_setting, show_link, show_reset', [
61 37 ('hg.password_reset.enabled', True, True),
62 38 ('hg.password_reset.hidden', False, True),
63 39 ('hg.password_reset.disabled', False, False),
64 40 ])
65 41 def test_password_reset_settings(
66 42 self, pwd_reset_setting, show_link, show_reset):
67 43 clear_cache_regions()
68 44 self.log_user(TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS)
69 45 params = {
70 46 'csrf_token': self.csrf_token,
71 47 'anonymous': 'True',
72 48 'default_register': 'hg.register.auto_activate',
73 49 'default_register_message': '',
74 50 'default_password_reset': pwd_reset_setting,
75 51 'default_extern_activate': 'hg.extern_activate.auto',
76 52 }
77 53 resp = self.app.post(
78 54 route_path('admin_permissions_application_update'), params=params)
79 55 self.logout_user()
80 56
81 57 login_page = self.app.get(route_path('login'))
82 58 asr_login = AssertResponse(login_page)
83 59
84 60 if show_link:
85 61 asr_login.one_element_exists('a.pwd_reset')
86 62 else:
87 63 asr_login.no_element_exists('a.pwd_reset')
88 64
89 65 response = self.app.get(route_path('reset_password'))
90 66
91 67 assert_response = response.assert_response()
92 68 if show_reset:
93 69 response.mustcontain('Send password reset email')
94 70 assert_response.one_element_exists('#email')
95 71 assert_response.one_element_exists('#send')
96 72 else:
97 73 response.mustcontain('Password reset is disabled.')
98 74 assert_response.no_element_exists('#email')
99 75 assert_response.no_element_exists('#send')
100 76
101 77 def test_password_form_disabled(self):
102 78 self.log_user(TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS)
103 79 params = {
104 80 'csrf_token': self.csrf_token,
105 81 'anonymous': 'True',
106 82 'default_register': 'hg.register.auto_activate',
107 83 'default_register_message': '',
108 84 'default_password_reset': 'hg.password_reset.disabled',
109 85 'default_extern_activate': 'hg.extern_activate.auto',
110 86 }
111 87 self.app.post(route_path('admin_permissions_application_update'), params=params)
112 88 self.logout_user()
113 89
114 90 response = self.app.post(
115 91 route_path('reset_password'), {'email': 'lisa@rhodecode.com',}
116 92 )
117 93 response = response.follow()
118 94 response.mustcontain('Password reset is disabled.')
@@ -1,109 +1,98 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import pytest
20 20
21 21 from rhodecode.apps._base import ADMIN_PREFIX
22 22 from rhodecode.model.db import User
23 23 from rhodecode.tests import (
24 TestController, route_path_generator, assert_session_flash)
24 TestController, assert_session_flash)
25 25 from rhodecode.tests.fixture import Fixture
26 from rhodecode.tests.utils import AssertResponse
27
28 fixture = Fixture()
26 from rhodecode.tests.routes import route_path
29 27
30 28
31 def route_path(name, params=None, **kwargs):
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)
29 fixture = Fixture()
41 30
42 31
43 32 class TestMyAccountAuthTokens(TestController):
44 33
45 34 def test_my_account_auth_tokens(self):
46 35 usr = self.log_user('test_regular2', 'test12')
47 36 user = User.get(usr['user_id'])
48 37 response = self.app.get(route_path('my_account_auth_tokens'))
49 38 for token in user.auth_tokens:
50 39 response.mustcontain(token[:4])
51 40 response.mustcontain('never')
52 41
53 42 def test_my_account_add_auth_tokens_wrong_csrf(self, user_util):
54 43 user = user_util.create_user(password='qweqwe')
55 44 self.log_user(user.username, 'qweqwe')
56 45
57 46 self.app.post(
58 47 route_path('my_account_auth_tokens_add'),
59 48 {'description': 'desc', 'lifetime': -1}, status=403)
60 49
61 50 @pytest.mark.parametrize("desc, lifetime", [
62 51 ('forever', -1),
63 52 ('5mins', 60*5),
64 53 ('30days', 60*60*24*30),
65 54 ])
66 55 def test_my_account_add_auth_tokens(self, desc, lifetime, user_util):
67 56 user = user_util.create_user(password='qweqwe')
68 57 user_id = user.user_id
69 58 self.log_user(user.username, 'qweqwe')
70 59
71 60 response = self.app.post(
72 61 route_path('my_account_auth_tokens_add'),
73 62 {'description': desc, 'lifetime': lifetime,
74 63 'csrf_token': self.csrf_token})
75 64 assert_session_flash(response, 'Auth token successfully created')
76 65
77 66 response = response.follow()
78 67 user = User.get(user_id)
79 68 for auth_token in user.auth_tokens:
80 69 response.mustcontain(auth_token[:4])
81 70
82 71 def test_my_account_delete_auth_token(self, user_util):
83 72 user = user_util.create_user(password='qweqwe')
84 73 user_id = user.user_id
85 74 self.log_user(user.username, 'qweqwe')
86 75
87 76 user = User.get(user_id)
88 77 keys = user.get_auth_tokens()
89 78 assert 2 == len(keys)
90 79
91 80 response = self.app.post(
92 81 route_path('my_account_auth_tokens_add'),
93 82 {'description': 'desc', 'lifetime': -1,
94 83 'csrf_token': self.csrf_token})
95 84 assert_session_flash(response, 'Auth token successfully created')
96 85 response.follow()
97 86
98 87 user = User.get(user_id)
99 88 keys = user.get_auth_tokens()
100 89 assert 3 == len(keys)
101 90
102 91 response = self.app.post(
103 92 route_path('my_account_auth_tokens_delete'),
104 93 {'del_auth_token': keys[0].user_api_key_id, 'csrf_token': self.csrf_token})
105 94 assert_session_flash(response, 'Auth token successfully deleted')
106 95
107 96 user = User.get(user_id)
108 97 keys = user.auth_tokens
109 98 assert 2 == len(keys)
@@ -1,208 +1,190 b''
1 1 # Copyright (C) 2016-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19
20 20 # Copyright (C) 2016-2023 RhodeCode GmbH
21 21 #
22 22 # This program is free software: you can redistribute it and/or modify
23 23 # it under the terms of the GNU Affero General Public License, version 3
24 24 # (only), as published by the Free Software Foundation.
25 25 #
26 26 # This program is distributed in the hope that it will be useful,
27 27 # but WITHOUT ANY WARRANTY; without even the implied warranty of
28 28 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
29 29 # GNU General Public License for more details.
30 30 #
31 31 # You should have received a copy of the GNU Affero General Public License
32 32 # along with this program. If not, see <http://www.gnu.org/licenses/>.
33 33 #
34 34 # This program is dual-licensed. If you wish to learn more about the
35 35 # RhodeCode Enterprise Edition, including its added features, Support services,
36 36 # and proprietary license terms, please see https://rhodecode.com/licenses/
37 37
38 38 import pytest
39 39
40 40 from rhodecode.model.db import User
41 41 from rhodecode.tests import TestController, assert_session_flash
42 from rhodecode.lib import helpers as h
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
42 from rhodecode.tests.routes import route_path
61 43
62 44
63 45 class TestMyAccountEdit(TestController):
64 46
65 47 def test_my_account_edit(self):
66 48 self.log_user()
67 49 response = self.app.get(route_path('my_account_edit'))
68 50
69 51 response.mustcontain('value="test_admin')
70 52
71 53 @pytest.mark.backends("git", "hg")
72 54 def test_my_account_my_pullrequests(self, pr_util):
73 55 self.log_user()
74 56 response = self.app.get(route_path('my_account_pullrequests'))
75 57 response.mustcontain('There are currently no open pull '
76 58 'requests requiring your participation.')
77 59
78 60 @pytest.mark.backends("git", "hg")
79 61 @pytest.mark.parametrize('params, expected_title', [
80 62 ({'closed': 1}, 'Closed'),
81 63 ({'awaiting_my_review': 1}, 'Awaiting my review'),
82 64 ])
83 65 def test_my_account_my_pullrequests_data(self, pr_util, xhr_header, params, expected_title):
84 66 self.log_user()
85 67 response = self.app.get(route_path('my_account_pullrequests_data'),
86 68 extra_environ=xhr_header)
87 69 assert response.json == {
88 70 'data': [], 'draw': None,
89 71 'recordsFiltered': 0, 'recordsTotal': 0}
90 72
91 73 pr = pr_util.create_pull_request(title='TestMyAccountPR')
92 74 expected = {
93 75 'author_raw': 'RhodeCode Admin',
94 76 'name_raw': pr.pull_request_id
95 77 }
96 78 response = self.app.get(route_path('my_account_pullrequests_data'),
97 79 extra_environ=xhr_header)
98 80 assert response.json['recordsTotal'] == 1
99 81 assert response.json['data'][0]['author_raw'] == expected['author_raw']
100 82
101 83 assert response.json['data'][0]['author_raw'] == expected['author_raw']
102 84 assert response.json['data'][0]['name_raw'] == expected['name_raw']
103 85
104 86 @pytest.mark.parametrize(
105 87 "name, attrs", [
106 88 ('firstname', {'firstname': 'new_username'}),
107 89 ('lastname', {'lastname': 'new_username'}),
108 90 ('admin', {'admin': True}),
109 91 ('admin', {'admin': False}),
110 92 ('extern_type', {'extern_type': 'ldap'}),
111 93 ('extern_type', {'extern_type': None}),
112 94 # ('extern_name', {'extern_name': 'test'}),
113 95 # ('extern_name', {'extern_name': None}),
114 96 ('active', {'active': False}),
115 97 ('active', {'active': True}),
116 98 ('email', {'email': 'some@email.com'}),
117 99 ])
118 100 def test_my_account_update(self, name, attrs, user_util):
119 101 usr = user_util.create_user(password='qweqwe')
120 102 params = usr.get_api_data() # current user data
121 103 user_id = usr.user_id
122 104 self.log_user(
123 105 username=usr.username, password='qweqwe')
124 106
125 107 params.update({'password_confirmation': ''})
126 108 params.update({'new_password': ''})
127 109 params.update({'extern_type': 'rhodecode'})
128 110 params.update({'extern_name': 'rhodecode'})
129 111 params.update({'csrf_token': self.csrf_token})
130 112
131 113 params.update(attrs)
132 114 # my account page cannot set language param yet, only for admins
133 115 del params['language']
134 116 if name == 'email':
135 117 uem = user_util.create_additional_user_email(usr, attrs['email'])
136 118 email_before = User.get(user_id).email
137 119
138 120 response = self.app.post(route_path('my_account_update'), params)
139 121
140 122 assert_session_flash(
141 123 response, 'Your account was updated successfully')
142 124
143 125 del params['csrf_token']
144 126
145 127 updated_user = User.get(user_id)
146 128 updated_params = updated_user.get_api_data()
147 129 updated_params.update({'password_confirmation': ''})
148 130 updated_params.update({'new_password': ''})
149 131
150 132 params['last_login'] = updated_params['last_login']
151 133 params['last_activity'] = updated_params['last_activity']
152 134 # my account page cannot set language param yet, only for admins
153 135 # but we get this info from API anyway
154 136 params['language'] = updated_params['language']
155 137
156 138 if name == 'email':
157 139 params['emails'] = [attrs['email'], email_before]
158 140 if name == 'extern_type':
159 141 # cannot update this via form, expected value is original one
160 142 params['extern_type'] = "rhodecode"
161 143 if name == 'extern_name':
162 144 # cannot update this via form, expected value is original one
163 145 params['extern_name'] = str(user_id)
164 146 if name == 'active':
165 147 # my account cannot deactivate account
166 148 params['active'] = True
167 149 if name == 'admin':
168 150 # my account cannot make you an admin !
169 151 params['admin'] = False
170 152
171 153 assert params == updated_params
172 154
173 155 def test_my_account_update_err_email_not_exists_in_emails(self):
174 156 self.log_user()
175 157
176 158 new_email = 'test_regular@mail.com' # not in emails
177 159 params = {
178 160 'username': 'test_admin',
179 161 'new_password': 'test12',
180 162 'password_confirmation': 'test122',
181 163 'firstname': 'NewName',
182 164 'lastname': 'NewLastname',
183 165 'email': new_email,
184 166 'csrf_token': self.csrf_token,
185 167 }
186 168
187 169 response = self.app.post(route_path('my_account_update'),
188 170 params=params)
189 171
190 172 response.mustcontain('"test_regular@mail.com" is not one of test_admin@mail.com')
191 173
192 174 def test_my_account_update_bad_email_address(self):
193 175 self.log_user('test_regular2', 'test12')
194 176
195 177 new_email = 'newmail.pl'
196 178 params = {
197 179 'username': 'test_admin',
198 180 'new_password': 'test12',
199 181 'password_confirmation': 'test122',
200 182 'firstname': 'NewName',
201 183 'lastname': 'NewLastname',
202 184 'email': new_email,
203 185 'csrf_token': self.csrf_token,
204 186 }
205 187 response = self.app.post(route_path('my_account_update'),
206 188 params=params)
207 189
208 190 response.mustcontain('"newmail.pl" is not one of test_regular2@mail.com')
@@ -1,75 +1,66 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import pytest
20 20
21 21 from rhodecode.apps._base import ADMIN_PREFIX
22 22 from rhodecode.model.db import User, UserEmailMap
23 23 from rhodecode.tests import (
24 24 TestController, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_EMAIL,
25 25 assert_session_flash, TEST_USER_REGULAR_PASS)
26 26 from rhodecode.tests.fixture import Fixture
27
28 fixture = Fixture()
27 from rhodecode.tests.routes import route_path
29 28
30 29
31 def route_path(name, **kwargs):
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)
30 fixture = Fixture()
40 31
41 32
42 33 class TestMyAccountEmails(TestController):
43 34 def test_my_account_my_emails(self):
44 35 self.log_user()
45 36 response = self.app.get(route_path('my_account_emails'))
46 37 response.mustcontain('No additional emails specified')
47 38
48 39 def test_my_account_my_emails_add_remove(self):
49 40 self.log_user()
50 41 response = self.app.get(route_path('my_account_emails'))
51 42 response.mustcontain('No additional emails specified')
52 43
53 44 response = self.app.post(route_path('my_account_emails_add'),
54 45 {'email': 'foo@barz.com',
55 46 'current_password': TEST_USER_REGULAR_PASS,
56 47 'csrf_token': self.csrf_token})
57 48
58 49 response = self.app.get(route_path('my_account_emails'))
59 50
60 51 email_id = UserEmailMap.query().filter(
61 52 UserEmailMap.user == User.get_by_username(
62 53 TEST_USER_ADMIN_LOGIN)).filter(
63 54 UserEmailMap.email == 'foo@barz.com').one().email_id
64 55
65 56 response.mustcontain('foo@barz.com')
66 57 response.mustcontain('<input id="del_email_id" name="del_email_id" '
67 58 'type="hidden" value="%s" />' % email_id)
68 59
69 60 response = self.app.post(
70 61 route_path('my_account_emails_delete'), {
71 62 'del_email_id': email_id,
72 63 'csrf_token': self.csrf_token})
73 64 assert_session_flash(response, 'Email successfully deleted')
74 65 response = self.app.get(route_path('my_account_emails'))
75 66 response.mustcontain('No additional emails specified')
@@ -1,207 +1,186 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import pytest
20 20
21 from rhodecode.apps._base import ADMIN_PREFIX
22 21 from rhodecode.tests import (
23 22 TestController, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS,
24 23 TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS)
25 24 from rhodecode.tests.fixture import Fixture
25 from rhodecode.tests.routes import route_path
26 26
27 27 from rhodecode.model.db import Notification, User
28 from rhodecode.model.user import UserModel
29 28 from rhodecode.model.notification import NotificationModel
30 29 from rhodecode.model.meta import Session
31 30
32 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 34 class TestNotificationsController(TestController):
56 35
57 36 def teardown_method(self, method):
58 37 for n in Notification.query().all():
59 38 inst = Notification.get(n.notification_id)
60 39 Session().delete(inst)
61 40 Session().commit()
62 41
63 42 def test_mark_all_read(self, user_util):
64 43 user = user_util.create_user(password='qweqwe')
65 44 self.log_user(user.username, 'qweqwe')
66 45
67 46 self.app.post(
68 47 route_path('notifications_mark_all_read'), status=302,
69 48 params={'csrf_token': self.csrf_token}
70 49 )
71 50
72 51 def test_show_all(self, user_util):
73 52 user = user_util.create_user(password='qweqwe')
74 53 user_id = user.user_id
75 54 self.log_user(user.username, 'qweqwe')
76 55
77 56 response = self.app.get(
78 57 route_path('notifications_show_all', params={'type': 'all'}))
79 58 response.mustcontain(
80 59 '<div class="table">No notifications here yet</div>')
81 60
82 61 notification = NotificationModel().create(
83 62 created_by=user_id, notification_subject=u'test_notification_1',
84 63 notification_body=u'notification_1', recipients=[user_id])
85 64 Session().commit()
86 65 notification_id = notification.notification_id
87 66
88 67 response = self.app.get(route_path('notifications_show_all',
89 68 params={'type': 'all'}))
90 69 response.mustcontain('id="notification_%s"' % notification_id)
91 70
92 71 def test_show_unread(self, user_util):
93 72 user = user_util.create_user(password='qweqwe')
94 73 user_id = user.user_id
95 74 self.log_user(user.username, 'qweqwe')
96 75
97 76 response = self.app.get(route_path('notifications_show_all'))
98 77 response.mustcontain(
99 78 '<div class="table">No notifications here yet</div>')
100 79
101 80 notification = NotificationModel().create(
102 81 created_by=user_id, notification_subject=u'test_notification_1',
103 82 notification_body=u'notification_1', recipients=[user_id])
104 83
105 84 # mark the USER notification as unread
106 85 user_notification = NotificationModel().get_user_notification(
107 86 user_id, notification)
108 87 user_notification.read = False
109 88
110 89 Session().commit()
111 90 notification_id = notification.notification_id
112 91
113 92 response = self.app.get(route_path('notifications_show_all'))
114 93 response.mustcontain('id="notification_%s"' % notification_id)
115 94 response.mustcontain('<div class="desc unread')
116 95
117 96 @pytest.mark.parametrize('user,password', [
118 97 (TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS),
119 98 (TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS),
120 99 ])
121 100 def test_delete(self, user, password, user_util):
122 101 self.log_user(user, password)
123 102 cur_user = self._get_logged_user()
124 103
125 104 u1 = user_util.create_user()
126 105 u2 = user_util.create_user()
127 106
128 107 # make notifications
129 108 notification = NotificationModel().create(
130 109 created_by=cur_user, notification_subject=u'test',
131 110 notification_body=u'hi there', recipients=[cur_user, u1, u2])
132 111 Session().commit()
133 112 u1 = User.get(u1.user_id)
134 113 u2 = User.get(u2.user_id)
135 114
136 115 # check DB
137 116 def get_notif(un):
138 117 return [x.notification for x in un]
139 118 assert get_notif(cur_user.notifications) == [notification]
140 119 assert get_notif(u1.notifications) == [notification]
141 120 assert get_notif(u2.notifications) == [notification]
142 121 cur_usr_id = cur_user.user_id
143 122
144 123 response = self.app.post(
145 124 route_path('notifications_delete',
146 125 notification_id=notification.notification_id),
147 126 params={'csrf_token': self.csrf_token})
148 127 assert response.json == 'ok'
149 128
150 129 cur_user = User.get(cur_usr_id)
151 130 assert cur_user.notifications == []
152 131
153 132 @pytest.mark.parametrize('user,password', [
154 133 (TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS),
155 134 (TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS),
156 135 ])
157 136 def test_show(self, user, password, user_util):
158 137 self.log_user(user, password)
159 138 cur_user = self._get_logged_user()
160 139 u1 = user_util.create_user()
161 140 u2 = user_util.create_user()
162 141
163 142 subject = u'test'
164 143 notif_body = u'hi there'
165 144 notification = NotificationModel().create(
166 145 created_by=cur_user, notification_subject=subject,
167 146 notification_body=notif_body, recipients=[cur_user, u1, u2])
168 147 Session().commit()
169 148
170 149 response = self.app.get(
171 150 route_path('notifications_show',
172 151 notification_id=notification.notification_id))
173 152
174 153 response.mustcontain(subject)
175 154 response.mustcontain(notif_body)
176 155
177 156 @pytest.mark.parametrize('user,password', [
178 157 (TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS),
179 158 (TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS),
180 159 ])
181 160 def test_update(self, user, password, user_util):
182 161 self.log_user(user, password)
183 162 cur_user = self._get_logged_user()
184 163 u1 = user_util.create_user()
185 164 u2 = user_util.create_user()
186 165
187 166 # make notifications
188 167 recipients = [cur_user, u1, u2]
189 168 notification = NotificationModel().create(
190 169 created_by=cur_user, notification_subject=u'test',
191 170 notification_body=u'hi there', recipients=recipients)
192 171 Session().commit()
193 172
194 173 for u_obj in recipients:
195 174 # if it's current user, he has his message already read
196 175 read = u_obj.username == user
197 176 assert len(u_obj.notifications) == 1
198 177 assert u_obj.notifications[0].read == read
199 178
200 179 response = self.app.post(
201 180 route_path('notifications_update',
202 181 notification_id=notification.notification_id),
203 182 params={'csrf_token': self.csrf_token})
204 183 assert response.json == 'ok'
205 184
206 185 cur_user = self._get_logged_user()
207 186 assert True is cur_user.notifications[0].read
@@ -1,143 +1,134 b''
1 1
2 2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19
20 20 import pytest
21 21 import mock
22 22
23 23 from rhodecode.apps._base import ADMIN_PREFIX
24 24 from rhodecode.lib import helpers as h
25 25 from rhodecode.lib.auth import check_password
26 26 from rhodecode.model.meta import Session
27 27 from rhodecode.model.user import UserModel
28 28 from rhodecode.tests import assert_session_flash
29 29 from rhodecode.tests.fixture import Fixture, TestController, error_function
30 from rhodecode.tests.routes import route_path
30 31
31 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 35 test_user_1 = 'testme'
45 36 test_user_1_password = '0jd83nHNS/d23n'
46 37
47 38
48 39 class TestMyAccountPassword(TestController):
49 40 def test_valid_change_password(self, user_util):
50 41 new_password = 'my_new_valid_password'
51 42 user = user_util.create_user(password=test_user_1_password)
52 43 self.log_user(user.username, test_user_1_password)
53 44
54 45 form_data = [
55 46 ('current_password', test_user_1_password),
56 47 ('__start__', 'new_password:mapping'),
57 48 ('new_password', new_password),
58 49 ('new_password-confirm', new_password),
59 50 ('__end__', 'new_password:mapping'),
60 51 ('csrf_token', self.csrf_token),
61 52 ]
62 53 response = self.app.post(
63 54 route_path('my_account_password_update'), form_data).follow()
64 55 assert 'Successfully updated password' in response
65 56
66 57 # check_password depends on user being in session
67 58 Session().add(user)
68 59 try:
69 60 assert check_password(new_password, user.password)
70 61 finally:
71 62 Session().expunge(user)
72 63
73 64 @pytest.mark.parametrize('current_pw, new_pw, confirm_pw', [
74 65 ('', 'abcdef123', 'abcdef123'),
75 66 ('wrong_pw', 'abcdef123', 'abcdef123'),
76 67 (test_user_1_password, test_user_1_password, test_user_1_password),
77 68 (test_user_1_password, '', ''),
78 69 (test_user_1_password, 'abcdef123', ''),
79 70 (test_user_1_password, '', 'abcdef123'),
80 71 (test_user_1_password, 'not_the', 'same_pw'),
81 72 (test_user_1_password, 'short', 'short'),
82 73 ])
83 74 def test_invalid_change_password(self, current_pw, new_pw, confirm_pw,
84 75 user_util):
85 76 user = user_util.create_user(password=test_user_1_password)
86 77 self.log_user(user.username, test_user_1_password)
87 78
88 79 form_data = [
89 80 ('current_password', current_pw),
90 81 ('__start__', 'new_password:mapping'),
91 82 ('new_password', new_pw),
92 83 ('new_password-confirm', confirm_pw),
93 84 ('__end__', 'new_password:mapping'),
94 85 ('csrf_token', self.csrf_token),
95 86 ]
96 87 response = self.app.post(
97 88 route_path('my_account_password_update'), form_data)
98 89
99 90 assert_response = response.assert_response()
100 91 assert assert_response.get_elements('.error-block')
101 92
102 93 @mock.patch.object(UserModel, 'update_user', error_function)
103 94 def test_invalid_change_password_exception(self, user_util):
104 95 user = user_util.create_user(password=test_user_1_password)
105 96 self.log_user(user.username, test_user_1_password)
106 97
107 98 form_data = [
108 99 ('current_password', test_user_1_password),
109 100 ('__start__', 'new_password:mapping'),
110 101 ('new_password', '123456'),
111 102 ('new_password-confirm', '123456'),
112 103 ('__end__', 'new_password:mapping'),
113 104 ('csrf_token', self.csrf_token),
114 105 ]
115 106 response = self.app.post(
116 107 route_path('my_account_password_update'), form_data)
117 108 assert_session_flash(
118 109 response, 'Error occurred during update of user password')
119 110
120 111 def test_password_is_updated_in_session_on_password_change(self, user_util):
121 112 old_password = 'abcdef123'
122 113 new_password = 'abcdef124'
123 114
124 115 user = user_util.create_user(password=old_password)
125 116 session = self.log_user(user.username, old_password)
126 117 old_password_hash = session['password']
127 118
128 119 form_data = [
129 120 ('current_password', old_password),
130 121 ('__start__', 'new_password:mapping'),
131 122 ('new_password', new_password),
132 123 ('new_password-confirm', new_password),
133 124 ('__end__', 'new_password:mapping'),
134 125 ('csrf_token', self.csrf_token),
135 126 ]
136 127 self.app.post(
137 128 route_path('my_account_password_update'), form_data)
138 129
139 130 response = self.app.get(route_path('home'))
140 131 session = response.get_session_from_response()
141 132 new_password_hash = session['rhodecode_user']['password']
142 133
143 134 assert old_password_hash != new_password_hash No newline at end of file
@@ -1,54 +1,45 b''
1 1
2 2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 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 20 from rhodecode.tests import (
24 21 TestController, TEST_USER_ADMIN_LOGIN,
25 22 TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
26 23 from rhodecode.tests.fixture import Fixture
24 from rhodecode.tests.routes import route_path
27 25
28 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 29 class TestMyAccountProfile(TestController):
39 30
40 31 def test_my_account(self):
41 32 self.log_user()
42 33 response = self.app.get(route_path('my_account'))
43 34
44 35 response.mustcontain(TEST_USER_ADMIN_LOGIN)
45 36 response.mustcontain('href="/_admin/my_account/edit"')
46 37 response.mustcontain('Photo')
47 38
48 39 def test_my_account_regular_user(self):
49 40 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
50 41 response = self.app.get(route_path('my_account'))
51 42
52 43 response.mustcontain(TEST_USER_REGULAR_LOGIN)
53 44 response.mustcontain('href="/_admin/my_account/edit"')
54 45 response.mustcontain('Photo')
@@ -1,74 +1,57 b''
1 1
2 2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 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.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)
20 from rhodecode.model.db import User, Repository, UserFollowing
21 from rhodecode.tests import TestController, TEST_USER_ADMIN_LOGIN
27 22 from rhodecode.tests.fixture import Fixture
23 from rhodecode.tests.routes import route_path
28 24
29 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 28 class TestMyAccountSimpleViews(TestController):
46 29
47 30 def test_my_account_my_repos(self, autologin_user):
48 31 response = self.app.get(route_path('my_account_repos'))
49 32 repos = Repository.query().filter(
50 33 Repository.user == User.get_by_username(
51 34 TEST_USER_ADMIN_LOGIN)).all()
52 35 for repo in repos:
53 36 response.mustcontain(f'"name_raw":"{repo.repo_name}"')
54 37
55 38 def test_my_account_my_watched(self, autologin_user):
56 39 response = self.app.get(route_path('my_account_watched'))
57 40
58 41 repos = UserFollowing.query().filter(
59 42 UserFollowing.user == User.get_by_username(
60 43 TEST_USER_ADMIN_LOGIN)).all()
61 44 for repo in repos:
62 45 response.mustcontain(f'"name_raw":"{repo.follows_repository.repo_name}"')
63 46
64 47 def test_my_account_perms(self, autologin_user):
65 48 response = self.app.get(route_path('my_account_perms'))
66 49 assert_response = response.assert_response()
67 50 assert assert_response.get_elements('.perm_tag.none')
68 51 assert assert_response.get_elements('.perm_tag.read')
69 52 assert assert_response.get_elements('.perm_tag.write')
70 53 assert assert_response.get_elements('.perm_tag.admin')
71 54
72 55 def test_my_account_notifications(self, autologin_user):
73 56 response = self.app.get(route_path('my_account_notifications'))
74 57 response.mustcontain('Test flash message')
@@ -1,164 +1,142 b''
1 1
2 2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19
20 import pytest
21 20
22 21 from rhodecode.model.db import User, UserSshKeys
23 22
24 23 from rhodecode.tests import TestController, assert_session_flash
25 24 from rhodecode.tests.fixture import Fixture
25 from rhodecode.tests.routes import route_path
26 26
27 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 30 class TestMyAccountSshKeysView(TestController):
53 31 INVALID_KEY = """\
54 32 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDk+77sjDzVeB6vevJsuZds1iNU5
55 33 LANOa5CU5G/9JYIA6RYsWWMO7mbsR82IUckdqOHmxSykfR1D1TdluyIpQLrwgH5kb
56 34 n8FkVI8zBMCKakxowvN67B0R7b1BT4PPzW2JlOXei/m9W12ZY484VTow6/B+kf2Q8
57 35 cP8tmCJmKWZma5Em7OTUhvjyQVNz3v7HfeY5Hq0Ci4ECJ59hepFDabJvtAXg9XrI6
58 36 jvdphZTc30I4fG8+hBHzpeFxUGvSGNtXPUbwaAY8j/oHYrTpMgkj6pUEFsiKfC5zP
59 37 qPFR5HyKTCHW0nFUJnZsbyFT5hMiF/hZkJc9A0ZbdSvJwCRQ/g3bmdL
60 38 your_email@example.com
61 39 """
62 40 VALID_KEY = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDk+77sjDzVeB6vev' \
63 41 'JsuZds1iNU5LANOa5CU5G/9JYIA6RYsWWMO7mbsR82IUckdqOHmxSy' \
64 42 'kfR1D1TdluyIpQLrwgH5kbn8FkVI8zBMCKakxowvN67B0R7b1BT4PP' \
65 43 'zW2JlOXei/m9W12ZY484VTow6/B+kf2Q8cP8tmCJmKWZma5Em7OTUh' \
66 44 'vjyQVNz3v7HfeY5Hq0Ci4ECJ59hepFDabJvtAXg9XrI6jvdphZTc30' \
67 45 'I4fG8+hBHzpeFxUGvSGNtXPUbwaAY8j/oHYrTpMgkj6pUEFsiKfC5zPq' \
68 46 'PFR5HyKTCHW0nFUJnZsbyFT5hMiF/hZkJc9A0ZbdSvJwCRQ/g3bmdL ' \
69 47 'your_email@example.com'
70 48 FINGERPRINT = 'MD5:01:4f:ad:29:22:6e:01:37:c9:d2:52:26:52:b0:2d:93'
71 49
72 50 def test_add_ssh_key_error(self, user_util):
73 51 user = user_util.create_user(password='qweqwe')
74 52 self.log_user(user.username, 'qweqwe')
75 53
76 54 key_data = self.INVALID_KEY
77 55
78 56 desc = 'MY SSH KEY'
79 57 response = self.app.post(
80 58 route_path('my_account_ssh_keys_add'),
81 59 {'description': desc, 'key_data': key_data,
82 60 'csrf_token': self.csrf_token})
83 61 assert_session_flash(response, 'An error occurred during ssh '
84 62 'key saving: Unable to decode the key')
85 63
86 64 def test_ssh_key_duplicate(self, user_util):
87 65 user = user_util.create_user(password='qweqwe')
88 66 self.log_user(user.username, 'qweqwe')
89 67 key_data = self.VALID_KEY
90 68
91 69 desc = 'MY SSH KEY'
92 70 response = self.app.post(
93 71 route_path('my_account_ssh_keys_add'),
94 72 {'description': desc, 'key_data': key_data,
95 73 'csrf_token': self.csrf_token})
96 74 assert_session_flash(response, 'Ssh Key successfully created')
97 75 response.follow() # flush session flash
98 76
99 77 # add the same key AGAIN
100 78 desc = 'MY SSH KEY'
101 79 response = self.app.post(
102 80 route_path('my_account_ssh_keys_add'),
103 81 {'description': desc, 'key_data': key_data,
104 82 'csrf_token': self.csrf_token})
105 83
106 84 err = 'Such key with fingerprint `{}` already exists, ' \
107 85 'please use a different one'.format(self.FINGERPRINT)
108 86 assert_session_flash(response, 'An error occurred during ssh key '
109 87 'saving: {}'.format(err))
110 88
111 89 def test_add_ssh_key(self, user_util):
112 90 user = user_util.create_user(password='qweqwe')
113 91 self.log_user(user.username, 'qweqwe')
114 92
115 93 key_data = self.VALID_KEY
116 94
117 95 desc = 'MY SSH KEY'
118 96 response = self.app.post(
119 97 route_path('my_account_ssh_keys_add'),
120 98 {'description': desc, 'key_data': key_data,
121 99 'csrf_token': self.csrf_token})
122 100 assert_session_flash(response, 'Ssh Key successfully created')
123 101
124 102 response = response.follow()
125 103 response.mustcontain(desc)
126 104
127 105 def test_delete_ssh_key(self, user_util):
128 106 user = user_util.create_user(password='qweqwe')
129 107 user_id = user.user_id
130 108 self.log_user(user.username, 'qweqwe')
131 109
132 110 key_data = self.VALID_KEY
133 111
134 112 desc = 'MY SSH KEY'
135 113 response = self.app.post(
136 114 route_path('my_account_ssh_keys_add'),
137 115 {'description': desc, 'key_data': key_data,
138 116 'csrf_token': self.csrf_token})
139 117 assert_session_flash(response, 'Ssh Key successfully created')
140 118 response = response.follow() # flush the Session flash
141 119
142 120 # now delete our key
143 121 keys = UserSshKeys.query().filter(UserSshKeys.user_id == user_id).all()
144 122 assert 1 == len(keys)
145 123
146 124 response = self.app.post(
147 125 route_path('my_account_ssh_keys_delete'),
148 126 {'del_ssh_key': keys[0].ssh_key_id,
149 127 'csrf_token': self.csrf_token})
150 128
151 129 assert_session_flash(response, 'Ssh key successfully deleted')
152 130 keys = UserSshKeys.query().filter(UserSshKeys.user_id == user_id).all()
153 131 assert 0 == len(keys)
154 132
155 133 def test_generate_keypair(self, user_util):
156 134 user = user_util.create_user(password='qweqwe')
157 135 self.log_user(user.username, 'qweqwe')
158 136
159 137 response = self.app.get(
160 138 route_path('my_account_ssh_keys_generate'))
161 139
162 140 response.mustcontain('Private key')
163 141 response.mustcontain('Public key')
164 142 response.mustcontain('-----BEGIN PRIVATE KEY-----')
@@ -1,89 +1,73 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import pytest
20 20
21 21 from rhodecode.tests import assert_session_flash
22
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
22 from rhodecode.tests.routes import route_path
39 23
40 24
41 25 @pytest.mark.usefixtures("app")
42 26 class TestRepoGroupsAdvancedView(object):
43 27
44 28 @pytest.mark.parametrize('repo_group_name', [
45 29 'gro',
46 30 '12345',
47 31 ])
48 32 def test_show_advanced_settings(self, autologin_user, user_util, repo_group_name):
49 33 user_util._test_name = repo_group_name
50 34 gr = user_util.create_repo_group()
51 35 self.app.get(
52 36 route_path('edit_repo_group_advanced',
53 37 repo_group_name=gr.group_name))
54 38
55 39 def test_show_advanced_settings_delete(self, autologin_user, user_util,
56 40 csrf_token):
57 41 gr = user_util.create_repo_group(auto_cleanup=False)
58 42 repo_group_name = gr.group_name
59 43
60 44 params = dict(
61 45 csrf_token=csrf_token
62 46 )
63 47 response = self.app.post(
64 48 route_path('edit_repo_group_advanced_delete',
65 49 repo_group_name=repo_group_name), params=params)
66 50 assert_session_flash(
67 51 response, 'Removed repository group `{}`'.format(repo_group_name))
68 52
69 53 def test_delete_not_possible_with_objects_inside(self, autologin_user,
70 54 repo_groups, csrf_token):
71 55 zombie_group, parent_group, child_group = repo_groups
72 56
73 57 response = self.app.get(
74 58 route_path('edit_repo_group_advanced',
75 59 repo_group_name=parent_group.group_name))
76 60
77 61 response.mustcontain(
78 62 'This repository group includes 1 children repository group')
79 63
80 64 params = dict(
81 65 csrf_token=csrf_token
82 66 )
83 67 response = self.app.post(
84 68 route_path('edit_repo_group_advanced_delete',
85 69 repo_group_name=parent_group.group_name), params=params)
86 70
87 71 assert_session_flash(
88 72 response, 'This repository group contains 1 subgroup '
89 73 'and cannot be deleted')
@@ -1,86 +1,70 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import pytest
20 20
21 21 from rhodecode.tests.utils import permission_update_data_generator
22
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
22 from rhodecode.tests.routes import route_path
39 23
40 24
41 25 @pytest.mark.usefixtures("app")
42 26 class TestRepoGroupPermissionsView(object):
43 27
44 28 def test_edit_perms_view(self, user_util, autologin_user):
45 29 repo_group = user_util.create_repo_group()
46 30
47 31 self.app.get(
48 32 route_path('edit_repo_group_perms',
49 33 repo_group_name=repo_group.group_name), status=200)
50 34
51 35 def test_update_permissions(self, csrf_token, user_util):
52 36 repo_group = user_util.create_repo_group()
53 37 repo_group_name = repo_group.group_name
54 38 user = user_util.create_user()
55 39 user_id = user.user_id
56 40 username = user.username
57 41
58 42 # grant new
59 43 form_data = permission_update_data_generator(
60 44 csrf_token,
61 45 default='group.write',
62 46 grant=[(user_id, 'group.write', username, 'user')])
63 47
64 48 # recursive flag required for repo groups
65 49 form_data.extend([('recursive', u'none')])
66 50
67 51 response = self.app.post(
68 52 route_path('edit_repo_group_perms_update',
69 53 repo_group_name=repo_group_name), form_data).follow()
70 54
71 55 assert 'Repository Group permissions updated' in response
72 56
73 57 # revoke given
74 58 form_data = permission_update_data_generator(
75 59 csrf_token,
76 60 default='group.read',
77 61 revoke=[(user_id, 'user')])
78 62
79 63 # recursive flag required for repo groups
80 64 form_data.extend([('recursive', u'none')])
81 65
82 66 response = self.app.post(
83 67 route_path('edit_repo_group_perms_update',
84 68 repo_group_name=repo_group_name), form_data).follow()
85 69
86 70 assert 'Repository Group permissions updated' in response
@@ -1,91 +1,78 b''
1 1
2 2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19
20 20 import pytest
21 21
22 22 from rhodecode.tests import assert_session_flash
23
24
25 def route_path(name, params=None, **kwargs):
26 import urllib.request
27 import urllib.parse
28 import urllib.error
23 from rhodecode.tests.routes import route_path
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 27 @pytest.mark.usefixtures("app")
41 28 class TestRepoGroupsSettingsView(object):
42 29
43 30 @pytest.mark.parametrize('repo_group_name', [
44 31 'gro',
45 32 u'12345',
46 33 ])
47 34 def test_edit(self, user_util, autologin_user, repo_group_name):
48 35 user_util._test_name = repo_group_name
49 36 repo_group = user_util.create_repo_group()
50 37
51 38 self.app.get(
52 39 route_path('edit_repo_group', repo_group_name=repo_group.group_name),
53 40 status=200)
54 41
55 42 def test_update(self, csrf_token, autologin_user, user_util, rc_fixture):
56 43 repo_group = user_util.create_repo_group()
57 44 repo_group_name = repo_group.group_name
58 45
59 46 description = 'description for newly created repo group'
60 47 form_data = rc_fixture._get_group_create_params(
61 48 group_name=repo_group.group_name,
62 49 group_description=description,
63 50 csrf_token=csrf_token,
64 51 repo_group_name=repo_group.group_name,
65 52 repo_group_owner=repo_group.user.username)
66 53
67 54 response = self.app.post(
68 55 route_path('edit_repo_group',
69 56 repo_group_name=repo_group.group_name),
70 57 form_data,
71 58 status=302)
72 59
73 60 assert_session_flash(
74 61 response, 'Repository Group `{}` updated successfully'.format(
75 62 repo_group_name))
76 63
77 64 def test_update_fails_when_parent_pointing_to_self(
78 65 self, csrf_token, user_util, autologin_user, rc_fixture):
79 66 group = user_util.create_repo_group()
80 67 response = self.app.post(
81 68 route_path('edit_repo_group', repo_group_name=group.group_name),
82 69 rc_fixture._get_group_create_params(
83 70 repo_group_name=group.group_name,
84 71 repo_group_owner=group.user.username,
85 72 repo_group=group.group_id,
86 73 csrf_token=csrf_token),
87 74 status=200
88 75 )
89 76 response.mustcontain(
90 77 '<span class="error-message">"{}" is not one of -1'.format(
91 78 group.group_id))
@@ -1,84 +1,69 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import pytest
20 from rhodecode.model.db import Repository
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
20 from rhodecode.tests.routes import route_path
36 21
37 22
38 23 @pytest.mark.backends("git", "hg")
39 24 @pytest.mark.usefixtures('autologin_user', 'app')
40 25 class TestPullRequestList(object):
41 26
42 27 @pytest.mark.parametrize('params, expected_title', [
43 28 ({'source': 0, 'closed': 1}, 'Closed'),
44 29 ({'source': 0, 'my': 1}, 'Created by me'),
45 30 ({'source': 0, 'awaiting_review': 1}, 'Awaiting review'),
46 31 ({'source': 0, 'awaiting_my_review': 1}, 'Awaiting my review'),
47 32 ({'source': 1}, 'From this repo'),
48 33 ])
49 34 def test_showing_list_page(self, backend, pr_util, params, expected_title):
50 35 pull_request = pr_util.create_pull_request()
51 36
52 37 response = self.app.get(
53 38 route_path('pullrequest_show_all',
54 39 repo_name=pull_request.target_repo.repo_name,
55 40 params=params))
56 41
57 42 assert_response = response.assert_response()
58 43
59 44 element = assert_response.get_element('.title .active')
60 45 element_text = element.text_content()
61 46 assert expected_title == element_text
62 47
63 48 def test_showing_list_page_data(self, backend, pr_util, xhr_header):
64 49 pull_request = pr_util.create_pull_request()
65 50 response = self.app.get(
66 51 route_path('pullrequest_show_all_data',
67 52 repo_name=pull_request.target_repo.repo_name),
68 53 extra_environ=xhr_header)
69 54
70 55 assert response.json['recordsTotal'] == 1
71 56 assert response.json['data'][0]['description'] == 'Description'
72 57
73 58 def test_description_is_escaped_on_index_page(self, backend, pr_util, xhr_header):
74 59 xss_description = "<script>alert('Hi!')</script>"
75 60 pull_request = pr_util.create_pull_request(description=xss_description)
76 61
77 62 response = self.app.get(
78 63 route_path('pullrequest_show_all_data',
79 64 repo_name=pull_request.target_repo.repo_name),
80 65 extra_environ=xhr_header)
81 66
82 67 assert response.json['recordsTotal'] == 1
83 68 assert response.json['data'][0]['description'] == \
84 69 "&lt;script&gt;alert(&#39;Hi!&#39;)&lt;/script&gt;"
@@ -1,52 +1,40 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import pytest
20 20 from rhodecode.model.db import Repository
21
22
23 def route_path(name, params=None, **kwargs):
24 import urllib.request
25 import urllib.parse
26 import urllib.error
21 from rhodecode.tests.routes import route_path
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 25 @pytest.mark.usefixtures('autologin_user', 'app')
38 26 class TestBookmarks(object):
39 27
40 28 def test_index(self, backend):
41 29 if backend.alias == 'hg':
42 30 response = self.app.get(
43 31 route_path('bookmarks_home', repo_name=backend.repo_name))
44 32
45 33 repo = Repository.get_by_repo_name(backend.repo_name)
46 34 for commit_id, obj_name in repo.scm_instance().bookmarks.items():
47 35 assert commit_id in response
48 36 assert obj_name in response
49 37 else:
50 38 self.app.get(
51 39 route_path('bookmarks_home', repo_name=backend.repo_name),
52 40 status=404)
@@ -1,48 +1,35 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import pytest
20 20 from rhodecode.model.db import Repository
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 '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
21 from rhodecode.tests.routes import route_path
35 22
36 23
37 24 @pytest.mark.usefixtures('autologin_user', 'app')
38 25 class TestBranchesController(object):
39 26
40 27 def test_index(self, backend):
41 28 response = self.app.get(
42 29 route_path('branches_home', repo_name=backend.repo_name))
43 30
44 31 repo = Repository.get_by_repo_name(backend.repo_name)
45 32
46 33 for commit_id, obj_name in repo.scm_instance().branches.items():
47 34 assert commit_id in response
48 35 assert obj_name in response
@@ -1,219 +1,204 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import re
20 20
21 21 import pytest
22 22
23 23 from rhodecode.apps.repository.views.repo_changelog import DEFAULT_CHANGELOG_SIZE
24 24 from rhodecode.tests import TestController
25
26 MATCH_HASH = re.compile(r'<span class="commit_hash">r(\d+):[\da-f]+</span>')
25 from rhodecode.tests.routes import route_path
27 26
28 27
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_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
28 MATCH_HASH = re.compile(r'<span class="commit_hash">r(\d+):[\da-f]+</span>')
44 29
45 30
46 31 def assert_commits_on_page(response, indexes):
47 32 found_indexes = [int(idx) for idx in MATCH_HASH.findall(response.text)]
48 33 assert found_indexes == indexes
49 34
50 35
51 36 class TestChangelogController(TestController):
52 37
53 38 def test_commits_page(self, backend):
54 39 self.log_user()
55 40 response = self.app.get(
56 41 route_path('repo_commits', repo_name=backend.repo_name))
57 42
58 43 first_idx = -1
59 44 last_idx = -DEFAULT_CHANGELOG_SIZE
60 45 self.assert_commit_range_on_page(response, first_idx, last_idx, backend)
61 46
62 47 def test_changelog(self, backend):
63 48 self.log_user()
64 49 response = self.app.get(
65 50 route_path('repo_changelog', repo_name=backend.repo_name))
66 51
67 52 first_idx = -1
68 53 last_idx = -DEFAULT_CHANGELOG_SIZE
69 54 self.assert_commit_range_on_page(
70 55 response, first_idx, last_idx, backend)
71 56
72 57 @pytest.mark.backends("hg", "git")
73 58 def test_changelog_filtered_by_branch(self, backend):
74 59 self.log_user()
75 60 self.app.get(
76 61 route_path('repo_changelog', repo_name=backend.repo_name,
77 62 params=dict(branch=backend.default_branch_name)),
78 63 status=200)
79 64
80 65 @pytest.mark.backends("hg", "git")
81 66 def test_commits_filtered_by_branch(self, backend):
82 67 self.log_user()
83 68 self.app.get(
84 69 route_path('repo_commits', repo_name=backend.repo_name,
85 70 params=dict(branch=backend.default_branch_name)),
86 71 status=200)
87 72
88 73 @pytest.mark.backends("svn")
89 74 def test_changelog_filtered_by_branch_svn(self, autologin_user, backend):
90 75 repo = backend['svn-simple-layout']
91 76 response = self.app.get(
92 77 route_path('repo_changelog', repo_name=repo.repo_name,
93 78 params=dict(branch='trunk')),
94 79 status=200)
95 80
96 81 assert_commits_on_page(response, indexes=[15, 12, 7, 3, 2, 1])
97 82
98 83 def test_commits_filtered_by_wrong_branch(self, backend):
99 84 self.log_user()
100 85 branch = 'wrong-branch-name'
101 86 response = self.app.get(
102 87 route_path('repo_commits', repo_name=backend.repo_name,
103 88 params=dict(branch=branch)),
104 89 status=302)
105 90 expected_url = '/{repo}/commits/{branch}'.format(
106 91 repo=backend.repo_name, branch=branch)
107 92 assert expected_url in response.location
108 93 response = response.follow()
109 94 expected_warning = f'Branch {branch} is not found.'
110 95 assert expected_warning in response.text
111 96
112 97 @pytest.mark.xfail_backends("svn", reason="Depends on branch support")
113 98 def test_changelog_filtered_by_branch_with_merges(self, autologin_user, backend):
114 99
115 100 # Note: The changelog of branch "b" does not contain the commit "a1"
116 101 # although this is a parent of commit "b1". And branch "b" has commits
117 102 # which have a smaller index than commit "a1".
118 103 commits = [
119 104 {'message': 'a'},
120 105 {'message': 'b', 'branch': 'b'},
121 106 {'message': 'a1', 'parents': ['a']},
122 107 {'message': 'b1', 'branch': 'b', 'parents': ['b', 'a1']},
123 108 ]
124 109 backend.create_repo(commits)
125 110
126 111 self.app.get(
127 112 route_path('repo_changelog', repo_name=backend.repo_name,
128 113 params=dict(branch='b')),
129 114 status=200)
130 115
131 116 @pytest.mark.backends("hg")
132 117 def test_commits_closed_branches(self, autologin_user, backend):
133 118 repo = backend['closed_branch']
134 119 response = self.app.get(
135 120 route_path('repo_commits', repo_name=repo.repo_name,
136 121 params=dict(branch='experimental')),
137 122 status=200)
138 123
139 124 assert_commits_on_page(response, indexes=[3, 1])
140 125
141 126 def test_changelog_pagination(self, backend):
142 127 self.log_user()
143 128 # pagination, walk up to page 6
144 129 changelog_url = route_path(
145 130 'repo_commits', repo_name=backend.repo_name)
146 131
147 132 for page in range(1, 7):
148 133 response = self.app.get(changelog_url, {'page': page})
149 134
150 135 first_idx = -DEFAULT_CHANGELOG_SIZE * (page - 1) - 1
151 136 last_idx = -DEFAULT_CHANGELOG_SIZE * page
152 137 self.assert_commit_range_on_page(response, first_idx, last_idx, backend)
153 138
154 139 def assert_commit_range_on_page(
155 140 self, response, first_idx, last_idx, backend):
156 141 input_template = (
157 142 """<input class="commit-range" """
158 143 """data-commit-id="%(raw_id)s" data-commit-idx="%(idx)s" """
159 144 """data-short-id="%(short_id)s" id="%(raw_id)s" """
160 145 """name="%(raw_id)s" type="checkbox" value="1" />"""
161 146 )
162 147
163 148 commit_span_template = """<span class="commit_hash">r%s:%s</span>"""
164 149 repo = backend.repo
165 150
166 151 first_commit_on_page = repo.get_commit(commit_idx=first_idx)
167 152 response.mustcontain(
168 153 input_template % {'raw_id': first_commit_on_page.raw_id,
169 154 'idx': first_commit_on_page.idx,
170 155 'short_id': first_commit_on_page.short_id})
171 156
172 157 response.mustcontain(commit_span_template % (
173 158 first_commit_on_page.idx, first_commit_on_page.short_id)
174 159 )
175 160
176 161 last_commit_on_page = repo.get_commit(commit_idx=last_idx)
177 162 response.mustcontain(
178 163 input_template % {'raw_id': last_commit_on_page.raw_id,
179 164 'idx': last_commit_on_page.idx,
180 165 'short_id': last_commit_on_page.short_id})
181 166 response.mustcontain(commit_span_template % (
182 167 last_commit_on_page.idx, last_commit_on_page.short_id)
183 168 )
184 169
185 170 first_commit_of_next_page = repo.get_commit(commit_idx=last_idx - 1)
186 171 first_span_of_next_page = commit_span_template % (
187 172 first_commit_of_next_page.idx, first_commit_of_next_page.short_id)
188 173 assert first_span_of_next_page not in response
189 174
190 175 @pytest.mark.parametrize('test_path', [
191 176 'vcs/exceptions.py',
192 177 '/vcs/exceptions.py',
193 178 '//vcs/exceptions.py'
194 179 ])
195 180 def test_commits_with_filenode(self, backend, test_path):
196 181 self.log_user()
197 182 response = self.app.get(
198 183 route_path('repo_commits_file', repo_name=backend.repo_name,
199 184 commit_id='tip', f_path=test_path),
200 185 )
201 186
202 187 # history commits messages
203 188 response.mustcontain('Added exceptions module, this time for real')
204 189 response.mustcontain('Added not implemented hg backend test case')
205 190 response.mustcontain('Added BaseChangeset class')
206 191
207 192 def test_commits_with_filenode_that_is_dirnode(self, backend):
208 193 self.log_user()
209 194 self.app.get(
210 195 route_path('repo_commits_file', repo_name=backend.repo_name,
211 196 commit_id='tip', f_path='/tests'),
212 197 status=302)
213 198
214 199 def test_commits_with_filenode_not_existing(self, backend):
215 200 self.log_user()
216 201 self.app.get(
217 202 route_path('repo_commits_file', repo_name=backend.repo_name,
218 203 commit_id='tip', f_path='wrong_path'),
219 204 status=302)
@@ -1,494 +1,477 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import pytest
20 20
21 21 from rhodecode.tests import TestController
22
22 from rhodecode.tests.routes import route_path
23 23 from rhodecode.model.db import ChangesetComment, Notification
24 24 from rhodecode.model.meta import Session
25 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 29 @pytest.mark.backends("git", "hg", "svn")
47 30 class TestRepoCommitCommentsView(TestController):
48 31
49 32 @pytest.fixture(autouse=True)
50 33 def prepare(self, request, baseapp):
51 34 for x in ChangesetComment.query().all():
52 35 Session().delete(x)
53 36 Session().commit()
54 37
55 38 for x in Notification.query().all():
56 39 Session().delete(x)
57 40 Session().commit()
58 41
59 42 request.addfinalizer(self.cleanup)
60 43
61 44 def cleanup(self):
62 45 for x in ChangesetComment.query().all():
63 46 Session().delete(x)
64 47 Session().commit()
65 48
66 49 for x in Notification.query().all():
67 50 Session().delete(x)
68 51 Session().commit()
69 52
70 53 @pytest.mark.parametrize('comment_type', ChangesetComment.COMMENT_TYPES)
71 54 def test_create(self, comment_type, backend):
72 55 self.log_user()
73 56 commit = backend.repo.get_commit('300')
74 57 commit_id = commit.raw_id
75 58 text = 'CommentOnCommit'
76 59
77 60 params = {'text': text, 'csrf_token': self.csrf_token,
78 61 'comment_type': comment_type}
79 62 self.app.post(
80 63 route_path('repo_commit_comment_create',
81 64 repo_name=backend.repo_name, commit_id=commit_id),
82 65 params=params)
83 66
84 67 response = self.app.get(
85 68 route_path('repo_commit',
86 69 repo_name=backend.repo_name, commit_id=commit_id))
87 70
88 71 # test DB
89 72 assert ChangesetComment.query().count() == 1
90 73 assert_comment_links(response, ChangesetComment.query().count(), 0)
91 74
92 75 assert Notification.query().count() == 1
93 76 assert ChangesetComment.query().count() == 1
94 77
95 78 notification = Notification.query().all()[0]
96 79
97 80 comment_id = ChangesetComment.query().first().comment_id
98 81 assert notification.type_ == Notification.TYPE_CHANGESET_COMMENT
99 82
100 83 author = notification.created_by_user.username_and_name
101 84 sbj = '@{0} left a {1} on commit `{2}` in the `{3}` repository'.format(
102 85 author, comment_type, h.show_id(commit), backend.repo_name)
103 86 assert sbj == notification.subject
104 87
105 88 lnk = ('/{0}/changeset/{1}#comment-{2}'.format(
106 89 backend.repo_name, commit_id, comment_id))
107 90 assert lnk in notification.body
108 91
109 92 @pytest.mark.parametrize('comment_type', ChangesetComment.COMMENT_TYPES)
110 93 def test_create_inline(self, comment_type, backend):
111 94 self.log_user()
112 95 commit = backend.repo.get_commit('300')
113 96 commit_id = commit.raw_id
114 97 text = 'CommentOnCommit'
115 98 f_path = 'vcs/web/simplevcs/views/repository.py'
116 99 line = 'n1'
117 100
118 101 params = {'text': text, 'f_path': f_path, 'line': line,
119 102 'comment_type': comment_type,
120 103 'csrf_token': self.csrf_token}
121 104
122 105 self.app.post(
123 106 route_path('repo_commit_comment_create',
124 107 repo_name=backend.repo_name, commit_id=commit_id),
125 108 params=params)
126 109
127 110 response = self.app.get(
128 111 route_path('repo_commit',
129 112 repo_name=backend.repo_name, commit_id=commit_id))
130 113
131 114 # test DB
132 115 assert ChangesetComment.query().count() == 1
133 116 assert_comment_links(response, 0, ChangesetComment.query().count())
134 117
135 118 if backend.alias == 'svn':
136 119 response.mustcontain(
137 120 '''data-f-path="vcs/commands/summary.py" '''
138 121 '''data-anchor-id="c-300-ad05457a43f8"'''
139 122 )
140 123 if backend.alias == 'git':
141 124 response.mustcontain(
142 125 '''data-f-path="vcs/backends/hg.py" '''
143 126 '''data-anchor-id="c-883e775e89ea-9c390eb52cd6"'''
144 127 )
145 128
146 129 if backend.alias == 'hg':
147 130 response.mustcontain(
148 131 '''data-f-path="vcs/backends/hg.py" '''
149 132 '''data-anchor-id="c-e58d85a3973b-9c390eb52cd6"'''
150 133 )
151 134
152 135 assert Notification.query().count() == 1
153 136 assert ChangesetComment.query().count() == 1
154 137
155 138 notification = Notification.query().all()[0]
156 139 comment = ChangesetComment.query().first()
157 140 assert notification.type_ == Notification.TYPE_CHANGESET_COMMENT
158 141
159 142 assert comment.revision == commit_id
160 143
161 144 author = notification.created_by_user.username_and_name
162 145 sbj = '@{0} left a {1} on file `{2}` in commit `{3}` in the `{4}` repository'.format(
163 146 author, comment_type, f_path, h.show_id(commit), backend.repo_name)
164 147
165 148 assert sbj == notification.subject
166 149
167 150 lnk = ('/{0}/changeset/{1}#comment-{2}'.format(
168 151 backend.repo_name, commit_id, comment.comment_id))
169 152 assert lnk in notification.body
170 153 assert 'on line n1' in notification.body
171 154
172 155 def test_create_with_mention(self, backend):
173 156 self.log_user()
174 157
175 158 commit_id = backend.repo.get_commit('300').raw_id
176 159 text = '@test_regular check CommentOnCommit'
177 160
178 161 params = {'text': text, 'csrf_token': self.csrf_token}
179 162 self.app.post(
180 163 route_path('repo_commit_comment_create',
181 164 repo_name=backend.repo_name, commit_id=commit_id),
182 165 params=params)
183 166
184 167 response = self.app.get(
185 168 route_path('repo_commit',
186 169 repo_name=backend.repo_name, commit_id=commit_id))
187 170 # test DB
188 171 assert ChangesetComment.query().count() == 1
189 172 assert_comment_links(response, ChangesetComment.query().count(), 0)
190 173
191 174 notification = Notification.query().one()
192 175
193 176 assert len(notification.recipients) == 2
194 177 users = [x.username for x in notification.recipients]
195 178
196 179 # test_regular gets notification by @mention
197 180 assert sorted(users) == ['test_admin', 'test_regular']
198 181
199 182 def test_create_with_status_change(self, backend):
200 183 self.log_user()
201 184 commit = backend.repo.get_commit('300')
202 185 commit_id = commit.raw_id
203 186 text = 'CommentOnCommit'
204 187 f_path = 'vcs/web/simplevcs/views/repository.py'
205 188 line = 'n1'
206 189
207 190 params = {'text': text, 'changeset_status': 'approved',
208 191 'csrf_token': self.csrf_token}
209 192
210 193 self.app.post(
211 194 route_path(
212 195 'repo_commit_comment_create',
213 196 repo_name=backend.repo_name, commit_id=commit_id),
214 197 params=params)
215 198
216 199 response = self.app.get(
217 200 route_path('repo_commit',
218 201 repo_name=backend.repo_name, commit_id=commit_id))
219 202
220 203 # test DB
221 204 assert ChangesetComment.query().count() == 1
222 205 assert_comment_links(response, ChangesetComment.query().count(), 0)
223 206
224 207 assert Notification.query().count() == 1
225 208 assert ChangesetComment.query().count() == 1
226 209
227 210 notification = Notification.query().all()[0]
228 211
229 212 comment_id = ChangesetComment.query().first().comment_id
230 213 assert notification.type_ == Notification.TYPE_CHANGESET_COMMENT
231 214
232 215 author = notification.created_by_user.username_and_name
233 216 sbj = '[status: Approved] @{0} left a note on commit `{1}` in the `{2}` repository'.format(
234 217 author, h.show_id(commit), backend.repo_name)
235 218 assert sbj == notification.subject
236 219
237 220 lnk = ('/{0}/changeset/{1}#comment-{2}'.format(
238 221 backend.repo_name, commit_id, comment_id))
239 222 assert lnk in notification.body
240 223
241 224 def test_delete(self, backend):
242 225 self.log_user()
243 226 commit_id = backend.repo.get_commit('300').raw_id
244 227 text = 'CommentOnCommit'
245 228
246 229 params = {'text': text, 'csrf_token': self.csrf_token}
247 230 self.app.post(
248 231 route_path(
249 232 'repo_commit_comment_create',
250 233 repo_name=backend.repo_name, commit_id=commit_id),
251 234 params=params)
252 235
253 236 comments = ChangesetComment.query().all()
254 237 assert len(comments) == 1
255 238 comment_id = comments[0].comment_id
256 239
257 240 self.app.post(
258 241 route_path('repo_commit_comment_delete',
259 242 repo_name=backend.repo_name,
260 243 commit_id=commit_id,
261 244 comment_id=comment_id),
262 245 params={'csrf_token': self.csrf_token})
263 246
264 247 comments = ChangesetComment.query().all()
265 248 assert len(comments) == 0
266 249
267 250 response = self.app.get(
268 251 route_path('repo_commit',
269 252 repo_name=backend.repo_name, commit_id=commit_id))
270 253 assert_comment_links(response, 0, 0)
271 254
272 255 def test_edit(self, backend):
273 256 self.log_user()
274 257 commit_id = backend.repo.get_commit('300').raw_id
275 258 text = 'CommentOnCommit'
276 259
277 260 params = {'text': text, 'csrf_token': self.csrf_token}
278 261 self.app.post(
279 262 route_path(
280 263 'repo_commit_comment_create',
281 264 repo_name=backend.repo_name, commit_id=commit_id),
282 265 params=params)
283 266
284 267 comments = ChangesetComment.query().all()
285 268 assert len(comments) == 1
286 269 comment_id = comments[0].comment_id
287 270 test_text = 'test_text'
288 271 self.app.post(
289 272 route_path(
290 273 'repo_commit_comment_edit',
291 274 repo_name=backend.repo_name,
292 275 commit_id=commit_id,
293 276 comment_id=comment_id,
294 277 ),
295 278 params={
296 279 'csrf_token': self.csrf_token,
297 280 'text': test_text,
298 281 'version': '0',
299 282 })
300 283
301 284 text_form_db = ChangesetComment.query().filter(
302 285 ChangesetComment.comment_id == comment_id).first().text
303 286 assert test_text == text_form_db
304 287
305 288 def test_edit_without_change(self, backend):
306 289 self.log_user()
307 290 commit_id = backend.repo.get_commit('300').raw_id
308 291 text = 'CommentOnCommit'
309 292
310 293 params = {'text': text, 'csrf_token': self.csrf_token}
311 294 self.app.post(
312 295 route_path(
313 296 'repo_commit_comment_create',
314 297 repo_name=backend.repo_name, commit_id=commit_id),
315 298 params=params)
316 299
317 300 comments = ChangesetComment.query().all()
318 301 assert len(comments) == 1
319 302 comment_id = comments[0].comment_id
320 303
321 304 response = self.app.post(
322 305 route_path(
323 306 'repo_commit_comment_edit',
324 307 repo_name=backend.repo_name,
325 308 commit_id=commit_id,
326 309 comment_id=comment_id,
327 310 ),
328 311 params={
329 312 'csrf_token': self.csrf_token,
330 313 'text': text,
331 314 'version': '0',
332 315 },
333 316 status=404,
334 317 )
335 318 assert response.status_int == 404
336 319
337 320 def test_edit_try_edit_already_edited(self, backend):
338 321 self.log_user()
339 322 commit_id = backend.repo.get_commit('300').raw_id
340 323 text = 'CommentOnCommit'
341 324
342 325 params = {'text': text, 'csrf_token': self.csrf_token}
343 326 self.app.post(
344 327 route_path(
345 328 'repo_commit_comment_create',
346 329 repo_name=backend.repo_name, commit_id=commit_id
347 330 ),
348 331 params=params,
349 332 )
350 333
351 334 comments = ChangesetComment.query().all()
352 335 assert len(comments) == 1
353 336 comment_id = comments[0].comment_id
354 337 test_text = 'test_text'
355 338 self.app.post(
356 339 route_path(
357 340 'repo_commit_comment_edit',
358 341 repo_name=backend.repo_name,
359 342 commit_id=commit_id,
360 343 comment_id=comment_id,
361 344 ),
362 345 params={
363 346 'csrf_token': self.csrf_token,
364 347 'text': test_text,
365 348 'version': '0',
366 349 }
367 350 )
368 351 test_text_v2 = 'test_v2'
369 352 response = self.app.post(
370 353 route_path(
371 354 'repo_commit_comment_edit',
372 355 repo_name=backend.repo_name,
373 356 commit_id=commit_id,
374 357 comment_id=comment_id,
375 358 ),
376 359 params={
377 360 'csrf_token': self.csrf_token,
378 361 'text': test_text_v2,
379 362 'version': '0',
380 363 },
381 364 status=409,
382 365 )
383 366 assert response.status_int == 409
384 367
385 368 text_form_db = ChangesetComment.query().filter(
386 369 ChangesetComment.comment_id == comment_id).first().text
387 370
388 371 assert test_text == text_form_db
389 372 assert test_text_v2 != text_form_db
390 373
391 374 def test_edit_forbidden_for_immutable_comments(self, backend):
392 375 self.log_user()
393 376 commit_id = backend.repo.get_commit('300').raw_id
394 377 text = 'CommentOnCommit'
395 378
396 379 params = {'text': text, 'csrf_token': self.csrf_token, 'version': '0'}
397 380 self.app.post(
398 381 route_path(
399 382 'repo_commit_comment_create',
400 383 repo_name=backend.repo_name,
401 384 commit_id=commit_id,
402 385 ),
403 386 params=params
404 387 )
405 388
406 389 comments = ChangesetComment.query().all()
407 390 assert len(comments) == 1
408 391 comment_id = comments[0].comment_id
409 392
410 393 comment = ChangesetComment.get(comment_id)
411 394 comment.immutable_state = ChangesetComment.OP_IMMUTABLE
412 395 Session().add(comment)
413 396 Session().commit()
414 397
415 398 response = self.app.post(
416 399 route_path(
417 400 'repo_commit_comment_edit',
418 401 repo_name=backend.repo_name,
419 402 commit_id=commit_id,
420 403 comment_id=comment_id,
421 404 ),
422 405 params={
423 406 'csrf_token': self.csrf_token,
424 407 'text': 'test_text',
425 408 },
426 409 status=403,
427 410 )
428 411 assert response.status_int == 403
429 412
430 413 def test_delete_forbidden_for_immutable_comments(self, backend):
431 414 self.log_user()
432 415 commit_id = backend.repo.get_commit('300').raw_id
433 416 text = 'CommentOnCommit'
434 417
435 418 params = {'text': text, 'csrf_token': self.csrf_token}
436 419 self.app.post(
437 420 route_path(
438 421 'repo_commit_comment_create',
439 422 repo_name=backend.repo_name, commit_id=commit_id),
440 423 params=params)
441 424
442 425 comments = ChangesetComment.query().all()
443 426 assert len(comments) == 1
444 427 comment_id = comments[0].comment_id
445 428
446 429 comment = ChangesetComment.get(comment_id)
447 430 comment.immutable_state = ChangesetComment.OP_IMMUTABLE
448 431 Session().add(comment)
449 432 Session().commit()
450 433
451 434 self.app.post(
452 435 route_path('repo_commit_comment_delete',
453 436 repo_name=backend.repo_name,
454 437 commit_id=commit_id,
455 438 comment_id=comment_id),
456 439 params={'csrf_token': self.csrf_token},
457 440 status=403)
458 441
459 442 @pytest.mark.parametrize('renderer, text_input, output', [
460 443 ('rst', 'plain text', '<p>plain text</p>'),
461 444 ('rst', 'header\n======', '<h1 class="title">header</h1>'),
462 445 ('rst', '*italics*', '<em>italics</em>'),
463 446 ('rst', '**bold**', '<strong>bold</strong>'),
464 447 ('markdown', 'plain text', '<p>plain text</p>'),
465 448 ('markdown', '# header', '<h1>header</h1>'),
466 449 ('markdown', '*italics*', '<em>italics</em>'),
467 450 ('markdown', '**bold**', '<strong>bold</strong>'),
468 451 ], ids=['rst-plain', 'rst-header', 'rst-italics', 'rst-bold', 'md-plain',
469 452 'md-header', 'md-italics', 'md-bold', ])
470 453 def test_preview(self, renderer, text_input, output, backend, xhr_header):
471 454 self.log_user()
472 455 params = {
473 456 'renderer': renderer,
474 457 'text': text_input,
475 458 'csrf_token': self.csrf_token
476 459 }
477 460 commit_id = '0' * 16 # fake this for tests
478 461 response = self.app.post(
479 462 route_path('repo_commit_comment_preview',
480 463 repo_name=backend.repo_name, commit_id=commit_id,),
481 464 params=params,
482 465 extra_environ=xhr_header)
483 466
484 467 response.mustcontain(output)
485 468
486 469
487 470 def assert_comment_links(response, comments, inline_comments):
488 471 response.mustcontain(
489 472 '<span class="display-none" id="general-comments-count">{}</span>'.format(comments))
490 473 response.mustcontain(
491 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 2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19
20 20 import pytest
21 21
22 22 from rhodecode.apps.repository.tests.test_repo_compare import ComparePage
23 23 from rhodecode.lib.helpers import _shorten_commit_id
24
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
24 from rhodecode.tests.routes import route_path
45 25
46 26
47 27 @pytest.mark.usefixtures("app")
48 28 class TestRepoCommitView(object):
49 29
50 30 def test_show_commit(self, backend):
51 31 commit_id = self.commit_id[backend.alias]
52 32 response = self.app.get(route_path(
53 33 'repo_commit', repo_name=backend.repo_name, commit_id=commit_id))
54 34 response.mustcontain('Added a symlink')
55 35 response.mustcontain(commit_id)
56 36 response.mustcontain('No newline at end of file')
57 37
58 38 def test_show_raw(self, backend):
59 39 commit_id = self.commit_id[backend.alias]
60 40 # webtest uses linter to check if response is bytes,
61 41 # and we use memoryview here as a wrapper, quick turn-off
62 42 self.app.lint = False
63 43
64 44 response = self.app.get(route_path(
65 45 'repo_commit_raw',
66 46 repo_name=backend.repo_name, commit_id=commit_id))
67 47 assert response.body == self.diffs[backend.alias]
68 48
69 49 def test_show_raw_patch(self, backend):
70 50 response = self.app.get(route_path(
71 51 'repo_commit_patch', repo_name=backend.repo_name,
72 52 commit_id=self.commit_id[backend.alias]))
73 53 assert response.body == self.patches[backend.alias]
74 54
75 55 def test_commit_download(self, backend):
76 56 # webtest uses linter to check if response is bytes,
77 57 # and we use memoryview here as a wrapper, quick turn-off
78 58 self.app.lint = False
79 59
80 60 response = self.app.get(route_path(
81 61 'repo_commit_download',
82 62 repo_name=backend.repo_name,
83 63 commit_id=self.commit_id[backend.alias]))
84 64 assert response.body == self.diffs[backend.alias]
85 65
86 66 def test_single_commit_page_different_ops(self, backend):
87 67 commit_id = {
88 68 'hg': '603d6c72c46d953420c89d36372f08d9f305f5dd',
89 69 'git': '03fa803d7e9fb14daa9a3089e0d1494eda75d986',
90 70 'svn': '337',
91 71 }
92 72 diff_stat = {
93 73 'hg': (21, 943, 288),
94 74 'git': (20, 941, 286),
95 75 'svn': (21, 943, 288),
96 76 }
97 77
98 78 commit_id = commit_id[backend.alias]
99 79 response = self.app.get(route_path(
100 80 'repo_commit',
101 81 repo_name=backend.repo_name, commit_id=commit_id))
102 82
103 83 response.mustcontain(_shorten_commit_id(commit_id))
104 84
105 85 compare_page = ComparePage(response)
106 86 file_changes = diff_stat[backend.alias]
107 87 compare_page.contains_change_summary(*file_changes)
108 88
109 89 # files op files
110 90 response.mustcontain('File not present at commit: %s' %
111 91 _shorten_commit_id(commit_id))
112 92
113 93 # svn uses a different filename
114 94 if backend.alias == 'svn':
115 95 response.mustcontain('new file 10644')
116 96 else:
117 97 response.mustcontain('new file 100644')
118 98 response.mustcontain('Changed theme to ADC theme') # commit msg
119 99
120 100 self._check_new_diff_menus(response, right_menu=True)
121 101
122 102 def test_commit_range_page_different_ops(self, backend):
123 103 commit_id_range = {
124 104 'hg': (
125 105 '25d7e49c18b159446cadfa506a5cf8ad1cb04067',
126 106 '603d6c72c46d953420c89d36372f08d9f305f5dd'),
127 107 'git': (
128 108 '6fc9270775aaf5544c1deb014f4ddd60c952fcbb',
129 109 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'),
130 110 'svn': (
131 111 '335',
132 112 '337'),
133 113 }
134 114 commit_ids = commit_id_range[backend.alias]
135 115 commit_id = '%s...%s' % (commit_ids[0], commit_ids[1])
136 116 response = self.app.get(route_path(
137 117 'repo_commit',
138 118 repo_name=backend.repo_name, commit_id=commit_id))
139 119
140 120 response.mustcontain(_shorten_commit_id(commit_ids[0]))
141 121 response.mustcontain(_shorten_commit_id(commit_ids[1]))
142 122
143 123 compare_page = ComparePage(response)
144 124
145 125 # svn is special
146 126 if backend.alias == 'svn':
147 127 response.mustcontain('new file 10644')
148 128 for file_changes in [(1, 5, 1), (12, 236, 22), (21, 943, 288)]:
149 129 compare_page.contains_change_summary(*file_changes)
150 130 elif backend.alias == 'git':
151 131 response.mustcontain('new file 100644')
152 132 for file_changes in [(12, 222, 20), (20, 941, 286)]:
153 133 compare_page.contains_change_summary(*file_changes)
154 134 else:
155 135 response.mustcontain('new file 100644')
156 136 for file_changes in [(12, 222, 20), (21, 943, 288)]:
157 137 compare_page.contains_change_summary(*file_changes)
158 138
159 139 # files op files
160 140 response.mustcontain('File not present at commit: %s' % _shorten_commit_id(commit_ids[1]))
161 141 response.mustcontain('Added docstrings to vcs.cli') # commit msg
162 142 response.mustcontain('Changed theme to ADC theme') # commit msg
163 143
164 144 self._check_new_diff_menus(response)
165 145
166 146 def test_combined_compare_commit_page_different_ops(self, backend):
167 147 commit_id_range = {
168 148 'hg': (
169 149 '4fdd71e9427417b2e904e0464c634fdee85ec5a7',
170 150 '603d6c72c46d953420c89d36372f08d9f305f5dd'),
171 151 'git': (
172 152 'f5fbf9cfd5f1f1be146f6d3b38bcd791a7480c13',
173 153 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'),
174 154 'svn': (
175 155 '335',
176 156 '337'),
177 157 }
178 158 commit_ids = commit_id_range[backend.alias]
179 159 response = self.app.get(route_path(
180 160 'repo_compare',
181 161 repo_name=backend.repo_name,
182 162 source_ref_type='rev', source_ref=commit_ids[0],
183 163 target_ref_type='rev', target_ref=commit_ids[1], ))
184 164
185 165 response.mustcontain(_shorten_commit_id(commit_ids[0]))
186 166 response.mustcontain(_shorten_commit_id(commit_ids[1]))
187 167
188 168 # files op files
189 169 response.mustcontain('File not present at commit: %s' %
190 170 _shorten_commit_id(commit_ids[1]))
191 171
192 172 compare_page = ComparePage(response)
193 173
194 174 # svn is special
195 175 if backend.alias == 'svn':
196 176 response.mustcontain('new file 10644')
197 177 file_changes = (32, 1179, 310)
198 178 compare_page.contains_change_summary(*file_changes)
199 179 elif backend.alias == 'git':
200 180 response.mustcontain('new file 100644')
201 181 file_changes = (31, 1163, 306)
202 182 compare_page.contains_change_summary(*file_changes)
203 183 else:
204 184 response.mustcontain('new file 100644')
205 185 file_changes = (32, 1165, 308)
206 186 compare_page.contains_change_summary(*file_changes)
207 187
208 188 response.mustcontain('Added docstrings to vcs.cli') # commit msg
209 189 response.mustcontain('Changed theme to ADC theme') # commit msg
210 190
211 191 self._check_new_diff_menus(response)
212 192
213 193 def test_changeset_range(self, backend):
214 194 self._check_changeset_range(
215 195 backend, self.commit_id_range, self.commit_id_range_result)
216 196
217 197 def test_changeset_range_with_initial_commit(self, backend):
218 198 commit_id_range = {
219 199 'hg': (
220 200 'b986218ba1c9b0d6a259fac9b050b1724ed8e545'
221 201 '...6cba7170863a2411822803fa77a0a264f1310b35'),
222 202 'git': (
223 203 'c1214f7e79e02fc37156ff215cd71275450cffc3'
224 204 '...fa6600f6848800641328adbf7811fd2372c02ab2'),
225 205 'svn': '1...3',
226 206 }
227 207 commit_id_range_result = {
228 208 'hg': ['b986218ba1c9', '3d8f361e72ab', '6cba7170863a'],
229 209 'git': ['c1214f7e79e0', '38b5fe81f109', 'fa6600f68488'],
230 210 'svn': ['1', '2', '3'],
231 211 }
232 212 self._check_changeset_range(
233 213 backend, commit_id_range, commit_id_range_result)
234 214
235 215 def _check_changeset_range(
236 216 self, backend, commit_id_ranges, commit_id_range_result):
237 217 response = self.app.get(
238 218 route_path('repo_commit',
239 219 repo_name=backend.repo_name,
240 220 commit_id=commit_id_ranges[backend.alias]))
241 221
242 222 expected_result = commit_id_range_result[backend.alias]
243 223 response.mustcontain('{} commits'.format(len(expected_result)))
244 224 for commit_id in expected_result:
245 225 response.mustcontain(commit_id)
246 226
247 227 commit_id = {
248 228 'hg': '2062ec7beeeaf9f44a1c25c41479565040b930b2',
249 229 'svn': '393',
250 230 'git': 'fd627b9e0dd80b47be81af07c4a98518244ed2f7',
251 231 }
252 232
253 233 commit_id_range = {
254 234 'hg': (
255 235 'a53d9201d4bc278910d416d94941b7ea007ecd52'
256 236 '...2062ec7beeeaf9f44a1c25c41479565040b930b2'),
257 237 'git': (
258 238 '7ab37bc680b4aa72c34d07b230c866c28e9fc204'
259 239 '...fd627b9e0dd80b47be81af07c4a98518244ed2f7'),
260 240 'svn': '391...393',
261 241 }
262 242
263 243 commit_id_range_result = {
264 244 'hg': ['a53d9201d4bc', '96507bd11ecc', '2062ec7beeea'],
265 245 'git': ['7ab37bc680b4', '5f2c6ee19592', 'fd627b9e0dd8'],
266 246 'svn': ['391', '392', '393'],
267 247 }
268 248
269 249 diffs = {
270 250 'hg': br"""diff --git a/README b/README
271 251 new file mode 120000
272 252 --- /dev/null
273 253 +++ b/README
274 254 @@ -0,0 +1,1 @@
275 255 +README.rst
276 256 \ No newline at end of file
277 257 """,
278 258 'git': br"""diff --git a/README b/README
279 259 new file mode 120000
280 260 index 0000000..92cacd2
281 261 --- /dev/null
282 262 +++ b/README
283 263 @@ -0,0 +1 @@
284 264 +README.rst
285 265 \ No newline at end of file
286 266 """,
287 267 'svn': b"""Index: README
288 268 ===================================================================
289 269 diff --git a/README b/README
290 270 new file mode 10644
291 271 --- /dev/null\t(revision 0)
292 272 +++ b/README\t(revision 393)
293 273 @@ -0,0 +1 @@
294 274 +link README.rst
295 275 \\ No newline at end of file
296 276 """,
297 277 }
298 278
299 279 patches = {
300 280 'hg': br"""# HG changeset patch
301 281 # User Marcin Kuzminski <marcin@python-works.com>
302 282 # Date 2014-01-07 12:21:40
303 283 # Node ID 2062ec7beeeaf9f44a1c25c41479565040b930b2
304 284 # Parent 96507bd11ecc815ebc6270fdf6db110928c09c1e
305 285
306 286 Added a symlink
307 287
308 288 """ + diffs['hg'],
309 289 'git': br"""From fd627b9e0dd80b47be81af07c4a98518244ed2f7 2014-01-07 12:22:20
310 290 From: Marcin Kuzminski <marcin@python-works.com>
311 291 Date: 2014-01-07 12:22:20
312 292 Subject: [PATCH] Added a symlink
313 293
314 294 ---
315 295
316 296 """ + diffs['git'],
317 297 'svn': br"""# SVN changeset patch
318 298 # User marcin
319 299 # Date 2014-09-02 12:25:22.071142
320 300 # Revision 393
321 301
322 302 Added a symlink
323 303
324 304 """ + diffs['svn'],
325 305 }
326 306
327 307 def _check_new_diff_menus(self, response, right_menu=False,):
328 308 # individual file diff menus
329 309 for elem in ['Show file before', 'Show file after']:
330 310 response.mustcontain(elem)
331 311
332 312 # right pane diff menus
333 313 if right_menu:
334 314 for elem in ['Hide whitespace changes', 'Toggle wide diff',
335 315 'Show full context diff']:
336 316 response.mustcontain(elem)
@@ -1,670 +1,656 b''
1 1
2 2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19
20 20 import mock
21 21 import pytest
22 22 import lxml.html
23 23
24 24 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
25 25 from rhodecode.tests import assert_session_flash
26 26 from rhodecode.tests.utils import AssertResponse, commit_change
27
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
27 from rhodecode.tests.routes import route_path
42 28
43 29
44 30 @pytest.mark.usefixtures("autologin_user", "app")
45 31 class TestCompareView(object):
46 32
47 33 def test_compare_index_is_reached_at_least_once(self, backend):
48 34 repo = backend.repo
49 35 self.app.get(
50 36 route_path('repo_compare_select', repo_name=repo.repo_name))
51 37
52 38 @pytest.mark.xfail_backends("svn", reason="Requires pull")
53 39 def test_compare_remote_with_different_commit_indexes(self, backend):
54 40 # Preparing the following repository structure:
55 41 #
56 42 # Origin repository has two commits:
57 43 #
58 44 # 0 1
59 45 # A -- D
60 46 #
61 47 # The fork of it has a few more commits and "D" has a commit index
62 48 # which does not exist in origin.
63 49 #
64 50 # 0 1 2 3 4
65 51 # A -- -- -- D -- E
66 52 # \- B -- C
67 53 #
68 54
69 55 fork = backend.create_repo()
70 56 origin = backend.create_repo()
71 57
72 58 # prepare fork
73 59 commit0 = commit_change(
74 60 fork.repo_name, filename=b'file1', content=b'A',
75 61 message='A - Initial Commit', vcs_type=backend.alias, parent=None, newfile=True)
76 62
77 63 commit1 = commit_change(
78 64 fork.repo_name, filename=b'file1', content=b'B',
79 65 message='B, child of A', vcs_type=backend.alias, parent=commit0)
80 66
81 67 commit_change( # commit 2
82 68 fork.repo_name, filename=b'file1', content=b'C',
83 69 message='C, child of B', vcs_type=backend.alias, parent=commit1)
84 70
85 71 commit3 = commit_change(
86 72 fork.repo_name, filename=b'file1', content=b'D',
87 73 message='D, child of A', vcs_type=backend.alias, parent=commit0)
88 74
89 75 commit4 = commit_change(
90 76 fork.repo_name, filename=b'file1', content=b'E',
91 77 message='E, child of D', vcs_type=backend.alias, parent=commit3)
92 78
93 79 # prepare origin repository, taking just the history up to D
94 80
95 81 origin_repo = origin.scm_instance(cache=False)
96 82 origin_repo.config.clear_section('hooks')
97 83 origin_repo.pull(fork.repo_full_path, commit_ids=[commit3.raw_id])
98 84 origin_repo = origin.scm_instance(cache=False) # cache rebuild
99 85
100 86 # Verify test fixture setup
101 87 # This does not work for git
102 88 if backend.alias != 'git':
103 89 assert 5 == len(fork.scm_instance(cache=False).commit_ids)
104 90 assert 2 == len(origin_repo.commit_ids)
105 91
106 92 # Comparing the revisions
107 93 response = self.app.get(
108 94 route_path('repo_compare',
109 95 repo_name=origin.repo_name,
110 96 source_ref_type="rev", source_ref=commit3.raw_id,
111 97 target_ref_type="rev", target_ref=commit4.raw_id,
112 98 params=dict(merge='1', target_repo=fork.repo_name)
113 99 ),
114 100 status=200)
115 101
116 102 compare_page = ComparePage(response)
117 103 compare_page.contains_commits([commit4])
118 104
119 105 @pytest.mark.xfail_backends("svn", reason="Depends on branch support")
120 106 def test_compare_forks_on_branch_extra_commits(self, backend):
121 107 repo1 = backend.create_repo()
122 108
123 109 # commit something !
124 110 commit0 = commit_change(
125 111 repo1.repo_name, filename=b'file1', content=b'line1\n',
126 112 message='commit1', vcs_type=backend.alias, parent=None,
127 113 newfile=True)
128 114
129 115 # fork this repo
130 116 repo2 = backend.create_fork()
131 117
132 118 # add two extra commit into fork
133 119 commit1 = commit_change(
134 120 repo2.repo_name, filename=b'file1', content=b'line1\nline2\n',
135 121 message='commit2', vcs_type=backend.alias, parent=commit0)
136 122
137 123 commit2 = commit_change(
138 124 repo2.repo_name, filename=b'file1', content=b'line1\nline2\nline3\n',
139 125 message='commit3', vcs_type=backend.alias, parent=commit1)
140 126
141 127 commit_id1 = repo1.scm_instance().DEFAULT_BRANCH_NAME
142 128 commit_id2 = repo2.scm_instance().DEFAULT_BRANCH_NAME
143 129
144 130 response = self.app.get(
145 131 route_path('repo_compare',
146 132 repo_name=repo1.repo_name,
147 133 source_ref_type="branch", source_ref=commit_id2,
148 134 target_ref_type="branch", target_ref=commit_id1,
149 135 params=dict(merge='1', target_repo=repo2.repo_name)
150 136 ))
151 137
152 138 response.mustcontain('%s@%s' % (repo1.repo_name, commit_id2))
153 139 response.mustcontain('%s@%s' % (repo2.repo_name, commit_id1))
154 140
155 141 compare_page = ComparePage(response)
156 142 compare_page.contains_change_summary(1, 2, 0)
157 143 compare_page.contains_commits([commit1, commit2])
158 144
159 145 anchor = 'a_c-{}-826e8142e6ba'.format(commit0.short_id)
160 146 compare_page.contains_file_links_and_anchors([('file1', anchor), ])
161 147
162 148 # Swap is removed when comparing branches since it's a PR feature and
163 149 # it is then a preview mode
164 150 compare_page.swap_is_hidden()
165 151 compare_page.target_source_are_disabled()
166 152
167 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 155 repo1 = backend.create_repo()
170 156
171 157 # commit something !
172 158 commit0 = commit_change(
173 159 repo1.repo_name, filename=b'file1', content=b'line1\n',
174 160 message='commit1', vcs_type=backend.alias, parent=None,
175 161 newfile=True)
176 162
177 163 # fork this repo
178 164 repo2 = backend.create_fork()
179 165
180 166 # now commit something to origin repo
181 167 commit_change(
182 168 repo1.repo_name, filename=b'file2', content=b'line1file2\n',
183 169 message='commit2', vcs_type=backend.alias, parent=commit0,
184 170 newfile=True)
185 171
186 172 # add two extra commit into fork
187 173 commit1 = commit_change(
188 174 repo2.repo_name, filename=b'file1', content=b'line1\nline2\n',
189 175 message='commit2', vcs_type=backend.alias, parent=commit0)
190 176
191 177 commit2 = commit_change(
192 178 repo2.repo_name, filename=b'file1', content=b'line1\nline2\nline3\n',
193 179 message='commit3', vcs_type=backend.alias, parent=commit1)
194 180
195 181 commit_id1 = repo1.scm_instance().DEFAULT_BRANCH_NAME
196 182 commit_id2 = repo2.scm_instance().DEFAULT_BRANCH_NAME
197 183
198 184 response = self.app.get(
199 185 route_path('repo_compare',
200 186 repo_name=repo1.repo_name,
201 187 source_ref_type="branch", source_ref=commit_id2,
202 188 target_ref_type="branch", target_ref=commit_id1,
203 189 params=dict(merge='1', target_repo=repo2.repo_name),
204 190 ))
205 191
206 response.mustcontain('%s@%s' % (repo1.repo_name, commit_id2))
207 response.mustcontain('%s@%s' % (repo2.repo_name, commit_id1))
192 response.mustcontain(f'{repo1.repo_name}@{commit_id2}')
193 response.mustcontain(f'{repo2.repo_name}@{commit_id1}')
208 194
209 195 compare_page = ComparePage(response)
210 196 compare_page.contains_change_summary(1, 2, 0)
211 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 199 compare_page.contains_file_links_and_anchors([('file1', anchor), ])
214 200
215 201 # Swap is removed when comparing branches since it's a PR feature and
216 202 # it is then a preview mode
217 203 compare_page.swap_is_hidden()
218 204 compare_page.target_source_are_disabled()
219 205
220 206 @pytest.mark.xfail_backends("svn")
221 207 # TODO(marcink): no svn support for compare two seperate repos
222 208 def test_compare_of_unrelated_forks(self, backend):
223 209 orig = backend.create_repo(number_of_commits=1)
224 210 fork = backend.create_repo(number_of_commits=1)
225 211
226 212 response = self.app.get(
227 213 route_path('repo_compare',
228 214 repo_name=orig.repo_name,
229 215 source_ref_type="rev", source_ref="tip",
230 216 target_ref_type="rev", target_ref="tip",
231 217 params=dict(merge='1', target_repo=fork.repo_name),
232 218 ),
233 219 status=302)
234 220 response = response.follow()
235 221 response.mustcontain("Repositories unrelated.")
236 222
237 223 @pytest.mark.xfail_backends("svn")
238 224 def test_compare_cherry_pick_commits_from_bottom(self, backend):
239 225
240 226 # repo1:
241 227 # commit0:
242 228 # commit1:
243 229 # repo1-fork- in which we will cherry pick bottom commits
244 230 # commit0:
245 231 # commit1:
246 232 # commit2: x
247 233 # commit3: x
248 234 # commit4: x
249 235 # commit5:
250 236 # make repo1, and commit1+commit2
251 237
252 238 repo1 = backend.create_repo()
253 239
254 240 # commit something !
255 241 commit0 = commit_change(
256 242 repo1.repo_name, filename=b'file1', content=b'line1\n',
257 243 message='commit1', vcs_type=backend.alias, parent=None,
258 244 newfile=True)
259 245 commit1 = commit_change(
260 246 repo1.repo_name, filename=b'file1', content=b'line1\nline2\n',
261 247 message='commit2', vcs_type=backend.alias, parent=commit0)
262 248
263 249 # fork this repo
264 250 repo2 = backend.create_fork()
265 251
266 252 # now make commit3-6
267 253 commit2 = commit_change(
268 254 repo1.repo_name, filename=b'file1', content=b'line1\nline2\nline3\n',
269 255 message='commit3', vcs_type=backend.alias, parent=commit1)
270 256 commit3 = commit_change(
271 257 repo1.repo_name, filename=b'file1',content=b'line1\nline2\nline3\nline4\n',
272 258 message='commit4', vcs_type=backend.alias, parent=commit2)
273 259 commit4 = commit_change(
274 260 repo1.repo_name, filename=b'file1', content=b'line1\nline2\nline3\nline4\nline5\n',
275 261 message='commit5', vcs_type=backend.alias, parent=commit3)
276 262 commit_change( # commit 5
277 263 repo1.repo_name, filename=b'file1', content=b'line1\nline2\nline3\nline4\nline5\nline6\n',
278 264 message='commit6', vcs_type=backend.alias, parent=commit4)
279 265
280 266 response = self.app.get(
281 267 route_path('repo_compare',
282 268 repo_name=repo2.repo_name,
283 269 # parent of commit2, in target repo2
284 270 source_ref_type="rev", source_ref=commit1.raw_id,
285 271 target_ref_type="rev", target_ref=commit4.raw_id,
286 272 params=dict(merge='1', target_repo=repo1.repo_name),
287 273 ))
288 274 response.mustcontain('%s@%s' % (repo2.repo_name, commit1.short_id))
289 275 response.mustcontain('%s@%s' % (repo1.repo_name, commit4.short_id))
290 276
291 277 # files
292 278 compare_page = ComparePage(response)
293 279 compare_page.contains_change_summary(1, 3, 0)
294 280 compare_page.contains_commits([commit2, commit3, commit4])
295 281 anchor = 'a_c-{}-826e8142e6ba'.format(commit1.short_id)
296 282 compare_page.contains_file_links_and_anchors([('file1', anchor),])
297 283
298 284 @pytest.mark.xfail_backends("svn")
299 285 def test_compare_cherry_pick_commits_from_top(self, backend):
300 286 # repo1:
301 287 # commit0:
302 288 # commit1:
303 289 # repo1-fork- in which we will cherry pick bottom commits
304 290 # commit0:
305 291 # commit1:
306 292 # commit2:
307 293 # commit3: x
308 294 # commit4: x
309 295 # commit5: x
310 296
311 297 # make repo1, and commit1+commit2
312 298 repo1 = backend.create_repo()
313 299
314 300 # commit something !
315 301 commit0 = commit_change(
316 302 repo1.repo_name, filename=b'file1', content=b'line1\n',
317 303 message='commit1', vcs_type=backend.alias, parent=None,
318 304 newfile=True)
319 305 commit1 = commit_change(
320 306 repo1.repo_name, filename=b'file1', content=b'line1\nline2\n',
321 307 message='commit2', vcs_type=backend.alias, parent=commit0)
322 308
323 309 # fork this repo
324 310 backend.create_fork()
325 311
326 312 # now make commit3-6
327 313 commit2 = commit_change(
328 314 repo1.repo_name, filename=b'file1', content=b'line1\nline2\nline3\n',
329 315 message='commit3', vcs_type=backend.alias, parent=commit1)
330 316 commit3 = commit_change(
331 317 repo1.repo_name, filename=b'file1',
332 318 content=b'line1\nline2\nline3\nline4\n', message='commit4',
333 319 vcs_type=backend.alias, parent=commit2)
334 320 commit4 = commit_change(
335 321 repo1.repo_name, filename=b'file1',
336 322 content=b'line1\nline2\nline3\nline4\nline5\n', message='commit5',
337 323 vcs_type=backend.alias, parent=commit3)
338 324 commit5 = commit_change(
339 325 repo1.repo_name, filename=b'file1',
340 326 content=b'line1\nline2\nline3\nline4\nline5\nline6\n',
341 327 message='commit6', vcs_type=backend.alias, parent=commit4)
342 328
343 329 response = self.app.get(
344 330 route_path('repo_compare',
345 331 repo_name=repo1.repo_name,
346 332 # parent of commit3, not in source repo2
347 333 source_ref_type="rev", source_ref=commit2.raw_id,
348 334 target_ref_type="rev", target_ref=commit5.raw_id,
349 335 params=dict(merge='1'),))
350 336
351 337 response.mustcontain('%s@%s' % (repo1.repo_name, commit2.short_id))
352 338 response.mustcontain('%s@%s' % (repo1.repo_name, commit5.short_id))
353 339
354 340 compare_page = ComparePage(response)
355 341 compare_page.contains_change_summary(1, 3, 0)
356 342 compare_page.contains_commits([commit3, commit4, commit5])
357 343
358 344 # files
359 345 anchor = 'a_c-{}-826e8142e6ba'.format(commit2.short_id)
360 346 compare_page.contains_file_links_and_anchors([('file1', anchor),])
361 347
362 348 @pytest.mark.xfail_backends("svn")
363 349 def test_compare_remote_branches(self, backend):
364 350 repo1 = backend.repo
365 351 repo2 = backend.create_fork()
366 352
367 353 commit_id1 = repo1.get_commit(commit_idx=3).raw_id
368 354 commit_id1_short = repo1.get_commit(commit_idx=3).short_id
369 355 commit_id2 = repo1.get_commit(commit_idx=6).raw_id
370 356 commit_id2_short = repo1.get_commit(commit_idx=6).short_id
371 357
372 358 response = self.app.get(
373 359 route_path('repo_compare',
374 360 repo_name=repo1.repo_name,
375 361 source_ref_type="rev", source_ref=commit_id1,
376 362 target_ref_type="rev", target_ref=commit_id2,
377 363 params=dict(merge='1', target_repo=repo2.repo_name),
378 364 ))
379 365
380 366 response.mustcontain('%s@%s' % (repo1.repo_name, commit_id1))
381 367 response.mustcontain('%s@%s' % (repo2.repo_name, commit_id2))
382 368
383 369 compare_page = ComparePage(response)
384 370
385 371 # outgoing commits between those commits
386 372 compare_page.contains_commits(
387 373 [repo2.get_commit(commit_idx=x) for x in [4, 5, 6]])
388 374
389 375 # files
390 376 compare_page.contains_file_links_and_anchors([
391 377 ('vcs/backends/hg.py', 'a_c-{}-9c390eb52cd6'.format(commit_id2_short)),
392 378 ('vcs/backends/__init__.py', 'a_c-{}-41b41c1f2796'.format(commit_id1_short)),
393 379 ('vcs/backends/base.py', 'a_c-{}-2f574d260608'.format(commit_id1_short)),
394 380 ])
395 381
396 382 @pytest.mark.xfail_backends("svn")
397 383 def test_source_repo_new_commits_after_forking_simple_diff(self, backend):
398 384 repo1 = backend.create_repo()
399 385 r1_name = repo1.repo_name
400 386
401 387 commit0 = commit_change(
402 388 repo=r1_name, filename=b'file1',
403 389 content=b'line1', message='commit1', vcs_type=backend.alias,
404 390 newfile=True)
405 391 assert repo1.scm_instance().commit_ids == [commit0.raw_id]
406 392
407 393 # fork the repo1
408 394 repo2 = backend.create_fork()
409 395 assert repo2.scm_instance().commit_ids == [commit0.raw_id]
410 396
411 397 self.r2_id = repo2.repo_id
412 398 r2_name = repo2.repo_name
413 399
414 400 commit1 = commit_change(
415 401 repo=r2_name, filename=b'file1-fork',
416 402 content=b'file1-line1-from-fork', message='commit1-fork',
417 403 vcs_type=backend.alias, parent=repo2.scm_instance()[-1],
418 404 newfile=True)
419 405
420 406 commit2 = commit_change(
421 407 repo=r2_name, filename=b'file2-fork',
422 408 content=b'file2-line1-from-fork', message='commit2-fork',
423 409 vcs_type=backend.alias, parent=commit1,
424 410 newfile=True)
425 411
426 412 commit_change( # commit 3
427 413 repo=r2_name, filename=b'file3-fork',
428 414 content=b'file3-line1-from-fork', message='commit3-fork',
429 415 vcs_type=backend.alias, parent=commit2, newfile=True)
430 416
431 417 # compare !
432 418 commit_id1 = repo1.scm_instance().DEFAULT_BRANCH_NAME
433 419 commit_id2 = repo2.scm_instance().DEFAULT_BRANCH_NAME
434 420
435 421 response = self.app.get(
436 422 route_path('repo_compare',
437 423 repo_name=r2_name,
438 424 source_ref_type="branch", source_ref=commit_id1,
439 425 target_ref_type="branch", target_ref=commit_id2,
440 426 params=dict(merge='1', target_repo=r1_name),
441 427 ))
442 428
443 429 response.mustcontain('%s@%s' % (r2_name, commit_id1))
444 430 response.mustcontain('%s@%s' % (r1_name, commit_id2))
445 431 response.mustcontain('No files')
446 432 response.mustcontain('No commits in this compare')
447 433
448 434 commit0 = commit_change(
449 435 repo=r1_name, filename=b'file2',
450 436 content=b'line1-added-after-fork', message='commit2-parent',
451 437 vcs_type=backend.alias, parent=None, newfile=True)
452 438
453 439 # compare !
454 440 response = self.app.get(
455 441 route_path('repo_compare',
456 442 repo_name=r2_name,
457 443 source_ref_type="branch", source_ref=commit_id1,
458 444 target_ref_type="branch", target_ref=commit_id2,
459 445 params=dict(merge='1', target_repo=r1_name),
460 446 ))
461 447
462 448 response.mustcontain('%s@%s' % (r2_name, commit_id1))
463 449 response.mustcontain('%s@%s' % (r1_name, commit_id2))
464 450
465 451 response.mustcontain("""commit2-parent""")
466 452 response.mustcontain("""line1-added-after-fork""")
467 453 compare_page = ComparePage(response)
468 454 compare_page.contains_change_summary(1, 1, 0)
469 455
470 456 @pytest.mark.xfail_backends("svn")
471 457 def test_compare_commits(self, backend, xhr_header):
472 458 commit0 = backend.repo.get_commit(commit_idx=0)
473 459 commit1 = backend.repo.get_commit(commit_idx=1)
474 460
475 461 response = self.app.get(
476 462 route_path('repo_compare',
477 463 repo_name=backend.repo_name,
478 464 source_ref_type="rev", source_ref=commit0.raw_id,
479 465 target_ref_type="rev", target_ref=commit1.raw_id,
480 466 params=dict(merge='1')
481 467 ),
482 468 extra_environ=xhr_header, )
483 469
484 470 # outgoing commits between those commits
485 471 compare_page = ComparePage(response)
486 472 compare_page.contains_commits(commits=[commit1])
487 473
488 474 def test_errors_when_comparing_unknown_source_repo(self, backend):
489 475 repo = backend.repo
490 476
491 477 self.app.get(
492 478 route_path('repo_compare',
493 479 repo_name='badrepo',
494 480 source_ref_type="rev", source_ref='tip',
495 481 target_ref_type="rev", target_ref='tip',
496 482 params=dict(merge='1', target_repo=repo.repo_name)
497 483 ),
498 484 status=404)
499 485
500 486 def test_errors_when_comparing_unknown_target_repo(self, backend):
501 487 repo = backend.repo
502 488 badrepo = 'badrepo'
503 489
504 490 response = self.app.get(
505 491 route_path('repo_compare',
506 492 repo_name=repo.repo_name,
507 493 source_ref_type="rev", source_ref='tip',
508 494 target_ref_type="rev", target_ref='tip',
509 495 params=dict(merge='1', target_repo=badrepo),
510 496 ),
511 497 status=302)
512 498 redirected = response.follow()
513 499 redirected.mustcontain(
514 500 'Could not find the target repo: `{}`'.format(badrepo))
515 501
516 502 def test_compare_not_in_preview_mode(self, backend_stub):
517 503 commit0 = backend_stub.repo.get_commit(commit_idx=0)
518 504 commit1 = backend_stub.repo.get_commit(commit_idx=1)
519 505
520 506 response = self.app.get(
521 507 route_path('repo_compare',
522 508 repo_name=backend_stub.repo_name,
523 509 source_ref_type="rev", source_ref=commit0.raw_id,
524 510 target_ref_type="rev", target_ref=commit1.raw_id,
525 511 ))
526 512
527 513 # outgoing commits between those commits
528 514 compare_page = ComparePage(response)
529 515 compare_page.swap_is_visible()
530 516 compare_page.target_source_are_enabled()
531 517
532 518 def test_compare_of_fork_with_largefiles(self, backend_hg, settings_util):
533 519 orig = backend_hg.create_repo(number_of_commits=1)
534 520 fork = backend_hg.create_fork()
535 521
536 522 settings_util.create_repo_rhodecode_ui(
537 523 orig, 'extensions', value='', key='largefiles', active=False)
538 524 settings_util.create_repo_rhodecode_ui(
539 525 fork, 'extensions', value='', key='largefiles', active=True)
540 526
541 527 compare_module = ('rhodecode.lib.vcs.backends.hg.repository.'
542 528 'MercurialRepository.compare')
543 529 with mock.patch(compare_module) as compare_mock:
544 530 compare_mock.side_effect = RepositoryRequirementError()
545 531
546 532 response = self.app.get(
547 533 route_path('repo_compare',
548 534 repo_name=orig.repo_name,
549 535 source_ref_type="rev", source_ref="tip",
550 536 target_ref_type="rev", target_ref="tip",
551 537 params=dict(merge='1', target_repo=fork.repo_name),
552 538 ),
553 539 status=302)
554 540
555 541 assert_session_flash(
556 542 response,
557 543 'Could not compare repos with different large file settings')
558 544
559 545
560 546 @pytest.mark.usefixtures("autologin_user")
561 547 class TestCompareControllerSvn(object):
562 548
563 549 def test_supports_references_with_path(self, app, backend_svn):
564 550 repo = backend_svn['svn-simple-layout']
565 551 commit_id = repo.get_commit(commit_idx=-1).raw_id
566 552 response = app.get(
567 553 route_path('repo_compare',
568 554 repo_name=repo.repo_name,
569 555 source_ref_type="tag",
570 556 source_ref="%s@%s" % ('tags/v0.1', commit_id),
571 557 target_ref_type="tag",
572 558 target_ref="%s@%s" % ('tags/v0.2', commit_id),
573 559 params=dict(merge='1'),
574 560 ),
575 561 status=200)
576 562
577 563 # Expecting no commits, since both paths are at the same revision
578 564 response.mustcontain('No commits in this compare')
579 565
580 566 # Should find only one file changed when comparing those two tags
581 567 response.mustcontain('example.py')
582 568 compare_page = ComparePage(response)
583 569 compare_page.contains_change_summary(1, 5, 1)
584 570
585 571 def test_shows_commits_if_different_ids(self, app, backend_svn):
586 572 repo = backend_svn['svn-simple-layout']
587 573 source_id = repo.get_commit(commit_idx=-6).raw_id
588 574 target_id = repo.get_commit(commit_idx=-1).raw_id
589 575 response = app.get(
590 576 route_path('repo_compare',
591 577 repo_name=repo.repo_name,
592 578 source_ref_type="tag",
593 579 source_ref="%s@%s" % ('tags/v0.1', source_id),
594 580 target_ref_type="tag",
595 581 target_ref="%s@%s" % ('tags/v0.2', target_id),
596 582 params=dict(merge='1')
597 583 ),
598 584 status=200)
599 585
600 586 # It should show commits
601 587 assert 'No commits in this compare' not in response.text
602 588
603 589 # Should find only one file changed when comparing those two tags
604 590 response.mustcontain('example.py')
605 591 compare_page = ComparePage(response)
606 592 compare_page.contains_change_summary(1, 5, 1)
607 593
608 594
609 595 class ComparePage(AssertResponse):
610 596 """
611 597 Abstracts the page template from the tests
612 598 """
613 599
614 600 def contains_file_links_and_anchors(self, files):
615 601 doc = lxml.html.fromstring(self.response.body)
616 602 for filename, file_id in files:
617 603 self.contains_one_anchor(file_id)
618 604 diffblock = doc.cssselect('[data-f-path="%s"]' % filename)
619 605 assert len(diffblock) == 2
620 606 for lnk in diffblock[0].cssselect('a'):
621 607 if 'permalink' in lnk.text:
622 608 assert '#{}'.format(file_id) in lnk.attrib['href']
623 609 break
624 610 else:
625 611 pytest.fail('Unable to find permalink')
626 612
627 613 def contains_change_summary(self, files_changed, inserted, deleted):
628 614 template = (
629 615 '{files_changed} file{plural} changed: '
630 616 '<span class="op-added">{inserted} inserted</span>, <span class="op-deleted">{deleted} deleted</span>')
631 617 self.response.mustcontain(template.format(
632 618 files_changed=files_changed,
633 619 plural="s" if files_changed > 1 else "",
634 620 inserted=inserted,
635 621 deleted=deleted))
636 622
637 623 def contains_commits(self, commits, ancestors=None):
638 624 response = self.response
639 625
640 626 for commit in commits:
641 627 # Expecting to see the commit message in an element which
642 628 # has the ID "c-{commit.raw_id}"
643 629 self.element_contains('#c-' + commit.raw_id, commit.message)
644 630 self.contains_one_link(
645 631 'r%s:%s' % (commit.idx, commit.short_id),
646 632 self._commit_url(commit))
647 633
648 634 if ancestors:
649 635 response.mustcontain('Ancestor')
650 636 for ancestor in ancestors:
651 637 self.contains_one_link(
652 638 ancestor.short_id, self._commit_url(ancestor))
653 639
654 640 def _commit_url(self, commit):
655 641 return '/%s/changeset/%s' % (commit.repository.name, commit.raw_id)
656 642
657 643 def swap_is_hidden(self):
658 644 assert '<a id="btn-swap"' not in self.response.text
659 645
660 646 def swap_is_visible(self):
661 647 assert '<a id="btn-swap"' in self.response.text
662 648
663 649 def target_source_are_disabled(self):
664 650 response = self.response
665 651 response.mustcontain("var enable_fields = false;")
666 652 response.mustcontain('.select2("enable", enable_fields)')
667 653
668 654 def target_source_are_enabled(self):
669 655 response = self.response
670 656 response.mustcontain("var enable_fields = true;")
@@ -1,168 +1,154 b''
1 1
2 2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19
20 20 import pytest
21 21
22 22 from .test_repo_compare import ComparePage
23
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
23 from rhodecode.tests.routes import route_path
38 24
39 25
40 26 @pytest.mark.usefixtures("autologin_user", "app")
41 27 class TestCompareView(object):
42 28
43 29 @pytest.mark.xfail_backends("svn", msg="Depends on branch and tag support")
44 30 def test_compare_tag(self, backend):
45 31 tag1 = 'v0.1.2'
46 32 tag2 = 'v0.1.3'
47 33 response = self.app.get(
48 34 route_path(
49 35 'repo_compare',
50 36 repo_name=backend.repo_name,
51 37 source_ref_type="tag", source_ref=tag1,
52 38 target_ref_type="tag", target_ref=tag2),
53 39 status=200)
54 40
55 41 response.mustcontain('%s@%s' % (backend.repo_name, tag1))
56 42 response.mustcontain('%s@%s' % (backend.repo_name, tag2))
57 43
58 44 # outgoing commits between tags
59 45 commit_indexes = {
60 46 'git': [113] + list(range(115, 121)),
61 47 'hg': [112] + list(range(115, 121)),
62 48 }
63 49 repo = backend.repo
64 50 commits = (repo.get_commit(commit_idx=idx)
65 51 for idx in commit_indexes[backend.alias])
66 52 compare_page = ComparePage(response)
67 53 compare_page.contains_change_summary(11, 94, 64)
68 54 compare_page.contains_commits(commits)
69 55
70 56 # files diff
71 57 short_id = short_id_new = ''
72 58 if backend.alias == 'git':
73 59 short_id = '5a3a8fb00555'
74 60 short_id_new = '0ba5f8a46600'
75 61 if backend.alias == 'hg':
76 62 short_id = '17544fbfcd33'
77 63 short_id_new = 'a7e60bff65d5'
78 64
79 65 compare_page.contains_file_links_and_anchors([
80 66 # modified
81 67 ('docs/api/utils/index.rst', 'a_c-{}-1c5cf9e91c12'.format(short_id)),
82 68 ('test_and_report.sh', 'a_c-{}-e3305437df55'.format(short_id)),
83 69 # added
84 70 ('.hgignore', 'a_c-{}-c8e92ef85cd1'.format(short_id_new)),
85 71 ('.hgtags', 'a_c-{}-6e08b694d687'.format(short_id_new)),
86 72 ('docs/api/index.rst', 'a_c-{}-2c14b00f3393'.format(short_id_new)),
87 73 ('vcs/__init__.py', 'a_c-{}-430ccbc82bdf'.format(short_id_new)),
88 74 ('vcs/backends/hg.py', 'a_c-{}-9c390eb52cd6'.format(short_id_new)),
89 75 ('vcs/utils/__init__.py', 'a_c-{}-ebb592c595c0'.format(short_id_new)),
90 76 ('vcs/utils/annotate.py', 'a_c-{}-7abc741b5052'.format(short_id_new)),
91 77 ('vcs/utils/diffs.py', 'a_c-{}-2ef0ef106c56'.format(short_id_new)),
92 78 ('vcs/utils/lazy.py', 'a_c-{}-3150cb87d4b7'.format(short_id_new)),
93 79 ])
94 80
95 81 @pytest.mark.xfail_backends("svn", msg="Depends on branch and tag support")
96 82 def test_compare_tag_branch(self, backend):
97 83 revisions = {
98 84 'hg': {
99 85 'tag': 'v0.2.0',
100 86 'branch': 'default',
101 87 'response': (147, 5701, 10177)
102 88 },
103 89 'git': {
104 90 'tag': 'v0.2.2',
105 91 'branch': 'master',
106 92 'response': (70, 1855, 3002)
107 93 },
108 94 }
109 95
110 96 # Backend specific data, depends on the test repository for
111 97 # functional tests.
112 98 data = revisions[backend.alias]
113 99
114 100 response = self.app.get(
115 101 route_path(
116 102 'repo_compare',
117 103 repo_name=backend.repo_name,
118 104 source_ref_type='branch', source_ref=data['branch'],
119 105 target_ref_type="tag", target_ref=data['tag'],
120 106 ))
121 107
122 108 response.mustcontain('%s@%s' % (backend.repo_name, data['branch']))
123 109 response.mustcontain('%s@%s' % (backend.repo_name, data['tag']))
124 110 compare_page = ComparePage(response)
125 111 compare_page.contains_change_summary(*data['response'])
126 112
127 113 def test_index_branch(self, backend):
128 114 head_id = backend.default_head_id
129 115 response = self.app.get(
130 116 route_path(
131 117 'repo_compare',
132 118 repo_name=backend.repo_name,
133 119 source_ref_type="branch", source_ref=head_id,
134 120 target_ref_type="branch", target_ref=head_id,
135 121 ))
136 122
137 123 response.mustcontain('%s@%s' % (backend.repo_name, head_id))
138 124
139 125 # branches are equal
140 126 response.mustcontain('No files')
141 127 response.mustcontain('No commits in this compare')
142 128
143 129 def test_compare_commits(self, backend):
144 130 repo = backend.repo
145 131 commit1 = repo.get_commit(commit_idx=0)
146 132 commit1_short_id = commit1.short_id
147 133 commit2 = repo.get_commit(commit_idx=1)
148 134 commit2_short_id = commit2.short_id
149 135
150 136 response = self.app.get(
151 137 route_path(
152 138 'repo_compare',
153 139 repo_name=backend.repo_name,
154 140 source_ref_type="rev", source_ref=commit1.raw_id,
155 141 target_ref_type="rev", target_ref=commit2.raw_id,
156 142 ))
157 143 response.mustcontain('%s@%s' % (backend.repo_name, commit1.raw_id))
158 144 response.mustcontain('%s@%s' % (backend.repo_name, commit2.raw_id))
159 145 compare_page = ComparePage(response)
160 146
161 147 # files
162 148 compare_page.contains_change_summary(1, 7, 0)
163 149
164 150 # outgoing commits between those commits
165 151 compare_page.contains_commits([commit2])
166 152 anchor = 'a_c-{}-c8e92ef85cd1'.format(commit2_short_id)
167 153 response.mustcontain(anchor)
168 154 compare_page.contains_file_links_and_anchors([('.hgignore', anchor),])
@@ -1,292 +1,279 b''
1 1
2 2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19
20 20 import pytest
21 21
22 22 from rhodecode.apps.repository.tests.test_repo_compare import ComparePage
23 23 from rhodecode.lib.vcs import nodes
24 24 from rhodecode.lib.vcs.backends.base import EmptyCommit
25 25 from rhodecode.tests.fixture import Fixture
26 26 from rhodecode.tests.utils import commit_change
27
28 fixture = Fixture()
27 from rhodecode.tests.routes import route_path
29 28
30 29
31 def route_path(name, params=None, **kwargs):
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
30 fixture = Fixture()
44 31
45 32
46 33 @pytest.mark.usefixtures("autologin_user", "app")
47 34 class TestSideBySideDiff(object):
48 35
49 36 def test_diff_sidebyside_single_commit(self, app, backend):
50 37 commit_id_range = {
51 38 'hg': {
52 39 'commits': ['25d7e49c18b159446cadfa506a5cf8ad1cb04067',
53 40 '603d6c72c46d953420c89d36372f08d9f305f5dd'],
54 41 'changes': (21, 943, 288),
55 42 },
56 43 'git': {
57 44 'commits': ['6fc9270775aaf5544c1deb014f4ddd60c952fcbb',
58 45 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'],
59 46 'changes': (20, 941, 286),
60 47 },
61 48
62 49 'svn': {
63 50 'commits': ['336',
64 51 '337'],
65 52 'changes': (21, 943, 288),
66 53 },
67 54 }
68 55
69 56 commit_info = commit_id_range[backend.alias]
70 57 commit2, commit1 = commit_info['commits']
71 58 file_changes = commit_info['changes']
72 59
73 60 response = self.app.get(route_path(
74 61 'repo_compare',
75 62 repo_name=backend.repo_name,
76 63 source_ref_type='rev',
77 64 source_ref=commit2,
78 65 target_repo=backend.repo_name,
79 66 target_ref_type='rev',
80 67 target_ref=commit1,
81 68 params=dict(target_repo=backend.repo_name, diffmode='sidebyside')
82 69 ))
83 70
84 71 compare_page = ComparePage(response)
85 72 compare_page.contains_change_summary(*file_changes)
86 73 response.mustcontain('Collapse 1 commit')
87 74
88 75 def test_diff_sidebyside_two_commits(self, app, backend):
89 76 commit_id_range = {
90 77 'hg': {
91 78 'commits': ['4fdd71e9427417b2e904e0464c634fdee85ec5a7',
92 79 '603d6c72c46d953420c89d36372f08d9f305f5dd'],
93 80 'changes': (32, 1165, 308),
94 81 },
95 82 'git': {
96 83 'commits': ['f5fbf9cfd5f1f1be146f6d3b38bcd791a7480c13',
97 84 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'],
98 85 'changes': (31, 1163, 306),
99 86 },
100 87
101 88 'svn': {
102 89 'commits': ['335',
103 90 '337'],
104 91 'changes': (32, 1179, 310),
105 92 },
106 93 }
107 94
108 95 commit_info = commit_id_range[backend.alias]
109 96 commit2, commit1 = commit_info['commits']
110 97 file_changes = commit_info['changes']
111 98
112 99 response = self.app.get(route_path(
113 100 'repo_compare',
114 101 repo_name=backend.repo_name,
115 102 source_ref_type='rev',
116 103 source_ref=commit2,
117 104 target_repo=backend.repo_name,
118 105 target_ref_type='rev',
119 106 target_ref=commit1,
120 107 params=dict(target_repo=backend.repo_name, diffmode='sidebyside')
121 108 ))
122 109
123 110 compare_page = ComparePage(response)
124 111 compare_page.contains_change_summary(*file_changes)
125 112
126 113 response.mustcontain('Collapse 2 commits')
127 114
128 115 def test_diff_sidebyside_collapsed_commits(self, app, backend_svn):
129 116 commit_id_range = {
130 117
131 118 'svn': {
132 119 'commits': ['330',
133 120 '337'],
134 121
135 122 },
136 123 }
137 124
138 125 commit_info = commit_id_range['svn']
139 126 commit2, commit1 = commit_info['commits']
140 127
141 128 response = self.app.get(route_path(
142 129 'repo_compare',
143 130 repo_name=backend_svn.repo_name,
144 131 source_ref_type='rev',
145 132 source_ref=commit2,
146 133 target_repo=backend_svn.repo_name,
147 134 target_ref_type='rev',
148 135 target_ref=commit1,
149 136 params=dict(target_repo=backend_svn.repo_name, diffmode='sidebyside')
150 137 ))
151 138
152 139 response.mustcontain('Expand 7 commits')
153 140
154 141 @pytest.mark.xfail(reason='GIT does not handle empty commit compare correct (missing 1 commit)')
155 142 def test_diff_side_by_side_from_0_commit(self, app, backend, backend_stub):
156 143 f_path = b'test_sidebyside_file.py'
157 144 commit1_content = b'content-25d7e49c18b159446c\n'
158 145 commit2_content = b'content-603d6c72c46d953420\n'
159 146 repo = backend.create_repo()
160 147
161 148 commit1 = commit_change(
162 149 repo.repo_name, filename=f_path, content=commit1_content,
163 150 message='A', vcs_type=backend.alias, parent=None, newfile=True)
164 151
165 152 commit2 = commit_change(
166 153 repo.repo_name, filename=f_path, content=commit2_content,
167 154 message='B, child of A', vcs_type=backend.alias, parent=commit1)
168 155
169 156 response = self.app.get(route_path(
170 157 'repo_compare',
171 158 repo_name=repo.repo_name,
172 159 source_ref_type='rev',
173 160 source_ref=EmptyCommit().raw_id,
174 161 target_ref_type='rev',
175 162 target_ref=commit2.raw_id,
176 163 params=dict(diffmode='sidebyside')
177 164 ))
178 165
179 166 response.mustcontain('Collapse 2 commits')
180 167 response.mustcontain('123 file changed')
181 168
182 169 response.mustcontain(
183 170 'r%s:%s...r%s:%s' % (
184 171 commit1.idx, commit1.short_id, commit2.idx, commit2.short_id))
185 172
186 173 response.mustcontain(f_path)
187 174
188 175 @pytest.mark.xfail(reason='GIT does not handle empty commit compare correct (missing 1 commit)')
189 176 def test_diff_side_by_side_from_0_commit_with_file_filter(self, app, backend, backend_stub):
190 177 f_path = b'test_sidebyside_file.py'
191 178 commit1_content = b'content-25d7e49c18b159446c\n'
192 179 commit2_content = b'content-603d6c72c46d953420\n'
193 180 repo = backend.create_repo()
194 181
195 182 commit1 = commit_change(
196 183 repo.repo_name, filename=f_path, content=commit1_content,
197 184 message='A', vcs_type=backend.alias, parent=None, newfile=True)
198 185
199 186 commit2 = commit_change(
200 187 repo.repo_name, filename=f_path, content=commit2_content,
201 188 message='B, child of A', vcs_type=backend.alias, parent=commit1)
202 189
203 190 response = self.app.get(route_path(
204 191 'repo_compare',
205 192 repo_name=repo.repo_name,
206 193 source_ref_type='rev',
207 194 source_ref=EmptyCommit().raw_id,
208 195 target_ref_type='rev',
209 196 target_ref=commit2.raw_id,
210 197 params=dict(f_path=f_path, target_repo=repo.repo_name, diffmode='sidebyside')
211 198 ))
212 199
213 200 response.mustcontain('Collapse 2 commits')
214 201 response.mustcontain('1 file changed')
215 202
216 203 response.mustcontain(
217 204 'r%s:%s...r%s:%s' % (
218 205 commit1.idx, commit1.short_id, commit2.idx, commit2.short_id))
219 206
220 207 response.mustcontain(f_path)
221 208
222 209 def test_diff_side_by_side_with_empty_file(self, app, backend, backend_stub):
223 210 commits = [
224 211 {'message': 'First commit'},
225 212 {'message': 'Second commit'},
226 213 {'message': 'Commit with binary',
227 214 'added': [nodes.FileNode(b'file.empty', content=b'')]},
228 215 ]
229 216 f_path = 'file.empty'
230 217 repo = backend.create_repo(commits=commits)
231 218 commit1 = repo.get_commit(commit_idx=0)
232 219 commit2 = repo.get_commit(commit_idx=1)
233 220 commit3 = repo.get_commit(commit_idx=2)
234 221
235 222 response = self.app.get(route_path(
236 223 'repo_compare',
237 224 repo_name=repo.repo_name,
238 225 source_ref_type='rev',
239 226 source_ref=commit1.raw_id,
240 227 target_ref_type='rev',
241 228 target_ref=commit3.raw_id,
242 229 params=dict(f_path=f_path, target_repo=repo.repo_name, diffmode='sidebyside')
243 230 ))
244 231
245 232 response.mustcontain('Collapse 2 commits')
246 233 response.mustcontain('1 file changed')
247 234
248 235 response.mustcontain(
249 236 'r%s:%s...r%s:%s' % (
250 237 commit2.idx, commit2.short_id, commit3.idx, commit3.short_id))
251 238
252 239 response.mustcontain(f_path)
253 240
254 241 def test_diff_sidebyside_two_commits_with_file_filter(self, app, backend):
255 242 commit_id_range = {
256 243 'hg': {
257 244 'commits': ['4fdd71e9427417b2e904e0464c634fdee85ec5a7',
258 245 '603d6c72c46d953420c89d36372f08d9f305f5dd'],
259 246 'changes': (1, 3, 3)
260 247 },
261 248 'git': {
262 249 'commits': ['f5fbf9cfd5f1f1be146f6d3b38bcd791a7480c13',
263 250 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'],
264 251 'changes': (1, 3, 3)
265 252 },
266 253
267 254 'svn': {
268 255 'commits': ['335',
269 256 '337'],
270 257 'changes': (1, 3, 3)
271 258 },
272 259 }
273 260 f_path = 'docs/conf.py'
274 261
275 262 commit_info = commit_id_range[backend.alias]
276 263 commit2, commit1 = commit_info['commits']
277 264 file_changes = commit_info['changes']
278 265
279 266 response = self.app.get(route_path(
280 267 'repo_compare',
281 268 repo_name=backend.repo_name,
282 269 source_ref_type='rev',
283 270 source_ref=commit2,
284 271 target_ref_type='rev',
285 272 target_ref=commit1,
286 273 params=dict(f_path=f_path, target_repo=backend.repo_name, diffmode='sidebyside')
287 274 ))
288 275
289 276 response.mustcontain('Collapse 2 commits')
290 277
291 278 compare_page = ComparePage(response)
292 279 compare_page.contains_change_summary(*file_changes)
@@ -1,138 +1,122 b''
1 1
2 2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19
20 20 import pytest
21 21 from rhodecode.model.auth_token import AuthTokenModel
22 22 from rhodecode.tests import TestController
23
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
23 from rhodecode.tests.routes import route_path
40 24
41 25
42 26 class TestFeedView(TestController):
43 27
44 28 @pytest.mark.parametrize("feed_type,response_types,content_type",[
45 29 ('rss', ['<rss version="2.0"'],
46 30 "application/rss+xml"),
47 31 ('atom', ['xmlns="http://www.w3.org/2005/Atom"', 'xml:lang="en-us"'],
48 32 "application/atom+xml"),
49 33 ])
50 34 def test_feed(self, backend, feed_type, response_types, content_type):
51 35 self.log_user()
52 36 response = self.app.get(
53 37 route_path('{}_feed_home'.format(feed_type),
54 38 repo_name=backend.repo_name))
55 39
56 40 for content in response_types:
57 41 response.mustcontain(content)
58 42
59 43 assert response.content_type == content_type
60 44
61 45 @pytest.mark.parametrize("feed_type, content_type", [
62 46 ('rss', "application/rss+xml"),
63 47 ('atom', "application/atom+xml")
64 48 ])
65 49 def test_feed_with_auth_token(
66 50 self, backend, user_admin, feed_type, content_type):
67 51 auth_token = user_admin.feed_token
68 52 assert auth_token != ''
69 53
70 54 response = self.app.get(
71 55 route_path(
72 56 '{}_feed_home'.format(feed_type),
73 57 repo_name=backend.repo_name,
74 58 params=dict(auth_token=auth_token)),
75 59 status=200)
76 60
77 61 assert response.content_type == content_type
78 62
79 63 @pytest.mark.parametrize("feed_type, content_type", [
80 64 ('rss', "application/rss+xml"),
81 65 ('atom', "application/atom+xml")
82 66 ])
83 67 def test_feed_with_auth_token_by_uid(
84 68 self, backend, user_admin, feed_type, content_type):
85 69 auth_token = user_admin.feed_token
86 70 assert auth_token != ''
87 71
88 72 response = self.app.get(
89 73 route_path(
90 74 '{}_feed_home'.format(feed_type),
91 75 repo_name='_{}'.format(backend.repo.repo_id),
92 76 params=dict(auth_token=auth_token)),
93 77 status=200)
94 78
95 79 assert response.content_type == content_type
96 80
97 81 @pytest.mark.parametrize("feed_type, content_type", [
98 82 ('rss', "application/rss+xml"),
99 83 ('atom', "application/atom+xml")
100 84 ])
101 85 def test_feed_old_urls_with_auth_token(
102 86 self, backend, user_admin, feed_type, content_type):
103 87 auth_token = user_admin.feed_token
104 88 assert auth_token != ''
105 89
106 90 response = self.app.get(
107 91 route_path(
108 92 '{}_feed_home_old'.format(feed_type),
109 93 repo_name=backend.repo_name,
110 94 params=dict(auth_token=auth_token)),
111 95 status=200)
112 96
113 97 assert response.content_type == content_type
114 98
115 99 @pytest.mark.parametrize("feed_type", ['rss', 'atom'])
116 100 def test_feed_with_auth_token_of_wrong_type(
117 101 self, backend, user_util, feed_type):
118 102 user = user_util.create_user()
119 103 auth_token = AuthTokenModel().create(
120 104 user.user_id, u'test-token', -1, AuthTokenModel.cls.ROLE_API)
121 105 auth_token = auth_token.api_key
122 106
123 107 self.app.get(
124 108 route_path(
125 109 '{}_feed_home'.format(feed_type),
126 110 repo_name=backend.repo_name,
127 111 params=dict(auth_token=auth_token)),
128 112 status=302)
129 113
130 114 auth_token = AuthTokenModel().create(
131 115 user.user_id, u'test-token', -1, AuthTokenModel.cls.ROLE_FEED)
132 116 auth_token = auth_token.api_key
133 117 self.app.get(
134 118 route_path(
135 119 '{}_feed_home'.format(feed_type),
136 120 repo_name=backend.repo_name,
137 121 params=dict(auth_token=auth_token)),
138 122 status=200)
@@ -1,1125 +1,1090 b''
1 1
2 2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19
20 20 import os
21 21
22 22 import mock
23 23 import pytest
24 24 from collections import OrderedDict
25 25
26 26 from rhodecode.apps.repository.tests.test_repo_compare import ComparePage
27 27 from rhodecode.apps.repository.views.repo_files import RepoFilesView, get_archive_name, get_path_sha
28 28 from rhodecode.lib import helpers as h
29 29 from rhodecode.lib.ext_json import json
30 30 from rhodecode.lib.str_utils import safe_str
31 31 from rhodecode.lib.vcs import nodes
32 32 from rhodecode.lib.vcs.conf import settings
33 33 from rhodecode.model.db import Session, Repository
34 34
35 35 from rhodecode.tests import assert_session_flash
36 36 from rhodecode.tests.fixture import Fixture
37 from rhodecode.tests.routes import route_path
38
37 39
38 40 fixture = Fixture()
39 41
40 42
41 43 def get_node_history(backend_type):
42 44 return {
43 45 'hg': json.loads(fixture.load_resource('hg_node_history_response.json')),
44 46 'git': json.loads(fixture.load_resource('git_node_history_response.json')),
45 47 'svn': json.loads(fixture.load_resource('svn_node_history_response.json')),
46 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 51 def assert_files_in_response(response, files, params):
87 52 template = (
88 53 'href="/%(repo_name)s/files/%(commit_id)s/%(name)s"')
89 54 _assert_items_in_response(response, files, template, params)
90 55
91 56
92 57 def assert_dirs_in_response(response, dirs, params):
93 58 template = (
94 59 'href="/%(repo_name)s/files/%(commit_id)s/%(name)s"')
95 60 _assert_items_in_response(response, dirs, template, params)
96 61
97 62
98 63 def _assert_items_in_response(response, items, template, params):
99 64 for item in items:
100 65 item_params = {'name': item}
101 66 item_params.update(params)
102 67 response.mustcontain(template % item_params)
103 68
104 69
105 70 def assert_timeago_in_response(response, items, params):
106 71 for item in items:
107 72 response.mustcontain(h.age_component(params['date']))
108 73
109 74
110 75 @pytest.mark.usefixtures("app")
111 76 class TestFilesViews(object):
112 77
113 78 def test_show_files(self, backend):
114 79 response = self.app.get(
115 80 route_path('repo_files',
116 81 repo_name=backend.repo_name,
117 82 commit_id='tip', f_path='/'))
118 83 commit = backend.repo.get_commit()
119 84
120 85 params = {
121 86 'repo_name': backend.repo_name,
122 87 'commit_id': commit.raw_id,
123 88 'date': commit.date
124 89 }
125 90 assert_dirs_in_response(response, ['docs', 'vcs'], params)
126 91 files = [
127 92 '.gitignore',
128 93 '.hgignore',
129 94 '.hgtags',
130 95 # TODO: missing in Git
131 96 # '.travis.yml',
132 97 'MANIFEST.in',
133 98 'README.rst',
134 99 # TODO: File is missing in svn repository
135 100 # 'run_test_and_report.sh',
136 101 'setup.cfg',
137 102 'setup.py',
138 103 'test_and_report.sh',
139 104 'tox.ini',
140 105 ]
141 106 assert_files_in_response(response, files, params)
142 107 assert_timeago_in_response(response, files, params)
143 108
144 109 def test_show_files_links_submodules_with_absolute_url(self, backend_hg):
145 110 repo = backend_hg['subrepos']
146 111 response = self.app.get(
147 112 route_path('repo_files',
148 113 repo_name=repo.repo_name,
149 114 commit_id='tip', f_path='/'))
150 115 assert_response = response.assert_response()
151 116 assert_response.contains_one_link(
152 117 'absolute-path @ 000000000000', 'http://example.com/absolute-path')
153 118
154 119 def test_show_files_links_submodules_with_absolute_url_subpaths(
155 120 self, backend_hg):
156 121 repo = backend_hg['subrepos']
157 122 response = self.app.get(
158 123 route_path('repo_files',
159 124 repo_name=repo.repo_name,
160 125 commit_id='tip', f_path='/'))
161 126 assert_response = response.assert_response()
162 127 assert_response.contains_one_link(
163 128 'subpaths-path @ 000000000000',
164 129 'http://sub-base.example.com/subpaths-path')
165 130
166 131 @pytest.mark.xfail_backends("svn", reason="Depends on branch support")
167 132 def test_files_menu(self, backend):
168 133 new_branch = "temp_branch_name"
169 134 commits = [
170 135 {'message': 'a'},
171 136 {'message': 'b', 'branch': new_branch}
172 137 ]
173 138 backend.create_repo(commits)
174 139 backend.repo.landing_rev = f"branch:{new_branch}"
175 140 Session().commit()
176 141
177 142 # get response based on tip and not new commit
178 143 response = self.app.get(
179 144 route_path('repo_files',
180 145 repo_name=backend.repo_name,
181 146 commit_id='tip', f_path='/'))
182 147
183 148 # make sure Files menu url is not tip but new commit
184 149 landing_rev = backend.repo.landing_ref_name
185 150 files_url = route_path('repo_files:default_path',
186 151 repo_name=backend.repo_name,
187 152 commit_id=landing_rev, params={'at': landing_rev})
188 153
189 154 assert landing_rev != 'tip'
190 155 response.mustcontain(f'<li class="active"><a class="menulink" href="{files_url}">')
191 156
192 157 def test_show_files_commit(self, backend):
193 158 commit = backend.repo.get_commit(commit_idx=32)
194 159
195 160 response = self.app.get(
196 161 route_path('repo_files',
197 162 repo_name=backend.repo_name,
198 163 commit_id=commit.raw_id, f_path='/'))
199 164
200 165 dirs = ['docs', 'tests']
201 166 files = ['README.rst']
202 167 params = {
203 168 'repo_name': backend.repo_name,
204 169 'commit_id': commit.raw_id,
205 170 }
206 171 assert_dirs_in_response(response, dirs, params)
207 172 assert_files_in_response(response, files, params)
208 173
209 174 def test_show_files_different_branch(self, backend):
210 175 branches = dict(
211 176 hg=(150, ['git']),
212 177 # TODO: Git test repository does not contain other branches
213 178 git=(633, ['master']),
214 179 # TODO: Branch support in Subversion
215 180 svn=(150, [])
216 181 )
217 182 idx, branches = branches[backend.alias]
218 183 commit = backend.repo.get_commit(commit_idx=idx)
219 184 response = self.app.get(
220 185 route_path('repo_files',
221 186 repo_name=backend.repo_name,
222 187 commit_id=commit.raw_id, f_path='/'))
223 188
224 189 assert_response = response.assert_response()
225 190 for branch in branches:
226 191 assert_response.element_contains('.tags .branchtag', branch)
227 192
228 193 def test_show_files_paging(self, backend):
229 194 repo = backend.repo
230 195 indexes = [73, 92, 109, 1, 0]
231 196 idx_map = [(rev, repo.get_commit(commit_idx=rev).raw_id)
232 197 for rev in indexes]
233 198
234 199 for idx in idx_map:
235 200 response = self.app.get(
236 201 route_path('repo_files',
237 202 repo_name=backend.repo_name,
238 203 commit_id=idx[1], f_path='/'))
239 204
240 205 response.mustcontain("""r%s:%s""" % (idx[0], idx[1][:8]))
241 206
242 207 def test_file_source(self, backend):
243 208 commit = backend.repo.get_commit(commit_idx=167)
244 209 response = self.app.get(
245 210 route_path('repo_files',
246 211 repo_name=backend.repo_name,
247 212 commit_id=commit.raw_id, f_path='vcs/nodes.py'))
248 213
249 214 msgbox = """<div class="commit">%s</div>"""
250 215 response.mustcontain(msgbox % (commit.message, ))
251 216
252 217 assert_response = response.assert_response()
253 218 if commit.branch:
254 219 assert_response.element_contains(
255 220 '.tags.tags-main .branchtag', commit.branch)
256 221 if commit.tags:
257 222 for tag in commit.tags:
258 223 assert_response.element_contains('.tags.tags-main .tagtag', tag)
259 224
260 225 def test_file_source_annotated(self, backend):
261 226 response = self.app.get(
262 227 route_path('repo_files:annotated',
263 228 repo_name=backend.repo_name,
264 229 commit_id='tip', f_path='vcs/nodes.py'))
265 230 expected_commits = {
266 231 'hg': 'r356',
267 232 'git': 'r345',
268 233 'svn': 'r208',
269 234 }
270 235 response.mustcontain(expected_commits[backend.alias])
271 236
272 237 def test_file_source_authors(self, backend):
273 238 response = self.app.get(
274 239 route_path('repo_file_authors',
275 240 repo_name=backend.repo_name,
276 241 commit_id='tip', f_path='vcs/nodes.py'))
277 242 expected_authors = {
278 243 'hg': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
279 244 'git': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
280 245 'svn': ('marcin', 'lukasz'),
281 246 }
282 247
283 248 for author in expected_authors[backend.alias]:
284 249 response.mustcontain(author)
285 250
286 251 def test_file_source_authors_with_annotation(self, backend):
287 252 response = self.app.get(
288 253 route_path('repo_file_authors',
289 254 repo_name=backend.repo_name,
290 255 commit_id='tip', f_path='vcs/nodes.py',
291 256 params=dict(annotate=1)))
292 257 expected_authors = {
293 258 'hg': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
294 259 'git': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
295 260 'svn': ('marcin', 'lukasz'),
296 261 }
297 262
298 263 for author in expected_authors[backend.alias]:
299 264 response.mustcontain(author)
300 265
301 266 def test_file_source_history(self, backend, xhr_header):
302 267 response = self.app.get(
303 268 route_path('repo_file_history',
304 269 repo_name=backend.repo_name,
305 270 commit_id='tip', f_path='vcs/nodes.py'),
306 271 extra_environ=xhr_header)
307 272 assert get_node_history(backend.alias) == json.loads(response.body)
308 273
309 274 def test_file_source_history_svn(self, backend_svn, xhr_header):
310 275 simple_repo = backend_svn['svn-simple-layout']
311 276 response = self.app.get(
312 277 route_path('repo_file_history',
313 278 repo_name=simple_repo.repo_name,
314 279 commit_id='tip', f_path='trunk/example.py'),
315 280 extra_environ=xhr_header)
316 281
317 282 expected_data = json.loads(
318 283 fixture.load_resource('svn_node_history_branches.json'))
319 284
320 285 assert expected_data == response.json
321 286
322 287 def test_file_source_history_with_annotation(self, backend, xhr_header):
323 288 response = self.app.get(
324 289 route_path('repo_file_history',
325 290 repo_name=backend.repo_name,
326 291 commit_id='tip', f_path='vcs/nodes.py',
327 292 params=dict(annotate=1)),
328 293
329 294 extra_environ=xhr_header)
330 295 assert get_node_history(backend.alias) == json.loads(response.body)
331 296
332 297 def test_tree_search_top_level(self, backend, xhr_header):
333 298 commit = backend.repo.get_commit(commit_idx=173)
334 299 response = self.app.get(
335 300 route_path('repo_files_nodelist',
336 301 repo_name=backend.repo_name,
337 302 commit_id=commit.raw_id, f_path='/'),
338 303 extra_environ=xhr_header)
339 304 assert 'nodes' in response.json
340 305 assert {'name': 'docs', 'type': 'dir'} in response.json['nodes']
341 306
342 307 def test_tree_search_missing_xhr(self, backend):
343 308 self.app.get(
344 309 route_path('repo_files_nodelist',
345 310 repo_name=backend.repo_name,
346 311 commit_id='tip', f_path='/'),
347 312 status=404)
348 313
349 314 def test_tree_search_at_path(self, backend, xhr_header):
350 315 commit = backend.repo.get_commit(commit_idx=173)
351 316 response = self.app.get(
352 317 route_path('repo_files_nodelist',
353 318 repo_name=backend.repo_name,
354 319 commit_id=commit.raw_id, f_path='/docs'),
355 320 extra_environ=xhr_header)
356 321 assert 'nodes' in response.json
357 322 nodes = response.json['nodes']
358 323 assert {'name': 'docs/api', 'type': 'dir'} in nodes
359 324 assert {'name': 'docs/index.rst', 'type': 'file'} in nodes
360 325
361 326 def test_tree_search_at_path_2nd_level(self, backend, xhr_header):
362 327 commit = backend.repo.get_commit(commit_idx=173)
363 328 response = self.app.get(
364 329 route_path('repo_files_nodelist',
365 330 repo_name=backend.repo_name,
366 331 commit_id=commit.raw_id, f_path='/docs/api'),
367 332 extra_environ=xhr_header)
368 333 assert 'nodes' in response.json
369 334 nodes = response.json['nodes']
370 335 assert {'name': 'docs/api/index.rst', 'type': 'file'} in nodes
371 336
372 337 def test_tree_search_at_path_missing_xhr(self, backend):
373 338 self.app.get(
374 339 route_path('repo_files_nodelist',
375 340 repo_name=backend.repo_name,
376 341 commit_id='tip', f_path='/docs'),
377 342 status=404)
378 343
379 344 def test_nodetree(self, backend, xhr_header):
380 345 commit = backend.repo.get_commit(commit_idx=173)
381 346 response = self.app.get(
382 347 route_path('repo_nodetree_full',
383 348 repo_name=backend.repo_name,
384 349 commit_id=commit.raw_id, f_path='/'),
385 350 extra_environ=xhr_header)
386 351
387 352 assert_response = response.assert_response()
388 353
389 354 for attr in ['data-commit-id', 'data-date', 'data-author']:
390 355 elements = assert_response.get_elements('[{}]'.format(attr))
391 356 assert len(elements) > 1
392 357
393 358 for element in elements:
394 359 assert element.get(attr)
395 360
396 361 def test_nodetree_if_file(self, backend, xhr_header):
397 362 commit = backend.repo.get_commit(commit_idx=173)
398 363 response = self.app.get(
399 364 route_path('repo_nodetree_full',
400 365 repo_name=backend.repo_name,
401 366 commit_id=commit.raw_id, f_path='README.rst'),
402 367 extra_environ=xhr_header)
403 368 assert response.text == ''
404 369
405 370 def test_nodetree_wrong_path(self, backend, xhr_header):
406 371 commit = backend.repo.get_commit(commit_idx=173)
407 372 response = self.app.get(
408 373 route_path('repo_nodetree_full',
409 374 repo_name=backend.repo_name,
410 375 commit_id=commit.raw_id, f_path='/dont-exist'),
411 376 extra_environ=xhr_header)
412 377
413 378 err = 'error: There is no file nor ' \
414 379 'directory at the given path'
415 380 assert err in response.text
416 381
417 382 def test_nodetree_missing_xhr(self, backend):
418 383 self.app.get(
419 384 route_path('repo_nodetree_full',
420 385 repo_name=backend.repo_name,
421 386 commit_id='tip', f_path='/'),
422 387 status=404)
423 388
424 389
425 390 @pytest.mark.usefixtures("app", "autologin_user")
426 391 class TestRawFileHandling(object):
427 392
428 393 def test_download_file(self, backend):
429 394 commit = backend.repo.get_commit(commit_idx=173)
430 395 response = self.app.get(
431 396 route_path('repo_file_download',
432 397 repo_name=backend.repo_name,
433 398 commit_id=commit.raw_id, f_path='vcs/nodes.py'),)
434 399
435 400 assert response.content_disposition == 'attachment; filename="nodes.py"; filename*=UTF-8\'\'nodes.py'
436 401 assert response.content_type == "text/x-python"
437 402
438 403 def test_download_file_wrong_cs(self, backend):
439 404 raw_id = u'ERRORce30c96924232dffcd24178a07ffeb5dfc'
440 405
441 406 response = self.app.get(
442 407 route_path('repo_file_download',
443 408 repo_name=backend.repo_name,
444 409 commit_id=raw_id, f_path='vcs/nodes.svg'),
445 410 status=404)
446 411
447 412 msg = """No such commit exists for this repository"""
448 413 response.mustcontain(msg)
449 414
450 415 def test_download_file_wrong_f_path(self, backend):
451 416 commit = backend.repo.get_commit(commit_idx=173)
452 417 f_path = 'vcs/ERRORnodes.py'
453 418
454 419 response = self.app.get(
455 420 route_path('repo_file_download',
456 421 repo_name=backend.repo_name,
457 422 commit_id=commit.raw_id, f_path=f_path),
458 423 status=404)
459 424
460 425 msg = (
461 426 "There is no file nor directory at the given path: "
462 427 "`%s` at commit %s" % (f_path, commit.short_id))
463 428 response.mustcontain(msg)
464 429
465 430 def test_file_raw(self, backend):
466 431 commit = backend.repo.get_commit(commit_idx=173)
467 432 response = self.app.get(
468 433 route_path('repo_file_raw',
469 434 repo_name=backend.repo_name,
470 435 commit_id=commit.raw_id, f_path='vcs/nodes.py'),)
471 436
472 437 assert response.content_type == "text/plain"
473 438
474 439 def test_file_raw_binary(self, backend):
475 440 commit = backend.repo.get_commit()
476 441 response = self.app.get(
477 442 route_path('repo_file_raw',
478 443 repo_name=backend.repo_name,
479 444 commit_id=commit.raw_id,
480 445 f_path='docs/theme/ADC/static/breadcrumb_background.png'),)
481 446
482 447 assert response.content_disposition == 'inline'
483 448
484 449 def test_raw_file_wrong_cs(self, backend):
485 450 raw_id = u'ERRORcce30c96924232dffcd24178a07ffeb5dfc'
486 451
487 452 response = self.app.get(
488 453 route_path('repo_file_raw',
489 454 repo_name=backend.repo_name,
490 455 commit_id=raw_id, f_path='vcs/nodes.svg'),
491 456 status=404)
492 457
493 458 msg = """No such commit exists for this repository"""
494 459 response.mustcontain(msg)
495 460
496 461 def test_raw_wrong_f_path(self, backend):
497 462 commit = backend.repo.get_commit(commit_idx=173)
498 463 f_path = 'vcs/ERRORnodes.py'
499 464 response = self.app.get(
500 465 route_path('repo_file_raw',
501 466 repo_name=backend.repo_name,
502 467 commit_id=commit.raw_id, f_path=f_path),
503 468 status=404)
504 469
505 470 msg = (
506 471 "There is no file nor directory at the given path: "
507 472 "`%s` at commit %s" % (f_path, commit.short_id))
508 473 response.mustcontain(msg)
509 474
510 475 def test_raw_svg_should_not_be_rendered(self, backend):
511 476 backend.create_repo()
512 477 backend.ensure_file(b"xss.svg")
513 478 response = self.app.get(
514 479 route_path('repo_file_raw',
515 480 repo_name=backend.repo_name,
516 481 commit_id='tip', f_path='xss.svg'),)
517 482 # If the content type is image/svg+xml then it allows to render HTML
518 483 # and malicious SVG.
519 484 assert response.content_type == "text/plain"
520 485
521 486
522 487 @pytest.mark.usefixtures("app")
523 488 class TestRepositoryArchival(object):
524 489
525 490 def test_archival(self, backend):
526 491 backend.enable_downloads()
527 492 commit = backend.repo.get_commit(commit_idx=173)
528 493
529 494 for a_type, content_type, extension in settings.ARCHIVE_SPECS:
530 495 path_sha = get_path_sha('/')
531 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 498 fname = commit.raw_id + extension
534 499 response = self.app.get(
535 500 route_path('repo_archivefile',
536 501 repo_name=backend.repo_name,
537 502 fname=fname))
538 503
539 504 assert response.status == '200 OK'
540 505 headers = [
541 506 ('Content-Disposition', f'attachment; filename={filename}'),
542 507 ('Content-Type', content_type),
543 508 ]
544 509
545 510 for header in headers:
546 511 assert header in list(response.headers.items())
547 512
548 513 def test_archival_no_hash(self, backend):
549 514 backend.enable_downloads()
550 515 commit = backend.repo.get_commit(commit_idx=173)
551 516 for a_type, content_type, extension in settings.ARCHIVE_SPECS:
552 517 path_sha = get_path_sha('/')
553 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 520 fname = commit.raw_id + extension
556 521 response = self.app.get(
557 522 route_path('repo_archivefile',
558 523 repo_name=backend.repo_name,
559 524 fname=fname, params={'with_hash': 0}))
560 525
561 526 assert response.status == '200 OK'
562 527 headers = [
563 528 ('Content-Disposition', f'attachment; filename={filename}'),
564 529 ('Content-Type', content_type),
565 530 ]
566 531
567 532 for header in headers:
568 533 assert header in list(response.headers.items())
569 534
570 535 def test_archival_at_path(self, backend):
571 536 backend.enable_downloads()
572 537 commit = backend.repo.get_commit(commit_idx=190)
573 538 at_path = 'vcs'
574 539
575 540 for a_type, content_type, extension in settings.ARCHIVE_SPECS:
576 541 path_sha = get_path_sha(at_path)
577 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 544 fname = commit.raw_id + extension
580 545 response = self.app.get(
581 546 route_path('repo_archivefile',
582 547 repo_name=backend.repo_name,
583 548 fname=fname, params={'at_path': at_path}))
584 549
585 550 assert response.status == '200 OK'
586 551 headers = [
587 552 ('Content-Disposition', f'attachment; filename={filename}'),
588 553 ('Content-Type', content_type),
589 554 ]
590 555
591 556 for header in headers:
592 557 assert header in list(response.headers.items())
593 558
594 559 @pytest.mark.parametrize('arch_ext',[
595 560 'tar', 'rar', 'x', '..ax', '.zipz', 'tar.gz.tar'])
596 561 def test_archival_wrong_ext(self, backend, arch_ext):
597 562 backend.enable_downloads()
598 563 commit = backend.repo.get_commit(commit_idx=173)
599 564
600 565 fname = commit.raw_id + '.' + arch_ext
601 566
602 567 response = self.app.get(
603 568 route_path('repo_archivefile',
604 569 repo_name=backend.repo_name,
605 570 fname=fname))
606 571 response.mustcontain(
607 572 'Unknown archive type for: `{}`'.format(fname))
608 573
609 574 @pytest.mark.parametrize('commit_id', [
610 575 '00x000000', 'tar', 'wrong', '@$@$42413232', '232dffcd'])
611 576 def test_archival_wrong_commit_id(self, backend, commit_id):
612 577 backend.enable_downloads()
613 578 fname = f'{commit_id}.zip'
614 579
615 580 response = self.app.get(
616 581 route_path('repo_archivefile',
617 582 repo_name=backend.repo_name,
618 583 fname=fname))
619 584 response.mustcontain('Unknown commit_id')
620 585
621 586
622 587 @pytest.mark.usefixtures("app")
623 588 class TestFilesDiff(object):
624 589
625 590 @pytest.mark.parametrize("diff", ['diff', 'download', 'raw'])
626 591 def test_file_full_diff(self, backend, diff):
627 592 commit1 = backend.repo.get_commit(commit_idx=-1)
628 593 commit2 = backend.repo.get_commit(commit_idx=-2)
629 594
630 595 response = self.app.get(
631 596 route_path('repo_files_diff',
632 597 repo_name=backend.repo_name,
633 598 f_path='README'),
634 599 params={
635 600 'diff1': commit2.raw_id,
636 601 'diff2': commit1.raw_id,
637 602 'fulldiff': '1',
638 603 'diff': diff,
639 604 })
640 605
641 606 if diff == 'diff':
642 607 # use redirect since this is OLD view redirecting to compare page
643 608 response = response.follow()
644 609
645 610 # It's a symlink to README.rst
646 611 response.mustcontain('README.rst')
647 612 response.mustcontain('No newline at end of file')
648 613
649 614 def test_file_binary_diff(self, backend):
650 615 commits = [
651 616 {'message': 'First commit'},
652 617 {'message': 'Commit with binary',
653 618 'added': [nodes.FileNode(b'file.bin', content='\0BINARY\0')]},
654 619 ]
655 620 repo = backend.create_repo(commits=commits)
656 621
657 622 response = self.app.get(
658 623 route_path('repo_files_diff',
659 624 repo_name=backend.repo_name,
660 625 f_path='file.bin'),
661 626 params={
662 627 'diff1': repo.get_commit(commit_idx=0).raw_id,
663 628 'diff2': repo.get_commit(commit_idx=1).raw_id,
664 629 'fulldiff': '1',
665 630 'diff': 'diff',
666 631 })
667 632 # use redirect since this is OLD view redirecting to compare page
668 633 response = response.follow()
669 634 response.mustcontain('Collapse 1 commit')
670 635 file_changes = (1, 0, 0)
671 636
672 637 compare_page = ComparePage(response)
673 638 compare_page.contains_change_summary(*file_changes)
674 639
675 640 if backend.alias == 'svn':
676 641 response.mustcontain('new file 10644')
677 642 # TODO(marcink): SVN doesn't yet detect binary changes
678 643 else:
679 644 response.mustcontain('new file 100644')
680 645 response.mustcontain('binary diff hidden')
681 646
682 647 def test_diff_2way(self, backend):
683 648 commit1 = backend.repo.get_commit(commit_idx=-1)
684 649 commit2 = backend.repo.get_commit(commit_idx=-2)
685 650 response = self.app.get(
686 651 route_path('repo_files_diff_2way_redirect',
687 652 repo_name=backend.repo_name,
688 653 f_path='README'),
689 654 params={
690 655 'diff1': commit2.raw_id,
691 656 'diff2': commit1.raw_id,
692 657 })
693 658 # use redirect since this is OLD view redirecting to compare page
694 659 response = response.follow()
695 660
696 661 # It's a symlink to README.rst
697 662 response.mustcontain('README.rst')
698 663 response.mustcontain('No newline at end of file')
699 664
700 665 def test_requires_one_commit_id(self, backend, autologin_user):
701 666 response = self.app.get(
702 667 route_path('repo_files_diff',
703 668 repo_name=backend.repo_name,
704 669 f_path='README.rst'),
705 670 status=400)
706 671 response.mustcontain(
707 672 'Need query parameter', 'diff1', 'diff2', 'to generate a diff.')
708 673
709 674 def test_returns_no_files_if_file_does_not_exist(self, vcsbackend):
710 675 repo = vcsbackend.repo
711 676 response = self.app.get(
712 677 route_path('repo_files_diff',
713 678 repo_name=repo.name,
714 679 f_path='does-not-exist-in-any-commit'),
715 680 params={
716 681 'diff1': repo[0].raw_id,
717 682 'diff2': repo[1].raw_id
718 683 })
719 684
720 685 response = response.follow()
721 686 response.mustcontain('No files')
722 687
723 688 def test_returns_redirect_if_file_not_changed(self, backend):
724 689 commit = backend.repo.get_commit(commit_idx=-1)
725 690 response = self.app.get(
726 691 route_path('repo_files_diff_2way_redirect',
727 692 repo_name=backend.repo_name,
728 693 f_path='README'),
729 694 params={
730 695 'diff1': commit.raw_id,
731 696 'diff2': commit.raw_id,
732 697 })
733 698
734 699 response = response.follow()
735 700 response.mustcontain('No files')
736 701 response.mustcontain('No commits in this compare')
737 702
738 703 def test_supports_diff_to_different_path_svn(self, backend_svn):
739 704 #TODO: check this case
740 705 return
741 706
742 707 repo = backend_svn['svn-simple-layout'].scm_instance()
743 708 commit_id_1 = '24'
744 709 commit_id_2 = '26'
745 710
746 711 response = self.app.get(
747 712 route_path('repo_files_diff',
748 713 repo_name=backend_svn.repo_name,
749 714 f_path='trunk/example.py'),
750 715 params={
751 716 'diff1': 'tags/v0.2/example.py@' + commit_id_1,
752 717 'diff2': commit_id_2,
753 718 })
754 719
755 720 response = response.follow()
756 721 response.mustcontain(
757 722 # diff contains this
758 723 "Will print out a useful message on invocation.")
759 724
760 725 # Note: Expecting that we indicate the user what's being compared
761 726 response.mustcontain("trunk/example.py")
762 727 response.mustcontain("tags/v0.2/example.py")
763 728
764 729 def test_show_rev_redirects_to_svn_path(self, backend_svn):
765 730 #TODO: check this case
766 731 return
767 732
768 733 repo = backend_svn['svn-simple-layout'].scm_instance()
769 734 commit_id = repo[-1].raw_id
770 735
771 736 response = self.app.get(
772 737 route_path('repo_files_diff',
773 738 repo_name=backend_svn.repo_name,
774 739 f_path='trunk/example.py'),
775 740 params={
776 741 'diff1': 'branches/argparse/example.py@' + commit_id,
777 742 'diff2': commit_id,
778 743 },
779 744 status=302)
780 745 response = response.follow()
781 746 assert response.headers['Location'].endswith(
782 747 'svn-svn-simple-layout/files/26/branches/argparse/example.py')
783 748
784 749 def test_show_rev_and_annotate_redirects_to_svn_path(self, backend_svn):
785 750 #TODO: check this case
786 751 return
787 752
788 753 repo = backend_svn['svn-simple-layout'].scm_instance()
789 754 commit_id = repo[-1].raw_id
790 755 response = self.app.get(
791 756 route_path('repo_files_diff',
792 757 repo_name=backend_svn.repo_name,
793 758 f_path='trunk/example.py'),
794 759 params={
795 760 'diff1': 'branches/argparse/example.py@' + commit_id,
796 761 'diff2': commit_id,
797 762 'show_rev': 'Show at Revision',
798 763 'annotate': 'true',
799 764 },
800 765 status=302)
801 766 response = response.follow()
802 767 assert response.headers['Location'].endswith(
803 768 'svn-svn-simple-layout/annotate/26/branches/argparse/example.py')
804 769
805 770
806 771 @pytest.mark.usefixtures("app", "autologin_user")
807 772 class TestModifyFilesWithWebInterface(object):
808 773
809 774 def test_add_file_view(self, backend):
810 775 self.app.get(
811 776 route_path('repo_files_add_file',
812 777 repo_name=backend.repo_name,
813 778 commit_id='tip', f_path='/')
814 779 )
815 780
816 781 @pytest.mark.xfail_backends("svn", reason="Depends on online editing")
817 782 def test_add_file_into_repo_missing_content(self, backend, csrf_token):
818 783 backend.create_repo()
819 784 filename = 'init.py'
820 785 response = self.app.post(
821 786 route_path('repo_files_create_file',
822 787 repo_name=backend.repo_name,
823 788 commit_id='tip', f_path='/'),
824 789 params={
825 790 'content': "",
826 791 'filename': filename,
827 792 'csrf_token': csrf_token,
828 793 },
829 794 status=302)
830 795 expected_msg = 'Successfully committed new file `{}`'.format(os.path.join(filename))
831 796 assert_session_flash(response, expected_msg)
832 797
833 798 def test_add_file_into_repo_missing_filename(self, backend, csrf_token):
834 799 commit_id = backend.repo.get_commit().raw_id
835 800 response = self.app.post(
836 801 route_path('repo_files_create_file',
837 802 repo_name=backend.repo_name,
838 803 commit_id=commit_id, f_path='/'),
839 804 params={
840 805 'content': "foo",
841 806 'csrf_token': csrf_token,
842 807 },
843 808 status=302)
844 809
845 810 assert_session_flash(response, 'No filename specified')
846 811
847 812 def test_add_file_into_repo_errors_and_no_commits(
848 813 self, backend, csrf_token):
849 814 repo = backend.create_repo()
850 815 # Create a file with no filename, it will display an error but
851 816 # the repo has no commits yet
852 817 response = self.app.post(
853 818 route_path('repo_files_create_file',
854 819 repo_name=repo.repo_name,
855 820 commit_id='tip', f_path='/'),
856 821 params={
857 822 'content': "foo",
858 823 'csrf_token': csrf_token,
859 824 },
860 825 status=302)
861 826
862 827 assert_session_flash(response, 'No filename specified')
863 828
864 829 # Not allowed, redirect to the summary
865 830 redirected = response.follow()
866 831 summary_url = h.route_path('repo_summary', repo_name=repo.repo_name)
867 832
868 833 # As there are no commits, displays the summary page with the error of
869 834 # creating a file with no filename
870 835
871 836 assert redirected.request.path == summary_url
872 837
873 838 @pytest.mark.parametrize("filename, clean_filename", [
874 839 ('/abs/foo', 'abs/foo'),
875 840 ('../rel/foo', 'rel/foo'),
876 841 ('file/../foo/foo', 'file/foo/foo'),
877 842 ])
878 843 def test_add_file_into_repo_bad_filenames(self, filename, clean_filename, backend, csrf_token):
879 844 repo = backend.create_repo()
880 845 commit_id = repo.get_commit().raw_id
881 846
882 847 response = self.app.post(
883 848 route_path('repo_files_create_file',
884 849 repo_name=repo.repo_name,
885 850 commit_id=commit_id, f_path='/'),
886 851 params={
887 852 'content': "foo",
888 853 'filename': filename,
889 854 'csrf_token': csrf_token,
890 855 },
891 856 status=302)
892 857
893 858 expected_msg = 'Successfully committed new file `{}`'.format(clean_filename)
894 859 assert_session_flash(response, expected_msg)
895 860
896 861 @pytest.mark.parametrize("cnt, filename, content", [
897 862 (1, 'foo.txt', "Content"),
898 863 (2, 'dir/foo.rst', "Content"),
899 864 (3, 'dir/foo-second.rst', "Content"),
900 865 (4, 'rel/dir/foo.bar', "Content"),
901 866 ])
902 867 def test_add_file_into_empty_repo(self, cnt, filename, content, backend, csrf_token):
903 868 repo = backend.create_repo()
904 869 commit_id = repo.get_commit().raw_id
905 870 response = self.app.post(
906 871 route_path('repo_files_create_file',
907 872 repo_name=repo.repo_name,
908 873 commit_id=commit_id, f_path='/'),
909 874 params={
910 875 'content': content,
911 876 'filename': filename,
912 877 'csrf_token': csrf_token,
913 878 },
914 879 status=302)
915 880
916 881 expected_msg = 'Successfully committed new file `{}`'.format(filename)
917 882 assert_session_flash(response, expected_msg)
918 883
919 884 def test_edit_file_view(self, backend):
920 885 response = self.app.get(
921 886 route_path('repo_files_edit_file',
922 887 repo_name=backend.repo_name,
923 888 commit_id=backend.default_head_id,
924 889 f_path='vcs/nodes.py'),
925 890 status=200)
926 891 response.mustcontain("Module holding everything related to vcs nodes.")
927 892
928 893 def test_edit_file_view_not_on_branch(self, backend):
929 894 repo = backend.create_repo()
930 895 backend.ensure_file(b"vcs/nodes.py")
931 896
932 897 response = self.app.get(
933 898 route_path('repo_files_edit_file',
934 899 repo_name=repo.repo_name,
935 900 commit_id='tip',
936 901 f_path='vcs/nodes.py'),
937 902 status=302)
938 903 assert_session_flash(
939 904 response, 'Cannot modify file. Given commit `tip` is not head of a branch.')
940 905
941 906 def test_edit_file_view_commit_changes(self, backend, csrf_token):
942 907 repo = backend.create_repo()
943 908 backend.ensure_file(b"vcs/nodes.py", content=b"print 'hello'")
944 909
945 910 response = self.app.post(
946 911 route_path('repo_files_update_file',
947 912 repo_name=repo.repo_name,
948 913 commit_id=backend.default_head_id,
949 914 f_path='vcs/nodes.py'),
950 915 params={
951 916 'content': "print 'hello world'",
952 917 'message': 'I committed',
953 918 'filename': "vcs/nodes.py",
954 919 'csrf_token': csrf_token,
955 920 },
956 921 status=302)
957 922 assert_session_flash(
958 923 response, 'Successfully committed changes to file `vcs/nodes.py`')
959 924 tip = repo.get_commit(commit_idx=-1)
960 925 assert tip.message == 'I committed'
961 926
962 927 def test_edit_file_view_commit_changes_default_message(self, backend,
963 928 csrf_token):
964 929 repo = backend.create_repo()
965 930 backend.ensure_file(b"vcs/nodes.py", content=b"print 'hello'")
966 931
967 932 commit_id = (
968 933 backend.default_branch_name or
969 934 backend.repo.scm_instance().commit_ids[-1])
970 935
971 936 response = self.app.post(
972 937 route_path('repo_files_update_file',
973 938 repo_name=repo.repo_name,
974 939 commit_id=commit_id,
975 940 f_path='vcs/nodes.py'),
976 941 params={
977 942 'content': "print 'hello world'",
978 943 'message': '',
979 944 'filename': "vcs/nodes.py",
980 945 'csrf_token': csrf_token,
981 946 },
982 947 status=302)
983 948 assert_session_flash(
984 949 response, 'Successfully committed changes to file `vcs/nodes.py`')
985 950 tip = repo.get_commit(commit_idx=-1)
986 951 assert tip.message == 'Edited file vcs/nodes.py via RhodeCode Enterprise'
987 952
988 953 def test_delete_file_view(self, backend):
989 954 self.app.get(
990 955 route_path('repo_files_remove_file',
991 956 repo_name=backend.repo_name,
992 957 commit_id=backend.default_head_id,
993 958 f_path='vcs/nodes.py'),
994 959 status=200)
995 960
996 961 def test_delete_file_view_not_on_branch(self, backend):
997 962 repo = backend.create_repo()
998 963 backend.ensure_file(b'vcs/nodes.py')
999 964
1000 965 response = self.app.get(
1001 966 route_path('repo_files_remove_file',
1002 967 repo_name=repo.repo_name,
1003 968 commit_id='tip',
1004 969 f_path='vcs/nodes.py'),
1005 970 status=302)
1006 971 assert_session_flash(
1007 972 response, 'Cannot modify file. Given commit `tip` is not head of a branch.')
1008 973
1009 974 def test_delete_file_view_commit_changes(self, backend, csrf_token):
1010 975 repo = backend.create_repo()
1011 976 backend.ensure_file(b"vcs/nodes.py")
1012 977
1013 978 response = self.app.post(
1014 979 route_path('repo_files_delete_file',
1015 980 repo_name=repo.repo_name,
1016 981 commit_id=backend.default_head_id,
1017 982 f_path='vcs/nodes.py'),
1018 983 params={
1019 984 'message': 'i committed',
1020 985 'csrf_token': csrf_token,
1021 986 },
1022 987 status=302)
1023 988 assert_session_flash(
1024 989 response, 'Successfully deleted file `vcs/nodes.py`')
1025 990
1026 991
1027 992 @pytest.mark.usefixtures("app")
1028 993 class TestFilesViewOtherCases(object):
1029 994
1030 995 def test_access_empty_repo_redirect_to_summary_with_alert_write_perms(
1031 996 self, backend_stub, autologin_regular_user, user_regular,
1032 997 user_util):
1033 998
1034 999 repo = backend_stub.create_repo()
1035 1000 user_util.grant_user_permission_to_repo(
1036 1001 repo, user_regular, 'repository.write')
1037 1002 response = self.app.get(
1038 1003 route_path('repo_files',
1039 1004 repo_name=repo.repo_name,
1040 1005 commit_id='tip', f_path='/'))
1041 1006
1042 1007 repo_file_add_url = route_path(
1043 1008 'repo_files_add_file',
1044 1009 repo_name=repo.repo_name,
1045 1010 commit_id=0, f_path='')
1046 1011 add_new = f'<a class="alert-link" href="{repo_file_add_url}">add a new file</a>'
1047 1012
1048 1013 repo_file_upload_url = route_path(
1049 1014 'repo_files_upload_file',
1050 1015 repo_name=repo.repo_name,
1051 1016 commit_id=0, f_path='')
1052 1017 upload_new = f'<a class="alert-link" href="{repo_file_upload_url}">upload a new file</a>'
1053 1018
1054 1019 assert_session_flash(
1055 1020 response,
1056 1021 'There are no files yet. Click here to %s or %s.' % (add_new, upload_new)
1057 1022 )
1058 1023
1059 1024 def test_access_empty_repo_redirect_to_summary_with_alert_no_write_perms(
1060 1025 self, backend_stub, autologin_regular_user):
1061 1026 repo = backend_stub.create_repo()
1062 1027 # init session for anon user
1063 1028 route_path('repo_summary', repo_name=repo.repo_name)
1064 1029
1065 1030 repo_file_add_url = route_path(
1066 1031 'repo_files_add_file',
1067 1032 repo_name=repo.repo_name,
1068 1033 commit_id=0, f_path='')
1069 1034
1070 1035 response = self.app.get(
1071 1036 route_path('repo_files',
1072 1037 repo_name=repo.repo_name,
1073 1038 commit_id='tip', f_path='/'))
1074 1039
1075 1040 assert_session_flash(response, no_=repo_file_add_url)
1076 1041
1077 1042 @pytest.mark.parametrize('file_node', [
1078 1043 b'archive/file.zip',
1079 1044 b'diff/my-file.txt',
1080 1045 b'render.py',
1081 1046 b'render',
1082 1047 b'remove_file',
1083 1048 b'remove_file/to-delete.txt',
1084 1049 ])
1085 1050 def test_file_names_equal_to_routes_parts(self, backend, file_node):
1086 1051 backend.create_repo()
1087 1052 backend.ensure_file(file_node)
1088 1053
1089 1054 self.app.get(
1090 1055 route_path('repo_files',
1091 1056 repo_name=backend.repo_name,
1092 1057 commit_id='tip', f_path=safe_str(file_node)),
1093 1058 status=200)
1094 1059
1095 1060
1096 1061 class TestAdjustFilePathForSvn(object):
1097 1062 """
1098 1063 SVN specific adjustments of node history in RepoFilesView.
1099 1064 """
1100 1065
1101 1066 def test_returns_path_relative_to_matched_reference(self):
1102 1067 repo = self._repo(branches=['trunk'])
1103 1068 self.assert_file_adjustment('trunk/file', 'file', repo)
1104 1069
1105 1070 def test_does_not_modify_file_if_no_reference_matches(self):
1106 1071 repo = self._repo(branches=['trunk'])
1107 1072 self.assert_file_adjustment('notes/file', 'notes/file', repo)
1108 1073
1109 1074 def test_does_not_adjust_partial_directory_names(self):
1110 1075 repo = self._repo(branches=['trun'])
1111 1076 self.assert_file_adjustment('trunk/file', 'trunk/file', repo)
1112 1077
1113 1078 def test_is_robust_to_patterns_which_prefix_other_patterns(self):
1114 1079 repo = self._repo(branches=['trunk', 'trunk/new', 'trunk/old'])
1115 1080 self.assert_file_adjustment('trunk/new/file', 'file', repo)
1116 1081
1117 1082 def assert_file_adjustment(self, f_path, expected, repo):
1118 1083 result = RepoFilesView.adjust_file_path_for_svn(f_path, repo)
1119 1084 assert result == expected
1120 1085
1121 1086 def _repo(self, branches=None):
1122 1087 repo = mock.Mock()
1123 1088 repo.branches = OrderedDict((name, '0') for name in branches or [])
1124 1089 repo.tags = {}
1125 1090 return repo
@@ -1,334 +1,317 b''
1 1
2 2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19
20 20 import pytest
21 21
22 22 from rhodecode.tests import TestController, assert_session_flash, HG_FORK, GIT_FORK
23 23
24 24 from rhodecode.tests.fixture import Fixture
25 25 from rhodecode.lib import helpers as h
26 26
27 27 from rhodecode.model.db import Repository
28 28 from rhodecode.model.repo import RepoModel
29 29 from rhodecode.model.user import UserModel
30 30 from rhodecode.model.meta import Session
31
32 fixture = Fixture()
31 from rhodecode.tests.routes import route_path
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
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
34 fixture = Fixture()
52 35
53 36
54 37 FORK_NAME = {
55 38 'hg': HG_FORK,
56 39 'git': GIT_FORK
57 40 }
58 41
59 42
60 43 @pytest.mark.skip_backends('svn')
61 44 class TestRepoForkViewTests(TestController):
62 45
63 46 def test_show_forks(self, backend, xhr_header):
64 47 self.log_user()
65 48 response = self.app.get(
66 49 route_path('repo_forks_data', repo_name=backend.repo_name),
67 50 extra_environ=xhr_header)
68 51
69 52 assert response.json == {u'data': [], u'draw': None,
70 53 u'recordsFiltered': 0, u'recordsTotal': 0}
71 54
72 55 def test_no_permissions_to_fork_page(self, backend, user_util):
73 56 user = user_util.create_user(password='qweqwe')
74 57 user_id = user.user_id
75 58 self.log_user(user.username, 'qweqwe')
76 59
77 60 user_model = UserModel()
78 61 user_model.revoke_perm(user_id, 'hg.fork.repository')
79 62 user_model.grant_perm(user_id, 'hg.fork.none')
80 63 u = UserModel().get(user_id)
81 64 u.inherit_default_permissions = False
82 65 Session().commit()
83 66 # try create a fork
84 67 self.app.get(
85 68 route_path('repo_fork_new', repo_name=backend.repo_name),
86 69 status=404)
87 70
88 71 def test_no_permissions_to_fork_submit(self, backend, csrf_token, user_util):
89 72 user = user_util.create_user(password='qweqwe')
90 73 user_id = user.user_id
91 74 self.log_user(user.username, 'qweqwe')
92 75
93 76 user_model = UserModel()
94 77 user_model.revoke_perm(user_id, 'hg.fork.repository')
95 78 user_model.grant_perm(user_id, 'hg.fork.none')
96 79 u = UserModel().get(user_id)
97 80 u.inherit_default_permissions = False
98 81 Session().commit()
99 82 # try create a fork
100 83 self.app.post(
101 84 route_path('repo_fork_create', repo_name=backend.repo_name),
102 85 {'csrf_token': csrf_token},
103 86 status=404)
104 87
105 88 def test_fork_missing_data(self, autologin_user, backend, csrf_token):
106 89 # try create a fork
107 90 response = self.app.post(
108 91 route_path('repo_fork_create', repo_name=backend.repo_name),
109 92 {'csrf_token': csrf_token},
110 93 status=200)
111 94 # test if html fill works fine
112 95 response.mustcontain('Missing value')
113 96
114 97 def test_create_fork_page(self, autologin_user, backend):
115 98 self.app.get(
116 99 route_path('repo_fork_new', repo_name=backend.repo_name),
117 100 status=200)
118 101
119 102 def test_create_and_show_fork(
120 103 self, autologin_user, backend, csrf_token, xhr_header):
121 104
122 105 # create a fork
123 106 fork_name = FORK_NAME[backend.alias]
124 107 description = 'fork of vcs test'
125 108 repo_name = backend.repo_name
126 109 source_repo = Repository.get_by_repo_name(repo_name)
127 110 creation_args = {
128 111 'repo_name': fork_name,
129 112 'repo_group': '',
130 113 'fork_parent_id': source_repo.repo_id,
131 114 'repo_type': backend.alias,
132 115 'description': description,
133 116 'private': 'False',
134 117 'csrf_token': csrf_token,
135 118 }
136 119
137 120 self.app.post(
138 121 route_path('repo_fork_create', repo_name=repo_name), creation_args)
139 122
140 123 response = self.app.get(
141 124 route_path('repo_forks_data', repo_name=repo_name),
142 125 extra_environ=xhr_header)
143 126
144 127 assert response.json['data'][0]['fork_name'] == \
145 128 """<a href="/%s">%s</a>""" % (fork_name, fork_name)
146 129
147 130 # remove this fork
148 131 fixture.destroy_repo(fork_name)
149 132
150 133 def test_fork_create(self, autologin_user, backend, csrf_token):
151 134 fork_name = FORK_NAME[backend.alias]
152 135 description = 'fork of vcs test'
153 136 repo_name = backend.repo_name
154 137 source_repo = Repository.get_by_repo_name(repo_name)
155 138 creation_args = {
156 139 'repo_name': fork_name,
157 140 'repo_group': '',
158 141 'fork_parent_id': source_repo.repo_id,
159 142 'repo_type': backend.alias,
160 143 'description': description,
161 144 'private': 'False',
162 145 'csrf_token': csrf_token,
163 146 }
164 147 self.app.post(
165 148 route_path('repo_fork_create', repo_name=repo_name), creation_args)
166 149 repo = Repository.get_by_repo_name(FORK_NAME[backend.alias])
167 150 assert repo.fork.repo_name == backend.repo_name
168 151
169 152 # run the check page that triggers the flash message
170 153 response = self.app.get(
171 154 route_path('repo_creating_check', repo_name=fork_name))
172 155 # test if we have a message that fork is ok
173 156 assert_session_flash(response,
174 157 'Forked repository %s as <a href="/%s">%s</a>' % (
175 158 repo_name, fork_name, fork_name))
176 159
177 160 # test if the fork was created in the database
178 161 fork_repo = Session().query(Repository)\
179 162 .filter(Repository.repo_name == fork_name).one()
180 163
181 164 assert fork_repo.repo_name == fork_name
182 165 assert fork_repo.fork.repo_name == repo_name
183 166
184 167 # test if the repository is visible in the list ?
185 168 response = self.app.get(
186 169 h.route_path('repo_summary', repo_name=fork_name))
187 170 response.mustcontain(fork_name)
188 171 response.mustcontain(backend.alias)
189 172 response.mustcontain('Fork of')
190 173 response.mustcontain('<a href="/%s">%s</a>' % (repo_name, repo_name))
191 174
192 175 def test_fork_create_into_group(self, autologin_user, backend, csrf_token):
193 176 group = fixture.create_repo_group('vc')
194 177 group_id = group.group_id
195 178 fork_name = FORK_NAME[backend.alias]
196 179 fork_name_full = 'vc/%s' % fork_name
197 180 description = 'fork of vcs test'
198 181 repo_name = backend.repo_name
199 182 source_repo = Repository.get_by_repo_name(repo_name)
200 183 creation_args = {
201 184 'repo_name': fork_name,
202 185 'repo_group': group_id,
203 186 'fork_parent_id': source_repo.repo_id,
204 187 'repo_type': backend.alias,
205 188 'description': description,
206 189 'private': 'False',
207 190 'csrf_token': csrf_token,
208 191 }
209 192 self.app.post(
210 193 route_path('repo_fork_create', repo_name=repo_name), creation_args)
211 194 repo = Repository.get_by_repo_name(fork_name_full)
212 195 assert repo.fork.repo_name == backend.repo_name
213 196
214 197 # run the check page that triggers the flash message
215 198 response = self.app.get(
216 199 route_path('repo_creating_check', repo_name=fork_name_full))
217 200 # test if we have a message that fork is ok
218 201 assert_session_flash(response,
219 202 'Forked repository %s as <a href="/%s">%s</a>' % (
220 203 repo_name, fork_name_full, fork_name_full))
221 204
222 205 # test if the fork was created in the database
223 206 fork_repo = Session().query(Repository)\
224 207 .filter(Repository.repo_name == fork_name_full).one()
225 208
226 209 assert fork_repo.repo_name == fork_name_full
227 210 assert fork_repo.fork.repo_name == repo_name
228 211
229 212 # test if the repository is visible in the list ?
230 213 response = self.app.get(
231 214 h.route_path('repo_summary', repo_name=fork_name_full))
232 215 response.mustcontain(fork_name_full)
233 216 response.mustcontain(backend.alias)
234 217
235 218 response.mustcontain('Fork of')
236 219 response.mustcontain('<a href="/%s">%s</a>' % (repo_name, repo_name))
237 220
238 221 fixture.destroy_repo(fork_name_full)
239 222 fixture.destroy_repo_group(group_id)
240 223
241 224 def test_fork_read_permission(self, backend, xhr_header, user_util):
242 225 user = user_util.create_user(password='qweqwe')
243 226 user_id = user.user_id
244 227 self.log_user(user.username, 'qweqwe')
245 228
246 229 # create a fake fork
247 230 fork = user_util.create_repo(repo_type=backend.alias)
248 231 source = user_util.create_repo(repo_type=backend.alias)
249 232 repo_name = source.repo_name
250 233
251 234 fork.fork_id = source.repo_id
252 235 fork_name = fork.repo_name
253 236 Session().commit()
254 237
255 238 forks = Repository.query()\
256 239 .filter(Repository.repo_type == backend.alias)\
257 240 .filter(Repository.fork_id == source.repo_id).all()
258 241 assert 1 == len(forks)
259 242
260 243 # set read permissions for this
261 244 RepoModel().grant_user_permission(
262 245 repo=forks[0], user=user_id, perm='repository.read')
263 246 Session().commit()
264 247
265 248 response = self.app.get(
266 249 route_path('repo_forks_data', repo_name=repo_name),
267 250 extra_environ=xhr_header)
268 251
269 252 assert response.json['data'][0]['fork_name'] == \
270 253 """<a href="/%s">%s</a>""" % (fork_name, fork_name)
271 254
272 255 def test_fork_none_permission(self, backend, xhr_header, user_util):
273 256 user = user_util.create_user(password='qweqwe')
274 257 user_id = user.user_id
275 258 self.log_user(user.username, 'qweqwe')
276 259
277 260 # create a fake fork
278 261 fork = user_util.create_repo(repo_type=backend.alias)
279 262 source = user_util.create_repo(repo_type=backend.alias)
280 263 repo_name = source.repo_name
281 264
282 265 fork.fork_id = source.repo_id
283 266
284 267 Session().commit()
285 268
286 269 forks = Repository.query()\
287 270 .filter(Repository.repo_type == backend.alias)\
288 271 .filter(Repository.fork_id == source.repo_id).all()
289 272 assert 1 == len(forks)
290 273
291 274 # set none
292 275 RepoModel().grant_user_permission(
293 276 repo=forks[0], user=user_id, perm='repository.none')
294 277 Session().commit()
295 278
296 279 # fork shouldn't be there
297 280 response = self.app.get(
298 281 route_path('repo_forks_data', repo_name=repo_name),
299 282 extra_environ=xhr_header)
300 283
301 284 assert response.json == {u'data': [], u'draw': None,
302 285 u'recordsFiltered': 0, u'recordsTotal': 0}
303 286
304 287 @pytest.mark.parametrize('url_type', [
305 288 'repo_fork_new',
306 289 'repo_fork_create'
307 290 ])
308 291 def test_fork_is_forbidden_on_archived_repo(self, backend, xhr_header, user_util, url_type):
309 292 user = user_util.create_user(password='qweqwe')
310 293 self.log_user(user.username, 'qweqwe')
311 294
312 295 # create a temporary repo
313 296 source = user_util.create_repo(repo_type=backend.alias)
314 297 repo_name = source.repo_name
315 298 repo = Repository.get_by_repo_name(repo_name)
316 299 repo.archived = True
317 300 Session().commit()
318 301
319 302 response = self.app.get(
320 303 route_path(url_type, repo_name=repo_name), status=302)
321 304
322 305 msg = 'Action not supported for archived repository.'
323 306 assert_session_flash(response, msg)
324 307
325 308
326 309 class TestSVNFork(TestController):
327 310 @pytest.mark.parametrize('route_name', [
328 311 'repo_fork_create', 'repo_fork_new'
329 312 ])
330 313 def test_fork_redirects(self, autologin_user, backend_svn, route_name):
331 314
332 315 self.app.get(route_path(
333 316 route_name, repo_name=backend_svn.repo_name),
334 317 status=404)
@@ -1,150 +1,134 b''
1 1
2 2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19
20 20 import pytest
21 21
22 22 from rhodecode.lib.hash_utils import md5_safe
23 23 from rhodecode.model.db import Repository
24 24 from rhodecode.model.meta import Session
25 25 from rhodecode.model.settings import SettingsModel, IssueTrackerSettingsModel
26
27
28 def route_path(name, params=None, **kwargs):
29 import urllib.request
30 import urllib.parse
31 import urllib.error
26 from rhodecode.tests.routes import route_path
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 30 @pytest.mark.usefixtures("app")
47 31 class TestRepoIssueTracker(object):
48 32 def test_issuetracker_index(self, autologin_user, backend):
49 33 repo = backend.create_repo()
50 34 response = self.app.get(route_path('edit_repo_issuetracker',
51 35 repo_name=repo.repo_name))
52 36 assert response.status_code == 200
53 37
54 38 def test_add_and_test_issuetracker_patterns(
55 39 self, autologin_user, backend, csrf_token, request, xhr_header):
56 40 pattern = 'issuetracker_pat'
57 41 another_pattern = pattern+'1'
58 42 post_url = route_path(
59 43 'edit_repo_issuetracker_update', repo_name=backend.repo.repo_name)
60 44 post_data = {
61 45 'new_pattern_pattern_0': pattern,
62 46 'new_pattern_url_0': 'http://url',
63 47 'new_pattern_prefix_0': 'prefix',
64 48 'new_pattern_description_0': 'description',
65 49 'new_pattern_pattern_1': another_pattern,
66 50 'new_pattern_url_1': '/url1',
67 51 'new_pattern_prefix_1': 'prefix1',
68 52 'new_pattern_description_1': 'description1',
69 53 'csrf_token': csrf_token
70 54 }
71 55 self.app.post(post_url, post_data, status=302)
72 56 self.settings_model = IssueTrackerSettingsModel(repo=backend.repo)
73 57 settings = self.settings_model.get_repo_settings()
74 58 self.uid = md5_safe(pattern)
75 59 assert settings[self.uid]['pat'] == pattern
76 60 self.another_uid = md5_safe(another_pattern)
77 61 assert settings[self.another_uid]['pat'] == another_pattern
78 62
79 63 # test pattern
80 64 data = {'test_text': 'example of issuetracker_pat replacement',
81 65 'csrf_token': csrf_token}
82 66 response = self.app.post(
83 67 route_path('edit_repo_issuetracker_test',
84 68 repo_name=backend.repo.repo_name),
85 69 extra_environ=xhr_header, params=data)
86 70
87 71 assert response.text == \
88 72 'example of <a class="tooltip issue-tracker-link" href="http://url" title="description">prefix</a> replacement'
89 73
90 74 @request.addfinalizer
91 75 def cleanup():
92 76 self.settings_model.delete_entries(self.uid)
93 77 self.settings_model.delete_entries(self.another_uid)
94 78
95 79 def test_edit_issuetracker_pattern(
96 80 self, autologin_user, backend, csrf_token, request):
97 81 entry_key = 'issuetracker_pat_'
98 82 pattern = 'issuetracker_pat2'
99 83 old_pattern = 'issuetracker_pat'
100 84 old_uid = md5_safe(old_pattern)
101 85
102 86 sett = SettingsModel(repo=backend.repo).create_or_update_setting(
103 87 entry_key+old_uid, old_pattern, 'unicode')
104 88 Session().add(sett)
105 89 Session().commit()
106 90 post_url = route_path(
107 91 'edit_repo_issuetracker_update', repo_name=backend.repo.repo_name)
108 92 post_data = {
109 93 'new_pattern_pattern_0': pattern,
110 94 'new_pattern_url_0': '/url',
111 95 'new_pattern_prefix_0': 'prefix',
112 96 'new_pattern_description_0': 'description',
113 97 'uid': old_uid,
114 98 'csrf_token': csrf_token
115 99 }
116 100 self.app.post(post_url, post_data, status=302)
117 101 self.settings_model = IssueTrackerSettingsModel(repo=backend.repo)
118 102 settings = self.settings_model.get_repo_settings()
119 103 self.uid = md5_safe(pattern)
120 104 assert settings[self.uid]['pat'] == pattern
121 105 with pytest.raises(KeyError):
122 106 key = settings[old_uid]
123 107
124 108 @request.addfinalizer
125 109 def cleanup():
126 110 self.settings_model.delete_entries(self.uid)
127 111
128 112 def test_delete_issuetracker_pattern(
129 113 self, autologin_user, backend, csrf_token, settings_util, xhr_header):
130 114 repo = backend.create_repo()
131 115 repo_name = repo.repo_name
132 116 entry_key = 'issuetracker_pat_'
133 117 pattern = 'issuetracker_pat3'
134 118 uid = md5_safe(pattern)
135 119 settings_util.create_repo_rhodecode_setting(
136 120 repo=backend.repo, name=entry_key+uid,
137 121 value=entry_key, type_='unicode', cleanup=False)
138 122
139 123 self.app.post(
140 124 route_path(
141 125 'edit_repo_issuetracker_delete',
142 126 repo_name=backend.repo.repo_name),
143 127 {
144 128 'uid': uid,
145 129 'csrf_token': csrf_token,
146 130 '': ''
147 131 }, extra_environ=xhr_header, status=200)
148 132 settings = IssueTrackerSettingsModel(
149 133 repo=Repository.get_by_repo_name(repo_name)).get_repo_settings()
150 134 assert 'rhodecode_%s%s' % (entry_key, uid) not in settings
@@ -1,75 +1,55 b''
1 1
2 2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19
20 import mock
21 20 import pytest
22 21
23 from rhodecode.lib.utils2 import str2bool
24 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
25 22 from rhodecode.model.db import Repository, UserRepoToPerm, Permission, User
26 from rhodecode.model.meta import Session
27 from rhodecode.tests import (
28 TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN, assert_session_flash)
23
29 24 from rhodecode.tests.fixture import Fixture
25 from rhodecode.tests.routes import route_path
30 26
31 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 30 def _get_permission_for_user(user, repo):
51 31 perm = UserRepoToPerm.query()\
52 32 .filter(UserRepoToPerm.repository ==
53 33 Repository.get_by_repo_name(repo))\
54 34 .filter(UserRepoToPerm.user == User.get_by_username(user))\
55 35 .all()
56 36 return perm
57 37
58 38
59 39 @pytest.mark.usefixtures('autologin_user', 'app')
60 40 class TestAdminRepoMaintenance(object):
61 41 @pytest.mark.parametrize('urlname', [
62 42 'edit_repo_maintenance',
63 43 ])
64 44 def test_show_page(self, urlname, app, backend):
65 45 app.get(route_path(urlname, repo_name=backend.repo_name), status=200)
66 46
67 47 def test_execute_maintenance_for_repo_hg(self, app, backend_hg, autologin_user, xhr_header):
68 48 repo_name = backend_hg.repo_name
69 49
70 50 response = app.get(
71 51 route_path('edit_repo_maintenance_execute',
72 52 repo_name=repo_name,),
73 53 extra_environ=xhr_header)
74 54
75 55 assert "HG Verify repo" in ''.join(response.json)
@@ -1,78 +1,64 b''
1 1
2 2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19
20 20 import pytest
21 21
22 22 from rhodecode.tests.utils import permission_update_data_generator
23
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
23 from rhodecode.tests.routes import route_path
38 24
39 25
40 26 @pytest.mark.usefixtures("app")
41 27 class TestRepoPermissionsView(object):
42 28
43 29 def test_edit_perms_view(self, user_util, autologin_user):
44 30 repo = user_util.create_repo()
45 31 self.app.get(
46 32 route_path('edit_repo_perms',
47 33 repo_name=repo.repo_name), status=200)
48 34
49 35 def test_update_permissions(self, csrf_token, user_util):
50 36 repo = user_util.create_repo()
51 37 repo_name = repo.repo_name
52 38 user = user_util.create_user()
53 39 user_id = user.user_id
54 40 username = user.username
55 41
56 42 # grant new
57 43 form_data = permission_update_data_generator(
58 44 csrf_token,
59 45 default='repository.write',
60 46 grant=[(user_id, 'repository.write', username, 'user')])
61 47
62 48 response = self.app.post(
63 49 route_path('edit_repo_perms',
64 50 repo_name=repo_name), form_data).follow()
65 51
66 52 assert 'Repository access permissions updated' in response
67 53
68 54 # revoke given
69 55 form_data = permission_update_data_generator(
70 56 csrf_token,
71 57 default='repository.read',
72 58 revoke=[(user_id, 'user')])
73 59
74 60 response = self.app.post(
75 61 route_path('edit_repo_perms',
76 62 repo_name=repo_name), form_data).follow()
77 63
78 64 assert 'Repository access permissions updated' in response
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 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