# -*- coding: utf-8 -*-
# Copyright (C) 2010-2020 RhodeCode GmbH
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License, version 3
# (only), as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
#
# This program is dual-licensed. If you wish to learn more about the
# RhodeCode Enterprise Edition, including its added features, Support services,
# and proprietary license terms, please see https://rhodecode.com/licenses/
import mock
import pytest
import rhodecode
from rhodecode.apps._base import ADMIN_PREFIX
from rhodecode.lib.utils2 import md5
from rhodecode.model.db import RhodeCodeUi
from rhodecode.model.meta import Session
from rhodecode.model.settings import SettingsModel, IssueTrackerSettingsModel
from rhodecode.tests import assert_session_flash
from rhodecode.tests.utils import AssertResponse
UPDATE_DATA_QUALNAME = 'rhodecode.model.update.UpdateModel.get_update_data'
def route_path(name, params=None, **kwargs):
import urllib
from rhodecode.apps._base import ADMIN_PREFIX
base_url = {
'admin_settings':
ADMIN_PREFIX +'/settings',
'admin_settings_update':
ADMIN_PREFIX + '/settings/update',
'admin_settings_global':
ADMIN_PREFIX + '/settings/global',
'admin_settings_global_update':
ADMIN_PREFIX + '/settings/global/update',
'admin_settings_vcs':
ADMIN_PREFIX + '/settings/vcs',
'admin_settings_vcs_update':
ADMIN_PREFIX + '/settings/vcs/update',
'admin_settings_vcs_svn_pattern_delete':
ADMIN_PREFIX + '/settings/vcs/svn_pattern_delete',
'admin_settings_mapping':
ADMIN_PREFIX + '/settings/mapping',
'admin_settings_mapping_update':
ADMIN_PREFIX + '/settings/mapping/update',
'admin_settings_visual':
ADMIN_PREFIX + '/settings/visual',
'admin_settings_visual_update':
ADMIN_PREFIX + '/settings/visual/update',
'admin_settings_issuetracker':
ADMIN_PREFIX + '/settings/issue-tracker',
'admin_settings_issuetracker_update':
ADMIN_PREFIX + '/settings/issue-tracker/update',
'admin_settings_issuetracker_test':
ADMIN_PREFIX + '/settings/issue-tracker/test',
'admin_settings_issuetracker_delete':
ADMIN_PREFIX + '/settings/issue-tracker/delete',
'admin_settings_email':
ADMIN_PREFIX + '/settings/email',
'admin_settings_email_update':
ADMIN_PREFIX + '/settings/email/update',
'admin_settings_hooks':
ADMIN_PREFIX + '/settings/hooks',
'admin_settings_hooks_update':
ADMIN_PREFIX + '/settings/hooks/update',
'admin_settings_hooks_delete':
ADMIN_PREFIX + '/settings/hooks/delete',
'admin_settings_search':
ADMIN_PREFIX + '/settings/search',
'admin_settings_labs':
ADMIN_PREFIX + '/settings/labs',
'admin_settings_labs_update':
ADMIN_PREFIX + '/settings/labs/update',
'admin_settings_sessions':
ADMIN_PREFIX + '/settings/sessions',
'admin_settings_sessions_cleanup':
ADMIN_PREFIX + '/settings/sessions/cleanup',
'admin_settings_system':
ADMIN_PREFIX + '/settings/system',
'admin_settings_system_update':
ADMIN_PREFIX + '/settings/system/updates',
'admin_settings_open_source':
ADMIN_PREFIX + '/settings/open_source',
}[name].format(**kwargs)
if params:
base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
return base_url
@pytest.mark.usefixtures('autologin_user', 'app')
class TestAdminSettingsController(object):
@pytest.mark.parametrize('urlname', [
'admin_settings_vcs',
'admin_settings_mapping',
'admin_settings_global',
'admin_settings_visual',
'admin_settings_email',
'admin_settings_hooks',
'admin_settings_search',
])
def test_simple_get(self, urlname):
self.app.get(route_path(urlname))
def test_create_custom_hook(self, csrf_token):
response = self.app.post(
route_path('admin_settings_hooks_update'),
params={
'new_hook_ui_key': 'test_hooks_1',
'new_hook_ui_value': 'cd /tmp',
'csrf_token': csrf_token})
response = response.follow()
response.mustcontain('test_hooks_1')
response.mustcontain('cd /tmp')
def test_create_custom_hook_delete(self, csrf_token):
response = self.app.post(
route_path('admin_settings_hooks_update'),
params={
'new_hook_ui_key': 'test_hooks_2',
'new_hook_ui_value': 'cd /tmp2',
'csrf_token': csrf_token})
response = response.follow()
response.mustcontain('test_hooks_2')
response.mustcontain('cd /tmp2')
hook_id = SettingsModel().get_ui_by_key('test_hooks_2').ui_id
# delete
self.app.post(
route_path('admin_settings_hooks_delete'),
params={'hook_id': hook_id, 'csrf_token': csrf_token})
response = self.app.get(route_path('admin_settings_hooks'))
response.mustcontain(no=['test_hooks_2'])
response.mustcontain(no=['cd /tmp2'])
@pytest.mark.usefixtures('autologin_user', 'app')
class TestAdminSettingsGlobal(object):
def test_pre_post_code_code_active(self, csrf_token):
pre_code = 'rc-pre-code-187652122'
post_code = 'rc-postcode-98165231'
response = self.post_and_verify_settings({
'rhodecode_pre_code': pre_code,
'rhodecode_post_code': post_code,
'csrf_token': csrf_token,
})
response = response.follow()
response.mustcontain(pre_code, post_code)
def test_pre_post_code_code_inactive(self, csrf_token):
pre_code = 'rc-pre-code-187652122'
post_code = 'rc-postcode-98165231'
response = self.post_and_verify_settings({
'rhodecode_pre_code': '',
'rhodecode_post_code': '',
'csrf_token': csrf_token,
})
response = response.follow()
response.mustcontain(no=[pre_code, post_code])
def test_captcha_activate(self, csrf_token):
self.post_and_verify_settings({
'rhodecode_captcha_private_key': '1234567890',
'rhodecode_captcha_public_key': '1234567890',
'csrf_token': csrf_token,
})
response = self.app.get(ADMIN_PREFIX + '/register')
response.mustcontain('captcha')
def test_captcha_deactivate(self, csrf_token):
self.post_and_verify_settings({
'rhodecode_captcha_private_key': '',
'rhodecode_captcha_public_key': '1234567890',
'csrf_token': csrf_token,
})
response = self.app.get(ADMIN_PREFIX + '/register')
response.mustcontain(no=['captcha'])
def test_title_change(self, csrf_token):
old_title = 'RhodeCode'
for new_title in ['Changed', 'Żółwik', old_title]:
response = self.post_and_verify_settings({
'rhodecode_title': new_title,
'csrf_token': csrf_token,
})
response = response.follow()
response.mustcontain(new_title)
def post_and_verify_settings(self, settings):
old_title = 'RhodeCode'
old_realm = 'RhodeCode authentication'
params = {
'rhodecode_title': old_title,
'rhodecode_realm': old_realm,
'rhodecode_pre_code': '',
'rhodecode_post_code': '',
'rhodecode_captcha_private_key': '',
'rhodecode_captcha_public_key': '',
'rhodecode_create_personal_repo_group': False,
'rhodecode_personal_repo_group_pattern': '${username}',
}
params.update(settings)
response = self.app.post(
route_path('admin_settings_global_update'), params=params)
assert_session_flash(response, 'Updated application settings')
app_settings = SettingsModel().get_all_settings()
del settings['csrf_token']
for key, value in settings.iteritems():
assert app_settings[key] == value.decode('utf-8')
return response
@pytest.mark.usefixtures('autologin_user', 'app')
class TestAdminSettingsVcs(object):
def test_contains_svn_default_patterns(self):
response = self.app.get(route_path('admin_settings_vcs'))
expected_patterns = [
'/trunk',
'/branches/*',
'/tags/*',
]
for pattern in expected_patterns:
response.mustcontain(pattern)
def test_add_new_svn_branch_and_tag_pattern(
self, backend_svn, form_defaults, disable_sql_cache,
csrf_token):
form_defaults.update({
'new_svn_branch': '/exp/branches/*',
'new_svn_tag': '/important_tags/*',
'csrf_token': csrf_token,
})
response = self.app.post(
route_path('admin_settings_vcs_update'),
params=form_defaults, status=302)
response = response.follow()
# Expect to find the new values on the page
response.mustcontain('/exp/branches/*')
response.mustcontain('/important_tags/*')
# Expect that those patterns are used to match branches and tags now
repo = backend_svn['svn-simple-layout'].scm_instance()
assert 'exp/branches/exp-sphinx-docs' in repo.branches
assert 'important_tags/v0.5' in repo.tags
def test_add_same_svn_value_twice_shows_an_error_message(
self, form_defaults, csrf_token, settings_util):
settings_util.create_rhodecode_ui('vcs_svn_branch', '/test')
settings_util.create_rhodecode_ui('vcs_svn_tag', '/test')
response = self.app.post(
route_path('admin_settings_vcs_update'),
params={
'paths_root_path': form_defaults['paths_root_path'],
'new_svn_branch': '/test',
'new_svn_tag': '/test',
'csrf_token': csrf_token,
},
status=200)
response.mustcontain("Pattern already exists")
response.mustcontain("Some form inputs contain invalid data.")
@pytest.mark.parametrize('section', [
'vcs_svn_branch',
'vcs_svn_tag',
])
def test_delete_svn_patterns(
self, section, csrf_token, settings_util):
setting = settings_util.create_rhodecode_ui(
section, '/test_delete', cleanup=False)
self.app.post(
route_path('admin_settings_vcs_svn_pattern_delete'),
params={
'delete_svn_pattern': setting.ui_id,
'csrf_token': csrf_token},
headers={'X-REQUESTED-WITH': 'XMLHttpRequest'})
@pytest.mark.parametrize('section', [
'vcs_svn_branch',
'vcs_svn_tag',
])
def test_delete_svn_patterns_raises_404_when_no_xhr(
self, section, csrf_token, settings_util):
setting = settings_util.create_rhodecode_ui(section, '/test_delete')
self.app.post(
route_path('admin_settings_vcs_svn_pattern_delete'),
params={
'delete_svn_pattern': setting.ui_id,
'csrf_token': csrf_token},
status=404)
def test_extensions_hgsubversion(self, form_defaults, csrf_token):
form_defaults.update({
'csrf_token': csrf_token,
'extensions_hgsubversion': 'True',
})
response = self.app.post(
route_path('admin_settings_vcs_update'),
params=form_defaults,
status=302)
response = response.follow()
extensions_input = (
'')
response.mustcontain(extensions_input)
def test_extensions_hgevolve(self, form_defaults, csrf_token):
form_defaults.update({
'csrf_token': csrf_token,
'extensions_evolve': 'True',
})
response = self.app.post(
route_path('admin_settings_vcs_update'),
params=form_defaults,
status=302)
response = response.follow()
extensions_input = (
'')
response.mustcontain(extensions_input)
def test_has_a_section_for_pull_request_settings(self):
response = self.app.get(route_path('admin_settings_vcs'))
response.mustcontain('Pull Request Settings')
def test_has_an_input_for_invalidation_of_inline_comments(self):
response = self.app.get(route_path('admin_settings_vcs'))
assert_response = response.assert_response()
assert_response.one_element_exists(
'[name=rhodecode_use_outdated_comments]')
@pytest.mark.parametrize('new_value', [True, False])
def test_allows_to_change_invalidation_of_inline_comments(
self, form_defaults, csrf_token, new_value):
setting_key = 'use_outdated_comments'
setting = SettingsModel().create_or_update_setting(
setting_key, not new_value, 'bool')
Session().add(setting)
Session().commit()
form_defaults.update({
'csrf_token': csrf_token,
'rhodecode_use_outdated_comments': str(new_value),
})
response = self.app.post(
route_path('admin_settings_vcs_update'),
params=form_defaults,
status=302)
response = response.follow()
setting = SettingsModel().get_setting_by_name(setting_key)
assert setting.app_settings_value is new_value
@pytest.mark.parametrize('new_value', [True, False])
def test_allows_to_change_hg_rebase_merge_strategy(
self, form_defaults, csrf_token, new_value):
setting_key = 'hg_use_rebase_for_merging'
form_defaults.update({
'csrf_token': csrf_token,
'rhodecode_' + setting_key: str(new_value),
})
with mock.patch.dict(
rhodecode.CONFIG, {'labs_settings_active': 'true'}):
self.app.post(
route_path('admin_settings_vcs_update'),
params=form_defaults,
status=302)
setting = SettingsModel().get_setting_by_name(setting_key)
assert setting.app_settings_value is new_value
@pytest.fixture()
def disable_sql_cache(self, request):
patcher = mock.patch(
'rhodecode.lib.caching_query.FromCache.process_query')
request.addfinalizer(patcher.stop)
patcher.start()
@pytest.fixture()
def form_defaults(self):
from rhodecode.apps.admin.views.settings import AdminSettingsView
return AdminSettingsView._form_defaults()
# TODO: johbo: What we really want is to checkpoint before a test run and
# reset the session afterwards.
@pytest.fixture(scope='class', autouse=True)
def cleanup_settings(self, request, baseapp):
ui_id = RhodeCodeUi.ui_id
original_ids = list(
r.ui_id for r in RhodeCodeUi.query().values(ui_id))
@request.addfinalizer
def cleanup():
RhodeCodeUi.query().filter(
ui_id.notin_(original_ids)).delete(False)
@pytest.mark.usefixtures('autologin_user', 'app')
class TestLabsSettings(object):
def test_get_settings_page_disabled(self):
with mock.patch.dict(
rhodecode.CONFIG, {'labs_settings_active': 'false'}):
response = self.app.get(
route_path('admin_settings_labs'), status=302)
assert response.location.endswith(route_path('admin_settings'))
def test_get_settings_page_enabled(self):
from rhodecode.apps.admin.views import settings
lab_settings = [
settings.LabSetting(
key='rhodecode_bool',
type='bool',
group='bool group',
label='bool label',
help='bool help'
),
settings.LabSetting(
key='rhodecode_text',
type='unicode',
group='text group',
label='text label',
help='text help'
),
]
with mock.patch.dict(rhodecode.CONFIG,
{'labs_settings_active': 'true'}):
with mock.patch.object(settings, '_LAB_SETTINGS', lab_settings):
response = self.app.get(route_path('admin_settings_labs'))
assert '' in response
assert '' in response
assert '
bool help
' in response
assert 'name="rhodecode_bool" type="checkbox"' in response
assert '' in response
assert '' in response
assert 'text help
' in response
assert 'name="rhodecode_text" size="60" type="text"' in response
@pytest.mark.usefixtures('app')
class TestOpenSourceLicenses(object):
def test_records_are_displayed(self, autologin_user):
sample_licenses = [
{
"license": [
{
"fullName": "BSD 4-clause \"Original\" or \"Old\" License",
"shortName": "bsdOriginal",
"spdxId": "BSD-4-Clause",
"url": "http://spdx.org/licenses/BSD-4-Clause.html"
}
],
"name": "python2.7-coverage-3.7.1"
},
{
"license": [
{
"fullName": "MIT License",
"shortName": "mit",
"spdxId": "MIT",
"url": "http://spdx.org/licenses/MIT.html"
}
],
"name": "python2.7-bootstrapped-pip-9.0.1"
},
]
read_licenses_patch = mock.patch(
'rhodecode.apps.admin.views.open_source_licenses.read_opensource_licenses',
return_value=sample_licenses)
with read_licenses_patch:
response = self.app.get(
route_path('admin_settings_open_source'), status=200)
assert_response = response.assert_response()
assert_response.element_contains(
'.panel-heading', 'Licenses of Third Party Packages')
for license_data in sample_licenses:
response.mustcontain(license_data["license"][0]["spdxId"])
assert_response.element_contains('.panel-body', license_data["name"])
def test_records_can_be_read(self, autologin_user):
response = self.app.get(
route_path('admin_settings_open_source'), status=200)
assert_response = response.assert_response()
assert_response.element_contains(
'.panel-heading', 'Licenses of Third Party Packages')
def test_forbidden_when_normal_user(self, autologin_regular_user):
self.app.get(
route_path('admin_settings_open_source'), status=404)
@pytest.mark.usefixtures('app')
class TestUserSessions(object):
def test_forbidden_when_normal_user(self, autologin_regular_user):
self.app.get(route_path('admin_settings_sessions'), status=404)
def test_show_sessions_page(self, autologin_user):
response = self.app.get(route_path('admin_settings_sessions'), status=200)
response.mustcontain('file')
def test_cleanup_old_sessions(self, autologin_user, csrf_token):
post_data = {
'csrf_token': csrf_token,
'expire_days': '60'
}
response = self.app.post(
route_path('admin_settings_sessions_cleanup'), params=post_data,
status=302)
assert_session_flash(response, 'Cleaned up old sessions')
@pytest.mark.usefixtures('app')
class TestAdminSystemInfo(object):
def test_forbidden_when_normal_user(self, autologin_regular_user):
self.app.get(route_path('admin_settings_system'), status=404)
def test_system_info_page(self, autologin_user):
response = self.app.get(route_path('admin_settings_system'))
response.mustcontain('RhodeCode Community Edition, version {}'.format(
rhodecode.__version__))
def test_system_update_new_version(self, autologin_user):
update_data = {
'versions': [
{
'version': '100.3.1415926535',
'general': 'The latest version we are ever going to ship'
},
{
'version': '0.0.0',
'general': 'The first version we ever shipped'
}
]
}
with mock.patch(UPDATE_DATA_QUALNAME, return_value=update_data):
response = self.app.get(route_path('admin_settings_system_update'))
response.mustcontain('A new version is available')
def test_system_update_nothing_new(self, autologin_user):
update_data = {
'versions': [
{
'version': '0.0.0',
'general': 'The first version we ever shipped'
}
]
}
with mock.patch(UPDATE_DATA_QUALNAME, return_value=update_data):
response = self.app.get(route_path('admin_settings_system_update'))
response.mustcontain(
'This instance is already running the latest stable version')
def test_system_update_bad_response(self, autologin_user):
with mock.patch(UPDATE_DATA_QUALNAME, side_effect=ValueError('foo')):
response = self.app.get(route_path('admin_settings_system_update'))
response.mustcontain(
'Bad data sent from update server')
@pytest.mark.usefixtures("app")
class TestAdminSettingsIssueTracker(object):
RC_PREFIX = 'rhodecode_'
SHORT_PATTERN_KEY = 'issuetracker_pat_'
PATTERN_KEY = RC_PREFIX + SHORT_PATTERN_KEY
DESC_KEY = RC_PREFIX + 'issuetracker_desc_'
def test_issuetracker_index(self, autologin_user):
response = self.app.get(route_path('admin_settings_issuetracker'))
assert response.status_code == 200
def test_add_empty_issuetracker_pattern(
self, request, autologin_user, csrf_token):
post_url = route_path('admin_settings_issuetracker_update')
post_data = {
'csrf_token': csrf_token
}
self.app.post(post_url, post_data, status=302)
def test_add_issuetracker_pattern(
self, request, autologin_user, csrf_token):
pattern = 'issuetracker_pat'
another_pattern = pattern+'1'
post_url = route_path('admin_settings_issuetracker_update')
post_data = {
'new_pattern_pattern_0': pattern,
'new_pattern_url_0': 'http://url',
'new_pattern_prefix_0': 'prefix',
'new_pattern_description_0': 'description',
'new_pattern_pattern_1': another_pattern,
'new_pattern_url_1': 'https://url1',
'new_pattern_prefix_1': 'prefix1',
'new_pattern_description_1': 'description1',
'csrf_token': csrf_token
}
self.app.post(post_url, post_data, status=302)
settings = SettingsModel().get_all_settings()
self.uid = md5(pattern)
assert settings[self.PATTERN_KEY+self.uid] == pattern
self.another_uid = md5(another_pattern)
assert settings[self.PATTERN_KEY+self.another_uid] == another_pattern
@request.addfinalizer
def cleanup():
defaults = SettingsModel().get_all_settings()
entries = [name for name in defaults if (
(self.uid in name) or (self.another_uid) in name)]
start = len(self.RC_PREFIX)
for del_key in entries:
# TODO: anderson: get_by_name needs name without prefix
entry = SettingsModel().get_setting_by_name(del_key[start:])
Session().delete(entry)
Session().commit()
def test_edit_issuetracker_pattern(
self, autologin_user, backend, csrf_token, request):
old_pattern = 'issuetracker_pat1'
old_uid = md5(old_pattern)
post_url = route_path('admin_settings_issuetracker_update')
post_data = {
'new_pattern_pattern_0': old_pattern,
'new_pattern_url_0': 'http://url',
'new_pattern_prefix_0': 'prefix',
'new_pattern_description_0': 'description',
'csrf_token': csrf_token
}
self.app.post(post_url, post_data, status=302)
new_pattern = 'issuetracker_pat1_edited'
self.new_uid = md5(new_pattern)
post_url = route_path('admin_settings_issuetracker_update')
post_data = {
'new_pattern_pattern_{}'.format(old_uid): new_pattern,
'new_pattern_url_{}'.format(old_uid): 'https://url_edited',
'new_pattern_prefix_{}'.format(old_uid): 'prefix_edited',
'new_pattern_description_{}'.format(old_uid): 'description_edited',
'uid': old_uid,
'csrf_token': csrf_token
}
self.app.post(post_url, post_data, status=302)
settings = SettingsModel().get_all_settings()
assert settings[self.PATTERN_KEY+self.new_uid] == new_pattern
assert settings[self.DESC_KEY + self.new_uid] == 'description_edited'
assert self.PATTERN_KEY+old_uid not in settings
@request.addfinalizer
def cleanup():
IssueTrackerSettingsModel().delete_entries(old_uid)
IssueTrackerSettingsModel().delete_entries(self.new_uid)
def test_replace_issuetracker_pattern_description(
self, autologin_user, csrf_token, request, settings_util):
prefix = 'issuetracker'
pattern = 'issuetracker_pat'
self.uid = md5(pattern)
pattern_key = '_'.join([prefix, 'pat', self.uid])
rc_pattern_key = '_'.join(['rhodecode', pattern_key])
desc_key = '_'.join([prefix, 'desc', self.uid])
rc_desc_key = '_'.join(['rhodecode', desc_key])
new_description = 'new_description'
settings_util.create_rhodecode_setting(
pattern_key, pattern, 'unicode', cleanup=False)
settings_util.create_rhodecode_setting(
desc_key, 'old description', 'unicode', cleanup=False)
post_url = route_path('admin_settings_issuetracker_update')
post_data = {
'new_pattern_pattern_0': pattern,
'new_pattern_url_0': 'https://url',
'new_pattern_prefix_0': 'prefix',
'new_pattern_description_0': new_description,
'uid': self.uid,
'csrf_token': csrf_token
}
self.app.post(post_url, post_data, status=302)
settings = SettingsModel().get_all_settings()
assert settings[rc_pattern_key] == pattern
assert settings[rc_desc_key] == new_description
@request.addfinalizer
def cleanup():
IssueTrackerSettingsModel().delete_entries(self.uid)
def test_delete_issuetracker_pattern(
self, autologin_user, backend, csrf_token, settings_util, xhr_header):
old_pattern = 'issuetracker_pat_deleted'
old_uid = md5(old_pattern)
post_url = route_path('admin_settings_issuetracker_update')
post_data = {
'new_pattern_pattern_0': old_pattern,
'new_pattern_url_0': 'http://url',
'new_pattern_prefix_0': 'prefix',
'new_pattern_description_0': 'description',
'csrf_token': csrf_token
}
self.app.post(post_url, post_data, status=302)
post_url = route_path('admin_settings_issuetracker_delete')
post_data = {
'uid': old_uid,
'csrf_token': csrf_token
}
self.app.post(post_url, post_data, extra_environ=xhr_header, status=200)
settings = SettingsModel().get_all_settings()
assert self.PATTERN_KEY+old_uid not in settings
assert self.DESC_KEY + old_uid not in settings