##// END OF EJS Templates
tests: fixed all tests for python3 BIG changes
super-admin -
r5087:0a27bd22 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 NO CONTENT: new file 100644
The requested commit or file is 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,201 +1,58 b''
1 # -*- coding: utf-8 -*-
2 1
3 2 # Copyright (C) 2010-2020 RhodeCode GmbH
4 3 #
5 4 # This program is free software: you can redistribute it and/or modify
6 5 # it under the terms of the GNU Affero General Public License, version 3
7 6 # (only), as published by the Free Software Foundation.
8 7 #
9 8 # This program is distributed in the hope that it will be useful,
10 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 11 # GNU General Public License for more details.
13 12 #
14 13 # You should have received a copy of the GNU Affero General Public License
15 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 15 #
17 16 # This program is dual-licensed. If you wish to learn more about the
18 17 # RhodeCode Enterprise Edition, including its added features, Support services,
19 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 19
21 import pytest
22 from rhodecode.lib import ext_json
20 import pytest # noqa
21
22 # keep the imports to have a toplevel conftest.py but still importable from EE edition
23 from rhodecode.tests.conftest_common import ( # noqa
24 pytest_generate_tests,
25 pytest_runtest_makereport,
26 pytest_addoption
27 )
23 28
24 29
25 30 pytest_plugins = [
26 31 "rhodecode.tests.fixture_mods.fixture_pyramid",
27 32 "rhodecode.tests.fixture_mods.fixture_utils",
28 33 ]
29 34
30 35
31 36 def pytest_configure(config):
32 from rhodecode.config import patches
33
34
35 def pytest_addoption(parser):
36
37 def _parse_json(value):
38 return ext_json.str_json(value) if value else None
39
40 def _split_comma(value):
41 return value.split(',')
42
43 parser.addoption(
44 '--keep-tmp-path', action='store_true',
45 help="Keep the test temporary directories")
46
47 parser.addoption(
48 '--backends', action='store', type=_split_comma,
49 default=['git', 'hg', 'svn'],
50 help="Select which backends to test for backend specific tests.")
51 parser.addoption(
52 '--dbs', action='store', type=_split_comma,
53 default=['sqlite'],
54 help="Select which database to test for database specific tests. "
55 "Possible options are sqlite,postgres,mysql")
56 parser.addoption(
57 '--appenlight', '--ae', action='store_true',
58 help="Track statistics in appenlight.")
59 parser.addoption(
60 '--appenlight-api-key', '--ae-key',
61 help="API key for Appenlight.")
62 parser.addoption(
63 '--appenlight-url', '--ae-url',
64 default="https://ae.rhodecode.com",
65 help="Appenlight service URL, defaults to https://ae.rhodecode.com")
66 parser.addoption(
67 '--sqlite-connection-string', action='store',
68 default='', help="Connection string for the dbs tests with SQLite")
69 parser.addoption(
70 '--postgres-connection-string', action='store',
71 default='', help="Connection string for the dbs tests with Postgres")
72 parser.addoption(
73 '--mysql-connection-string', action='store',
74 default='', help="Connection string for the dbs tests with MySQL")
75 parser.addoption(
76 '--repeat', type=int, default=100,
77 help="Number of repetitions in performance tests.")
78
79 parser.addoption(
80 '--test-loglevel', dest='test_loglevel',
81 help="Set default Logging level for tests, critical(default), error, warn , info, debug")
82 group = parser.getgroup('pylons')
83 group.addoption(
84 '--with-pylons', dest='pyramid_config',
85 help="Set up a Pylons environment with the specified config file.")
86 group.addoption(
87 '--ini-config-override', action='store', type=_parse_json,
88 default=None, dest='pyramid_config_override', help=(
89 "Overrides the .ini file settings. Should be specified in JSON"
90 " format, e.g. '{\"section\": {\"parameter\": \"value\", ...}}'"
91 )
92 )
93 parser.addini(
94 'pyramid_config',
95 "Set up a Pyramid environment with the specified config file.")
96
97 vcsgroup = parser.getgroup('vcs')
98 vcsgroup.addoption(
99 '--without-vcsserver', dest='with_vcsserver', action='store_false',
100 help="Do not start the VCSServer in a background process.")
101 vcsgroup.addoption(
102 '--with-vcsserver-http', dest='vcsserver_config_http',
103 help="Start the HTTP VCSServer with the specified config file.")
104 vcsgroup.addoption(
105 '--vcsserver-protocol', dest='vcsserver_protocol',
106 help="Start the VCSServer with HTTP protocol support.")
107 vcsgroup.addoption(
108 '--vcsserver-config-override', action='store', type=_parse_json,
109 default=None, dest='vcsserver_config_override', help=(
110 "Overrides the .ini file settings for the VCSServer. "
111 "Should be specified in JSON "
112 "format, e.g. '{\"section\": {\"parameter\": \"value\", ...}}'"
113 )
114 )
115 vcsgroup.addoption(
116 '--vcsserver-port', action='store', type=int,
117 default=None, help=(
118 "Allows to set the port of the vcsserver. Useful when testing "
119 "against an already running server and random ports cause "
120 "trouble."))
121 parser.addini(
122 'vcsserver_config_http',
123 "Start the HTTP VCSServer with the specified config file.")
124 parser.addini(
125 'vcsserver_protocol',
126 "Start the VCSServer with HTTP protocol support.")
127
128
129 @pytest.hookimpl(tryfirst=True, hookwrapper=True)
130 def pytest_runtest_makereport(item, call):
131 """
132 Adding the remote traceback if the exception has this information.
133
134 VCSServer attaches this information as the attribute `_vcs_server_traceback`
135 to the exception instance.
136 """
137 outcome = yield
138 report = outcome.get_result()
139
140 if call.excinfo:
141 exc = call.excinfo.value
142 vcsserver_traceback = getattr(exc, '_vcs_server_traceback', None)
143
144 if vcsserver_traceback and report.outcome == 'failed':
145 section = f'VCSServer remote traceback {report.when}'
146 report.sections.append((section, vcsserver_traceback))
37 from rhodecode.config import patches # noqa
147 38
148 39
149 40 def pytest_collection_modifyitems(session, config, items):
150 41 # nottest marked, compare nose, used for transition from nose to pytest
151 42 remaining = [
152 43 i for i in items if getattr(i.obj, '__test__', True)]
153 44 items[:] = remaining
154 45
155 # NOTE(marcink): custom test ordering, db tests and vcstests are slowes and should
46 # NOTE(marcink): custom test ordering, db tests and vcstests are slowest and should
156 47 # be executed at the end for faster test feedback
157 48 def sorter(item):
158 49 pos = 0
159 50 key = item._nodeid
160 51 if key.startswith('rhodecode/tests/database'):
161 52 pos = 1
162 53 elif key.startswith('rhodecode/tests/vcs_operations'):
163 54 pos = 2
164 55
165 56 return pos
166 57
167 58 items.sort(key=sorter)
168
169
170 def get_backends_from_metafunc(metafunc):
171 requested_backends = set(metafunc.config.getoption('--backends'))
172 backend_mark = metafunc.definition.get_closest_marker('backends')
173 if backend_mark:
174 # Supported backends by this test function, created from
175 # pytest.mark.backends
176 backends = backend_mark.args
177 elif hasattr(metafunc.cls, 'backend_alias'):
178 # Support class attribute "backend_alias", this is mainly
179 # for legacy reasons for tests not yet using pytest.mark.backends
180 backends = [metafunc.cls.backend_alias]
181 else:
182 backends = metafunc.config.getoption('--backends')
183 return requested_backends.intersection(backends)
184
185
186 def pytest_generate_tests(metafunc):
187
188 # Support test generation based on --backend parameter
189 if 'backend_alias' in metafunc.fixturenames:
190 backends = get_backends_from_metafunc(metafunc)
191 scope = None
192 if not backends:
193 pytest.skip("Not enabled for any of selected backends")
194
195 metafunc.parametrize('backend_alias', backends, scope=scope)
196
197 backend_mark = metafunc.definition.get_closest_marker('backends')
198 if backend_mark:
199 backends = get_backends_from_metafunc(metafunc)
200 if not backends:
201 pytest.skip("Not enabled for any of selected backends")
@@ -1,21 +1,23 b''
1 1 [pytest]
2 2 testpaths = rhodecode
3 3 norecursedirs = rhodecode/public rhodecode/templates tests/scripts
4 4 cache_dir = /tmp/.pytest_cache
5 5
6 6 pyramid_config = rhodecode/tests/rhodecode.ini
7 7 vcsserver_protocol = http
8 8 vcsserver_config_http = rhodecode/tests/vcsserver_http.ini
9 9
10 10 addopts =
11 11 --pdbcls=IPython.terminal.debugger:TerminalPdb
12 12 --strict-markers
13 13 --capture=no
14 14 --show-capture=all
15 15
16 # --test-loglevel=INFO, show log-level during execution
17
16 18 markers =
17 19 vcs_operations: Mark tests depending on a running RhodeCode instance.
18 20 xfail_backends: Mark tests as xfail for given backends.
19 21 skip_backends: Mark tests as skipped for given backends.
20 22 backends: Mark backends
21 23 dbs: database markers for running tests for given DB
@@ -1,170 +1,172 b''
1 1
2 2 # Copyright (C) 2010-2020 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 from rhodecode.lib.str_utils import safe_str
26 27 from rhodecode.tests import *
27 28 from rhodecode.tests.fixture import FIXTURES
28 29 from rhodecode.model.db import UserLog
29 30 from rhodecode.model.meta import Session
30 from rhodecode.lib.utils2 import safe_unicode
31 31
32 32
33 33 def route_path(name, params=None, **kwargs):
34 import urllib.request, urllib.parse, urllib.error
34 import urllib.request
35 import urllib.parse
36 import urllib.error
35 37 from rhodecode.apps._base import ADMIN_PREFIX
36 38
37 39 base_url = {
38 40 'admin_home': ADMIN_PREFIX,
39 41 'admin_audit_logs': ADMIN_PREFIX + '/audit_logs',
40 42
41 43 }[name].format(**kwargs)
42 44
43 45 if params:
44 46 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
45 47 return base_url
46 48
47 49
48 50 @pytest.mark.usefixtures('app')
49 51 class TestAdminController(object):
50 52
51 53 @pytest.fixture(scope='class', autouse=True)
52 54 def prepare(self, request, baseapp):
53 55 UserLog.query().delete()
54 56 Session().commit()
55 57
56 58 def strptime(val):
57 59 fmt = '%Y-%m-%d %H:%M:%S'
58 60 if '.' not in val:
59 61 return datetime.datetime.strptime(val, fmt)
60 62
61 63 nofrag, frag = val.split(".")
62 64 date = datetime.datetime.strptime(nofrag, fmt)
63 65
64 66 frag = frag[:6] # truncate to microseconds
65 67 frag += (6 - len(frag)) * '0' # add 0s
66 68 return date.replace(microsecond=int(frag))
67 69
68 70 with open(os.path.join(FIXTURES, 'journal_dump.csv')) as f:
69 71 for row in csv.DictReader(f):
70 72 ul = UserLog()
71 73 for k, v in row.items():
72 v = safe_unicode(v)
74 v = safe_str(v)
73 75 if k == 'action_date':
74 76 v = strptime(v)
75 77 if k in ['user_id', 'repository_id']:
76 78 # nullable due to FK problems
77 79 v = None
78 80 setattr(ul, k, v)
79 81 Session().add(ul)
80 82 Session().commit()
81 83
82 84 @request.addfinalizer
83 85 def cleanup():
84 86 UserLog.query().delete()
85 87 Session().commit()
86 88
87 89 def test_index(self, autologin_user):
88 90 response = self.app.get(route_path('admin_audit_logs'))
89 91 response.mustcontain('Admin audit logs')
90 92
91 93 def test_filter_all_entries(self, autologin_user):
92 94 response = self.app.get(route_path('admin_audit_logs'))
93 95 all_count = UserLog.query().count()
94 96 response.mustcontain('%s entries' % all_count)
95 97
96 98 def test_filter_journal_filter_exact_match_on_repository(self, autologin_user):
97 99 response = self.app.get(route_path('admin_audit_logs',
98 100 params=dict(filter='repository:rhodecode')))
99 101 response.mustcontain('3 entries')
100 102
101 103 def test_filter_journal_filter_exact_match_on_repository_CamelCase(self, autologin_user):
102 104 response = self.app.get(route_path('admin_audit_logs',
103 105 params=dict(filter='repository:RhodeCode')))
104 106 response.mustcontain('3 entries')
105 107
106 108 def test_filter_journal_filter_wildcard_on_repository(self, autologin_user):
107 109 response = self.app.get(route_path('admin_audit_logs',
108 110 params=dict(filter='repository:*test*')))
109 111 response.mustcontain('862 entries')
110 112
111 113 def test_filter_journal_filter_prefix_on_repository(self, autologin_user):
112 114 response = self.app.get(route_path('admin_audit_logs',
113 115 params=dict(filter='repository:test*')))
114 116 response.mustcontain('257 entries')
115 117
116 118 def test_filter_journal_filter_prefix_on_repository_CamelCase(self, autologin_user):
117 119 response = self.app.get(route_path('admin_audit_logs',
118 120 params=dict(filter='repository:Test*')))
119 121 response.mustcontain('257 entries')
120 122
121 123 def test_filter_journal_filter_prefix_on_repository_and_user(self, autologin_user):
122 124 response = self.app.get(route_path('admin_audit_logs',
123 125 params=dict(filter='repository:test* AND username:demo')))
124 126 response.mustcontain('130 entries')
125 127
126 128 def test_filter_journal_filter_prefix_on_repository_or_target_repo(self, autologin_user):
127 129 response = self.app.get(route_path('admin_audit_logs',
128 130 params=dict(filter='repository:test* OR repository:rhodecode')))
129 131 response.mustcontain('260 entries') # 257 + 3
130 132
131 133 def test_filter_journal_filter_exact_match_on_username(self, autologin_user):
132 134 response = self.app.get(route_path('admin_audit_logs',
133 135 params=dict(filter='username:demo')))
134 136 response.mustcontain('1087 entries')
135 137
136 138 def test_filter_journal_filter_exact_match_on_username_camelCase(self, autologin_user):
137 139 response = self.app.get(route_path('admin_audit_logs',
138 140 params=dict(filter='username:DemO')))
139 141 response.mustcontain('1087 entries')
140 142
141 143 def test_filter_journal_filter_wildcard_on_username(self, autologin_user):
142 144 response = self.app.get(route_path('admin_audit_logs',
143 145 params=dict(filter='username:*test*')))
144 146 entries_count = UserLog.query().filter(UserLog.username.ilike('%test%')).count()
145 147 response.mustcontain('{} entries'.format(entries_count))
146 148
147 149 def test_filter_journal_filter_prefix_on_username(self, autologin_user):
148 150 response = self.app.get(route_path('admin_audit_logs',
149 151 params=dict(filter='username:demo*')))
150 152 response.mustcontain('1101 entries')
151 153
152 154 def test_filter_journal_filter_prefix_on_user_or_other_user(self, autologin_user):
153 155 response = self.app.get(route_path('admin_audit_logs',
154 156 params=dict(filter='username:demo OR username:volcan')))
155 157 response.mustcontain('1095 entries') # 1087 + 8
156 158
157 159 def test_filter_journal_filter_wildcard_on_action(self, autologin_user):
158 160 response = self.app.get(route_path('admin_audit_logs',
159 161 params=dict(filter='action:*pull_request*')))
160 162 response.mustcontain('187 entries')
161 163
162 164 def test_filter_journal_filter_on_date(self, autologin_user):
163 165 response = self.app.get(route_path('admin_audit_logs',
164 166 params=dict(filter='date:20121010')))
165 167 response.mustcontain('47 entries')
166 168
167 169 def test_filter_journal_filter_on_date_2(self, autologin_user):
168 170 response = self.app.get(route_path('admin_audit_logs',
169 171 params=dict(filter='date:20121020')))
170 172 response.mustcontain('17 entries')
@@ -1,84 +1,86 b''
1 1
2 2 # Copyright (C) 2010-2020 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 23 from rhodecode.model.settings import SettingsModel
24 24
25 25
26 26 def route_path(name, params=None, **kwargs):
27 import urllib.request, urllib.parse, urllib.error
27 import urllib.request
28 import urllib.parse
29 import urllib.error
28 30 from rhodecode.apps._base import ADMIN_PREFIX
29 31
30 32 base_url = {
31 33 'admin_defaults_repositories':
32 34 ADMIN_PREFIX + '/defaults/repositories',
33 35 'admin_defaults_repositories_update':
34 36 ADMIN_PREFIX + '/defaults/repositories/update',
35 37 }[name].format(**kwargs)
36 38
37 39 if params:
38 40 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
39 41 return base_url
40 42
41 43
42 44 @pytest.mark.usefixtures("app")
43 45 class TestDefaultsView(object):
44 46
45 47 def test_index(self, autologin_user):
46 48 response = self.app.get(route_path('admin_defaults_repositories'))
47 49 response.mustcontain('default_repo_private')
48 50 response.mustcontain('default_repo_enable_statistics')
49 51 response.mustcontain('default_repo_enable_downloads')
50 52 response.mustcontain('default_repo_enable_locking')
51 53
52 54 def test_update_params_true_hg(self, autologin_user, csrf_token):
53 55 params = {
54 56 'default_repo_enable_locking': True,
55 57 'default_repo_enable_downloads': True,
56 58 'default_repo_enable_statistics': True,
57 59 'default_repo_private': True,
58 60 'default_repo_type': 'hg',
59 61 'csrf_token': csrf_token,
60 62 }
61 63 response = self.app.post(
62 64 route_path('admin_defaults_repositories_update'), params=params)
63 65 assert_session_flash(response, 'Default settings updated successfully')
64 66
65 67 defs = SettingsModel().get_default_repo_settings()
66 68 del params['csrf_token']
67 69 assert params == defs
68 70
69 71 def test_update_params_false_git(self, autologin_user, csrf_token):
70 72 params = {
71 73 'default_repo_enable_locking': False,
72 74 'default_repo_enable_downloads': False,
73 75 'default_repo_enable_statistics': False,
74 76 'default_repo_private': False,
75 77 'default_repo_type': 'git',
76 78 'csrf_token': csrf_token,
77 79 }
78 80 response = self.app.post(
79 81 route_path('admin_defaults_repositories_update'), params=params)
80 82 assert_session_flash(response, 'Default settings updated successfully')
81 83
82 84 defs = SettingsModel().get_default_repo_settings()
83 85 del params['csrf_token']
84 86 assert params == defs
@@ -1,81 +1,86 b''
1 1
2 2 # Copyright (C) 2010-2020 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 24
25 25 fixture = Fixture()
26 26
27 27
28 28 def route_path(name, params=None, **kwargs):
29 import urllib.request, urllib.parse, urllib.error
29 import urllib.request
30 import urllib.parse
31 import urllib.error
30 32 from rhodecode.apps._base import ADMIN_PREFIX
31 33
32 34 base_url = {
33 35 'admin_home': ADMIN_PREFIX,
34 36 'pullrequest_show': '/{repo_name}/pull-request/{pull_request_id}',
35 37 'pull_requests_global': ADMIN_PREFIX + '/pull-request/{pull_request_id}',
36 38 'pull_requests_global_0': ADMIN_PREFIX + '/pull_requests/{pull_request_id}',
37 39 'pull_requests_global_1': ADMIN_PREFIX + '/pull-requests/{pull_request_id}',
38 40
39 41 }[name].format(**kwargs)
40 42
41 43 if params:
42 44 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
43 45 return base_url
44 46
45 47
46 48 class TestAdminMainView(TestController):
47 49
48 50 def test_access_admin_home(self):
49 51 self.log_user()
50 52 response = self.app.get(route_path('admin_home'), status=200)
51 53 response.mustcontain("Administration area")
52 54
53 def test_redirect_pull_request_view(self, view):
55 @pytest.mark.parametrize('view', [
56 'pull_requests_global',
57 ])
58 def test_redirect_pull_request_view_global(self, view):
54 59 self.log_user()
55 60 self.app.get(
56 61 route_path(view, pull_request_id='xxxx'),
57 62 status=404)
58 63
59 64 @pytest.mark.backends("git", "hg")
60 65 @pytest.mark.parametrize('view', [
61 66 'pull_requests_global',
62 67 'pull_requests_global_0',
63 68 'pull_requests_global_1',
64 69 ])
65 70 def test_redirect_pull_request_view(self, view, pr_util):
66 71 self.log_user()
67 72 pull_request = pr_util.create_pull_request()
68 73 pull_request_id = pull_request.pull_request_id
69 74 repo_name = pull_request.target_repo.repo_name
70 75
71 76 response = self.app.get(
72 77 route_path(view, pull_request_id=pull_request_id),
73 78 status=302)
74 79 assert response.location.endswith(
75 80 'pull-request/{}'.format(pull_request_id))
76 81
77 82 redirect_url = route_path(
78 83 'pullrequest_show', repo_name=repo_name,
79 84 pull_request_id=pull_request_id)
80 85
81 86 assert redirect_url in response.location
@@ -1,298 +1,300 b''
1 1
2 2 # Copyright (C) 2010-2020 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 28
29 29
30 30 def route_path(name, params=None, **kwargs):
31 import urllib.request, urllib.parse, urllib.error
31 import urllib.request
32 import urllib.parse
33 import urllib.error
32 34 from rhodecode.apps._base import ADMIN_PREFIX
33 35
34 36 base_url = {
35 37 'edit_user_ips':
36 38 ADMIN_PREFIX + '/users/{user_id}/edit/ips',
37 39 'edit_user_ips_add':
38 40 ADMIN_PREFIX + '/users/{user_id}/edit/ips/new',
39 41 'edit_user_ips_delete':
40 42 ADMIN_PREFIX + '/users/{user_id}/edit/ips/delete',
41 43
42 44 'admin_permissions_application':
43 45 ADMIN_PREFIX + '/permissions/application',
44 46 'admin_permissions_application_update':
45 47 ADMIN_PREFIX + '/permissions/application/update',
46 48
47 49 'admin_permissions_global':
48 50 ADMIN_PREFIX + '/permissions/global',
49 51 'admin_permissions_global_update':
50 52 ADMIN_PREFIX + '/permissions/global/update',
51 53
52 54 'admin_permissions_object':
53 55 ADMIN_PREFIX + '/permissions/object',
54 56 'admin_permissions_object_update':
55 57 ADMIN_PREFIX + '/permissions/object/update',
56 58
57 59 'admin_permissions_ips':
58 60 ADMIN_PREFIX + '/permissions/ips',
59 61 'admin_permissions_overview':
60 62 ADMIN_PREFIX + '/permissions/overview',
61 63
62 64 'admin_permissions_ssh_keys':
63 65 ADMIN_PREFIX + '/permissions/ssh_keys',
64 66 'admin_permissions_ssh_keys_data':
65 67 ADMIN_PREFIX + '/permissions/ssh_keys/data',
66 68 'admin_permissions_ssh_keys_update':
67 69 ADMIN_PREFIX + '/permissions/ssh_keys/update'
68 70
69 71 }[name].format(**kwargs)
70 72
71 73 if params:
72 74 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
73 75 return base_url
74 76
75 77
76 78 class TestAdminPermissionsController(TestController):
77 79
78 80 @pytest.fixture(scope='class', autouse=True)
79 81 def prepare(self, request):
80 82 # cleanup and reset to default permissions after
81 83 @request.addfinalizer
82 84 def cleanup():
83 85 PermissionModel().create_default_user_permissions(
84 86 User.get_default_user(), force=True)
85 87
86 88 def test_index_application(self):
87 89 self.log_user()
88 90 self.app.get(route_path('admin_permissions_application'))
89 91
90 92 @pytest.mark.parametrize(
91 93 'anonymous, default_register, default_register_message, default_password_reset,'
92 94 'default_extern_activate, expect_error, expect_form_error', [
93 95 (True, 'hg.register.none', '', 'hg.password_reset.enabled', 'hg.extern_activate.manual',
94 96 False, False),
95 97 (True, 'hg.register.manual_activate', '', 'hg.password_reset.enabled', 'hg.extern_activate.auto',
96 98 False, False),
97 99 (True, 'hg.register.auto_activate', '', 'hg.password_reset.enabled', 'hg.extern_activate.manual',
98 100 False, False),
99 101 (True, 'hg.register.auto_activate', '', 'hg.password_reset.enabled', 'hg.extern_activate.manual',
100 102 False, False),
101 103 (True, 'hg.register.XXX', '', 'hg.password_reset.enabled', 'hg.extern_activate.manual',
102 104 False, True),
103 105 (True, '', '', 'hg.password_reset.enabled', '', True, False),
104 106 ])
105 107 def test_update_application_permissions(
106 108 self, anonymous, default_register, default_register_message, default_password_reset,
107 109 default_extern_activate, expect_error, expect_form_error):
108 110
109 111 self.log_user()
110 112
111 113 # TODO: anonymous access set here to False, breaks some other tests
112 114 params = {
113 115 'csrf_token': self.csrf_token,
114 116 'anonymous': anonymous,
115 117 'default_register': default_register,
116 118 'default_register_message': default_register_message,
117 119 'default_password_reset': default_password_reset,
118 120 'default_extern_activate': default_extern_activate,
119 121 }
120 122 response = self.app.post(route_path('admin_permissions_application_update'),
121 123 params=params)
122 124 if expect_form_error:
123 125 assert response.status_int == 200
124 126 response.mustcontain('Value must be one of')
125 127 else:
126 128 if expect_error:
127 129 msg = 'Error occurred during update of permissions'
128 130 else:
129 131 msg = 'Application permissions updated successfully'
130 132 assert_session_flash(response, msg)
131 133
132 134 def test_index_object(self):
133 135 self.log_user()
134 136 self.app.get(route_path('admin_permissions_object'))
135 137
136 138 @pytest.mark.parametrize(
137 139 'repo, repo_group, user_group, expect_error, expect_form_error', [
138 140 ('repository.none', 'group.none', 'usergroup.none', False, False),
139 141 ('repository.read', 'group.read', 'usergroup.read', False, False),
140 142 ('repository.write', 'group.write', 'usergroup.write',
141 143 False, False),
142 144 ('repository.admin', 'group.admin', 'usergroup.admin',
143 145 False, False),
144 146 ('repository.XXX', 'group.admin', 'usergroup.admin', False, True),
145 147 ('', '', '', True, False),
146 148 ])
147 149 def test_update_object_permissions(self, repo, repo_group, user_group,
148 150 expect_error, expect_form_error):
149 151 self.log_user()
150 152
151 153 params = {
152 154 'csrf_token': self.csrf_token,
153 155 'default_repo_perm': repo,
154 156 'overwrite_default_repo': False,
155 157 'default_group_perm': repo_group,
156 158 'overwrite_default_group': False,
157 159 'default_user_group_perm': user_group,
158 160 'overwrite_default_user_group': False,
159 161 }
160 162 response = self.app.post(route_path('admin_permissions_object_update'),
161 163 params=params)
162 164 if expect_form_error:
163 165 assert response.status_int == 200
164 166 response.mustcontain('Value must be one of')
165 167 else:
166 168 if expect_error:
167 169 msg = 'Error occurred during update of permissions'
168 170 else:
169 171 msg = 'Object permissions updated successfully'
170 172 assert_session_flash(response, msg)
171 173
172 174 def test_index_global(self):
173 175 self.log_user()
174 176 self.app.get(route_path('admin_permissions_global'))
175 177
176 178 @pytest.mark.parametrize(
177 179 'repo_create, repo_create_write, user_group_create, repo_group_create,'
178 180 'fork_create, inherit_default_permissions, expect_error,'
179 181 'expect_form_error', [
180 182 ('hg.create.none', 'hg.create.write_on_repogroup.false',
181 183 'hg.usergroup.create.false', 'hg.repogroup.create.false',
182 184 'hg.fork.none', 'hg.inherit_default_perms.false', False, False),
183 185 ('hg.create.repository', 'hg.create.write_on_repogroup.true',
184 186 'hg.usergroup.create.true', 'hg.repogroup.create.true',
185 187 'hg.fork.repository', 'hg.inherit_default_perms.false',
186 188 False, False),
187 189 ('hg.create.XXX', 'hg.create.write_on_repogroup.true',
188 190 'hg.usergroup.create.true', 'hg.repogroup.create.true',
189 191 'hg.fork.repository', 'hg.inherit_default_perms.false',
190 192 False, True),
191 193 ('', '', '', '', '', '', True, False),
192 194 ])
193 195 def test_update_global_permissions(
194 196 self, repo_create, repo_create_write, user_group_create,
195 197 repo_group_create, fork_create, inherit_default_permissions,
196 198 expect_error, expect_form_error):
197 199 self.log_user()
198 200
199 201 params = {
200 202 'csrf_token': self.csrf_token,
201 203 'default_repo_create': repo_create,
202 204 'default_repo_create_on_write': repo_create_write,
203 205 'default_user_group_create': user_group_create,
204 206 'default_repo_group_create': repo_group_create,
205 207 'default_fork_create': fork_create,
206 208 'default_inherit_default_permissions': inherit_default_permissions
207 209 }
208 210 response = self.app.post(route_path('admin_permissions_global_update'),
209 211 params=params)
210 212 if expect_form_error:
211 213 assert response.status_int == 200
212 214 response.mustcontain('Value must be one of')
213 215 else:
214 216 if expect_error:
215 217 msg = 'Error occurred during update of permissions'
216 218 else:
217 219 msg = 'Global permissions updated successfully'
218 220 assert_session_flash(response, msg)
219 221
220 222 def test_index_ips(self):
221 223 self.log_user()
222 224 response = self.app.get(route_path('admin_permissions_ips'))
223 225 response.mustcontain('All IP addresses are allowed')
224 226
225 227 def test_add_delete_ips(self):
226 228 clear_cache_regions(['sql_cache_short'])
227 229 self.log_user()
228 230
229 231 # ADD
230 232 default_user_id = User.get_default_user_id()
231 233 self.app.post(
232 234 route_path('edit_user_ips_add', user_id=default_user_id),
233 235 params={'new_ip': '0.0.0.0/24', 'csrf_token': self.csrf_token})
234 236
235 237 response = self.app.get(route_path('admin_permissions_ips'))
236 238 response.mustcontain('0.0.0.0/24')
237 239 response.mustcontain('0.0.0.0 - 0.0.0.255')
238 240
239 241 # DELETE
240 242 default_user_id = User.get_default_user_id()
241 243 del_ip_id = UserIpMap.query().filter(UserIpMap.user_id ==
242 244 default_user_id).first().ip_id
243 245
244 246 response = self.app.post(
245 247 route_path('edit_user_ips_delete', user_id=default_user_id),
246 248 params={'del_ip_id': del_ip_id, 'csrf_token': self.csrf_token})
247 249
248 250 assert_session_flash(response, 'Removed ip address from user whitelist')
249 251
250 252 clear_cache_regions(['sql_cache_short'])
251 253 response = self.app.get(route_path('admin_permissions_ips'))
252 254 response.mustcontain('All IP addresses are allowed')
253 255 response.mustcontain(no=['0.0.0.0/24'])
254 256 response.mustcontain(no=['0.0.0.0 - 0.0.0.255'])
255 257
256 258 def test_index_overview(self):
257 259 self.log_user()
258 260 self.app.get(route_path('admin_permissions_overview'))
259 261
260 262 def test_ssh_keys(self):
261 263 self.log_user()
262 264 self.app.get(route_path('admin_permissions_ssh_keys'), status=200)
263 265
264 266 def test_ssh_keys_data(self, user_util, xhr_header):
265 267 self.log_user()
266 268 response = self.app.get(route_path('admin_permissions_ssh_keys_data'),
267 269 extra_environ=xhr_header)
268 270 assert response.json == {u'data': [], u'draw': None,
269 271 u'recordsFiltered': 0, u'recordsTotal': 0}
270 272
271 273 dummy_user = user_util.create_user()
272 274 SshKeyModel().create(dummy_user, 'ab:cd:ef', 'KEYKEY', 'test_key')
273 275 Session().commit()
274 276 response = self.app.get(route_path('admin_permissions_ssh_keys_data'),
275 277 extra_environ=xhr_header)
276 278 assert response.json['data'][0]['fingerprint'] == 'ab:cd:ef'
277 279
278 280 def test_ssh_keys_update(self):
279 281 self.log_user()
280 282 response = self.app.post(
281 283 route_path('admin_permissions_ssh_keys_update'),
282 284 dict(csrf_token=self.csrf_token), status=302)
283 285
284 286 assert_session_flash(
285 287 response, 'Updated SSH keys file')
286 288
287 289 def test_ssh_keys_update_disabled(self):
288 290 self.log_user()
289 291
290 292 from rhodecode.apps.admin.views.permissions import AdminPermissionsView
291 293 with mock.patch.object(AdminPermissionsView, 'ssh_enabled',
292 294 return_value=False):
293 295 response = self.app.post(
294 296 route_path('admin_permissions_ssh_keys_update'),
295 297 dict(csrf_token=self.csrf_token), status=302)
296 298
297 299 assert_session_flash(
298 300 response, 'SSH key support is disabled in .ini file') No newline at end of file
@@ -1,511 +1,515 b''
1 1
2 2 # Copyright (C) 2010-2020 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 urllib.request, urllib.parse, urllib.error
20 import urllib.request
21 import urllib.parse
22 import urllib.error
21 23
22 24 import mock
23 25 import pytest
24 26
25 27 from rhodecode.apps._base import ADMIN_PREFIX
26 28 from rhodecode.lib import auth
27 29 from rhodecode.lib.utils2 import safe_str
28 30 from rhodecode.lib import helpers as h
29 31 from rhodecode.model.db import (
30 32 Repository, RepoGroup, UserRepoToPerm, User, Permission)
31 33 from rhodecode.model.meta import Session
32 34 from rhodecode.model.repo import RepoModel
33 35 from rhodecode.model.repo_group import RepoGroupModel
34 36 from rhodecode.model.user import UserModel
35 37 from rhodecode.tests import (
36 38 login_user_session, assert_session_flash, TEST_USER_ADMIN_LOGIN,
37 39 TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
38 40 from rhodecode.tests.fixture import Fixture, error_function
39 41 from rhodecode.tests.utils import AssertResponse, repo_on_filesystem
40 42
41 43 fixture = Fixture()
42 44
43 45
44 46 def route_path(name, params=None, **kwargs):
45 import urllib.request, urllib.parse, urllib.error
47 import urllib.request
48 import urllib.parse
49 import urllib.error
46 50
47 51 base_url = {
48 52 'repos': ADMIN_PREFIX + '/repos',
49 53 'repos_data': ADMIN_PREFIX + '/repos_data',
50 54 'repo_new': ADMIN_PREFIX + '/repos/new',
51 55 'repo_create': ADMIN_PREFIX + '/repos/create',
52 56
53 57 'repo_creating_check': '/{repo_name}/repo_creating_check',
54 58 }[name].format(**kwargs)
55 59
56 60 if params:
57 61 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
58 62 return base_url
59 63
60 64
61 65 def _get_permission_for_user(user, repo):
62 66 perm = UserRepoToPerm.query()\
63 67 .filter(UserRepoToPerm.repository ==
64 68 Repository.get_by_repo_name(repo))\
65 69 .filter(UserRepoToPerm.user == User.get_by_username(user))\
66 70 .all()
67 71 return perm
68 72
69 73
70 74 @pytest.mark.usefixtures("app")
71 75 class TestAdminRepos(object):
72 76
73 77 def test_repo_list(self, autologin_user, user_util, xhr_header):
74 78 repo = user_util.create_repo()
75 79 repo_name = repo.repo_name
76 80 response = self.app.get(
77 81 route_path('repos_data'), status=200,
78 82 extra_environ=xhr_header)
79 83
80 84 response.mustcontain(repo_name)
81 85
82 86 def test_create_page_restricted_to_single_backend(self, autologin_user, backend):
83 87 with mock.patch('rhodecode.BACKENDS', {'git': 'git'}):
84 88 response = self.app.get(route_path('repo_new'), status=200)
85 89 assert_response = response.assert_response()
86 90 element = assert_response.get_element('[name=repo_type]')
87 91 assert element.get('value') == 'git'
88 92
89 93 def test_create_page_non_restricted_backends(self, autologin_user, backend):
90 94 response = self.app.get(route_path('repo_new'), status=200)
91 95 assert_response = response.assert_response()
92 96 assert ['hg', 'git', 'svn'] == [x.get('value') for x in assert_response.get_elements('[name=repo_type]')]
93 97
94 98 @pytest.mark.parametrize(
95 "suffix", [u'', u'xxa'], ids=['', 'non-ascii'])
99 "suffix", ['', 'xxa'], ids=['', 'non-ascii'])
96 100 def test_create(self, autologin_user, backend, suffix, csrf_token):
97 101 repo_name_unicode = backend.new_repo_name(suffix=suffix)
98 repo_name = repo_name_unicode.encode('utf8')
99 description_unicode = u'description for newly created repo' + suffix
100 description = description_unicode.encode('utf8')
102 repo_name = repo_name_unicode
103
104 description_unicode = 'description for newly created repo' + suffix
105 description = description_unicode
106
101 107 response = self.app.post(
102 108 route_path('repo_create'),
103 109 fixture._get_repo_create_params(
104 110 repo_private=False,
105 111 repo_name=repo_name,
106 112 repo_type=backend.alias,
107 113 repo_description=description,
108 114 csrf_token=csrf_token),
109 115 status=302)
110 116
111 117 self.assert_repository_is_created_correctly(
112 118 repo_name, description, backend)
113 119
114 120 def test_create_numeric_name(self, autologin_user, backend, csrf_token):
115 121 numeric_repo = '1234'
116 122 repo_name = numeric_repo
117 123 description = 'description for newly created repo' + numeric_repo
118 124 self.app.post(
119 125 route_path('repo_create'),
120 126 fixture._get_repo_create_params(
121 127 repo_private=False,
122 128 repo_name=repo_name,
123 129 repo_type=backend.alias,
124 130 repo_description=description,
125 131 csrf_token=csrf_token))
126 132
127 133 self.assert_repository_is_created_correctly(
128 134 repo_name, description, backend)
129 135
130 @pytest.mark.parametrize("suffix", [u'', u'ąćę'], ids=['', 'non-ascii'])
136 @pytest.mark.parametrize("suffix", ['', '_ąćę'], ids=['', 'non-ascii'])
131 137 def test_create_in_group(
132 138 self, autologin_user, backend, suffix, csrf_token):
133 139 # create GROUP
134 group_name = 'sometest_%s' % backend.alias
140 group_name = f'sometest_{backend.alias}'
135 141 gr = RepoGroupModel().create(group_name=group_name,
136 142 group_description='test',
137 143 owner=TEST_USER_ADMIN_LOGIN)
138 144 Session().commit()
139 145
140 repo_name = u'ingroup' + suffix
141 repo_name_full = RepoGroup.url_sep().join(
142 [group_name, repo_name])
143 description = u'description for newly created repo'
146 repo_name = f'ingroup{suffix}'
147 repo_name_full = RepoGroup.url_sep().join([group_name, repo_name])
148 description = 'description for newly created repo'
149
144 150 self.app.post(
145 151 route_path('repo_create'),
146 152 fixture._get_repo_create_params(
147 153 repo_private=False,
148 154 repo_name=safe_str(repo_name),
149 155 repo_type=backend.alias,
150 156 repo_description=description,
151 157 repo_group=gr.group_id,
152 158 csrf_token=csrf_token))
153 159
154 160 # TODO: johbo: Cleanup work to fixture
155 161 try:
156 162 self.assert_repository_is_created_correctly(
157 163 repo_name_full, description, backend)
158 164
159 165 new_repo = RepoModel().get_by_repo_name(repo_name_full)
160 166 inherited_perms = UserRepoToPerm.query().filter(
161 167 UserRepoToPerm.repository_id == new_repo.repo_id).all()
162 168 assert len(inherited_perms) == 1
163 169 finally:
164 170 RepoModel().delete(repo_name_full)
165 171 RepoGroupModel().delete(group_name)
166 172 Session().commit()
167 173
168 174 def test_create_in_group_numeric_name(
169 175 self, autologin_user, backend, csrf_token):
170 176 # create GROUP
171 177 group_name = 'sometest_%s' % backend.alias
172 178 gr = RepoGroupModel().create(group_name=group_name,
173 179 group_description='test',
174 180 owner=TEST_USER_ADMIN_LOGIN)
175 181 Session().commit()
176 182
177 183 repo_name = '12345'
178 184 repo_name_full = RepoGroup.url_sep().join([group_name, repo_name])
179 185 description = 'description for newly created repo'
180 186 self.app.post(
181 187 route_path('repo_create'),
182 188 fixture._get_repo_create_params(
183 189 repo_private=False,
184 190 repo_name=repo_name,
185 191 repo_type=backend.alias,
186 192 repo_description=description,
187 193 repo_group=gr.group_id,
188 194 csrf_token=csrf_token))
189 195
190 196 # TODO: johbo: Cleanup work to fixture
191 197 try:
192 198 self.assert_repository_is_created_correctly(
193 199 repo_name_full, description, backend)
194 200
195 201 new_repo = RepoModel().get_by_repo_name(repo_name_full)
196 202 inherited_perms = UserRepoToPerm.query()\
197 203 .filter(UserRepoToPerm.repository_id == new_repo.repo_id).all()
198 204 assert len(inherited_perms) == 1
199 205 finally:
200 206 RepoModel().delete(repo_name_full)
201 207 RepoGroupModel().delete(group_name)
202 208 Session().commit()
203 209
204 210 def test_create_in_group_without_needed_permissions(self, backend):
205 211 session = login_user_session(
206 212 self.app, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
207 213 csrf_token = auth.get_csrf_token(session)
208 214 # revoke
209 215 user_model = UserModel()
210 216 # disable fork and create on default user
211 217 user_model.revoke_perm(User.DEFAULT_USER, 'hg.create.repository')
212 218 user_model.grant_perm(User.DEFAULT_USER, 'hg.create.none')
213 219 user_model.revoke_perm(User.DEFAULT_USER, 'hg.fork.repository')
214 220 user_model.grant_perm(User.DEFAULT_USER, 'hg.fork.none')
215 221
216 222 # disable on regular user
217 223 user_model.revoke_perm(TEST_USER_REGULAR_LOGIN, 'hg.create.repository')
218 224 user_model.grant_perm(TEST_USER_REGULAR_LOGIN, 'hg.create.none')
219 225 user_model.revoke_perm(TEST_USER_REGULAR_LOGIN, 'hg.fork.repository')
220 226 user_model.grant_perm(TEST_USER_REGULAR_LOGIN, 'hg.fork.none')
221 227 Session().commit()
222 228
223 229 # create GROUP
224 230 group_name = 'reg_sometest_%s' % backend.alias
225 231 gr = RepoGroupModel().create(group_name=group_name,
226 232 group_description='test',
227 233 owner=TEST_USER_ADMIN_LOGIN)
228 234 Session().commit()
229 235 repo_group_id = gr.group_id
230 236
231 237 group_name_allowed = 'reg_sometest_allowed_%s' % backend.alias
232 238 gr_allowed = RepoGroupModel().create(
233 239 group_name=group_name_allowed,
234 240 group_description='test',
235 241 owner=TEST_USER_REGULAR_LOGIN)
236 242 allowed_repo_group_id = gr_allowed.group_id
237 243 Session().commit()
238 244
239 245 repo_name = 'ingroup'
240 246 description = 'description for newly created repo'
241 247 response = self.app.post(
242 248 route_path('repo_create'),
243 249 fixture._get_repo_create_params(
244 250 repo_private=False,
245 251 repo_name=repo_name,
246 252 repo_type=backend.alias,
247 253 repo_description=description,
248 254 repo_group=repo_group_id,
249 255 csrf_token=csrf_token))
250 256
251 257 response.mustcontain('Invalid value')
252 258
253 259 # user is allowed to create in this group
254 260 repo_name = 'ingroup'
255 261 repo_name_full = RepoGroup.url_sep().join(
256 262 [group_name_allowed, repo_name])
257 263 description = 'description for newly created repo'
258 264 response = self.app.post(
259 265 route_path('repo_create'),
260 266 fixture._get_repo_create_params(
261 267 repo_private=False,
262 268 repo_name=repo_name,
263 269 repo_type=backend.alias,
264 270 repo_description=description,
265 271 repo_group=allowed_repo_group_id,
266 272 csrf_token=csrf_token))
267 273
268 274 # TODO: johbo: Cleanup in pytest fixture
269 275 try:
270 276 self.assert_repository_is_created_correctly(
271 277 repo_name_full, description, backend)
272 278
273 279 new_repo = RepoModel().get_by_repo_name(repo_name_full)
274 280 inherited_perms = UserRepoToPerm.query().filter(
275 281 UserRepoToPerm.repository_id == new_repo.repo_id).all()
276 282 assert len(inherited_perms) == 1
277 283
278 284 assert repo_on_filesystem(repo_name_full)
279 285 finally:
280 286 RepoModel().delete(repo_name_full)
281 287 RepoGroupModel().delete(group_name)
282 288 RepoGroupModel().delete(group_name_allowed)
283 289 Session().commit()
284 290
285 291 def test_create_in_group_inherit_permissions(self, autologin_user, backend,
286 292 csrf_token):
287 293 # create GROUP
288 294 group_name = 'sometest_%s' % backend.alias
289 295 gr = RepoGroupModel().create(group_name=group_name,
290 296 group_description='test',
291 297 owner=TEST_USER_ADMIN_LOGIN)
292 298 perm = Permission.get_by_key('repository.write')
293 299 RepoGroupModel().grant_user_permission(
294 300 gr, TEST_USER_REGULAR_LOGIN, perm)
295 301
296 302 # add repo permissions
297 303 Session().commit()
298 304 repo_group_id = gr.group_id
299 305 repo_name = 'ingroup_inherited_%s' % backend.alias
300 306 repo_name_full = RepoGroup.url_sep().join([group_name, repo_name])
301 307 description = 'description for newly created repo'
302 308 self.app.post(
303 309 route_path('repo_create'),
304 310 fixture._get_repo_create_params(
305 311 repo_private=False,
306 312 repo_name=repo_name,
307 313 repo_type=backend.alias,
308 314 repo_description=description,
309 315 repo_group=repo_group_id,
310 316 repo_copy_permissions=True,
311 317 csrf_token=csrf_token))
312 318
313 319 # TODO: johbo: Cleanup to pytest fixture
314 320 try:
315 321 self.assert_repository_is_created_correctly(
316 322 repo_name_full, description, backend)
317 323 except Exception:
318 324 RepoGroupModel().delete(group_name)
319 325 Session().commit()
320 326 raise
321 327
322 328 # check if inherited permissions are applied
323 329 new_repo = RepoModel().get_by_repo_name(repo_name_full)
324 330 inherited_perms = UserRepoToPerm.query().filter(
325 331 UserRepoToPerm.repository_id == new_repo.repo_id).all()
326 332 assert len(inherited_perms) == 2
327 333
328 334 assert TEST_USER_REGULAR_LOGIN in [
329 335 x.user.username for x in inherited_perms]
330 336 assert 'repository.write' in [
331 337 x.permission.permission_name for x in inherited_perms]
332 338
333 339 RepoModel().delete(repo_name_full)
334 340 RepoGroupModel().delete(group_name)
335 341 Session().commit()
336 342
337 343 @pytest.mark.xfail_backends(
338 344 "git", "hg", reason="Missing reposerver support")
339 345 def test_create_with_clone_uri(self, autologin_user, backend, reposerver,
340 346 csrf_token):
341 347 source_repo = backend.create_repo(number_of_commits=2)
342 348 source_repo_name = source_repo.repo_name
343 349 reposerver.serve(source_repo.scm_instance())
344 350
345 351 repo_name = backend.new_repo_name()
346 352 response = self.app.post(
347 353 route_path('repo_create'),
348 354 fixture._get_repo_create_params(
349 355 repo_private=False,
350 356 repo_name=repo_name,
351 357 repo_type=backend.alias,
352 358 repo_description='',
353 359 clone_uri=reposerver.url,
354 360 csrf_token=csrf_token),
355 361 status=302)
356 362
357 363 # Should be redirected to the creating page
358 364 response.mustcontain('repo_creating')
359 365
360 366 # Expecting that both repositories have same history
361 367 source_repo = RepoModel().get_by_repo_name(source_repo_name)
362 368 source_vcs = source_repo.scm_instance()
363 369 repo = RepoModel().get_by_repo_name(repo_name)
364 370 repo_vcs = repo.scm_instance()
365 371 assert source_vcs[0].message == repo_vcs[0].message
366 372 assert source_vcs.count() == repo_vcs.count()
367 373 assert source_vcs.commit_ids == repo_vcs.commit_ids
368 374
369 375 @pytest.mark.xfail_backends("svn", reason="Depends on import support")
370 376 def test_create_remote_repo_wrong_clone_uri(self, autologin_user, backend,
371 377 csrf_token):
372 378 repo_name = backend.new_repo_name()
373 379 description = 'description for newly created repo'
374 380 response = self.app.post(
375 381 route_path('repo_create'),
376 382 fixture._get_repo_create_params(
377 383 repo_private=False,
378 384 repo_name=repo_name,
379 385 repo_type=backend.alias,
380 386 repo_description=description,
381 387 clone_uri='http://repo.invalid/repo',
382 388 csrf_token=csrf_token))
383 389 response.mustcontain('invalid clone url')
384 390
385 391 @pytest.mark.xfail_backends("svn", reason="Depends on import support")
386 392 def test_create_remote_repo_wrong_clone_uri_hg_svn(
387 393 self, autologin_user, backend, csrf_token):
388 394 repo_name = backend.new_repo_name()
389 395 description = 'description for newly created repo'
390 396 response = self.app.post(
391 397 route_path('repo_create'),
392 398 fixture._get_repo_create_params(
393 399 repo_private=False,
394 400 repo_name=repo_name,
395 401 repo_type=backend.alias,
396 402 repo_description=description,
397 403 clone_uri='svn+http://svn.invalid/repo',
398 404 csrf_token=csrf_token))
399 405 response.mustcontain('invalid clone url')
400 406
401 407 def test_create_with_git_suffix(
402 408 self, autologin_user, backend, csrf_token):
403 409 repo_name = backend.new_repo_name() + ".git"
404 410 description = 'description for newly created repo'
405 411 response = self.app.post(
406 412 route_path('repo_create'),
407 413 fixture._get_repo_create_params(
408 414 repo_private=False,
409 415 repo_name=repo_name,
410 416 repo_type=backend.alias,
411 417 repo_description=description,
412 418 csrf_token=csrf_token))
413 419 response.mustcontain('Repository name cannot end with .git')
414 420
415 421 def test_default_user_cannot_access_private_repo_in_a_group(
416 422 self, autologin_user, user_util, backend):
417 423
418 424 group = user_util.create_repo_group()
419 425
420 426 repo = backend.create_repo(
421 427 repo_private=True, repo_group=group, repo_copy_permissions=True)
422 428
423 429 permissions = _get_permission_for_user(
424 430 user='default', repo=repo.repo_name)
425 431 assert len(permissions) == 1
426 432 assert permissions[0].permission.permission_name == 'repository.none'
427 433 assert permissions[0].repository.private is True
428 434
429 435 def test_create_on_top_level_without_permissions(self, backend):
430 436 session = login_user_session(
431 437 self.app, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
432 438 csrf_token = auth.get_csrf_token(session)
433 439
434 440 # revoke
435 441 user_model = UserModel()
436 442 # disable fork and create on default user
437 443 user_model.revoke_perm(User.DEFAULT_USER, 'hg.create.repository')
438 444 user_model.grant_perm(User.DEFAULT_USER, 'hg.create.none')
439 445 user_model.revoke_perm(User.DEFAULT_USER, 'hg.fork.repository')
440 446 user_model.grant_perm(User.DEFAULT_USER, 'hg.fork.none')
441 447
442 448 # disable on regular user
443 449 user_model.revoke_perm(TEST_USER_REGULAR_LOGIN, 'hg.create.repository')
444 450 user_model.grant_perm(TEST_USER_REGULAR_LOGIN, 'hg.create.none')
445 451 user_model.revoke_perm(TEST_USER_REGULAR_LOGIN, 'hg.fork.repository')
446 452 user_model.grant_perm(TEST_USER_REGULAR_LOGIN, 'hg.fork.none')
447 453 Session().commit()
448 454
449 455 repo_name = backend.new_repo_name()
450 456 description = 'description for newly created repo'
451 457 response = self.app.post(
452 458 route_path('repo_create'),
453 459 fixture._get_repo_create_params(
454 460 repo_private=False,
455 461 repo_name=repo_name,
456 462 repo_type=backend.alias,
457 463 repo_description=description,
458 464 csrf_token=csrf_token))
459 465
460 466 response.mustcontain(
461 467 u"You do not have the permission to store repositories in "
462 468 u"the root location.")
463 469
464 470 @mock.patch.object(RepoModel, '_create_filesystem_repo', error_function)
465 471 def test_create_repo_when_filesystem_op_fails(
466 472 self, autologin_user, backend, csrf_token):
467 473 repo_name = backend.new_repo_name()
468 474 description = 'description for newly created repo'
469 475
470 476 response = self.app.post(
471 477 route_path('repo_create'),
472 478 fixture._get_repo_create_params(
473 479 repo_private=False,
474 480 repo_name=repo_name,
475 481 repo_type=backend.alias,
476 482 repo_description=description,
477 483 csrf_token=csrf_token))
478 484
479 485 assert_session_flash(
480 486 response, 'Error creating repository %s' % repo_name)
481 487 # repo must not be in db
482 488 assert backend.repo is None
483 489 # repo must not be in filesystem !
484 490 assert not repo_on_filesystem(repo_name)
485 491
486 def assert_repository_is_created_correctly(
487 self, repo_name, description, backend):
488 repo_name_utf8 = safe_str(repo_name)
492 def assert_repository_is_created_correctly(self, repo_name, description, backend):
493 url_quoted_repo_name = urllib.parse.quote(repo_name)
489 494
490 495 # run the check page that triggers the flash message
491 496 response = self.app.get(
492 route_path('repo_creating_check', repo_name=safe_str(repo_name)))
493 assert response.json == {u'result': True}
497 route_path('repo_creating_check', repo_name=repo_name))
498 assert response.json == {'result': True}
494 499
495 flash_msg = u'Created repository <a href="/{}">{}</a>'.format(
496 urllib.parse.quote(repo_name_utf8), repo_name)
500 flash_msg = 'Created repository <a href="/{}">{}</a>'.format(url_quoted_repo_name, repo_name)
497 501 assert_session_flash(response, flash_msg)
498 502
499 503 # test if the repo was created in the database
500 504 new_repo = RepoModel().get_by_repo_name(repo_name)
501 505
502 506 assert new_repo.repo_name == repo_name
503 507 assert new_repo.description == description
504 508
505 509 # test if the repository is visible in the list ?
506 510 response = self.app.get(
507 h.route_path('repo_summary', repo_name=safe_str(repo_name)))
511 h.route_path('repo_summary', repo_name=repo_name))
508 512 response.mustcontain(repo_name)
509 513 response.mustcontain(backend.alias)
510 514
511 515 assert repo_on_filesystem(repo_name)
@@ -1,193 +1,195 b''
1 1
2 2 # Copyright (C) 2010-2020 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 31
32 32 fixture = Fixture()
33 33
34 34
35 35 def route_path(name, params=None, **kwargs):
36 import urllib.request, urllib.parse, urllib.error
36 import urllib.request
37 import urllib.parse
38 import urllib.error
37 39
38 40 base_url = {
39 41 'repo_groups': ADMIN_PREFIX + '/repo_groups',
40 42 'repo_groups_data': ADMIN_PREFIX + '/repo_groups_data',
41 43 'repo_group_new': ADMIN_PREFIX + '/repo_group/new',
42 44 'repo_group_create': ADMIN_PREFIX + '/repo_group/create',
43 45
44 46 }[name].format(**kwargs)
45 47
46 48 if params:
47 49 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
48 50 return base_url
49 51
50 52
51 53 def _get_permission_for_user(user, repo):
52 54 perm = UserRepoToPerm.query()\
53 55 .filter(UserRepoToPerm.repository ==
54 56 Repository.get_by_repo_name(repo))\
55 57 .filter(UserRepoToPerm.user == User.get_by_username(user))\
56 58 .all()
57 59 return perm
58 60
59 61
60 62 @pytest.mark.usefixtures("app")
61 63 class TestAdminRepositoryGroups(object):
62 64
63 65 def test_show_repo_groups(self, autologin_user):
64 66 self.app.get(route_path('repo_groups'))
65 67
66 68 def test_show_repo_groups_data(self, autologin_user, xhr_header):
67 69 response = self.app.get(route_path(
68 70 'repo_groups_data'), extra_environ=xhr_header)
69 71
70 72 all_repo_groups = RepoGroup.query().count()
71 73 assert response.json['recordsTotal'] == all_repo_groups
72 74
73 75 def test_show_repo_groups_data_filtered(self, autologin_user, xhr_header):
74 76 response = self.app.get(route_path(
75 77 'repo_groups_data', params={'search[value]': 'empty_search'}),
76 78 extra_environ=xhr_header)
77 79
78 80 all_repo_groups = RepoGroup.query().count()
79 81 assert response.json['recordsTotal'] == all_repo_groups
80 82 assert response.json['recordsFiltered'] == 0
81 83
82 84 def test_show_repo_groups_after_creating_group(self, autologin_user, xhr_header):
83 85 fixture.create_repo_group('test_repo_group')
84 86 response = self.app.get(route_path(
85 87 'repo_groups_data'), extra_environ=xhr_header)
86 88 response.mustcontain('<a href=\\"/{}/_edit\\" title=\\"Edit\\">Edit</a>'.format('test_repo_group'))
87 89 fixture.destroy_repo_group('test_repo_group')
88 90
89 91 def test_new(self, autologin_user):
90 92 self.app.get(route_path('repo_group_new'))
91 93
92 94 def test_new_with_parent_group(self, autologin_user, user_util):
93 95 gr = user_util.create_repo_group()
94 96
95 97 self.app.get(route_path('repo_group_new'),
96 98 params=dict(parent_group=gr.group_name))
97 99
98 100 def test_new_by_regular_user_no_permission(self, autologin_regular_user):
99 101 self.app.get(route_path('repo_group_new'), status=403)
100 102
101 103 @pytest.mark.parametrize('repo_group_name', [
102 104 'git_repo',
103 105 'git_repo_ąć',
104 106 'hg_repo',
105 107 '12345',
106 108 'hg_repo_ąć',
107 109 ])
108 110 def test_create(self, autologin_user, repo_group_name, csrf_token):
109 repo_group_name_unicode = repo_group_name.decode('utf8')
111 repo_group_name_non_ascii = repo_group_name
110 112 description = 'description for newly created repo group'
111 113
112 114 response = self.app.post(
113 115 route_path('repo_group_create'),
114 116 fixture._get_group_create_params(
115 117 group_name=repo_group_name,
116 118 group_description=description,
117 119 csrf_token=csrf_token))
118 120
119 121 # run the check page that triggers the flash message
120 122 repo_gr_url = h.route_path(
121 123 'repo_group_home', repo_group_name=repo_group_name)
122 124
123 125 assert_session_flash(
124 126 response,
125 127 'Created repository group <a href="%s">%s</a>' % (
126 repo_gr_url, repo_group_name_unicode))
128 repo_gr_url, repo_group_name_non_ascii))
127 129
128 130 # # test if the repo group was created in the database
129 131 new_repo_group = RepoGroupModel()._get_repo_group(
130 repo_group_name_unicode)
132 repo_group_name_non_ascii)
131 133 assert new_repo_group is not None
132 134
133 assert new_repo_group.group_name == repo_group_name_unicode
135 assert new_repo_group.group_name == repo_group_name_non_ascii
134 136 assert new_repo_group.group_description == description
135 137
136 138 # test if the repository is visible in the list ?
137 139 response = self.app.get(repo_gr_url)
138 140 response.mustcontain(repo_group_name)
139 141
140 142 # test if the repository group was created on filesystem
141 143 is_on_filesystem = os.path.isdir(
142 144 os.path.join(TESTS_TMP_PATH, repo_group_name))
143 145 if not is_on_filesystem:
144 146 self.fail('no repo group %s in filesystem' % repo_group_name)
145 147
146 RepoGroupModel().delete(repo_group_name_unicode)
148 RepoGroupModel().delete(repo_group_name_non_ascii)
147 149 Session().commit()
148 150
149 151 @pytest.mark.parametrize('repo_group_name', [
150 152 'git_repo',
151 153 'git_repo_ąć',
152 154 'hg_repo',
153 155 '12345',
154 156 'hg_repo_ąć',
155 157 ])
156 158 def test_create_subgroup(self, autologin_user, user_util, repo_group_name, csrf_token):
157 159 parent_group = user_util.create_repo_group()
158 160 parent_group_name = parent_group.group_name
159 161
160 162 expected_group_name = '{}/{}'.format(
161 163 parent_group_name, repo_group_name)
162 expected_group_name_unicode = expected_group_name.decode('utf8')
164 expected_group_name_non_ascii = expected_group_name
163 165
164 166 try:
165 167 response = self.app.post(
166 168 route_path('repo_group_create'),
167 169 fixture._get_group_create_params(
168 170 group_name=repo_group_name,
169 171 group_parent_id=parent_group.group_id,
170 172 group_description='Test desciption',
171 173 csrf_token=csrf_token))
172 174
173 175 assert_session_flash(
174 176 response,
175 177 u'Created repository group <a href="%s">%s</a>' % (
176 178 h.route_path('repo_group_home',
177 179 repo_group_name=expected_group_name),
178 expected_group_name_unicode))
180 expected_group_name_non_ascii))
179 181 finally:
180 RepoGroupModel().delete(expected_group_name_unicode)
182 RepoGroupModel().delete(expected_group_name_non_ascii)
181 183 Session().commit()
182 184
183 185 def test_user_with_creation_permissions_cannot_create_subgroups(
184 186 self, autologin_regular_user, user_util):
185 187
186 188 user_util.grant_user_permission(
187 189 TEST_USER_REGULAR_LOGIN, 'hg.repogroup.create.true')
188 190 parent_group = user_util.create_repo_group()
189 191 parent_group_id = parent_group.group_id
190 192 self.app.get(
191 193 route_path('repo_group_new',
192 194 params=dict(parent_group=parent_group_id), ),
193 195 status=403)
@@ -1,766 +1,768 b''
1 1
2 2 # Copyright (C) 2010-2020 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 from rhodecode.lib.utils2 import md5
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.utils import AssertResponse
31 30
32 31
33 32 UPDATE_DATA_QUALNAME = 'rhodecode.model.update.UpdateModel.get_update_data'
34 33
35 34
36 35 def route_path(name, params=None, **kwargs):
37 import urllib.request, urllib.parse, urllib.error
36 import urllib.request
37 import urllib.parse
38 import urllib.error
38 39 from rhodecode.apps._base import ADMIN_PREFIX
39 40
40 41 base_url = {
41 42
42 43 'admin_settings':
43 44 ADMIN_PREFIX +'/settings',
44 45 'admin_settings_update':
45 46 ADMIN_PREFIX + '/settings/update',
46 47 'admin_settings_global':
47 48 ADMIN_PREFIX + '/settings/global',
48 49 'admin_settings_global_update':
49 50 ADMIN_PREFIX + '/settings/global/update',
50 51 'admin_settings_vcs':
51 52 ADMIN_PREFIX + '/settings/vcs',
52 53 'admin_settings_vcs_update':
53 54 ADMIN_PREFIX + '/settings/vcs/update',
54 55 'admin_settings_vcs_svn_pattern_delete':
55 56 ADMIN_PREFIX + '/settings/vcs/svn_pattern_delete',
56 57 'admin_settings_mapping':
57 58 ADMIN_PREFIX + '/settings/mapping',
58 59 'admin_settings_mapping_update':
59 60 ADMIN_PREFIX + '/settings/mapping/update',
60 61 'admin_settings_visual':
61 62 ADMIN_PREFIX + '/settings/visual',
62 63 'admin_settings_visual_update':
63 64 ADMIN_PREFIX + '/settings/visual/update',
64 65 'admin_settings_issuetracker':
65 66 ADMIN_PREFIX + '/settings/issue-tracker',
66 67 'admin_settings_issuetracker_update':
67 68 ADMIN_PREFIX + '/settings/issue-tracker/update',
68 69 'admin_settings_issuetracker_test':
69 70 ADMIN_PREFIX + '/settings/issue-tracker/test',
70 71 'admin_settings_issuetracker_delete':
71 72 ADMIN_PREFIX + '/settings/issue-tracker/delete',
72 73 'admin_settings_email':
73 74 ADMIN_PREFIX + '/settings/email',
74 75 'admin_settings_email_update':
75 76 ADMIN_PREFIX + '/settings/email/update',
76 77 'admin_settings_hooks':
77 78 ADMIN_PREFIX + '/settings/hooks',
78 79 'admin_settings_hooks_update':
79 80 ADMIN_PREFIX + '/settings/hooks/update',
80 81 'admin_settings_hooks_delete':
81 82 ADMIN_PREFIX + '/settings/hooks/delete',
82 83 'admin_settings_search':
83 84 ADMIN_PREFIX + '/settings/search',
84 85 'admin_settings_labs':
85 86 ADMIN_PREFIX + '/settings/labs',
86 87 'admin_settings_labs_update':
87 88 ADMIN_PREFIX + '/settings/labs/update',
88 89
89 90 'admin_settings_sessions':
90 91 ADMIN_PREFIX + '/settings/sessions',
91 92 'admin_settings_sessions_cleanup':
92 93 ADMIN_PREFIX + '/settings/sessions/cleanup',
93 94 'admin_settings_system':
94 95 ADMIN_PREFIX + '/settings/system',
95 96 'admin_settings_system_update':
96 97 ADMIN_PREFIX + '/settings/system/updates',
97 98 'admin_settings_open_source':
98 99 ADMIN_PREFIX + '/settings/open_source',
99 100
100 101
101 102 }[name].format(**kwargs)
102 103
103 104 if params:
104 105 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
105 106 return base_url
106 107
107 108
108 109 @pytest.mark.usefixtures('autologin_user', 'app')
109 110 class TestAdminSettingsController(object):
110 111
111 112 @pytest.mark.parametrize('urlname', [
112 113 'admin_settings_vcs',
113 114 'admin_settings_mapping',
114 115 'admin_settings_global',
115 116 'admin_settings_visual',
116 117 'admin_settings_email',
117 118 'admin_settings_hooks',
118 119 'admin_settings_search',
119 120 ])
120 121 def test_simple_get(self, urlname):
121 122 self.app.get(route_path(urlname))
122 123
123 124 def test_create_custom_hook(self, csrf_token):
124 125 response = self.app.post(
125 126 route_path('admin_settings_hooks_update'),
126 127 params={
127 128 'new_hook_ui_key': 'test_hooks_1',
128 129 'new_hook_ui_value': 'cd /tmp',
129 130 'csrf_token': csrf_token})
130 131
131 132 response = response.follow()
132 133 response.mustcontain('test_hooks_1')
133 134 response.mustcontain('cd /tmp')
134 135
135 136 def test_create_custom_hook_delete(self, csrf_token):
136 137 response = self.app.post(
137 138 route_path('admin_settings_hooks_update'),
138 139 params={
139 140 'new_hook_ui_key': 'test_hooks_2',
140 141 'new_hook_ui_value': 'cd /tmp2',
141 142 'csrf_token': csrf_token})
142 143
143 144 response = response.follow()
144 145 response.mustcontain('test_hooks_2')
145 146 response.mustcontain('cd /tmp2')
146 147
147 148 hook_id = SettingsModel().get_ui_by_key('test_hooks_2').ui_id
148 149
149 150 # delete
150 151 self.app.post(
151 152 route_path('admin_settings_hooks_delete'),
152 153 params={'hook_id': hook_id, 'csrf_token': csrf_token})
153 154 response = self.app.get(route_path('admin_settings_hooks'))
154 155 response.mustcontain(no=['test_hooks_2'])
155 156 response.mustcontain(no=['cd /tmp2'])
156 157
157 158
158 159 @pytest.mark.usefixtures('autologin_user', 'app')
159 160 class TestAdminSettingsGlobal(object):
160 161
161 162 def test_pre_post_code_code_active(self, csrf_token):
162 163 pre_code = 'rc-pre-code-187652122'
163 164 post_code = 'rc-postcode-98165231'
164 165
165 166 response = self.post_and_verify_settings({
166 167 'rhodecode_pre_code': pre_code,
167 168 'rhodecode_post_code': post_code,
168 169 'csrf_token': csrf_token,
169 170 })
170 171
171 172 response = response.follow()
172 173 response.mustcontain(pre_code, post_code)
173 174
174 175 def test_pre_post_code_code_inactive(self, csrf_token):
175 176 pre_code = 'rc-pre-code-187652122'
176 177 post_code = 'rc-postcode-98165231'
177 178 response = self.post_and_verify_settings({
178 179 'rhodecode_pre_code': '',
179 180 'rhodecode_post_code': '',
180 181 'csrf_token': csrf_token,
181 182 })
182 183
183 184 response = response.follow()
184 185 response.mustcontain(no=[pre_code, post_code])
185 186
186 187 def test_captcha_activate(self, csrf_token):
187 188 self.post_and_verify_settings({
188 189 'rhodecode_captcha_private_key': '1234567890',
189 190 'rhodecode_captcha_public_key': '1234567890',
190 191 'csrf_token': csrf_token,
191 192 })
192 193
193 194 response = self.app.get(ADMIN_PREFIX + '/register')
194 195 response.mustcontain('captcha')
195 196
196 197 def test_captcha_deactivate(self, csrf_token):
197 198 self.post_and_verify_settings({
198 199 'rhodecode_captcha_private_key': '',
199 200 'rhodecode_captcha_public_key': '1234567890',
200 201 'csrf_token': csrf_token,
201 202 })
202 203
203 204 response = self.app.get(ADMIN_PREFIX + '/register')
204 205 response.mustcontain(no=['captcha'])
205 206
206 207 def test_title_change(self, csrf_token):
207 208 old_title = 'RhodeCode'
208 209
209 210 for new_title in ['Changed', 'Żółwik', old_title]:
210 211 response = self.post_and_verify_settings({
211 212 'rhodecode_title': new_title,
212 213 'csrf_token': csrf_token,
213 214 })
214 215
215 216 response = response.follow()
216 217 response.mustcontain(new_title)
217 218
218 219 def post_and_verify_settings(self, settings):
219 220 old_title = 'RhodeCode'
220 221 old_realm = 'RhodeCode authentication'
221 222 params = {
222 223 'rhodecode_title': old_title,
223 224 'rhodecode_realm': old_realm,
224 225 'rhodecode_pre_code': '',
225 226 'rhodecode_post_code': '',
226 227 'rhodecode_captcha_private_key': '',
227 228 'rhodecode_captcha_public_key': '',
228 229 'rhodecode_create_personal_repo_group': False,
229 230 'rhodecode_personal_repo_group_pattern': '${username}',
230 231 }
231 232 params.update(settings)
232 233 response = self.app.post(
233 234 route_path('admin_settings_global_update'), params=params)
234 235
235 236 assert_session_flash(response, 'Updated application settings')
237
236 238 app_settings = SettingsModel().get_all_settings()
237 239 del settings['csrf_token']
238 240 for key, value in settings.items():
239 241 assert app_settings[key] == value
240 242
241 243 return response
242 244
243 245
244 246 @pytest.mark.usefixtures('autologin_user', 'app')
245 247 class TestAdminSettingsVcs(object):
246 248
247 249 def test_contains_svn_default_patterns(self):
248 250 response = self.app.get(route_path('admin_settings_vcs'))
249 251 expected_patterns = [
250 252 '/trunk',
251 253 '/branches/*',
252 254 '/tags/*',
253 255 ]
254 256 for pattern in expected_patterns:
255 257 response.mustcontain(pattern)
256 258
257 259 def test_add_new_svn_branch_and_tag_pattern(
258 260 self, backend_svn, form_defaults, disable_sql_cache,
259 261 csrf_token):
260 262 form_defaults.update({
261 263 'new_svn_branch': '/exp/branches/*',
262 264 'new_svn_tag': '/important_tags/*',
263 265 'csrf_token': csrf_token,
264 266 })
265 267
266 268 response = self.app.post(
267 269 route_path('admin_settings_vcs_update'),
268 270 params=form_defaults, status=302)
269 271 response = response.follow()
270 272
271 273 # Expect to find the new values on the page
272 274 response.mustcontain('/exp/branches/*')
273 275 response.mustcontain('/important_tags/*')
274 276
275 277 # Expect that those patterns are used to match branches and tags now
276 278 repo = backend_svn['svn-simple-layout'].scm_instance()
277 279 assert 'exp/branches/exp-sphinx-docs' in repo.branches
278 280 assert 'important_tags/v0.5' in repo.tags
279 281
280 282 def test_add_same_svn_value_twice_shows_an_error_message(
281 283 self, form_defaults, csrf_token, settings_util):
282 284 settings_util.create_rhodecode_ui('vcs_svn_branch', '/test')
283 285 settings_util.create_rhodecode_ui('vcs_svn_tag', '/test')
284 286
285 287 response = self.app.post(
286 288 route_path('admin_settings_vcs_update'),
287 289 params={
288 290 'paths_root_path': form_defaults['paths_root_path'],
289 291 'new_svn_branch': '/test',
290 292 'new_svn_tag': '/test',
291 293 'csrf_token': csrf_token,
292 294 },
293 295 status=200)
294 296
295 297 response.mustcontain("Pattern already exists")
296 298 response.mustcontain("Some form inputs contain invalid data.")
297 299
298 300 @pytest.mark.parametrize('section', [
299 301 'vcs_svn_branch',
300 302 'vcs_svn_tag',
301 303 ])
302 304 def test_delete_svn_patterns(
303 305 self, section, csrf_token, settings_util):
304 306 setting = settings_util.create_rhodecode_ui(
305 307 section, '/test_delete', cleanup=False)
306 308
307 309 self.app.post(
308 310 route_path('admin_settings_vcs_svn_pattern_delete'),
309 311 params={
310 312 'delete_svn_pattern': setting.ui_id,
311 313 'csrf_token': csrf_token},
312 314 headers={'X-REQUESTED-WITH': 'XMLHttpRequest'})
313 315
314 316 @pytest.mark.parametrize('section', [
315 317 'vcs_svn_branch',
316 318 'vcs_svn_tag',
317 319 ])
318 320 def test_delete_svn_patterns_raises_404_when_no_xhr(
319 321 self, section, csrf_token, settings_util):
320 322 setting = settings_util.create_rhodecode_ui(section, '/test_delete')
321 323
322 324 self.app.post(
323 325 route_path('admin_settings_vcs_svn_pattern_delete'),
324 326 params={
325 327 'delete_svn_pattern': setting.ui_id,
326 328 'csrf_token': csrf_token},
327 329 status=404)
328 330
329 331 def test_extensions_hgsubversion(self, form_defaults, csrf_token):
330 332 form_defaults.update({
331 333 'csrf_token': csrf_token,
332 334 'extensions_hgsubversion': 'True',
333 335 })
334 336 response = self.app.post(
335 337 route_path('admin_settings_vcs_update'),
336 338 params=form_defaults,
337 339 status=302)
338 340
339 341 response = response.follow()
340 342 extensions_input = (
341 343 '<input id="extensions_hgsubversion" '
342 344 'name="extensions_hgsubversion" type="checkbox" '
343 345 'value="True" checked="checked" />')
344 346 response.mustcontain(extensions_input)
345 347
346 348 def test_extensions_hgevolve(self, form_defaults, csrf_token):
347 349 form_defaults.update({
348 350 'csrf_token': csrf_token,
349 351 'extensions_evolve': 'True',
350 352 })
351 353 response = self.app.post(
352 354 route_path('admin_settings_vcs_update'),
353 355 params=form_defaults,
354 356 status=302)
355 357
356 358 response = response.follow()
357 359 extensions_input = (
358 360 '<input id="extensions_evolve" '
359 361 'name="extensions_evolve" type="checkbox" '
360 362 'value="True" checked="checked" />')
361 363 response.mustcontain(extensions_input)
362 364
363 365 def test_has_a_section_for_pull_request_settings(self):
364 366 response = self.app.get(route_path('admin_settings_vcs'))
365 367 response.mustcontain('Pull Request Settings')
366 368
367 369 def test_has_an_input_for_invalidation_of_inline_comments(self):
368 370 response = self.app.get(route_path('admin_settings_vcs'))
369 371 assert_response = response.assert_response()
370 372 assert_response.one_element_exists(
371 373 '[name=rhodecode_use_outdated_comments]')
372 374
373 375 @pytest.mark.parametrize('new_value', [True, False])
374 376 def test_allows_to_change_invalidation_of_inline_comments(
375 377 self, form_defaults, csrf_token, new_value):
376 378 setting_key = 'use_outdated_comments'
377 379 setting = SettingsModel().create_or_update_setting(
378 380 setting_key, not new_value, 'bool')
379 381 Session().add(setting)
380 382 Session().commit()
381 383
382 384 form_defaults.update({
383 385 'csrf_token': csrf_token,
384 386 'rhodecode_use_outdated_comments': str(new_value),
385 387 })
386 388 response = self.app.post(
387 389 route_path('admin_settings_vcs_update'),
388 390 params=form_defaults,
389 391 status=302)
390 392 response = response.follow()
391 393 setting = SettingsModel().get_setting_by_name(setting_key)
392 394 assert setting.app_settings_value is new_value
393 395
394 396 @pytest.mark.parametrize('new_value', [True, False])
395 397 def test_allows_to_change_hg_rebase_merge_strategy(
396 398 self, form_defaults, csrf_token, new_value):
397 399 setting_key = 'hg_use_rebase_for_merging'
398 400
399 401 form_defaults.update({
400 402 'csrf_token': csrf_token,
401 403 'rhodecode_' + setting_key: str(new_value),
402 404 })
403 405
404 406 with mock.patch.dict(
405 407 rhodecode.CONFIG, {'labs_settings_active': 'true'}):
406 408 self.app.post(
407 409 route_path('admin_settings_vcs_update'),
408 410 params=form_defaults,
409 411 status=302)
410 412
411 413 setting = SettingsModel().get_setting_by_name(setting_key)
412 414 assert setting.app_settings_value is new_value
413 415
414 416 @pytest.fixture()
415 417 def disable_sql_cache(self, request):
418 # patch _do_orm_execute so it returns None similar like if we don't use a cached query
416 419 patcher = mock.patch(
417 'rhodecode.lib.caching_query.FromCache.process_query')
420 'rhodecode.lib.caching_query.ORMCache._do_orm_execute', return_value=None)
418 421 request.addfinalizer(patcher.stop)
419 422 patcher.start()
420 423
421 424 @pytest.fixture()
422 425 def form_defaults(self):
423 426 from rhodecode.apps.admin.views.settings import AdminSettingsView
424 427 return AdminSettingsView._form_defaults()
425 428
426 429 # TODO: johbo: What we really want is to checkpoint before a test run and
427 430 # reset the session afterwards.
428 431 @pytest.fixture(scope='class', autouse=True)
429 432 def cleanup_settings(self, request, baseapp):
430 433 ui_id = RhodeCodeUi.ui_id
431 original_ids = list(
432 r.ui_id for r in RhodeCodeUi.query().values(ui_id))
434 original_ids = [r.ui_id for r in RhodeCodeUi.query().with_entities(ui_id)]
433 435
434 436 @request.addfinalizer
435 437 def cleanup():
436 438 RhodeCodeUi.query().filter(
437 439 ui_id.notin_(original_ids)).delete(False)
438 440
439 441
440 442 @pytest.mark.usefixtures('autologin_user', 'app')
441 443 class TestLabsSettings(object):
442 444 def test_get_settings_page_disabled(self):
443 445 with mock.patch.dict(
444 446 rhodecode.CONFIG, {'labs_settings_active': 'false'}):
445 447
446 448 response = self.app.get(
447 449 route_path('admin_settings_labs'), status=302)
448 450
449 451 assert response.location.endswith(route_path('admin_settings'))
450 452
451 453 def test_get_settings_page_enabled(self):
452 454 from rhodecode.apps.admin.views import settings
453 455 lab_settings = [
454 456 settings.LabSetting(
455 457 key='rhodecode_bool',
456 458 type='bool',
457 459 group='bool group',
458 460 label='bool label',
459 461 help='bool help'
460 462 ),
461 463 settings.LabSetting(
462 464 key='rhodecode_text',
463 465 type='unicode',
464 466 group='text group',
465 467 label='text label',
466 468 help='text help'
467 469 ),
468 470 ]
469 471 with mock.patch.dict(rhodecode.CONFIG,
470 472 {'labs_settings_active': 'true'}):
471 473 with mock.patch.object(settings, '_LAB_SETTINGS', lab_settings):
472 474 response = self.app.get(route_path('admin_settings_labs'))
473 475
474 476 assert '<label>bool group:</label>' in response
475 477 assert '<label for="rhodecode_bool">bool label</label>' in response
476 478 assert '<p class="help-block">bool help</p>' in response
477 479 assert 'name="rhodecode_bool" type="checkbox"' in response
478 480
479 481 assert '<label>text group:</label>' in response
480 482 assert '<label for="rhodecode_text">text label</label>' in response
481 483 assert '<p class="help-block">text help</p>' in response
482 484 assert 'name="rhodecode_text" size="60" type="text"' in response
483 485
484 486
485 487 @pytest.mark.usefixtures('app')
486 488 class TestOpenSourceLicenses(object):
487 489
488 490 def test_records_are_displayed(self, autologin_user):
489 491 sample_licenses = [
490 492 {
491 493 "license": [
492 494 {
493 495 "fullName": "BSD 4-clause \"Original\" or \"Old\" License",
494 496 "shortName": "bsdOriginal",
495 497 "spdxId": "BSD-4-Clause",
496 498 "url": "http://spdx.org/licenses/BSD-4-Clause.html"
497 499 }
498 500 ],
499 501 "name": "python2.7-coverage-3.7.1"
500 502 },
501 503 {
502 504 "license": [
503 505 {
504 506 "fullName": "MIT License",
505 507 "shortName": "mit",
506 508 "spdxId": "MIT",
507 509 "url": "http://spdx.org/licenses/MIT.html"
508 510 }
509 511 ],
510 512 "name": "python2.7-bootstrapped-pip-9.0.1"
511 513 },
512 514 ]
513 515 read_licenses_patch = mock.patch(
514 516 'rhodecode.apps.admin.views.open_source_licenses.read_opensource_licenses',
515 517 return_value=sample_licenses)
516 518 with read_licenses_patch:
517 519 response = self.app.get(
518 520 route_path('admin_settings_open_source'), status=200)
519 521
520 522 assert_response = response.assert_response()
521 523 assert_response.element_contains(
522 524 '.panel-heading', 'Licenses of Third Party Packages')
523 525 for license_data in sample_licenses:
524 526 response.mustcontain(license_data["license"][0]["spdxId"])
525 527 assert_response.element_contains('.panel-body', license_data["name"])
526 528
527 529 def test_records_can_be_read(self, autologin_user):
528 530 response = self.app.get(
529 531 route_path('admin_settings_open_source'), status=200)
530 532 assert_response = response.assert_response()
531 533 assert_response.element_contains(
532 534 '.panel-heading', 'Licenses of Third Party Packages')
533 535
534 536 def test_forbidden_when_normal_user(self, autologin_regular_user):
535 537 self.app.get(
536 538 route_path('admin_settings_open_source'), status=404)
537 539
538 540
539 541 @pytest.mark.usefixtures('app')
540 542 class TestUserSessions(object):
541 543
542 544 def test_forbidden_when_normal_user(self, autologin_regular_user):
543 545 self.app.get(route_path('admin_settings_sessions'), status=404)
544 546
545 547 def test_show_sessions_page(self, autologin_user):
546 548 response = self.app.get(route_path('admin_settings_sessions'), status=200)
547 549 response.mustcontain('file')
548 550
549 551 def test_cleanup_old_sessions(self, autologin_user, csrf_token):
550 552
551 553 post_data = {
552 554 'csrf_token': csrf_token,
553 555 'expire_days': '60'
554 556 }
555 557 response = self.app.post(
556 558 route_path('admin_settings_sessions_cleanup'), params=post_data,
557 559 status=302)
558 560 assert_session_flash(response, 'Cleaned up old sessions')
559 561
560 562
561 563 @pytest.mark.usefixtures('app')
562 564 class TestAdminSystemInfo(object):
563 565
564 566 def test_forbidden_when_normal_user(self, autologin_regular_user):
565 567 self.app.get(route_path('admin_settings_system'), status=404)
566 568
567 569 def test_system_info_page(self, autologin_user):
568 570 response = self.app.get(route_path('admin_settings_system'))
569 571 response.mustcontain('RhodeCode Community Edition, version {}'.format(
570 572 rhodecode.__version__))
571 573
572 574 def test_system_update_new_version(self, autologin_user):
573 575 update_data = {
574 576 'versions': [
575 577 {
576 578 'version': '100.3.1415926535',
577 579 'general': 'The latest version we are ever going to ship'
578 580 },
579 581 {
580 582 'version': '0.0.0',
581 583 'general': 'The first version we ever shipped'
582 584 }
583 585 ]
584 586 }
585 587 with mock.patch(UPDATE_DATA_QUALNAME, return_value=update_data):
586 588 response = self.app.get(route_path('admin_settings_system_update'))
587 589 response.mustcontain('A <b>new version</b> is available')
588 590
589 591 def test_system_update_nothing_new(self, autologin_user):
590 592 update_data = {
591 593 'versions': [
592 594 {
593 595 'version': '0.0.0',
594 596 'general': 'The first version we ever shipped'
595 597 }
596 598 ]
597 599 }
598 600 with mock.patch(UPDATE_DATA_QUALNAME, return_value=update_data):
599 601 response = self.app.get(route_path('admin_settings_system_update'))
600 602 response.mustcontain(
601 603 'This instance is already running the <b>latest</b> stable version')
602 604
603 605 def test_system_update_bad_response(self, autologin_user):
604 606 with mock.patch(UPDATE_DATA_QUALNAME, side_effect=ValueError('foo')):
605 607 response = self.app.get(route_path('admin_settings_system_update'))
606 608 response.mustcontain(
607 609 'Bad data sent from update server')
608 610
609 611
610 612 @pytest.mark.usefixtures("app")
611 613 class TestAdminSettingsIssueTracker(object):
612 614 RC_PREFIX = 'rhodecode_'
613 615 SHORT_PATTERN_KEY = 'issuetracker_pat_'
614 616 PATTERN_KEY = RC_PREFIX + SHORT_PATTERN_KEY
615 617 DESC_KEY = RC_PREFIX + 'issuetracker_desc_'
616 618
617 619 def test_issuetracker_index(self, autologin_user):
618 620 response = self.app.get(route_path('admin_settings_issuetracker'))
619 621 assert response.status_code == 200
620 622
621 623 def test_add_empty_issuetracker_pattern(
622 624 self, request, autologin_user, csrf_token):
623 625 post_url = route_path('admin_settings_issuetracker_update')
624 626 post_data = {
625 627 'csrf_token': csrf_token
626 628 }
627 629 self.app.post(post_url, post_data, status=302)
628 630
629 631 def test_add_issuetracker_pattern(
630 632 self, request, autologin_user, csrf_token):
631 633 pattern = 'issuetracker_pat'
632 634 another_pattern = pattern+'1'
633 635 post_url = route_path('admin_settings_issuetracker_update')
634 636 post_data = {
635 637 'new_pattern_pattern_0': pattern,
636 638 'new_pattern_url_0': 'http://url',
637 639 'new_pattern_prefix_0': 'prefix',
638 640 'new_pattern_description_0': 'description',
639 641 'new_pattern_pattern_1': another_pattern,
640 642 'new_pattern_url_1': 'https://url1',
641 643 'new_pattern_prefix_1': 'prefix1',
642 644 'new_pattern_description_1': 'description1',
643 645 'csrf_token': csrf_token
644 646 }
645 647 self.app.post(post_url, post_data, status=302)
646 648 settings = SettingsModel().get_all_settings()
647 self.uid = md5(pattern)
649 self.uid = md5_safe(pattern)
648 650 assert settings[self.PATTERN_KEY+self.uid] == pattern
649 self.another_uid = md5(another_pattern)
651 self.another_uid = md5_safe(another_pattern)
650 652 assert settings[self.PATTERN_KEY+self.another_uid] == another_pattern
651 653
652 654 @request.addfinalizer
653 655 def cleanup():
654 656 defaults = SettingsModel().get_all_settings()
655 657
656 658 entries = [name for name in defaults if (
657 (self.uid in name) or (self.another_uid) in name)]
659 (self.uid in name) or (self.another_uid in name))]
658 660 start = len(self.RC_PREFIX)
659 661 for del_key in entries:
660 662 # TODO: anderson: get_by_name needs name without prefix
661 663 entry = SettingsModel().get_setting_by_name(del_key[start:])
662 664 Session().delete(entry)
663 665
664 666 Session().commit()
665 667
666 668 def test_edit_issuetracker_pattern(
667 669 self, autologin_user, backend, csrf_token, request):
668 670
669 671 old_pattern = 'issuetracker_pat1'
670 old_uid = md5(old_pattern)
672 old_uid = md5_safe(old_pattern)
671 673
672 674 post_url = route_path('admin_settings_issuetracker_update')
673 675 post_data = {
674 676 'new_pattern_pattern_0': old_pattern,
675 677 'new_pattern_url_0': 'http://url',
676 678 'new_pattern_prefix_0': 'prefix',
677 679 'new_pattern_description_0': 'description',
678 680
679 681 'csrf_token': csrf_token
680 682 }
681 683 self.app.post(post_url, post_data, status=302)
682 684
683 685 new_pattern = 'issuetracker_pat1_edited'
684 self.new_uid = md5(new_pattern)
686 self.new_uid = md5_safe(new_pattern)
685 687
686 688 post_url = route_path('admin_settings_issuetracker_update')
687 689 post_data = {
688 690 'new_pattern_pattern_{}'.format(old_uid): new_pattern,
689 691 'new_pattern_url_{}'.format(old_uid): 'https://url_edited',
690 692 'new_pattern_prefix_{}'.format(old_uid): 'prefix_edited',
691 693 'new_pattern_description_{}'.format(old_uid): 'description_edited',
692 694 'uid': old_uid,
693 695 'csrf_token': csrf_token
694 696 }
695 697 self.app.post(post_url, post_data, status=302)
696 698
697 699 settings = SettingsModel().get_all_settings()
698 700 assert settings[self.PATTERN_KEY+self.new_uid] == new_pattern
699 701 assert settings[self.DESC_KEY + self.new_uid] == 'description_edited'
700 702 assert self.PATTERN_KEY+old_uid not in settings
701 703
702 704 @request.addfinalizer
703 705 def cleanup():
704 706 IssueTrackerSettingsModel().delete_entries(old_uid)
705 707 IssueTrackerSettingsModel().delete_entries(self.new_uid)
706 708
707 709 def test_replace_issuetracker_pattern_description(
708 710 self, autologin_user, csrf_token, request, settings_util):
709 711 prefix = 'issuetracker'
710 712 pattern = 'issuetracker_pat'
711 self.uid = md5(pattern)
713 self.uid = md5_safe(pattern)
712 714 pattern_key = '_'.join([prefix, 'pat', self.uid])
713 715 rc_pattern_key = '_'.join(['rhodecode', pattern_key])
714 716 desc_key = '_'.join([prefix, 'desc', self.uid])
715 717 rc_desc_key = '_'.join(['rhodecode', desc_key])
716 718 new_description = 'new_description'
717 719
718 720 settings_util.create_rhodecode_setting(
719 721 pattern_key, pattern, 'unicode', cleanup=False)
720 722 settings_util.create_rhodecode_setting(
721 723 desc_key, 'old description', 'unicode', cleanup=False)
722 724
723 725 post_url = route_path('admin_settings_issuetracker_update')
724 726 post_data = {
725 727 'new_pattern_pattern_0': pattern,
726 728 'new_pattern_url_0': 'https://url',
727 729 'new_pattern_prefix_0': 'prefix',
728 730 'new_pattern_description_0': new_description,
729 731 'uid': self.uid,
730 732 'csrf_token': csrf_token
731 733 }
732 734 self.app.post(post_url, post_data, status=302)
733 735 settings = SettingsModel().get_all_settings()
734 736 assert settings[rc_pattern_key] == pattern
735 737 assert settings[rc_desc_key] == new_description
736 738
737 739 @request.addfinalizer
738 740 def cleanup():
739 741 IssueTrackerSettingsModel().delete_entries(self.uid)
740 742
741 743 def test_delete_issuetracker_pattern(
742 744 self, autologin_user, backend, csrf_token, settings_util, xhr_header):
743 745
744 746 old_pattern = 'issuetracker_pat_deleted'
745 old_uid = md5(old_pattern)
747 old_uid = md5_safe(old_pattern)
746 748
747 749 post_url = route_path('admin_settings_issuetracker_update')
748 750 post_data = {
749 751 'new_pattern_pattern_0': old_pattern,
750 752 'new_pattern_url_0': 'http://url',
751 753 'new_pattern_prefix_0': 'prefix',
752 754 'new_pattern_description_0': 'description',
753 755
754 756 'csrf_token': csrf_token
755 757 }
756 758 self.app.post(post_url, post_data, status=302)
757 759
758 760 post_url = route_path('admin_settings_issuetracker_delete')
759 761 post_data = {
760 762 'uid': old_uid,
761 763 'csrf_token': csrf_token
762 764 }
763 765 self.app.post(post_url, post_data, extra_environ=xhr_header, status=200)
764 766 settings = SettingsModel().get_all_settings()
765 767 assert self.PATTERN_KEY+old_uid not in settings
766 768 assert self.DESC_KEY + old_uid not in settings
@@ -1,169 +1,171 b''
1 1
2 2 # Copyright (C) 2010-2020 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 26 TestController, TEST_USER_REGULAR_LOGIN, assert_session_flash)
27 27 from rhodecode.tests.fixture import Fixture
28 28
29 29 fixture = Fixture()
30 30
31 31
32 32 def route_path(name, params=None, **kwargs):
33 import urllib.request, urllib.parse, urllib.error
33 import urllib.request
34 import urllib.parse
35 import urllib.error
34 36 from rhodecode.apps._base import ADMIN_PREFIX
35 37
36 38 base_url = {
37 39 'user_groups': ADMIN_PREFIX + '/user_groups',
38 40 'user_groups_data': ADMIN_PREFIX + '/user_groups_data',
39 41 'user_group_members_data': ADMIN_PREFIX + '/user_groups/{user_group_id}/members',
40 42 'user_groups_new': ADMIN_PREFIX + '/user_groups/new',
41 43 'user_groups_create': ADMIN_PREFIX + '/user_groups/create',
42 44 'edit_user_group': ADMIN_PREFIX + '/user_groups/{user_group_id}/edit',
43 45 }[name].format(**kwargs)
44 46
45 47 if params:
46 48 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
47 49 return base_url
48 50
49 51
50 52 class TestAdminUserGroupsView(TestController):
51 53
52 54 def test_show_users(self):
53 55 self.log_user()
54 56 self.app.get(route_path('user_groups'))
55 57
56 58 def test_show_user_groups_data(self, xhr_header):
57 59 self.log_user()
58 60 response = self.app.get(route_path(
59 61 'user_groups_data'), extra_environ=xhr_header)
60 62
61 63 all_user_groups = UserGroup.query().count()
62 64 assert response.json['recordsTotal'] == all_user_groups
63 65
64 66 def test_show_user_groups_data_filtered(self, xhr_header):
65 67 self.log_user()
66 68 response = self.app.get(route_path(
67 69 'user_groups_data', params={'search[value]': 'empty_search'}),
68 70 extra_environ=xhr_header)
69 71
70 72 all_user_groups = UserGroup.query().count()
71 73 assert response.json['recordsTotal'] == all_user_groups
72 74 assert response.json['recordsFiltered'] == 0
73 75
74 76 def test_usergroup_escape(self, user_util, xhr_header):
75 77 self.log_user()
76 78
77 79 xss_img = '<img src="/image1" onload="alert(\'Hello, World!\');">'
78 80 user = user_util.create_user()
79 81 user.name = xss_img
80 82 user.lastname = xss_img
81 83 Session().add(user)
82 84 Session().commit()
83 85
84 86 user_group = user_util.create_user_group()
85 87
86 88 user_group.users_group_name = xss_img
87 89 user_group.user_group_description = '<strong onload="alert();">DESC</strong>'
88 90
89 91 response = self.app.get(
90 92 route_path('user_groups_data'), extra_environ=xhr_header)
91 93
92 94 response.mustcontain(
93 95 '&lt;strong onload=&#34;alert();&#34;&gt;DESC&lt;/strong&gt;')
94 96 response.mustcontain(
95 97 '&lt;img src=&#34;/image1&#34; onload=&#34;'
96 98 'alert(&#39;Hello, World!&#39;);&#34;&gt;')
97 99
98 100 def test_edit_user_group_autocomplete_empty_members(self, xhr_header, user_util):
99 101 self.log_user()
100 102 ug = user_util.create_user_group()
101 103 response = self.app.get(
102 104 route_path('user_group_members_data', user_group_id=ug.users_group_id),
103 105 extra_environ=xhr_header)
104 106
105 107 assert response.json == {'members': []}
106 108
107 109 def test_edit_user_group_autocomplete_members(self, xhr_header, user_util):
108 110 self.log_user()
109 111 members = [u.user_id for u in User.get_all()]
110 112 ug = user_util.create_user_group(members=members)
111 113 response = self.app.get(
112 114 route_path('user_group_members_data',
113 115 user_group_id=ug.users_group_id),
114 116 extra_environ=xhr_header)
115 117
116 118 assert len(response.json['members']) == len(members)
117 119
118 120 def test_creation_page(self):
119 121 self.log_user()
120 122 self.app.get(route_path('user_groups_new'), status=200)
121 123
122 124 def test_create(self):
123 125 from rhodecode.lib import helpers as h
124 126
125 127 self.log_user()
126 128 users_group_name = 'test_user_group'
127 129 response = self.app.post(route_path('user_groups_create'), {
128 130 'users_group_name': users_group_name,
129 131 'user_group_description': 'DESC',
130 132 'active': True,
131 133 'csrf_token': self.csrf_token})
132 134
133 135 user_group_id = UserGroup.get_by_group_name(
134 136 users_group_name).users_group_id
135 137
136 138 user_group_link = h.link_to(
137 139 users_group_name,
138 140 route_path('edit_user_group', user_group_id=user_group_id))
139 141
140 142 assert_session_flash(
141 143 response,
142 144 'Created user group %s' % user_group_link)
143 145
144 146 fixture.destroy_user_group(users_group_name)
145 147
146 148 def test_create_with_empty_name(self):
147 149 self.log_user()
148 150
149 151 response = self.app.post(route_path('user_groups_create'), {
150 152 'users_group_name': '',
151 153 'user_group_description': 'DESC',
152 154 'active': True,
153 155 'csrf_token': self.csrf_token}, status=200)
154 156
155 157 response.mustcontain('Please enter a value')
156 158
157 159 def test_create_duplicate(self, user_util):
158 160 self.log_user()
159 161
160 162 user_group = user_util.create_user_group()
161 163 duplicate_name = user_group.users_group_name
162 164 response = self.app.post(route_path('user_groups_create'), {
163 165 'users_group_name': duplicate_name,
164 166 'user_group_description': 'DESC',
165 167 'active': True,
166 168 'csrf_token': self.csrf_token}, status=200)
167 169
168 170 response.mustcontain(
169 171 'User group `{}` already exists'.format(duplicate_name))
@@ -1,793 +1,795 b''
1 1
2 2 # Copyright (C) 2010-2020 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 32
33 33 fixture = Fixture()
34 34
35 35
36 36 def route_path(name, params=None, **kwargs):
37 import urllib.request, urllib.parse, urllib.error
37 import urllib.request
38 import urllib.parse
39 import urllib.error
38 40 from rhodecode.apps._base import ADMIN_PREFIX
39 41
40 42 base_url = {
41 43 'users':
42 44 ADMIN_PREFIX + '/users',
43 45 'users_data':
44 46 ADMIN_PREFIX + '/users_data',
45 47 'users_create':
46 48 ADMIN_PREFIX + '/users/create',
47 49 'users_new':
48 50 ADMIN_PREFIX + '/users/new',
49 51 'user_edit':
50 52 ADMIN_PREFIX + '/users/{user_id}/edit',
51 53 'user_edit_advanced':
52 54 ADMIN_PREFIX + '/users/{user_id}/edit/advanced',
53 55 'user_edit_global_perms':
54 56 ADMIN_PREFIX + '/users/{user_id}/edit/global_permissions',
55 57 'user_edit_global_perms_update':
56 58 ADMIN_PREFIX + '/users/{user_id}/edit/global_permissions/update',
57 59 'user_update':
58 60 ADMIN_PREFIX + '/users/{user_id}/update',
59 61 'user_delete':
60 62 ADMIN_PREFIX + '/users/{user_id}/delete',
61 63 'user_create_personal_repo_group':
62 64 ADMIN_PREFIX + '/users/{user_id}/create_repo_group',
63 65
64 66 'edit_user_auth_tokens':
65 67 ADMIN_PREFIX + '/users/{user_id}/edit/auth_tokens',
66 68 'edit_user_auth_tokens_add':
67 69 ADMIN_PREFIX + '/users/{user_id}/edit/auth_tokens/new',
68 70 'edit_user_auth_tokens_delete':
69 71 ADMIN_PREFIX + '/users/{user_id}/edit/auth_tokens/delete',
70 72
71 73 'edit_user_emails':
72 74 ADMIN_PREFIX + '/users/{user_id}/edit/emails',
73 75 'edit_user_emails_add':
74 76 ADMIN_PREFIX + '/users/{user_id}/edit/emails/new',
75 77 'edit_user_emails_delete':
76 78 ADMIN_PREFIX + '/users/{user_id}/edit/emails/delete',
77 79
78 80 'edit_user_ips':
79 81 ADMIN_PREFIX + '/users/{user_id}/edit/ips',
80 82 'edit_user_ips_add':
81 83 ADMIN_PREFIX + '/users/{user_id}/edit/ips/new',
82 84 'edit_user_ips_delete':
83 85 ADMIN_PREFIX + '/users/{user_id}/edit/ips/delete',
84 86
85 87 'edit_user_perms_summary':
86 88 ADMIN_PREFIX + '/users/{user_id}/edit/permissions_summary',
87 89 'edit_user_perms_summary_json':
88 90 ADMIN_PREFIX + '/users/{user_id}/edit/permissions_summary/json',
89 91
90 92 'edit_user_audit_logs':
91 93 ADMIN_PREFIX + '/users/{user_id}/edit/audit',
92 94
93 95 'edit_user_audit_logs_download':
94 96 ADMIN_PREFIX + '/users/{user_id}/edit/audit/download',
95 97
96 98 }[name].format(**kwargs)
97 99
98 100 if params:
99 101 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
100 102 return base_url
101 103
102 104
103 105 class TestAdminUsersView(TestController):
104 106
105 107 def test_show_users(self):
106 108 self.log_user()
107 109 self.app.get(route_path('users'))
108 110
109 111 def test_show_users_data(self, xhr_header):
110 112 self.log_user()
111 113 response = self.app.get(route_path(
112 114 'users_data'), extra_environ=xhr_header)
113 115
114 116 all_users = User.query().filter(
115 117 User.username != User.DEFAULT_USER).count()
116 118 assert response.json['recordsTotal'] == all_users
117 119
118 120 def test_show_users_data_filtered(self, xhr_header):
119 121 self.log_user()
120 122 response = self.app.get(route_path(
121 123 'users_data', params={'search[value]': 'empty_search'}),
122 124 extra_environ=xhr_header)
123 125
124 126 all_users = User.query().filter(
125 127 User.username != User.DEFAULT_USER).count()
126 128 assert response.json['recordsTotal'] == all_users
127 129 assert response.json['recordsFiltered'] == 0
128 130
129 131 def test_auth_tokens_default_user(self):
130 132 self.log_user()
131 133 user = User.get_default_user()
132 134 response = self.app.get(
133 135 route_path('edit_user_auth_tokens', user_id=user.user_id),
134 136 status=302)
135 137
136 138 def test_auth_tokens(self):
137 139 self.log_user()
138 140
139 141 user = User.get_by_username(TEST_USER_REGULAR_LOGIN)
140 142 user_id = user.user_id
141 143 auth_tokens = user.auth_tokens
142 144 response = self.app.get(
143 145 route_path('edit_user_auth_tokens', user_id=user_id))
144 146 for token in auth_tokens:
145 147 response.mustcontain(token[:4])
146 148 response.mustcontain('never')
147 149
148 150 @pytest.mark.parametrize("desc, lifetime", [
149 151 ('forever', -1),
150 152 ('5mins', 60*5),
151 153 ('30days', 60*60*24*30),
152 154 ])
153 155 def test_add_auth_token(self, desc, lifetime, user_util):
154 156 self.log_user()
155 157 user = user_util.create_user()
156 158 user_id = user.user_id
157 159
158 160 response = self.app.post(
159 161 route_path('edit_user_auth_tokens_add', user_id=user_id),
160 162 {'description': desc, 'lifetime': lifetime,
161 163 'csrf_token': self.csrf_token})
162 164 assert_session_flash(response, 'Auth token successfully created')
163 165
164 166 response = response.follow()
165 167 user = User.get(user_id)
166 168 for auth_token in user.auth_tokens:
167 169 response.mustcontain(auth_token[:4])
168 170
169 171 def test_delete_auth_token(self, user_util):
170 172 self.log_user()
171 173 user = user_util.create_user()
172 174 user_id = user.user_id
173 175 keys = user.auth_tokens
174 176 assert 2 == len(keys)
175 177
176 178 response = self.app.post(
177 179 route_path('edit_user_auth_tokens_add', user_id=user_id),
178 180 {'description': 'desc', 'lifetime': -1,
179 181 'csrf_token': self.csrf_token})
180 182 assert_session_flash(response, 'Auth token successfully created')
181 183 response.follow()
182 184
183 185 # now delete our key
184 186 keys = UserApiKeys.query().filter(UserApiKeys.user_id == user_id).all()
185 187 assert 3 == len(keys)
186 188
187 189 response = self.app.post(
188 190 route_path('edit_user_auth_tokens_delete', user_id=user_id),
189 191 {'del_auth_token': keys[0].user_api_key_id,
190 192 'csrf_token': self.csrf_token})
191 193
192 194 assert_session_flash(response, 'Auth token successfully deleted')
193 195 keys = UserApiKeys.query().filter(UserApiKeys.user_id == user_id).all()
194 196 assert 2 == len(keys)
195 197
196 198 def test_ips(self):
197 199 self.log_user()
198 200 user = User.get_by_username(TEST_USER_REGULAR_LOGIN)
199 201 response = self.app.get(route_path('edit_user_ips', user_id=user.user_id))
200 202 response.mustcontain('All IP addresses are allowed')
201 203
202 204 @pytest.mark.parametrize("test_name, ip, ip_range, failure", [
203 205 ('127/24', '127.0.0.1/24', '127.0.0.0 - 127.0.0.255', False),
204 206 ('10/32', '10.0.0.10/32', '10.0.0.10 - 10.0.0.10', False),
205 207 ('0/16', '0.0.0.0/16', '0.0.0.0 - 0.0.255.255', False),
206 208 ('0/8', '0.0.0.0/8', '0.0.0.0 - 0.255.255.255', False),
207 209 ('127_bad_mask', '127.0.0.1/99', '127.0.0.1 - 127.0.0.1', True),
208 210 ('127_bad_ip', 'foobar', 'foobar', True),
209 211 ])
210 212 def test_ips_add(self, user_util, test_name, ip, ip_range, failure):
211 213 self.log_user()
212 214 user = user_util.create_user(username=test_name)
213 215 user_id = user.user_id
214 216
215 217 response = self.app.post(
216 218 route_path('edit_user_ips_add', user_id=user_id),
217 219 params={'new_ip': ip, 'csrf_token': self.csrf_token})
218 220
219 221 if failure:
220 222 assert_session_flash(
221 223 response, 'Please enter a valid IPv4 or IpV6 address')
222 224 response = self.app.get(route_path('edit_user_ips', user_id=user_id))
223 225
224 226 response.mustcontain(no=[ip])
225 227 response.mustcontain(no=[ip_range])
226 228
227 229 else:
228 230 response = self.app.get(route_path('edit_user_ips', user_id=user_id))
229 231 response.mustcontain(ip)
230 232 response.mustcontain(ip_range)
231 233
232 234 def test_ips_delete(self, user_util):
233 235 self.log_user()
234 236 user = user_util.create_user()
235 237 user_id = user.user_id
236 238 ip = '127.0.0.1/32'
237 239 ip_range = '127.0.0.1 - 127.0.0.1'
238 240 new_ip = UserModel().add_extra_ip(user_id, ip)
239 241 Session().commit()
240 242 new_ip_id = new_ip.ip_id
241 243
242 244 response = self.app.get(route_path('edit_user_ips', user_id=user_id))
243 245 response.mustcontain(ip)
244 246 response.mustcontain(ip_range)
245 247
246 248 self.app.post(
247 249 route_path('edit_user_ips_delete', user_id=user_id),
248 250 params={'del_ip_id': new_ip_id, 'csrf_token': self.csrf_token})
249 251
250 252 response = self.app.get(route_path('edit_user_ips', user_id=user_id))
251 253 response.mustcontain('All IP addresses are allowed')
252 254 response.mustcontain(no=[ip])
253 255 response.mustcontain(no=[ip_range])
254 256
255 257 def test_emails(self):
256 258 self.log_user()
257 259 user = User.get_by_username(TEST_USER_REGULAR_LOGIN)
258 260 response = self.app.get(
259 261 route_path('edit_user_emails', user_id=user.user_id))
260 262 response.mustcontain('No additional emails specified')
261 263
262 264 def test_emails_add(self, user_util):
263 265 self.log_user()
264 266 user = user_util.create_user()
265 267 user_id = user.user_id
266 268
267 269 self.app.post(
268 270 route_path('edit_user_emails_add', user_id=user_id),
269 271 params={'new_email': 'example@rhodecode.com',
270 272 'csrf_token': self.csrf_token})
271 273
272 274 response = self.app.get(
273 275 route_path('edit_user_emails', user_id=user_id))
274 276 response.mustcontain('example@rhodecode.com')
275 277
276 278 def test_emails_add_existing_email(self, user_util, user_regular):
277 279 existing_email = user_regular.email
278 280
279 281 self.log_user()
280 282 user = user_util.create_user()
281 283 user_id = user.user_id
282 284
283 285 response = self.app.post(
284 286 route_path('edit_user_emails_add', user_id=user_id),
285 287 params={'new_email': existing_email,
286 288 'csrf_token': self.csrf_token})
287 289 assert_session_flash(
288 290 response, 'This e-mail address is already taken')
289 291
290 292 response = self.app.get(
291 293 route_path('edit_user_emails', user_id=user_id))
292 294 response.mustcontain(no=[existing_email])
293 295
294 296 def test_emails_delete(self, user_util):
295 297 self.log_user()
296 298 user = user_util.create_user()
297 299 user_id = user.user_id
298 300
299 301 self.app.post(
300 302 route_path('edit_user_emails_add', user_id=user_id),
301 303 params={'new_email': 'example@rhodecode.com',
302 304 'csrf_token': self.csrf_token})
303 305
304 306 response = self.app.get(
305 307 route_path('edit_user_emails', user_id=user_id))
306 308 response.mustcontain('example@rhodecode.com')
307 309
308 310 user_email = UserEmailMap.query()\
309 311 .filter(UserEmailMap.email == 'example@rhodecode.com') \
310 312 .filter(UserEmailMap.user_id == user_id)\
311 313 .one()
312 314
313 315 del_email_id = user_email.email_id
314 316 self.app.post(
315 317 route_path('edit_user_emails_delete', user_id=user_id),
316 318 params={'del_email_id': del_email_id,
317 319 'csrf_token': self.csrf_token})
318 320
319 321 response = self.app.get(
320 322 route_path('edit_user_emails', user_id=user_id))
321 323 response.mustcontain(no=['example@rhodecode.com'])
322 324
323 325 def test_create(self, request, xhr_header):
324 326 self.log_user()
325 327 username = 'newtestuser'
326 328 password = 'test12'
327 329 password_confirmation = password
328 330 name = 'name'
329 331 lastname = 'lastname'
330 332 email = 'mail@mail.com'
331 333
332 334 self.app.get(route_path('users_new'))
333 335
334 336 response = self.app.post(route_path('users_create'), params={
335 337 'username': username,
336 338 'password': password,
337 339 'description': 'mr CTO',
338 340 'password_confirmation': password_confirmation,
339 341 'firstname': name,
340 342 'active': True,
341 343 'lastname': lastname,
342 344 'extern_name': 'rhodecode',
343 345 'extern_type': 'rhodecode',
344 346 'email': email,
345 347 'csrf_token': self.csrf_token,
346 348 })
347 349 user_link = h.link_to(
348 350 username,
349 351 route_path(
350 352 'user_edit', user_id=User.get_by_username(username).user_id))
351 353 assert_session_flash(response, 'Created user %s' % (user_link,))
352 354
353 355 @request.addfinalizer
354 356 def cleanup():
355 357 fixture.destroy_user(username)
356 358 Session().commit()
357 359
358 360 new_user = User.query().filter(User.username == username).one()
359 361
360 362 assert new_user.username == username
361 363 assert auth.check_password(password, new_user.password)
362 364 assert new_user.name == name
363 365 assert new_user.lastname == lastname
364 366 assert new_user.email == email
365 367
366 368 response = self.app.get(route_path('users_data'),
367 369 extra_environ=xhr_header)
368 370 response.mustcontain(username)
369 371
370 372 def test_create_err(self):
371 373 self.log_user()
372 374 username = 'new_user'
373 375 password = ''
374 376 name = 'name'
375 377 lastname = 'lastname'
376 378 email = 'errmail.com'
377 379
378 380 self.app.get(route_path('users_new'))
379 381
380 382 response = self.app.post(route_path('users_create'), params={
381 383 'username': username,
382 384 'password': password,
383 385 'name': name,
384 386 'active': False,
385 387 'lastname': lastname,
386 388 'description': 'mr CTO',
387 389 'email': email,
388 390 'csrf_token': self.csrf_token,
389 391 })
390 392
391 393 msg = u'Username "%(username)s" is forbidden'
392 394 msg = h.html_escape(msg % {'username': 'new_user'})
393 395 response.mustcontain('<span class="error-message">%s</span>' % msg)
394 396 response.mustcontain(
395 397 '<span class="error-message">Please enter a value</span>')
396 398 response.mustcontain(
397 399 '<span class="error-message">An email address must contain a'
398 400 ' single @</span>')
399 401
400 402 def get_user():
401 403 Session().query(User).filter(User.username == username).one()
402 404
403 405 with pytest.raises(NoResultFound):
404 406 get_user()
405 407
406 408 def test_new(self):
407 409 self.log_user()
408 410 self.app.get(route_path('users_new'))
409 411
410 412 @pytest.mark.parametrize("name, attrs", [
411 413 ('firstname', {'firstname': 'new_username'}),
412 414 ('lastname', {'lastname': 'new_username'}),
413 415 ('admin', {'admin': True}),
414 416 ('admin', {'admin': False}),
415 417 ('extern_type', {'extern_type': 'ldap'}),
416 418 ('extern_type', {'extern_type': None}),
417 419 ('extern_name', {'extern_name': 'test'}),
418 420 ('extern_name', {'extern_name': None}),
419 421 ('active', {'active': False}),
420 422 ('active', {'active': True}),
421 423 ('email', {'email': 'some@email.com'}),
422 424 ('language', {'language': 'de'}),
423 425 ('language', {'language': 'en'}),
424 426 ('description', {'description': 'hello CTO'}),
425 427 # ('new_password', {'new_password': 'foobar123',
426 428 # 'password_confirmation': 'foobar123'})
427 429 ])
428 430 def test_update(self, name, attrs, user_util):
429 431 self.log_user()
430 432 usr = user_util.create_user(
431 433 password='qweqwe',
432 434 email='testme@rhodecode.org',
433 435 extern_type='rhodecode',
434 436 extern_name='xxx',
435 437 )
436 438 user_id = usr.user_id
437 439 Session().commit()
438 440
439 441 params = usr.get_api_data()
440 442 cur_lang = params['language'] or 'en'
441 443 params.update({
442 444 'password_confirmation': '',
443 445 'new_password': '',
444 446 'language': cur_lang,
445 447 'csrf_token': self.csrf_token,
446 448 })
447 449 params.update({'new_password': ''})
448 450 params.update(attrs)
449 451 if name == 'email':
450 452 params['emails'] = [attrs['email']]
451 453 elif name == 'extern_type':
452 454 # cannot update this via form, expected value is original one
453 455 params['extern_type'] = "rhodecode"
454 456 elif name == 'extern_name':
455 457 # cannot update this via form, expected value is original one
456 458 params['extern_name'] = 'xxx'
457 459 # special case since this user is not
458 460 # logged in yet his data is not filled
459 461 # so we use creation data
460 462
461 463 response = self.app.post(
462 464 route_path('user_update', user_id=usr.user_id), params)
463 465 assert response.status_int == 302
464 466 assert_session_flash(response, 'User updated successfully')
465 467
466 468 updated_user = User.get(user_id)
467 469 updated_params = updated_user.get_api_data()
468 470 updated_params.update({'password_confirmation': ''})
469 471 updated_params.update({'new_password': ''})
470 472
471 473 del params['csrf_token']
472 474 assert params == updated_params
473 475
474 476 def test_update_and_migrate_password(
475 477 self, autologin_user, real_crypto_backend, user_util):
476 478
477 479 user = user_util.create_user()
478 480 temp_user = user.username
479 481 user.password = auth._RhodeCodeCryptoSha256().hash_create(
480 482 b'test123')
481 483 Session().add(user)
482 484 Session().commit()
483 485
484 486 params = user.get_api_data()
485 487
486 488 params.update({
487 489 'password_confirmation': 'qweqwe123',
488 490 'new_password': 'qweqwe123',
489 491 'language': 'en',
490 492 'csrf_token': autologin_user.csrf_token,
491 493 })
492 494
493 495 response = self.app.post(
494 496 route_path('user_update', user_id=user.user_id), params)
495 497 assert response.status_int == 302
496 498 assert_session_flash(response, 'User updated successfully')
497 499
498 500 # new password should be bcrypted, after log-in and transfer
499 501 user = User.get_by_username(temp_user)
500 502 assert user.password.startswith('$')
501 503
502 504 updated_user = User.get_by_username(temp_user)
503 505 updated_params = updated_user.get_api_data()
504 506 updated_params.update({'password_confirmation': 'qweqwe123'})
505 507 updated_params.update({'new_password': 'qweqwe123'})
506 508
507 509 del params['csrf_token']
508 510 assert params == updated_params
509 511
510 512 def test_delete(self):
511 513 self.log_user()
512 514 username = 'newtestuserdeleteme'
513 515
514 516 fixture.create_user(name=username)
515 517
516 518 new_user = Session().query(User)\
517 519 .filter(User.username == username).one()
518 520 response = self.app.post(
519 521 route_path('user_delete', user_id=new_user.user_id),
520 522 params={'csrf_token': self.csrf_token})
521 523
522 524 assert_session_flash(response, 'Successfully deleted user `{}`'.format(username))
523 525
524 526 def test_delete_owner_of_repository(self, request, user_util):
525 527 self.log_user()
526 528 obj_name = 'test_repo'
527 529 usr = user_util.create_user()
528 530 username = usr.username
529 531 fixture.create_repo(obj_name, cur_user=usr.username)
530 532
531 533 new_user = Session().query(User)\
532 534 .filter(User.username == username).one()
533 535 response = self.app.post(
534 536 route_path('user_delete', user_id=new_user.user_id),
535 537 params={'csrf_token': self.csrf_token})
536 538
537 539 msg = 'user "%s" still owns 1 repositories and cannot be removed. ' \
538 540 'Switch owners or remove those repositories:%s' % (username, obj_name)
539 541 assert_session_flash(response, msg)
540 542 fixture.destroy_repo(obj_name)
541 543
542 544 def test_delete_owner_of_repository_detaching(self, request, user_util):
543 545 self.log_user()
544 546 obj_name = 'test_repo'
545 547 usr = user_util.create_user(auto_cleanup=False)
546 548 username = usr.username
547 549 fixture.create_repo(obj_name, cur_user=usr.username)
548 550 Session().commit()
549 551
550 552 new_user = Session().query(User)\
551 553 .filter(User.username == username).one()
552 554 response = self.app.post(
553 555 route_path('user_delete', user_id=new_user.user_id),
554 556 params={'user_repos': 'detach', 'csrf_token': self.csrf_token})
555 557
556 558 msg = 'Detached 1 repositories'
557 559 assert_session_flash(response, msg)
558 560 fixture.destroy_repo(obj_name)
559 561
560 562 def test_delete_owner_of_repository_deleting(self, request, user_util):
561 563 self.log_user()
562 564 obj_name = 'test_repo'
563 565 usr = user_util.create_user(auto_cleanup=False)
564 566 username = usr.username
565 567 fixture.create_repo(obj_name, cur_user=usr.username)
566 568
567 569 new_user = Session().query(User)\
568 570 .filter(User.username == username).one()
569 571 response = self.app.post(
570 572 route_path('user_delete', user_id=new_user.user_id),
571 573 params={'user_repos': 'delete', 'csrf_token': self.csrf_token})
572 574
573 575 msg = 'Deleted 1 repositories'
574 576 assert_session_flash(response, msg)
575 577
576 578 def test_delete_owner_of_repository_group(self, request, user_util):
577 579 self.log_user()
578 580 obj_name = 'test_group'
579 581 usr = user_util.create_user()
580 582 username = usr.username
581 583 fixture.create_repo_group(obj_name, cur_user=usr.username)
582 584
583 585 new_user = Session().query(User)\
584 586 .filter(User.username == username).one()
585 587 response = self.app.post(
586 588 route_path('user_delete', user_id=new_user.user_id),
587 589 params={'csrf_token': self.csrf_token})
588 590
589 591 msg = 'user "%s" still owns 1 repository groups and cannot be removed. ' \
590 592 'Switch owners or remove those repository groups:%s' % (username, obj_name)
591 593 assert_session_flash(response, msg)
592 594 fixture.destroy_repo_group(obj_name)
593 595
594 596 def test_delete_owner_of_repository_group_detaching(self, request, user_util):
595 597 self.log_user()
596 598 obj_name = 'test_group'
597 599 usr = user_util.create_user(auto_cleanup=False)
598 600 username = usr.username
599 601 fixture.create_repo_group(obj_name, cur_user=usr.username)
600 602
601 603 new_user = Session().query(User)\
602 604 .filter(User.username == username).one()
603 605 response = self.app.post(
604 606 route_path('user_delete', user_id=new_user.user_id),
605 607 params={'user_repo_groups': 'delete', 'csrf_token': self.csrf_token})
606 608
607 609 msg = 'Deleted 1 repository groups'
608 610 assert_session_flash(response, msg)
609 611
610 612 def test_delete_owner_of_repository_group_deleting(self, request, user_util):
611 613 self.log_user()
612 614 obj_name = 'test_group'
613 615 usr = user_util.create_user(auto_cleanup=False)
614 616 username = usr.username
615 617 fixture.create_repo_group(obj_name, cur_user=usr.username)
616 618
617 619 new_user = Session().query(User)\
618 620 .filter(User.username == username).one()
619 621 response = self.app.post(
620 622 route_path('user_delete', user_id=new_user.user_id),
621 623 params={'user_repo_groups': 'detach', 'csrf_token': self.csrf_token})
622 624
623 625 msg = 'Detached 1 repository groups'
624 626 assert_session_flash(response, msg)
625 627 fixture.destroy_repo_group(obj_name)
626 628
627 629 def test_delete_owner_of_user_group(self, request, user_util):
628 630 self.log_user()
629 631 obj_name = 'test_user_group'
630 632 usr = user_util.create_user()
631 633 username = usr.username
632 634 fixture.create_user_group(obj_name, cur_user=usr.username)
633 635
634 636 new_user = Session().query(User)\
635 637 .filter(User.username == username).one()
636 638 response = self.app.post(
637 639 route_path('user_delete', user_id=new_user.user_id),
638 640 params={'csrf_token': self.csrf_token})
639 641
640 642 msg = 'user "%s" still owns 1 user groups and cannot be removed. ' \
641 643 'Switch owners or remove those user groups:%s' % (username, obj_name)
642 644 assert_session_flash(response, msg)
643 645 fixture.destroy_user_group(obj_name)
644 646
645 647 def test_delete_owner_of_user_group_detaching(self, request, user_util):
646 648 self.log_user()
647 649 obj_name = 'test_user_group'
648 650 usr = user_util.create_user(auto_cleanup=False)
649 651 username = usr.username
650 652 fixture.create_user_group(obj_name, cur_user=usr.username)
651 653
652 654 new_user = Session().query(User)\
653 655 .filter(User.username == username).one()
654 656 try:
655 657 response = self.app.post(
656 658 route_path('user_delete', user_id=new_user.user_id),
657 659 params={'user_user_groups': 'detach',
658 660 'csrf_token': self.csrf_token})
659 661
660 662 msg = 'Detached 1 user groups'
661 663 assert_session_flash(response, msg)
662 664 finally:
663 665 fixture.destroy_user_group(obj_name)
664 666
665 667 def test_delete_owner_of_user_group_deleting(self, request, user_util):
666 668 self.log_user()
667 669 obj_name = 'test_user_group'
668 670 usr = user_util.create_user(auto_cleanup=False)
669 671 username = usr.username
670 672 fixture.create_user_group(obj_name, cur_user=usr.username)
671 673
672 674 new_user = Session().query(User)\
673 675 .filter(User.username == username).one()
674 676 response = self.app.post(
675 677 route_path('user_delete', user_id=new_user.user_id),
676 678 params={'user_user_groups': 'delete', 'csrf_token': self.csrf_token})
677 679
678 680 msg = 'Deleted 1 user groups'
679 681 assert_session_flash(response, msg)
680 682
681 683 def test_edit(self, user_util):
682 684 self.log_user()
683 685 user = user_util.create_user()
684 686 self.app.get(route_path('user_edit', user_id=user.user_id))
685 687
686 688 def test_edit_default_user_redirect(self):
687 689 self.log_user()
688 690 user = User.get_default_user()
689 691 self.app.get(route_path('user_edit', user_id=user.user_id), status=302)
690 692
691 693 @pytest.mark.parametrize(
692 694 'repo_create, repo_create_write, user_group_create, repo_group_create,'
693 695 'fork_create, inherit_default_permissions, expect_error,'
694 696 'expect_form_error', [
695 697 ('hg.create.none', 'hg.create.write_on_repogroup.false',
696 698 'hg.usergroup.create.false', 'hg.repogroup.create.false',
697 699 'hg.fork.none', 'hg.inherit_default_perms.false', False, False),
698 700 ('hg.create.repository', 'hg.create.write_on_repogroup.false',
699 701 'hg.usergroup.create.false', 'hg.repogroup.create.false',
700 702 'hg.fork.none', 'hg.inherit_default_perms.false', False, False),
701 703 ('hg.create.repository', 'hg.create.write_on_repogroup.true',
702 704 'hg.usergroup.create.true', 'hg.repogroup.create.true',
703 705 'hg.fork.repository', 'hg.inherit_default_perms.false', False,
704 706 False),
705 707 ('hg.create.XXX', 'hg.create.write_on_repogroup.true',
706 708 'hg.usergroup.create.true', 'hg.repogroup.create.true',
707 709 'hg.fork.repository', 'hg.inherit_default_perms.false', False,
708 710 True),
709 711 ('', '', '', '', '', '', True, False),
710 712 ])
711 713 def test_global_perms_on_user(
712 714 self, repo_create, repo_create_write, user_group_create,
713 715 repo_group_create, fork_create, expect_error, expect_form_error,
714 716 inherit_default_permissions, user_util):
715 717 self.log_user()
716 718 user = user_util.create_user()
717 719 uid = user.user_id
718 720
719 721 # ENABLE REPO CREATE ON A GROUP
720 722 perm_params = {
721 723 'inherit_default_permissions': False,
722 724 'default_repo_create': repo_create,
723 725 'default_repo_create_on_write': repo_create_write,
724 726 'default_user_group_create': user_group_create,
725 727 'default_repo_group_create': repo_group_create,
726 728 'default_fork_create': fork_create,
727 729 'default_inherit_default_permissions': inherit_default_permissions,
728 730 'csrf_token': self.csrf_token,
729 731 }
730 732 response = self.app.post(
731 733 route_path('user_edit_global_perms_update', user_id=uid),
732 734 params=perm_params)
733 735
734 736 if expect_form_error:
735 737 assert response.status_int == 200
736 738 response.mustcontain('Value must be one of')
737 739 else:
738 740 if expect_error:
739 741 msg = 'An error occurred during permissions saving'
740 742 else:
741 743 msg = 'User global permissions updated successfully'
742 744 ug = User.get(uid)
743 745 del perm_params['inherit_default_permissions']
744 746 del perm_params['csrf_token']
745 747 assert perm_params == ug.get_default_perms()
746 748 assert_session_flash(response, msg)
747 749
748 750 def test_global_permissions_initial_values(self, user_util):
749 751 self.log_user()
750 752 user = user_util.create_user()
751 753 uid = user.user_id
752 754 response = self.app.get(
753 755 route_path('user_edit_global_perms', user_id=uid))
754 756 default_user = User.get_default_user()
755 757 default_permissions = default_user.get_default_perms()
756 758 assert_response = response.assert_response()
757 759 expected_permissions = (
758 760 'default_repo_create', 'default_repo_create_on_write',
759 761 'default_fork_create', 'default_repo_group_create',
760 762 'default_user_group_create', 'default_inherit_default_permissions')
761 763 for permission in expected_permissions:
762 764 css_selector = '[name={}][checked=checked]'.format(permission)
763 765 element = assert_response.get_element(css_selector)
764 766 assert element.value == default_permissions[permission]
765 767
766 768 def test_perms_summary_page(self):
767 769 user = self.log_user()
768 770 response = self.app.get(
769 771 route_path('edit_user_perms_summary', user_id=user['user_id']))
770 772 for repo in Repository.query().all():
771 773 response.mustcontain(repo.repo_name)
772 774
773 775 def test_perms_summary_page_json(self):
774 776 user = self.log_user()
775 777 response = self.app.get(
776 778 route_path('edit_user_perms_summary_json', user_id=user['user_id']))
777 779 for repo in Repository.query().all():
778 780 response.mustcontain(repo.repo_name)
779 781
780 782 def test_audit_log_page(self):
781 783 user = self.log_user()
782 784 self.app.get(
783 785 route_path('edit_user_audit_logs', user_id=user['user_id']))
784 786
785 787 def test_audit_log_page_download(self):
786 788 user = self.log_user()
787 789 user_id = user['user_id']
788 790 response = self.app.get(
789 791 route_path('edit_user_audit_logs_download', user_id=user_id))
790 792
791 793 assert response.content_disposition == \
792 794 'attachment; filename=user_{}_audit_logs.json'.format(user_id)
793 795 assert response.content_type == "application/json"
@@ -1,175 +1,177 b''
1 1
2 2 # Copyright (C) 2010-2020 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 26
27 27 fixture = Fixture()
28 28
29 29
30 30 def route_path(name, params=None, **kwargs):
31 import urllib.request, urllib.parse, urllib.error
31 import urllib.request
32 import urllib.parse
33 import urllib.error
32 34 from rhodecode.apps._base import ADMIN_PREFIX
33 35
34 36 base_url = {
35 37 'edit_user_ssh_keys':
36 38 ADMIN_PREFIX + '/users/{user_id}/edit/ssh_keys',
37 39 'edit_user_ssh_keys_generate_keypair':
38 40 ADMIN_PREFIX + '/users/{user_id}/edit/ssh_keys/generate',
39 41 'edit_user_ssh_keys_add':
40 42 ADMIN_PREFIX + '/users/{user_id}/edit/ssh_keys/new',
41 43 'edit_user_ssh_keys_delete':
42 44 ADMIN_PREFIX + '/users/{user_id}/edit/ssh_keys/delete',
43 45
44 46 }[name].format(**kwargs)
45 47
46 48 if params:
47 49 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
48 50 return base_url
49 51
50 52
51 53 class TestAdminUsersSshKeysView(TestController):
52 54 INVALID_KEY = """\
53 55 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDk+77sjDzVeB6vevJsuZds1iNU5
54 56 LANOa5CU5G/9JYIA6RYsWWMO7mbsR82IUckdqOHmxSykfR1D1TdluyIpQLrwgH5kb
55 57 n8FkVI8zBMCKakxowvN67B0R7b1BT4PPzW2JlOXei/m9W12ZY484VTow6/B+kf2Q8
56 58 cP8tmCJmKWZma5Em7OTUhvjyQVNz3v7HfeY5Hq0Ci4ECJ59hepFDabJvtAXg9XrI6
57 59 jvdphZTc30I4fG8+hBHzpeFxUGvSGNtXPUbwaAY8j/oHYrTpMgkj6pUEFsiKfC5zP
58 60 qPFR5HyKTCHW0nFUJnZsbyFT5hMiF/hZkJc9A0ZbdSvJwCRQ/g3bmdL
59 61 your_email@example.com
60 62 """
61 63 VALID_KEY = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDk+77sjDzVeB6vev' \
62 64 'JsuZds1iNU5LANOa5CU5G/9JYIA6RYsWWMO7mbsR82IUckdqOHmxSy' \
63 65 'kfR1D1TdluyIpQLrwgH5kbn8FkVI8zBMCKakxowvN67B0R7b1BT4PP' \
64 66 'zW2JlOXei/m9W12ZY484VTow6/B+kf2Q8cP8tmCJmKWZma5Em7OTUh' \
65 67 'vjyQVNz3v7HfeY5Hq0Ci4ECJ59hepFDabJvtAXg9XrI6jvdphZTc30' \
66 68 'I4fG8+hBHzpeFxUGvSGNtXPUbwaAY8j/oHYrTpMgkj6pUEFsiKfC5zPq' \
67 69 'PFR5HyKTCHW0nFUJnZsbyFT5hMiF/hZkJc9A0ZbdSvJwCRQ/g3bmdL ' \
68 70 'your_email@example.com'
69 71 FINGERPRINT = 'MD5:01:4f:ad:29:22:6e:01:37:c9:d2:52:26:52:b0:2d:93'
70 72
71 73 def test_ssh_keys_default_user(self):
72 74 self.log_user()
73 75 user = User.get_default_user()
74 76 self.app.get(
75 77 route_path('edit_user_ssh_keys', user_id=user.user_id),
76 78 status=302)
77 79
78 80 def test_add_ssh_key_error(self, user_util):
79 81 self.log_user()
80 82 user = user_util.create_user()
81 83 user_id = user.user_id
82 84
83 85 key_data = self.INVALID_KEY
84 86
85 87 desc = 'MY SSH KEY'
86 88 response = self.app.post(
87 89 route_path('edit_user_ssh_keys_add', user_id=user_id),
88 90 {'description': desc, 'key_data': key_data,
89 91 'csrf_token': self.csrf_token})
90 92 assert_session_flash(response, 'An error occurred during ssh '
91 93 'key saving: Unable to decode the key')
92 94
93 95 def test_ssh_key_duplicate(self, user_util):
94 96 self.log_user()
95 97 user = user_util.create_user()
96 98 user_id = user.user_id
97 99
98 100 key_data = self.VALID_KEY
99 101
100 102 desc = 'MY SSH KEY'
101 103 response = self.app.post(
102 104 route_path('edit_user_ssh_keys_add', user_id=user_id),
103 105 {'description': desc, 'key_data': key_data,
104 106 'csrf_token': self.csrf_token})
105 107 assert_session_flash(response, 'Ssh Key successfully created')
106 108 response.follow() # flush session flash
107 109
108 110 # add the same key AGAIN
109 111 desc = 'MY SSH KEY'
110 112 response = self.app.post(
111 113 route_path('edit_user_ssh_keys_add', user_id=user_id),
112 114 {'description': desc, 'key_data': key_data,
113 115 'csrf_token': self.csrf_token})
114 116
115 117 err = 'Such key with fingerprint `{}` already exists, ' \
116 118 'please use a different one'.format(self.FINGERPRINT)
117 119 assert_session_flash(response, 'An error occurred during ssh key '
118 120 'saving: {}'.format(err))
119 121
120 122 def test_add_ssh_key(self, user_util):
121 123 self.log_user()
122 124 user = user_util.create_user()
123 125 user_id = user.user_id
124 126
125 127 key_data = self.VALID_KEY
126 128
127 129 desc = 'MY SSH KEY'
128 130 response = self.app.post(
129 131 route_path('edit_user_ssh_keys_add', user_id=user_id),
130 132 {'description': desc, 'key_data': key_data,
131 133 'csrf_token': self.csrf_token})
132 134 assert_session_flash(response, 'Ssh Key successfully created')
133 135
134 136 response = response.follow()
135 137 response.mustcontain(desc)
136 138
137 139 def test_delete_ssh_key(self, user_util):
138 140 self.log_user()
139 141 user = user_util.create_user()
140 142 user_id = user.user_id
141 143
142 144 key_data = self.VALID_KEY
143 145
144 146 desc = 'MY SSH KEY'
145 147 response = self.app.post(
146 148 route_path('edit_user_ssh_keys_add', user_id=user_id),
147 149 {'description': desc, 'key_data': key_data,
148 150 'csrf_token': self.csrf_token})
149 151 assert_session_flash(response, 'Ssh Key successfully created')
150 152 response = response.follow() # flush the Session flash
151 153
152 154 # now delete our key
153 155 keys = UserSshKeys.query().filter(UserSshKeys.user_id == user_id).all()
154 156 assert 1 == len(keys)
155 157
156 158 response = self.app.post(
157 159 route_path('edit_user_ssh_keys_delete', user_id=user_id),
158 160 {'del_ssh_key': keys[0].ssh_key_id,
159 161 'csrf_token': self.csrf_token})
160 162
161 163 assert_session_flash(response, 'Ssh key successfully deleted')
162 164 keys = UserSshKeys.query().filter(UserSshKeys.user_id == user_id).all()
163 165 assert 0 == len(keys)
164 166
165 167 def test_generate_keypair(self, user_util):
166 168 self.log_user()
167 169 user = user_util.create_user()
168 170 user_id = user.user_id
169 171
170 172 response = self.app.get(
171 173 route_path('edit_user_ssh_keys_generate_keypair', user_id=user_id))
172 174
173 175 response.mustcontain('Private key')
174 176 response.mustcontain('Public key')
175 177 response.mustcontain('-----BEGIN PRIVATE KEY-----')
@@ -1,260 +1,262 b''
1 1
2 2 # Copyright (C) 2010-2020 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 import os
20 20 import pytest
21 21
22 22 from rhodecode.lib.ext_json import json
23 23 from rhodecode.model.auth_token import AuthTokenModel
24 24 from rhodecode.model.db import Session, FileStore, Repository, User
25 25 from rhodecode.tests import TestController
26 26 from rhodecode.apps.file_store import utils, config_keys
27 27
28 28
29 29 def route_path(name, params=None, **kwargs):
30 import urllib.request, urllib.parse, urllib.error
30 import urllib.request
31 import urllib.parse
32 import urllib.error
31 33
32 34 base_url = {
33 35 'upload_file': '/_file_store/upload',
34 36 'download_file': '/_file_store/download/{fid}',
35 37 'download_file_by_token': '/_file_store/token-download/{_auth_token}/{fid}'
36 38
37 39 }[name].format(**kwargs)
38 40
39 41 if params:
40 42 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
41 43 return base_url
42 44
43 45
44 46 class TestFileStoreViews(TestController):
45 47
46 48 @pytest.mark.parametrize("fid, content, exists", [
47 49 ('abcde-0.jpg', "xxxxx", True),
48 50 ('abcde-0.exe', "1234567", True),
49 51 ('abcde-0.jpg', "xxxxx", False),
50 52 ])
51 53 def test_get_files_from_store(self, fid, content, exists, tmpdir, user_util):
52 54 user = self.log_user()
53 55 user_id = user['user_id']
54 56 repo_id = user_util.create_repo().repo_id
55 57 store_path = self.app._pyramid_settings[config_keys.store_path]
56 58 store_uid = fid
57 59
58 60 if exists:
59 61 status = 200
60 62 store = utils.get_file_storage({config_keys.store_path: store_path})
61 63 filesystem_file = os.path.join(str(tmpdir), fid)
62 with open(filesystem_file, 'wb') as f:
64 with open(filesystem_file, 'wt') as f:
63 65 f.write(content)
64 66
65 67 with open(filesystem_file, 'rb') as f:
66 68 store_uid, metadata = store.save_file(f, fid, extra_metadata={'filename': fid})
67 69
68 70 entry = FileStore.create(
69 71 file_uid=store_uid, filename=metadata["filename"],
70 72 file_hash=metadata["sha256"], file_size=metadata["size"],
71 73 file_display_name='file_display_name',
72 74 file_description='repo artifact `{}`'.format(metadata["filename"]),
73 75 check_acl=True, user_id=user_id,
74 76 scope_repo_id=repo_id
75 77 )
76 78 Session().add(entry)
77 79 Session().commit()
78 80
79 81 else:
80 82 status = 404
81 83
82 84 response = self.app.get(route_path('download_file', fid=store_uid), status=status)
83 85
84 86 if exists:
85 87 assert response.text == content
86 88 file_store_path = os.path.dirname(store.resolve_name(store_uid, store_path)[1])
87 89 metadata_file = os.path.join(file_store_path, store_uid + '.meta')
88 90 assert os.path.exists(metadata_file)
89 91 with open(metadata_file, 'rb') as f:
90 92 json_data = json.loads(f.read())
91 93
92 94 assert json_data
93 95 assert 'size' in json_data
94 96
95 97 def test_upload_files_without_content_to_store(self):
96 98 self.log_user()
97 99 response = self.app.post(
98 100 route_path('upload_file'),
99 101 params={'csrf_token': self.csrf_token},
100 102 status=200)
101 103
102 104 assert response.json == {
103 105 u'error': u'store_file data field is missing',
104 106 u'access_path': None,
105 107 u'store_fid': None}
106 108
107 109 def test_upload_files_bogus_content_to_store(self):
108 110 self.log_user()
109 111 response = self.app.post(
110 112 route_path('upload_file'),
111 113 params={'csrf_token': self.csrf_token, 'store_file': 'bogus'},
112 114 status=200)
113 115
114 116 assert response.json == {
115 117 u'error': u'filename cannot be read from the data field',
116 118 u'access_path': None,
117 119 u'store_fid': None}
118 120
119 121 def test_upload_content_to_store(self):
120 122 self.log_user()
121 123 response = self.app.post(
122 124 route_path('upload_file'),
123 upload_files=[('store_file', 'myfile.txt', 'SOME CONTENT')],
125 upload_files=[('store_file', b'myfile.txt', b'SOME CONTENT')],
124 126 params={'csrf_token': self.csrf_token},
125 127 status=200)
126 128
127 129 assert response.json['store_fid']
128 130
129 131 @pytest.fixture()
130 132 def create_artifact_factory(self, tmpdir):
131 133 def factory(user_id, content):
132 134 store_path = self.app._pyramid_settings[config_keys.store_path]
133 135 store = utils.get_file_storage({config_keys.store_path: store_path})
134 136 fid = 'example.txt'
135 137
136 138 filesystem_file = os.path.join(str(tmpdir), fid)
137 with open(filesystem_file, 'wb') as f:
139 with open(filesystem_file, 'wt') as f:
138 140 f.write(content)
139 141
140 142 with open(filesystem_file, 'rb') as f:
141 143 store_uid, metadata = store.save_file(f, fid, extra_metadata={'filename': fid})
142 144
143 145 entry = FileStore.create(
144 146 file_uid=store_uid, filename=metadata["filename"],
145 147 file_hash=metadata["sha256"], file_size=metadata["size"],
146 148 file_display_name='file_display_name',
147 149 file_description='repo artifact `{}`'.format(metadata["filename"]),
148 150 check_acl=True, user_id=user_id,
149 151 )
150 152 Session().add(entry)
151 153 Session().commit()
152 154 return entry
153 155 return factory
154 156
155 157 def test_download_file_non_scoped(self, user_util, create_artifact_factory):
156 158 user = self.log_user()
157 159 user_id = user['user_id']
158 160 content = 'HELLO MY NAME IS ARTIFACT !'
159 161
160 162 artifact = create_artifact_factory(user_id, content)
161 163 file_uid = artifact.file_uid
162 164 response = self.app.get(route_path('download_file', fid=file_uid), status=200)
163 165 assert response.text == content
164 166
165 167 # log-in to new user and test download again
166 168 user = user_util.create_user(password='qweqwe')
167 169 self.log_user(user.username, 'qweqwe')
168 170 response = self.app.get(route_path('download_file', fid=file_uid), status=200)
169 171 assert response.text == content
170 172
171 173 def test_download_file_scoped_to_repo(self, user_util, create_artifact_factory):
172 174 user = self.log_user()
173 175 user_id = user['user_id']
174 176 content = 'HELLO MY NAME IS ARTIFACT !'
175 177
176 178 artifact = create_artifact_factory(user_id, content)
177 179 # bind to repo
178 180 repo = user_util.create_repo()
179 181 repo_id = repo.repo_id
180 182 artifact.scope_repo_id = repo_id
181 183 Session().add(artifact)
182 184 Session().commit()
183 185
184 186 file_uid = artifact.file_uid
185 187 response = self.app.get(route_path('download_file', fid=file_uid), status=200)
186 188 assert response.text == content
187 189
188 190 # log-in to new user and test download again
189 191 user = user_util.create_user(password='qweqwe')
190 192 self.log_user(user.username, 'qweqwe')
191 193 response = self.app.get(route_path('download_file', fid=file_uid), status=200)
192 194 assert response.text == content
193 195
194 196 # forbid user the rights to repo
195 197 repo = Repository.get(repo_id)
196 198 user_util.grant_user_permission_to_repo(repo, user, 'repository.none')
197 199 self.app.get(route_path('download_file', fid=file_uid), status=404)
198 200
199 201 def test_download_file_scoped_to_user(self, user_util, create_artifact_factory):
200 202 user = self.log_user()
201 203 user_id = user['user_id']
202 204 content = 'HELLO MY NAME IS ARTIFACT !'
203 205
204 206 artifact = create_artifact_factory(user_id, content)
205 207 # bind to user
206 208 user = user_util.create_user(password='qweqwe')
207 209
208 210 artifact.scope_user_id = user.user_id
209 211 Session().add(artifact)
210 212 Session().commit()
211 213
212 214 # artifact creator doesn't have access since it's bind to another user
213 215 file_uid = artifact.file_uid
214 216 self.app.get(route_path('download_file', fid=file_uid), status=404)
215 217
216 218 # log-in to new user and test download again, should be ok since we're bind to this artifact
217 219 self.log_user(user.username, 'qweqwe')
218 220 response = self.app.get(route_path('download_file', fid=file_uid), status=200)
219 221 assert response.text == content
220 222
221 223 def test_download_file_scoped_to_repo_with_bad_token(self, user_util, create_artifact_factory):
222 224 user_id = User.get_first_super_admin().user_id
223 225 content = 'HELLO MY NAME IS ARTIFACT !'
224 226
225 227 artifact = create_artifact_factory(user_id, content)
226 228 # bind to repo
227 229 repo = user_util.create_repo()
228 230 repo_id = repo.repo_id
229 231 artifact.scope_repo_id = repo_id
230 232 Session().add(artifact)
231 233 Session().commit()
232 234
233 235 file_uid = artifact.file_uid
234 236 self.app.get(route_path('download_file_by_token',
235 237 _auth_token='bogus', fid=file_uid), status=302)
236 238
237 239 def test_download_file_scoped_to_repo_with_token(self, user_util, create_artifact_factory):
238 240 user = User.get_first_super_admin()
239 241 AuthTokenModel().create(user, 'test artifact token',
240 242 role=AuthTokenModel.cls.ROLE_ARTIFACT_DOWNLOAD)
241 243
242 244 user = User.get_first_super_admin()
243 245 artifact_token = user.artifact_token
244 246
245 247 user_id = User.get_first_super_admin().user_id
246 248 content = 'HELLO MY NAME IS ARTIFACT !'
247 249
248 250 artifact = create_artifact_factory(user_id, content)
249 251 # bind to repo
250 252 repo = user_util.create_repo()
251 253 repo_id = repo.repo_id
252 254 artifact.scope_repo_id = repo_id
253 255 Session().add(artifact)
254 256 Session().commit()
255 257
256 258 file_uid = artifact.file_uid
257 259 response = self.app.get(
258 260 route_path('download_file_by_token',
259 261 _auth_token=artifact_token, fid=file_uid), status=200)
260 262 assert response.text == content
@@ -1,390 +1,390 b''
1 1
2 2 # Copyright (C) 2010-2020 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 from rhodecode.lib import helpers as h
24 24 from rhodecode.model.db import User, Gist
25 25 from rhodecode.model.gist import GistModel
26 26 from rhodecode.model.meta import Session
27 27 from rhodecode.tests import (
28 28 TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS,
29 29 TestController, assert_session_flash)
30 30
31 31
32 32 def route_path(name, params=None, **kwargs):
33 import urllib.request, urllib.parse, urllib.error
33 import urllib.parse
34 import urllib.error
34 35 from rhodecode.apps._base import ADMIN_PREFIX
35 36
36 37 base_url = {
37 38 'gists_show': ADMIN_PREFIX + '/gists',
38 39 'gists_new': ADMIN_PREFIX + '/gists/new',
39 40 'gists_create': ADMIN_PREFIX + '/gists/create',
40 41 'gist_show': ADMIN_PREFIX + '/gists/{gist_id}',
41 42 'gist_delete': ADMIN_PREFIX + '/gists/{gist_id}/delete',
42 43 'gist_edit': ADMIN_PREFIX + '/gists/{gist_id}/edit',
43 44 'gist_edit_check_revision': ADMIN_PREFIX + '/gists/{gist_id}/edit/check_revision',
44 45 'gist_update': ADMIN_PREFIX + '/gists/{gist_id}/update',
45 46 'gist_show_rev': ADMIN_PREFIX + '/gists/{gist_id}/rev/{revision}',
46 47 'gist_show_formatted': ADMIN_PREFIX + '/gists/{gist_id}/rev/{revision}/{format}',
47 48 'gist_show_formatted_path': ADMIN_PREFIX + '/gists/{gist_id}/rev/{revision}/{format}/{f_path}',
48 49
49 50 }[name].format(**kwargs)
50 51
51 52 if params:
52 53 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
53 54 return base_url
54 55
55 56
56 57 class GistUtility(object):
57 58
58 59 def __init__(self):
59 60 self._gist_ids = []
60 61
61 62 def __call__(
62 self, f_name, content='some gist', lifetime=-1,
63 self, f_name: bytes, content: bytes = b'some gist', lifetime=-1,
63 64 description='gist-desc', gist_type='public',
64 65 acl_level=Gist.GIST_PUBLIC, owner=TEST_USER_ADMIN_LOGIN):
65 66 gist_mapping = {
66 67 f_name: {'content': content}
67 68 }
68 69 user = User.get_by_username(owner)
69 70 gist = GistModel().create(
70 71 description, owner=user, gist_mapping=gist_mapping,
71 72 gist_type=gist_type, lifetime=lifetime, gist_acl_level=acl_level)
72 73 Session().commit()
73 74 self._gist_ids.append(gist.gist_id)
74 75 return gist
75 76
76 77 def cleanup(self):
77 78 for gist_id in self._gist_ids:
78 79 gist = Gist.get(gist_id)
79 80 if gist:
80 81 Session().delete(gist)
81 82
82 83 Session().commit()
83 84
84 85
85 86 @pytest.fixture()
86 87 def create_gist(request):
87 88 gist_utility = GistUtility()
88 89 request.addfinalizer(gist_utility.cleanup)
89 90 return gist_utility
90 91
91 92
92 93 class TestGistsController(TestController):
93 94
94 95 def test_index_empty(self, create_gist):
95 96 self.log_user()
96 97 response = self.app.get(route_path('gists_show'))
97 response.mustcontain('data: [],')
98 response.mustcontain('var gist_data = [];')
98 99
99 100 def test_index(self, create_gist):
100 101 self.log_user()
101 g1 = create_gist('gist1')
102 g2 = create_gist('gist2', lifetime=1400)
103 g3 = create_gist('gist3', description='gist3-desc')
104 g4 = create_gist('gist4', gist_type='private').gist_access_id
102 g1 = create_gist(b'gist1')
103 g2 = create_gist(b'gist2', lifetime=1400)
104 g3 = create_gist(b'gist3', description='gist3-desc')
105 g4 = create_gist(b'gist4', gist_type='private').gist_access_id
105 106 response = self.app.get(route_path('gists_show'))
106 107
107 108 response.mustcontain(g1.gist_access_id)
108 109 response.mustcontain(g2.gist_access_id)
109 110 response.mustcontain(g3.gist_access_id)
110 111 response.mustcontain('gist3-desc')
111 112 response.mustcontain(no=[g4])
112 113
113 114 # Expiration information should be visible
114 expires_tag = '%s' % h.age_component(
115 h.time_to_utcdatetime(g2.gist_expires))
115 expires_tag = str(h.age_component(h.time_to_utcdatetime(g2.gist_expires)))
116 116 response.mustcontain(expires_tag.replace('"', '\\"'))
117 117
118 118 def test_index_private_gists(self, create_gist):
119 119 self.log_user()
120 gist = create_gist('gist5', gist_type='private')
120 gist = create_gist(b'gist5', gist_type='private')
121 121 response = self.app.get(route_path('gists_show', params=dict(private=1)))
122 122
123 123 # and privates
124 124 response.mustcontain(gist.gist_access_id)
125 125
126 126 def test_index_show_all(self, create_gist):
127 127 self.log_user()
128 create_gist('gist1')
129 create_gist('gist2', lifetime=1400)
130 create_gist('gist3', description='gist3-desc')
131 create_gist('gist4', gist_type='private')
128 create_gist(b'gist1')
129 create_gist(b'gist2', lifetime=1400)
130 create_gist(b'gist3', description='gist3-desc')
131 create_gist(b'gist4', gist_type='private')
132 132
133 133 response = self.app.get(route_path('gists_show', params=dict(all=1)))
134 134
135 135 assert len(GistModel.get_all()) == 4
136 136 # and privates
137 137 for gist in GistModel.get_all():
138 138 response.mustcontain(gist.gist_access_id)
139 139
140 140 def test_index_show_all_hidden_from_regular(self, create_gist):
141 141 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
142 create_gist('gist2', gist_type='private')
143 create_gist('gist3', gist_type='private')
144 create_gist('gist4', gist_type='private')
142 create_gist(b'gist2', gist_type='private')
143 create_gist(b'gist3', gist_type='private')
144 create_gist(b'gist4', gist_type='private')
145 145
146 146 response = self.app.get(route_path('gists_show', params=dict(all=1)))
147 147
148 148 assert len(GistModel.get_all()) == 3
149 149 # since we don't have access to private in this view, we
150 150 # should see nothing
151 151 for gist in GistModel.get_all():
152 152 response.mustcontain(no=[gist.gist_access_id])
153 153
154 154 def test_create(self):
155 155 self.log_user()
156 156 response = self.app.post(
157 157 route_path('gists_create'),
158 158 params={'lifetime': -1,
159 159 'content': 'gist test',
160 160 'filename': 'foo',
161 161 'gist_type': 'public',
162 162 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
163 163 'csrf_token': self.csrf_token},
164 164 status=302)
165 165 response = response.follow()
166 166 response.mustcontain('added file: foo')
167 167 response.mustcontain('gist test')
168 168
169 169 def test_create_with_path_with_dirs(self):
170 170 self.log_user()
171 171 response = self.app.post(
172 172 route_path('gists_create'),
173 173 params={'lifetime': -1,
174 174 'content': 'gist test',
175 175 'filename': '/home/foo',
176 176 'gist_type': 'public',
177 177 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
178 178 'csrf_token': self.csrf_token},
179 179 status=200)
180 180 response.mustcontain('Filename /home/foo cannot be inside a directory')
181 181
182 182 def test_access_expired_gist(self, create_gist):
183 183 self.log_user()
184 gist = create_gist('never-see-me')
184 gist = create_gist(b'never-see-me')
185 185 gist.gist_expires = 0 # 1970
186 186 Session().add(gist)
187 187 Session().commit()
188 188
189 189 self.app.get(route_path('gist_show', gist_id=gist.gist_access_id),
190 190 status=404)
191 191
192 192 def test_create_private(self):
193 193 self.log_user()
194 194 response = self.app.post(
195 195 route_path('gists_create'),
196 196 params={'lifetime': -1,
197 197 'content': 'private gist test',
198 198 'filename': 'private-foo',
199 199 'gist_type': 'private',
200 200 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
201 201 'csrf_token': self.csrf_token},
202 202 status=302)
203 203 response = response.follow()
204 204 response.mustcontain('added file: private-foo<')
205 205 response.mustcontain('private gist test')
206 206 response.mustcontain('Private Gist')
207 207 # Make sure private gists are not indexed by robots
208 208 response.mustcontain(
209 209 '<meta name="robots" content="noindex, nofollow">')
210 210
211 211 def test_create_private_acl_private(self):
212 212 self.log_user()
213 213 response = self.app.post(
214 214 route_path('gists_create'),
215 215 params={'lifetime': -1,
216 216 'content': 'private gist test',
217 217 'filename': 'private-foo',
218 218 'gist_type': 'private',
219 219 'gist_acl_level': Gist.ACL_LEVEL_PRIVATE,
220 220 'csrf_token': self.csrf_token},
221 221 status=302)
222 222 response = response.follow()
223 223 response.mustcontain('added file: private-foo<')
224 224 response.mustcontain('private gist test')
225 225 response.mustcontain('Private Gist')
226 226 # Make sure private gists are not indexed by robots
227 227 response.mustcontain(
228 228 '<meta name="robots" content="noindex, nofollow">')
229 229
230 230 def test_create_with_description(self):
231 231 self.log_user()
232 232 response = self.app.post(
233 233 route_path('gists_create'),
234 234 params={'lifetime': -1,
235 235 'content': 'gist test',
236 236 'filename': 'foo-desc',
237 237 'description': 'gist-desc',
238 238 'gist_type': 'public',
239 239 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
240 240 'csrf_token': self.csrf_token},
241 241 status=302)
242 242 response = response.follow()
243 243 response.mustcontain('added file: foo-desc')
244 244 response.mustcontain('gist test')
245 245 response.mustcontain('gist-desc')
246 246
247 247 def test_create_public_with_anonymous_access(self):
248 248 self.log_user()
249 249 params = {
250 250 'lifetime': -1,
251 251 'content': 'gist test',
252 252 'filename': 'foo-desc',
253 253 'description': 'gist-desc',
254 254 'gist_type': 'public',
255 255 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
256 256 'csrf_token': self.csrf_token
257 257 }
258 258 response = self.app.post(
259 259 route_path('gists_create'), params=params, status=302)
260 260 self.logout_user()
261 261 response = response.follow()
262 262 response.mustcontain('added file: foo-desc')
263 263 response.mustcontain('gist test')
264 264 response.mustcontain('gist-desc')
265 265
266 266 def test_new(self):
267 267 self.log_user()
268 268 self.app.get(route_path('gists_new'))
269 269
270 270 def test_delete(self, create_gist):
271 271 self.log_user()
272 gist = create_gist('delete-me')
272 gist = create_gist(b'delete-me')
273 273 response = self.app.post(
274 274 route_path('gist_delete', gist_id=gist.gist_id),
275 275 params={'csrf_token': self.csrf_token})
276 276 assert_session_flash(response, 'Deleted gist %s' % gist.gist_id)
277 277
278 278 def test_delete_normal_user_his_gist(self, create_gist):
279 279 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
280 gist = create_gist('delete-me', owner=TEST_USER_REGULAR_LOGIN)
280 gist = create_gist(b'delete-me', owner=TEST_USER_REGULAR_LOGIN)
281 281
282 282 response = self.app.post(
283 283 route_path('gist_delete', gist_id=gist.gist_id),
284 284 params={'csrf_token': self.csrf_token})
285 285 assert_session_flash(response, 'Deleted gist %s' % gist.gist_id)
286 286
287 287 def test_delete_normal_user_not_his_own_gist(self, create_gist):
288 288 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
289 gist = create_gist('delete-me-2')
289 gist = create_gist(b'delete-me-2')
290 290
291 291 self.app.post(
292 292 route_path('gist_delete', gist_id=gist.gist_id),
293 293 params={'csrf_token': self.csrf_token}, status=404)
294 294
295 295 def test_show(self, create_gist):
296 gist = create_gist('gist-show-me')
296 gist = create_gist(b'gist-show-me')
297 297 response = self.app.get(route_path('gist_show', gist_id=gist.gist_access_id))
298 298
299 299 response.mustcontain('added file: gist-show-me<')
300 300
301 301 assert_response = response.assert_response()
302 302 assert_response.element_equals_to(
303 303 'div.rc-user span.user',
304 304 '<a href="/_profiles/test_admin">test_admin</a>')
305 305
306 306 response.mustcontain('gist-desc')
307 307
308 308 def test_show_without_hg(self, create_gist):
309 309 with mock.patch(
310 310 'rhodecode.lib.vcs.settings.ALIASES', ['git']):
311 gist = create_gist('gist-show-me-again')
311 gist = create_gist(b'gist-show-me-again')
312 312 self.app.get(
313 313 route_path('gist_show', gist_id=gist.gist_access_id), status=200)
314 314
315 315 def test_show_acl_private(self, create_gist):
316 gist = create_gist('gist-show-me-only-when-im-logged-in',
316 gist = create_gist(b'gist-show-me-only-when-im-logged-in',
317 317 acl_level=Gist.ACL_LEVEL_PRIVATE)
318 318 self.app.get(
319 319 route_path('gist_show', gist_id=gist.gist_access_id), status=404)
320 320
321 321 # now we log-in we should see thi gist
322 322 self.log_user()
323 323 response = self.app.get(
324 324 route_path('gist_show', gist_id=gist.gist_access_id))
325 325 response.mustcontain('added file: gist-show-me-only-when-im-logged-in')
326 326
327 327 assert_response = response.assert_response()
328 328 assert_response.element_equals_to(
329 329 'div.rc-user span.user',
330 330 '<a href="/_profiles/test_admin">test_admin</a>')
331 331 response.mustcontain('gist-desc')
332 332
333 333 def test_show_as_raw(self, create_gist):
334 gist = create_gist('gist-show-me', content='GIST CONTENT')
334 gist = create_gist(b'gist-show-me', content=b'GIST CONTENT')
335 335 response = self.app.get(
336 336 route_path('gist_show_formatted',
337 337 gist_id=gist.gist_access_id, revision='tip',
338 338 format='raw'))
339 339 assert response.text == 'GIST CONTENT'
340 340
341 341 def test_show_as_raw_individual_file(self, create_gist):
342 gist = create_gist('gist-show-me-raw', content='GIST BODY')
342 gist = create_gist(b'gist-show-me-raw', content=b'GIST BODY')
343 343 response = self.app.get(
344 344 route_path('gist_show_formatted_path',
345 345 gist_id=gist.gist_access_id, format='raw',
346 346 revision='tip', f_path='gist-show-me-raw'))
347 347 assert response.text == 'GIST BODY'
348 348
349 349 def test_edit_page(self, create_gist):
350 350 self.log_user()
351 gist = create_gist('gist-for-edit', content='GIST EDIT BODY')
351 gist = create_gist(b'gist-for-edit', content=b'GIST EDIT BODY')
352 352 response = self.app.get(route_path('gist_edit', gist_id=gist.gist_access_id))
353 353 response.mustcontain('GIST EDIT BODY')
354 354
355 355 def test_edit_page_non_logged_user(self, create_gist):
356 gist = create_gist('gist-for-edit', content='GIST EDIT BODY')
356 gist = create_gist(b'gist-for-edit', content=b'GIST EDIT BODY')
357 357 self.app.get(route_path('gist_edit', gist_id=gist.gist_access_id),
358 358 status=302)
359 359
360 360 def test_edit_normal_user_his_gist(self, create_gist):
361 361 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
362 gist = create_gist('gist-for-edit', owner=TEST_USER_REGULAR_LOGIN)
362 gist = create_gist(b'gist-for-edit', owner=TEST_USER_REGULAR_LOGIN)
363 363 self.app.get(route_path('gist_edit', gist_id=gist.gist_access_id,
364 364 status=200))
365 365
366 366 def test_edit_normal_user_not_his_own_gist(self, create_gist):
367 367 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
368 gist = create_gist('delete-me')
368 gist = create_gist(b'delete-me')
369 369 self.app.get(route_path('gist_edit', gist_id=gist.gist_access_id),
370 370 status=404)
371 371
372 372 def test_user_first_name_is_escaped(self, user_util, create_gist):
373 373 xss_atack_string = '"><script>alert(\'First Name\')</script>'
374 374 xss_escaped_string = h.html_escape(h.escape(xss_atack_string))
375 375 password = 'test'
376 376 user = user_util.create_user(
377 377 firstname=xss_atack_string, password=password)
378 create_gist('gist', gist_type='public', owner=user.username)
378 create_gist(b'gist', gist_type='public', owner=user.username)
379 379 response = self.app.get(route_path('gists_show'))
380 380 response.mustcontain(xss_escaped_string)
381 381
382 382 def test_user_last_name_is_escaped(self, user_util, create_gist):
383 383 xss_atack_string = '"><script>alert(\'Last Name\')</script>'
384 384 xss_escaped_string = h.html_escape(h.escape(xss_atack_string))
385 385 password = 'test'
386 386 user = user_util.create_user(
387 387 lastname=xss_atack_string, password=password)
388 create_gist('gist', gist_type='public', owner=user.username)
388 create_gist(b'gist', gist_type='public', owner=user.username)
389 389 response = self.app.get(route_path('gists_show'))
390 390 response.mustcontain(xss_escaped_string)
@@ -1,95 +1,97 b''
1 1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 import json
22
23 21 from . import assert_and_get_repo_list_content
24 22 from rhodecode.tests import TestController
25 23 from rhodecode.tests.fixture import Fixture
26 24 from rhodecode.model.db import Repository
25 from rhodecode.lib.ext_json import json
26
27 27
28 28 fixture = Fixture()
29 29
30 30
31 31 def route_path(name, params=None, **kwargs):
32 import urllib.request, urllib.parse, urllib.error
32 import urllib.request
33 import urllib.parse
34 import urllib.error
33 35
34 36 base_url = {
35 37 'repo_list_data': '/_repos',
36 38 }[name].format(**kwargs)
37 39
38 40 if params:
39 41 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
40 42 return base_url
41 43
42 44
43 45 class TestRepoListData(TestController):
44 46
45 47 def test_returns_list_of_repos_and_groups(self, xhr_header):
46 48 self.log_user()
47 49
48 50 response = self.app.get(
49 51 route_path('repo_list_data'),
50 52 extra_environ=xhr_header, status=200)
51 53 result = json.loads(response.body)['results']
52 54
53 55 repos = assert_and_get_repo_list_content(result)
54 56
55 57 assert len(repos) == len(Repository.get_all())
56 58
57 59 def test_returns_list_of_repos_and_groups_filtered(self, xhr_header):
58 60 self.log_user()
59 61
60 62 response = self.app.get(
61 63 route_path('repo_list_data'),
62 64 params={'query': 'vcs_test_git'},
63 65 extra_environ=xhr_header, status=200)
64 66 result = json.loads(response.body)['results']
65 67
66 68 repos = assert_and_get_repo_list_content(result)
67 69
68 70 assert len(repos) == len(Repository.query().filter(
69 71 Repository.repo_name.ilike('%vcs_test_git%')).all())
70 72
71 73 def test_returns_list_of_repos_and_groups_filtered_with_type(self, xhr_header):
72 74 self.log_user()
73 75
74 76 response = self.app.get(
75 77 route_path('repo_list_data'),
76 78 params={'query': 'vcs_test_git', 'repo_type': 'git'},
77 79 extra_environ=xhr_header, status=200)
78 80 result = json.loads(response.body)['results']
79 81
80 82 repos = assert_and_get_repo_list_content(result)
81 83
82 84 assert len(repos) == len(Repository.query().filter(
83 85 Repository.repo_name.ilike('%vcs_test_git%')).all())
84 86
85 87 def test_returns_list_of_repos_non_ascii_query(self, xhr_header):
86 88 self.log_user()
87 89 response = self.app.get(
88 90 route_path('repo_list_data'),
89 91 params={'query': 'ć_vcs_test_ą', 'repo_type': 'git'},
90 92 extra_environ=xhr_header, status=200)
91 93 result = json.loads(response.body)['results']
92 94
93 95 repos = assert_and_get_repo_list_content(result)
94 96
95 97 assert len(repos) == 0
@@ -1,112 +1,112 b''
1 1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import json
22 20 import pytest
23 21
24 22 from rhodecode.tests import TestController
25 23 from rhodecode.tests.fixture import Fixture
26
24 from rhodecode.lib.ext_json import json
27 25
28 26 fixture = Fixture()
29 27
30 28
31 29 def route_path(name, params=None, **kwargs):
32 import urllib.request, urllib.parse, urllib.error
30 import urllib.request
31 import urllib.parse
32 import urllib.error
33 33
34 34 base_url = {
35 35 'user_autocomplete_data': '/_users',
36 36 'user_group_autocomplete_data': '/_user_groups'
37 37 }[name].format(**kwargs)
38 38
39 39 if params:
40 40 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
41 41 return base_url
42 42
43 43
44 44 class TestUserAutocompleteData(TestController):
45 45
46 46 def test_returns_list_of_users(self, user_util, xhr_header):
47 47 self.log_user()
48 48 user = user_util.create_user(active=True)
49 49 user_name = user.username
50 50 response = self.app.get(
51 51 route_path('user_autocomplete_data'),
52 52 extra_environ=xhr_header, status=200)
53 53 result = json.loads(response.body)
54 54 values = [suggestion['value'] for suggestion in result['suggestions']]
55 55 assert user_name in values
56 56
57 57 def test_returns_inactive_users_when_active_flag_sent(
58 58 self, user_util, xhr_header):
59 59 self.log_user()
60 60 user = user_util.create_user(active=False)
61 61 user_name = user.username
62 62
63 63 response = self.app.get(
64 64 route_path('user_autocomplete_data',
65 65 params=dict(user_groups='true', active='0')),
66 66 extra_environ=xhr_header, status=200)
67 67 result = json.loads(response.body)
68 68 values = [suggestion['value'] for suggestion in result['suggestions']]
69 69 assert user_name in values
70 70
71 71 response = self.app.get(
72 72 route_path('user_autocomplete_data',
73 73 params=dict(user_groups='true', active='1')),
74 74 extra_environ=xhr_header, status=200)
75 75 result = json.loads(response.body)
76 76 values = [suggestion['value'] for suggestion in result['suggestions']]
77 77 assert user_name not in values
78 78
79 79 def test_returns_groups_when_user_groups_flag_sent(
80 80 self, user_util, xhr_header):
81 81 self.log_user()
82 82 group = user_util.create_user_group(user_groups_active=True)
83 83 group_name = group.users_group_name
84 84 response = self.app.get(
85 85 route_path('user_autocomplete_data',
86 86 params=dict(user_groups='true')),
87 87 extra_environ=xhr_header, status=200)
88 88 result = json.loads(response.body)
89 89 values = [suggestion['value'] for suggestion in result['suggestions']]
90 90 assert group_name in values
91 91
92 92 @pytest.mark.parametrize('query, count', [
93 93 ('hello1', 0),
94 94 ('dev', 2),
95 95 ])
96 96 def test_result_is_limited_when_query_is_sent(self, user_util, xhr_header,
97 97 query, count):
98 98 self.log_user()
99 99
100 100 user_util._test_name = 'dev-test'
101 101 user_util.create_user()
102 102
103 103 user_util._test_name = 'dev-group-test'
104 104 user_util.create_user_group()
105 105
106 106 response = self.app.get(
107 107 route_path('user_autocomplete_data',
108 108 params=dict(user_groups='true', query=query)),
109 109 extra_environ=xhr_header, status=200)
110 110
111 111 result = json.loads(response.body)
112 112 assert len(result['suggestions']) == count
@@ -1,116 +1,118 b''
1 1
2 # Copyright (C) 2016-2020 RhodeCode GmbH
3 #
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
7 #
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
12 #
13 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
16 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # -*- coding: utf-8 -*-
20 2
21 3 # Copyright (C) 2016-2020 RhodeCode GmbH
22 4 #
23 5 # This program is free software: you can redistribute it and/or modify
24 6 # it under the terms of the GNU Affero General Public License, version 3
25 7 # (only), as published by the Free Software Foundation.
26 8 #
27 9 # This program is distributed in the hope that it will be useful,
28 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
29 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
30 12 # GNU General Public License for more details.
31 13 #
32 14 # You should have received a copy of the GNU Affero General Public License
33 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
34 16 #
35 17 # This program is dual-licensed. If you wish to learn more about the
36 18 # RhodeCode Enterprise Edition, including its added features, Support services,
37 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
38 20
39 import json
21
22 # Copyright (C) 2016-2020 RhodeCode GmbH
23 #
24 # This program is free software: you can redistribute it and/or modify
25 # it under the terms of the GNU Affero General Public License, version 3
26 # (only), as published by the Free Software Foundation.
27 #
28 # This program is distributed in the hope that it will be useful,
29 # but WITHOUT ANY WARRANTY; without even the implied warranty of
30 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
31 # GNU General Public License for more details.
32 #
33 # You should have received a copy of the GNU Affero General Public License
34 # along with this program. If not, see <http://www.gnu.org/licenses/>.
35 #
36 # This program is dual-licensed. If you wish to learn more about the
37 # RhodeCode Enterprise Edition, including its added features, Support services,
38 # and proprietary license terms, please see https://rhodecode.com/licenses/
40 39
41 40 import pytest
42 41
43 42 from rhodecode.tests import TestController
44 43 from rhodecode.tests.fixture import Fixture
44 from rhodecode.lib.ext_json import json
45 45
46 46
47 47 fixture = Fixture()
48 48
49 49
50 50 def route_path(name, params=None, **kwargs):
51 import urllib.request, urllib.parse, urllib.error
51 import urllib.request
52 import urllib.parse
53 import urllib.error
52 54
53 55 base_url = {
54 56 'user_autocomplete_data': '/_users',
55 57 'user_group_autocomplete_data': '/_user_groups'
56 58 }[name].format(**kwargs)
57 59
58 60 if params:
59 61 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
60 62 return base_url
61 63
62 64
63 65 class TestUserGroupAutocompleteData(TestController):
64 66
65 67 def test_returns_list_of_user_groups(self, user_util, xhr_header):
66 68 self.log_user()
67 69 user_group = user_util.create_user_group(active=True)
68 70 user_group_name = user_group.users_group_name
69 71 response = self.app.get(
70 72 route_path('user_group_autocomplete_data'),
71 73 extra_environ=xhr_header, status=200)
72 74 result = json.loads(response.body)
73 75 values = [suggestion['value'] for suggestion in result['suggestions']]
74 76 assert user_group_name in values
75 77
76 78 def test_returns_inactive_user_groups_when_active_flag_sent(
77 79 self, user_util, xhr_header):
78 80 self.log_user()
79 81 user_group = user_util.create_user_group(active=False)
80 82 user_group_name = user_group.users_group_name
81 83
82 84 response = self.app.get(
83 85 route_path('user_group_autocomplete_data',
84 86 params=dict(active='0')),
85 87 extra_environ=xhr_header, status=200)
86 88 result = json.loads(response.body)
87 89 values = [suggestion['value'] for suggestion in result['suggestions']]
88 90 assert user_group_name in values
89 91
90 92 response = self.app.get(
91 93 route_path('user_group_autocomplete_data',
92 94 params=dict(active='1')),
93 95 extra_environ=xhr_header, status=200)
94 96 result = json.loads(response.body)
95 97 values = [suggestion['value'] for suggestion in result['suggestions']]
96 98 assert user_group_name not in values
97 99
98 100 @pytest.mark.parametrize('query, count', [
99 101 ('hello1', 0),
100 102 ('dev', 1),
101 103 ])
102 104 def test_result_is_limited_when_query_is_sent(self, user_util, xhr_header, query, count):
103 105 self.log_user()
104 106
105 107 user_util._test_name = 'dev-test'
106 108 user_util.create_user_group()
107 109
108 110 response = self.app.get(
109 111 route_path('user_group_autocomplete_data',
110 112 params=dict(user_groups='true',
111 113 query=query)),
112 114 extra_environ=xhr_header, status=200)
113 115
114 116 result = json.loads(response.body)
115 117
116 118 assert len(result['suggestions']) == count
@@ -1,178 +1,178 b''
1 1
2 2 # Copyright (C) 2010-2020 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
21 21 import pytest
22 22
23 23 import rhodecode
24 24 from rhodecode.model.db import Repository, RepoGroup, User
25 25 from rhodecode.model.meta import Session
26 26 from rhodecode.model.repo import RepoModel
27 27 from rhodecode.model.repo_group import RepoGroupModel
28 28 from rhodecode.model.settings import SettingsModel
29 29 from rhodecode.tests import TestController
30 30 from rhodecode.tests.fixture import Fixture
31 31 from rhodecode.lib import helpers as h
32 32
33 33 fixture = Fixture()
34 34
35 35
36 36 def route_path(name, **kwargs):
37 37 return {
38 38 'home': '/',
39 39 'main_page_repos_data': '/_home_repos',
40 40 'main_page_repo_groups_data': '/_home_repo_groups',
41 41 'repo_group_home': '/{repo_group_name}'
42 42 }[name].format(**kwargs)
43 43
44 44
45 45 class TestHomeController(TestController):
46 46
47 47 def test_index(self):
48 48 self.log_user()
49 49 response = self.app.get(route_path('home'))
50 50 # if global permission is set
51 51 response.mustcontain('New Repository')
52 52
53 53 def test_index_grid_repos(self, xhr_header):
54 54 self.log_user()
55 55 response = self.app.get(route_path('main_page_repos_data'), extra_environ=xhr_header)
56 56 # search for objects inside the JavaScript JSON
57 57 for obj in Repository.getAll():
58 58 response.mustcontain('<a href=\\"/{}\\">'.format(obj.repo_name))
59 59
60 60 def test_index_grid_repo_groups(self, xhr_header):
61 61 self.log_user()
62 62 response = self.app.get(route_path('main_page_repo_groups_data'),
63 63 extra_environ=xhr_header,)
64 64
65 65 # search for objects inside the JavaScript JSON
66 66 for obj in RepoGroup.getAll():
67 67 response.mustcontain('<a href=\\"/{}\\">'.format(obj.group_name))
68 68
69 69 def test_index_grid_repo_groups_without_access(self, xhr_header, user_util):
70 70 user = user_util.create_user(password='qweqwe')
71 71 group_ok = user_util.create_repo_group(owner=user)
72 72 group_id_ok = group_ok.group_id
73 73
74 74 group_forbidden = user_util.create_repo_group(owner=User.get_first_super_admin())
75 75 group_id_forbidden = group_forbidden.group_id
76 76
77 77 user_util.grant_user_permission_to_repo_group(group_forbidden, user, 'group.none')
78 78 self.log_user(user.username, 'qweqwe')
79 79
80 80 self.app.get(route_path('main_page_repo_groups_data'),
81 81 extra_environ=xhr_header,
82 82 params={'repo_group_id': group_id_ok}, status=200)
83 83
84 84 self.app.get(route_path('main_page_repo_groups_data'),
85 85 extra_environ=xhr_header,
86 86 params={'repo_group_id': group_id_forbidden}, status=404)
87 87
88 88 def test_index_contains_statics_with_ver(self):
89 89 from rhodecode.lib.base import calculate_version_hash
90 90
91 91 self.log_user()
92 92 response = self.app.get(route_path('home'))
93 93
94 94 rhodecode_version_hash = calculate_version_hash(
95 95 {'beaker.session.secret': 'test-rc-uytcxaz'})
96 96 response.mustcontain('style.css?ver={0}'.format(rhodecode_version_hash))
97 97 response.mustcontain('scripts.min.js?ver={0}'.format(rhodecode_version_hash))
98 98
99 99 def test_index_contains_backend_specific_details(self, backend, xhr_header):
100 100 self.log_user()
101 101 response = self.app.get(route_path('main_page_repos_data'), extra_environ=xhr_header)
102 102 tip = backend.repo.get_commit().raw_id
103 103
104 104 # html in javascript variable:
105 105 response.mustcontain(r'<i class=\"icon-%s\"' % (backend.alias, ))
106 106 response.mustcontain(r'href=\"/%s\"' % (backend.repo_name, ))
107 107
108 108 response.mustcontain("""/%s/changeset/%s""" % (backend.repo_name, tip))
109 109 response.mustcontain("""Added a symlink""")
110 110
111 111 def test_index_with_anonymous_access_disabled(self):
112 112 with fixture.anon_access(False):
113 113 response = self.app.get(route_path('home'), status=302)
114 114 assert 'login' in response.location
115 115
116 116 def test_index_page_on_groups_with_wrong_group_id(self, autologin_user, xhr_header):
117 117 group_id = 918123
118 118 self.app.get(
119 119 route_path('main_page_repo_groups_data'),
120 120 params={'repo_group_id': group_id},
121 121 status=404, extra_environ=xhr_header)
122 122
123 123 def test_index_page_on_groups(self, autologin_user, user_util, xhr_header):
124 124 gr = user_util.create_repo_group()
125 125 repo = user_util.create_repo(parent=gr)
126 126 repo_name = repo.repo_name
127 127 group_id = gr.group_id
128 128
129 129 response = self.app.get(route_path(
130 130 'repo_group_home', repo_group_name=gr.group_name))
131 131 response.mustcontain('d.repo_group_id = {}'.format(group_id))
132 132
133 133 response = self.app.get(
134 134 route_path('main_page_repos_data'),
135 135 params={'repo_group_id': group_id},
136 136 extra_environ=xhr_header,)
137 137 response.mustcontain(repo_name)
138 138
139 139 def test_index_page_on_group_with_trailing_slash(self, autologin_user, user_util, xhr_header):
140 140 gr = user_util.create_repo_group()
141 141 repo = user_util.create_repo(parent=gr)
142 142 repo_name = repo.repo_name
143 143 group_id = gr.group_id
144 144
145 145 response = self.app.get(route_path(
146 146 'repo_group_home', repo_group_name=gr.group_name+'/'))
147 147 response.mustcontain('d.repo_group_id = {}'.format(group_id))
148 148
149 149 response = self.app.get(
150 150 route_path('main_page_repos_data'),
151 151 params={'repo_group_id': group_id},
152 152 extra_environ=xhr_header, )
153 153 response.mustcontain(repo_name)
154 154
155 155 @pytest.mark.parametrize("name, state", [
156 156 ('Disabled', False),
157 157 ('Enabled', True),
158 158 ])
159 159 def test_index_show_version(self, autologin_user, name, state):
160 160 version_string = 'RhodeCode %s' % rhodecode.__version__
161 161
162 162 sett = SettingsModel().create_or_update_setting(
163 163 'show_version', state, 'bool')
164 164 Session().add(sett)
165 165 Session().commit()
166 SettingsModel().invalidate_settings_cache()
166 SettingsModel().invalidate_settings_cache(hard=True)
167 167
168 168 response = self.app.get(route_path('home'))
169 169 if state is True:
170 170 response.mustcontain(version_string)
171 171 if state is False:
172 172 response.mustcontain(no=[version_string])
173 173
174 174 def test_logout_form_contains_csrf(self, autologin_user, csrf_token):
175 175 response = self.app.get(route_path('home'))
176 176 assert_response = response.assert_response()
177 177 element = assert_response.get_element('.logout [name=csrf_token]')
178 178 assert element.value == csrf_token
@@ -1,105 +1,107 b''
1 1
2 2 # Copyright (C) 2010-2020 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 25 from rhodecode.tests import TestController
26 26 from rhodecode.model.db import UserFollowing, Repository
27 27
28 28
29 29 def route_path(name, params=None, **kwargs):
30 import urllib.request, urllib.parse, urllib.error
30 import urllib.request
31 import urllib.parse
32 import urllib.error
31 33
32 34 base_url = {
33 35 'journal': ADMIN_PREFIX + '/journal',
34 36 'journal_rss': ADMIN_PREFIX + '/journal/rss',
35 37 'journal_atom': ADMIN_PREFIX + '/journal/atom',
36 38 'journal_public': ADMIN_PREFIX + '/public_journal',
37 39 'journal_public_atom': ADMIN_PREFIX + '/public_journal/atom',
38 40 'journal_public_atom_old': ADMIN_PREFIX + '/public_journal_atom',
39 41 'journal_public_rss': ADMIN_PREFIX + '/public_journal/rss',
40 42 'journal_public_rss_old': ADMIN_PREFIX + '/public_journal_rss',
41 43 'toggle_following': ADMIN_PREFIX + '/toggle_following',
42 44 }[name].format(**kwargs)
43 45
44 46 if params:
45 47 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
46 48 return base_url
47 49
48 50
49 51 class TestJournalViews(TestController):
50 52
51 53 def test_journal(self):
52 54 self.log_user()
53 55 response = self.app.get(route_path('journal'))
54 56 # response.mustcontain(
55 57 # """<div class="journal_day">%s</div>""" % datetime.date.today())
56 58
57 59 @pytest.mark.parametrize("feed_type, content_type", [
58 60 ('rss', "application/rss+xml"),
59 61 ('atom', "application/atom+xml")
60 62 ])
61 63 def test_journal_feed(self, feed_type, content_type):
62 64 self.log_user()
63 65 response = self.app.get(
64 66 route_path(
65 67 'journal_{}'.format(feed_type)),
66 68 status=200)
67 69
68 70 assert response.content_type == content_type
69 71
70 72 def test_toggle_following_repository(self, backend):
71 73 user = self.log_user()
72 74 repo = Repository.get_by_repo_name(backend.repo_name)
73 75 repo_id = repo.repo_id
74 76 self.app.post(
75 77 route_path('toggle_following'), {'follows_repo_id': repo_id,
76 78 'csrf_token': self.csrf_token})
77 79
78 80 followings = UserFollowing.query()\
79 81 .filter(UserFollowing.user_id == user['user_id'])\
80 82 .filter(UserFollowing.follows_repo_id == repo_id).all()
81 83
82 84 assert len(followings) == 0
83 85
84 86 self.app.post(
85 87 route_path('toggle_following'), {'follows_repo_id': repo_id,
86 88 'csrf_token': self.csrf_token})
87 89
88 90 followings = UserFollowing.query()\
89 91 .filter(UserFollowing.user_id == user['user_id'])\
90 92 .filter(UserFollowing.follows_repo_id == repo_id).all()
91 93
92 94 assert len(followings) == 1
93 95
94 96 @pytest.mark.parametrize("feed_type, content_type", [
95 97 ('rss', "application/rss+xml"),
96 98 ('atom', "application/atom+xml")
97 99 ])
98 100 def test_public_journal_feed(self, feed_type, content_type):
99 101 self.log_user()
100 102 response = self.app.get(
101 103 route_path(
102 104 'journal_public_{}'.format(feed_type)),
103 105 status=200)
104 106
105 107 assert response.content_type == content_type
@@ -1,579 +1,608 b''
1 1
2 2 # Copyright (C) 2010-2020 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.parse
21 21
22 22 import mock
23 23 import pytest
24 24
25 25 from rhodecode.tests import (
26 26 assert_session_flash, HG_REPO, TEST_USER_ADMIN_LOGIN,
27 27 no_newline_id_generator)
28 28 from rhodecode.tests.fixture import Fixture
29 29 from rhodecode.lib.auth import check_password
30 30 from rhodecode.lib import helpers as h
31 31 from rhodecode.model.auth_token import AuthTokenModel
32 32 from rhodecode.model.db import User, Notification, UserApiKeys
33 33 from rhodecode.model.meta import Session
34 34
35 35 fixture = Fixture()
36 36
37 37 whitelist_view = ['RepoCommitsView:repo_commit_raw']
38 38
39 39
40 40 def route_path(name, params=None, **kwargs):
41 import urllib.request, urllib.parse, urllib.error
41 import urllib.request
42 import urllib.parse
43 import urllib.error
42 44 from rhodecode.apps._base import ADMIN_PREFIX
43 45
44 46 base_url = {
45 47 'login': ADMIN_PREFIX + '/login',
46 48 'logout': ADMIN_PREFIX + '/logout',
47 49 'register': ADMIN_PREFIX + '/register',
48 50 'reset_password':
49 51 ADMIN_PREFIX + '/password_reset',
50 52 'reset_password_confirmation':
51 53 ADMIN_PREFIX + '/password_reset_confirmation',
52 54
53 55 'admin_permissions_application':
54 56 ADMIN_PREFIX + '/permissions/application',
55 57 'admin_permissions_application_update':
56 58 ADMIN_PREFIX + '/permissions/application/update',
57 59
58 60 'repo_commit_raw': '/{repo_name}/raw-changeset/{commit_id}'
59 61
60 62 }[name].format(**kwargs)
61 63
62 64 if params:
63 65 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
64 66 return base_url
65 67
66 68
67 69 @pytest.mark.usefixtures('app')
68 70 class TestLoginController(object):
69 71 destroy_users = set()
70 72
71 73 @classmethod
72 74 def teardown_class(cls):
73 75 fixture.destroy_users(cls.destroy_users)
74 76
75 77 def teardown_method(self, method):
76 78 for n in Notification.query().all():
77 79 Session().delete(n)
78 80
79 81 Session().commit()
80 82 assert Notification.query().all() == []
81 83
82 84 def test_index(self):
83 85 response = self.app.get(route_path('login'))
84 86 assert response.status == '200 OK'
85 87 # Test response...
86 88
87 89 def test_login_admin_ok(self):
88 90 response = self.app.post(route_path('login'),
89 91 {'username': 'test_admin',
90 92 'password': 'test12'}, status=302)
91 93 response = response.follow()
92 94 session = response.get_session_from_response()
93 95 username = session['rhodecode_user'].get('username')
94 96 assert username == 'test_admin'
95 97 response.mustcontain('logout')
96 98
97 99 def test_login_regular_ok(self):
98 100 response = self.app.post(route_path('login'),
99 101 {'username': 'test_regular',
100 102 'password': 'test12'}, status=302)
101 103
102 104 response = response.follow()
103 105 session = response.get_session_from_response()
104 106 username = session['rhodecode_user'].get('username')
105 107 assert username == 'test_regular'
106 108 response.mustcontain('logout')
107 109
108 110 def test_login_regular_forbidden_when_super_admin_restriction(self):
109 111 from rhodecode.authentication.plugins.auth_rhodecode import RhodeCodeAuthPlugin
110 112 with fixture.auth_restriction(self.app._pyramid_registry,
111 113 RhodeCodeAuthPlugin.AUTH_RESTRICTION_SUPER_ADMIN):
112 114 response = self.app.post(route_path('login'),
113 115 {'username': 'test_regular',
114 116 'password': 'test12'})
115 117
116 118 response.mustcontain('invalid user name')
117 119 response.mustcontain('invalid password')
118 120
119 121 def test_login_regular_forbidden_when_scope_restriction(self):
120 122 from rhodecode.authentication.plugins.auth_rhodecode import RhodeCodeAuthPlugin
121 123 with fixture.scope_restriction(self.app._pyramid_registry,
122 124 RhodeCodeAuthPlugin.AUTH_RESTRICTION_SCOPE_VCS):
123 125 response = self.app.post(route_path('login'),
124 126 {'username': 'test_regular',
125 127 'password': 'test12'})
126 128
127 129 response.mustcontain('invalid user name')
128 130 response.mustcontain('invalid password')
129 131
130 132 def test_login_ok_came_from(self):
131 133 test_came_from = '/_admin/users?branch=stable'
132 134 _url = '{}?came_from={}'.format(route_path('login'), test_came_from)
133 135 response = self.app.post(
134 136 _url, {'username': 'test_admin', 'password': 'test12'}, status=302)
135 137
136 138 assert 'branch=stable' in response.location
137 139 response = response.follow()
138 140
139 141 assert response.status == '200 OK'
140 142 response.mustcontain('Users administration')
141 143
142 144 def test_redirect_to_login_with_get_args(self):
143 145 with fixture.anon_access(False):
144 146 kwargs = {'branch': 'stable'}
145 147 response = self.app.get(
146 148 h.route_path('repo_summary', repo_name=HG_REPO, _query=kwargs),
147 149 status=302)
148 150
149 151 response_query = urllib.parse.parse_qsl(response.location)
150 152 assert 'branch=stable' in response_query[0][1]
151 153
152 154 def test_login_form_with_get_args(self):
153 155 _url = '{}?came_from=/_admin/users,branch=stable'.format(route_path('login'))
154 156 response = self.app.get(_url)
155 157 assert 'branch%3Dstable' in response.form.action
156 158
157 159 @pytest.mark.parametrize("url_came_from", [
158 160 'data:text/html,<script>window.alert("xss")</script>',
159 161 'mailto:test@rhodecode.org',
160 162 'file:///etc/passwd',
161 163 'ftp://some.ftp.server',
162 164 'http://other.domain',
163 '/\r\nX-Forwarded-Host: http://example.org',
164 165 ], ids=no_newline_id_generator)
165 166 def test_login_bad_came_froms(self, url_came_from):
166 167 _url = '{}?came_from={}'.format(route_path('login'), url_came_from)
167 168 response = self.app.post(
168 _url,
169 {'username': 'test_admin', 'password': 'test12'})
169 _url, {'username': 'test_admin', 'password': 'test12'}, status=302)
170 170 assert response.status == '302 Found'
171 171 response = response.follow()
172 172 assert response.status == '200 OK'
173 173 assert response.request.path == '/'
174 174
175 @pytest.mark.xfail(reason="newline params changed behaviour in python3")
176 @pytest.mark.parametrize("url_came_from", [
177 '/\r\nX-Forwarded-Host: \rhttp://example.org',
178 ], ids=no_newline_id_generator)
179 def test_login_bad_came_froms_404(self, url_came_from):
180 _url = '{}?came_from={}'.format(route_path('login'), url_came_from)
181 response = self.app.post(
182 _url, {'username': 'test_admin', 'password': 'test12'}, status=302)
183
184 response = response.follow()
185 assert response.status == '404 Not Found'
186
175 187 def test_login_short_password(self):
176 188 response = self.app.post(route_path('login'),
177 189 {'username': 'test_admin',
178 190 'password': 'as'})
179 191 assert response.status == '200 OK'
180 192
181 193 response.mustcontain('Enter 3 characters or more')
182 194
183 195 def test_login_wrong_non_ascii_password(self, user_regular):
184 196 response = self.app.post(
185 197 route_path('login'),
186 198 {'username': user_regular.username,
187 'password': u'invalid-non-asci\xe4'.encode('utf8')})
199 'password': 'invalid-non-asci\xe4'.encode('utf8')})
188 200
189 201 response.mustcontain('invalid user name')
190 202 response.mustcontain('invalid password')
191 203
192 204 def test_login_with_non_ascii_password(self, user_util):
193 205 password = u'valid-non-ascii\xe4'
194 206 user = user_util.create_user(password=password)
195 207 response = self.app.post(
196 208 route_path('login'),
197 209 {'username': user.username,
198 210 'password': password})
199 211 assert response.status_code == 302
200 212
201 213 def test_login_wrong_username_password(self):
202 214 response = self.app.post(route_path('login'),
203 215 {'username': 'error',
204 216 'password': 'test12'})
205 217
206 218 response.mustcontain('invalid user name')
207 219 response.mustcontain('invalid password')
208 220
209 221 def test_login_admin_ok_password_migration(self, real_crypto_backend):
210 222 from rhodecode.lib import auth
211 223
212 224 # create new user, with sha256 password
213 225 temp_user = 'test_admin_sha256'
214 226 user = fixture.create_user(temp_user)
215 227 user.password = auth._RhodeCodeCryptoSha256().hash_create(
216 228 b'test123')
217 229 Session().add(user)
218 230 Session().commit()
219 231 self.destroy_users.add(temp_user)
220 232 response = self.app.post(route_path('login'),
221 233 {'username': temp_user,
222 234 'password': 'test123'}, status=302)
223 235
224 236 response = response.follow()
225 237 session = response.get_session_from_response()
226 238 username = session['rhodecode_user'].get('username')
227 239 assert username == temp_user
228 240 response.mustcontain('logout')
229 241
230 242 # new password should be bcrypted, after log-in and transfer
231 243 user = User.get_by_username(temp_user)
232 244 assert user.password.startswith('$')
233 245
234 246 # REGISTRATIONS
235 247 def test_register(self):
236 248 response = self.app.get(route_path('register'))
237 249 response.mustcontain('Create an Account')
238 250
239 251 def test_register_err_same_username(self):
240 252 uname = 'test_admin'
241 253 response = self.app.post(
242 254 route_path('register'),
243 255 {
244 256 'username': uname,
245 257 'password': 'test12',
246 258 'password_confirmation': 'test12',
247 259 'email': 'goodmail@domain.com',
248 260 'firstname': 'test',
249 261 'lastname': 'test'
250 262 }
251 263 )
252 264
253 265 assertr = response.assert_response()
254 266 msg = 'Username "%(username)s" already exists'
255 267 msg = msg % {'username': uname}
256 268 assertr.element_contains('#username+.error-message', msg)
257 269
258 270 def test_register_err_same_email(self):
259 271 response = self.app.post(
260 272 route_path('register'),
261 273 {
262 274 'username': 'test_admin_0',
263 275 'password': 'test12',
264 276 'password_confirmation': 'test12',
265 277 'email': 'test_admin@mail.com',
266 278 'firstname': 'test',
267 279 'lastname': 'test'
268 280 }
269 281 )
270 282
271 283 assertr = response.assert_response()
272 284 msg = u'This e-mail address is already taken'
273 285 assertr.element_contains('#email+.error-message', msg)
274 286
275 287 def test_register_err_same_email_case_sensitive(self):
276 288 response = self.app.post(
277 289 route_path('register'),
278 290 {
279 291 'username': 'test_admin_1',
280 292 'password': 'test12',
281 293 'password_confirmation': 'test12',
282 294 'email': 'TesT_Admin@mail.COM',
283 295 'firstname': 'test',
284 296 'lastname': 'test'
285 297 }
286 298 )
287 299 assertr = response.assert_response()
288 300 msg = u'This e-mail address is already taken'
289 301 assertr.element_contains('#email+.error-message', msg)
290 302
291 303 def test_register_err_wrong_data(self):
292 304 response = self.app.post(
293 305 route_path('register'),
294 306 {
295 307 'username': 'xs',
296 308 'password': 'test',
297 309 'password_confirmation': 'test',
298 310 'email': 'goodmailm',
299 311 'firstname': 'test',
300 312 'lastname': 'test'
301 313 }
302 314 )
303 315 assert response.status == '200 OK'
304 316 response.mustcontain('An email address must contain a single @')
305 317 response.mustcontain('Enter a value 6 characters long or more')
306 318
307 319 def test_register_err_username(self):
308 320 response = self.app.post(
309 321 route_path('register'),
310 322 {
311 323 'username': 'error user',
312 324 'password': 'test12',
313 325 'password_confirmation': 'test12',
314 326 'email': 'goodmailm',
315 327 'firstname': 'test',
316 328 'lastname': 'test'
317 329 }
318 330 )
319 331
320 332 response.mustcontain('An email address must contain a single @')
321 333 response.mustcontain(
322 334 'Username may only contain '
323 335 'alphanumeric characters underscores, '
324 336 'periods or dashes and must begin with '
325 337 'alphanumeric character')
326 338
327 339 def test_register_err_case_sensitive(self):
328 340 usr = 'Test_Admin'
329 341 response = self.app.post(
330 342 route_path('register'),
331 343 {
332 344 'username': usr,
333 345 'password': 'test12',
334 346 'password_confirmation': 'test12',
335 347 'email': 'goodmailm',
336 348 'firstname': 'test',
337 349 'lastname': 'test'
338 350 }
339 351 )
340 352
341 353 assertr = response.assert_response()
342 354 msg = u'Username "%(username)s" already exists'
343 355 msg = msg % {'username': usr}
344 356 assertr.element_contains('#username+.error-message', msg)
345 357
346 358 def test_register_special_chars(self):
347 359 response = self.app.post(
348 360 route_path('register'),
349 361 {
350 362 'username': 'xxxaxn',
351 363 'password': 'ąćźżąśśśś',
352 364 'password_confirmation': 'ąćźżąśśśś',
353 365 'email': 'goodmailm@test.plx',
354 366 'firstname': 'test',
355 367 'lastname': 'test'
356 368 }
357 369 )
358 370
359 371 msg = u'Invalid characters (non-ascii) in password'
360 372 response.mustcontain(msg)
361 373
362 374 def test_register_password_mismatch(self):
363 375 response = self.app.post(
364 376 route_path('register'),
365 377 {
366 378 'username': 'xs',
367 379 'password': '123qwe',
368 380 'password_confirmation': 'qwe123',
369 381 'email': 'goodmailm@test.plxa',
370 382 'firstname': 'test',
371 383 'lastname': 'test'
372 384 }
373 385 )
374 386 msg = u'Passwords do not match'
375 387 response.mustcontain(msg)
376 388
377 389 def test_register_ok(self):
378 390 username = 'test_regular4'
379 391 password = 'qweqwe'
380 392 email = 'marcin@test.com'
381 393 name = 'testname'
382 394 lastname = 'testlastname'
383 395
384 396 # this initializes a session
385 397 response = self.app.get(route_path('register'))
386 398 response.mustcontain('Create an Account')
387 399
388 400
389 401 response = self.app.post(
390 402 route_path('register'),
391 403 {
392 404 'username': username,
393 405 'password': password,
394 406 'password_confirmation': password,
395 407 'email': email,
396 408 'firstname': name,
397 409 'lastname': lastname,
398 410 'admin': True
399 411 },
400 412 status=302
401 413 ) # This should be overridden
402 414
403 415 assert_session_flash(
404 416 response, 'You have successfully registered with RhodeCode. You can log-in now.')
405 417
406 418 ret = Session().query(User).filter(
407 419 User.username == 'test_regular4').one()
408 420 assert ret.username == username
409 421 assert check_password(password, ret.password)
410 422 assert ret.email == email
411 423 assert ret.name == name
412 424 assert ret.lastname == lastname
413 425 assert ret.auth_tokens is not None
414 426 assert not ret.admin
415 427
416 428 def test_forgot_password_wrong_mail(self):
417 429 bad_email = 'marcin@wrongmail.org'
418 430 # this initializes a session
419 431 self.app.get(route_path('reset_password'))
420 432
421 433 response = self.app.post(
422 434 route_path('reset_password'), {'email': bad_email, }
423 435 )
424 436 assert_session_flash(response,
425 437 'If such email exists, a password reset link was sent to it.')
426 438
427 439 def test_forgot_password(self, user_util):
428 440 # this initializes a session
429 441 self.app.get(route_path('reset_password'))
430 442
431 443 user = user_util.create_user()
432 444 user_id = user.user_id
433 445 email = user.email
434 446
435 447 response = self.app.post(route_path('reset_password'), {'email': email, })
436 448
437 449 assert_session_flash(response,
438 450 'If such email exists, a password reset link was sent to it.')
439 451
440 452 # BAD KEY
441 453 confirm_url = '{}?key={}'.format(route_path('reset_password_confirmation'), 'badkey')
442 454 response = self.app.get(confirm_url, status=302)
443 455 assert response.location.endswith(route_path('reset_password'))
444 456 assert_session_flash(response, 'Given reset token is invalid')
445 457
446 458 response.follow() # cleanup flash
447 459
448 460 # GOOD KEY
449 461 key = UserApiKeys.query()\
450 462 .filter(UserApiKeys.user_id == user_id)\
451 463 .filter(UserApiKeys.role == UserApiKeys.ROLE_PASSWORD_RESET)\
452 464 .first()
453 465
454 466 assert key
455 467
456 468 confirm_url = '{}?key={}'.format(route_path('reset_password_confirmation'), key.api_key)
457 469 response = self.app.get(confirm_url)
458 470 assert response.status == '302 Found'
459 471 assert response.location.endswith(route_path('login'))
460 472
461 473 assert_session_flash(
462 474 response,
463 475 'Your password reset was successful, '
464 476 'a new password has been sent to your email')
465 477
466 478 response.follow()
467 479
468 480 def _get_api_whitelist(self, values=None):
469 481 config = {'api_access_controllers_whitelist': values or []}
470 482 return config
471 483
472 484 @pytest.mark.parametrize("test_name, auth_token", [
473 485 ('none', None),
474 486 ('empty_string', ''),
475 487 ('fake_number', '123456'),
476 488 ('proper_auth_token', None)
477 489 ])
478 490 def test_access_not_whitelisted_page_via_auth_token(
479 491 self, test_name, auth_token, user_admin):
480 492
481 493 whitelist = self._get_api_whitelist([])
482 494 with mock.patch.dict('rhodecode.CONFIG', whitelist):
483 495 assert [] == whitelist['api_access_controllers_whitelist']
484 496 if test_name == 'proper_auth_token':
485 497 # use builtin if api_key is None
486 498 auth_token = user_admin.api_key
487 499
488 500 with fixture.anon_access(False):
501 # webtest uses linter to check if response is bytes,
502 # and we use memoryview here as a wrapper, quick turn-off
503 self.app.lint = False
504
489 505 self.app.get(
490 506 route_path('repo_commit_raw',
491 507 repo_name=HG_REPO, commit_id='tip',
492 508 params=dict(api_key=auth_token)),
493 509 status=302)
494 510
495 511 @pytest.mark.parametrize("test_name, auth_token, code", [
496 512 ('none', None, 302),
497 513 ('empty_string', '', 302),
498 514 ('fake_number', '123456', 302),
499 515 ('proper_auth_token', None, 200)
500 516 ])
501 517 def test_access_whitelisted_page_via_auth_token(
502 518 self, test_name, auth_token, code, user_admin):
503 519
504 520 whitelist = self._get_api_whitelist(whitelist_view)
505 521
506 522 with mock.patch.dict('rhodecode.CONFIG', whitelist):
507 523 assert whitelist_view == whitelist['api_access_controllers_whitelist']
508 524
509 525 if test_name == 'proper_auth_token':
510 526 auth_token = user_admin.api_key
511 527 assert auth_token
512 528
513 529 with fixture.anon_access(False):
530 # webtest uses linter to check if response is bytes,
531 # and we use memoryview here as a wrapper, quick turn-off
532 self.app.lint = False
514 533 self.app.get(
515 534 route_path('repo_commit_raw',
516 535 repo_name=HG_REPO, commit_id='tip',
517 536 params=dict(api_key=auth_token)),
518 537 status=code)
519 538
520 539 @pytest.mark.parametrize("test_name, auth_token, code", [
521 540 ('proper_auth_token', None, 200),
522 541 ('wrong_auth_token', '123456', 302),
523 542 ])
524 543 def test_access_whitelisted_page_via_auth_token_bound_to_token(
525 544 self, test_name, auth_token, code, user_admin):
526 545
527 546 expected_token = auth_token
528 547 if test_name == 'proper_auth_token':
529 548 auth_token = user_admin.api_key
530 549 expected_token = auth_token
531 550 assert auth_token
532 551
533 552 whitelist = self._get_api_whitelist([
534 553 'RepoCommitsView:repo_commit_raw@{}'.format(expected_token)])
535 554
536 555 with mock.patch.dict('rhodecode.CONFIG', whitelist):
537 556
538 557 with fixture.anon_access(False):
558 # webtest uses linter to check if response is bytes,
559 # and we use memoryview here as a wrapper, quick turn-off
560 self.app.lint = False
561
539 562 self.app.get(
540 563 route_path('repo_commit_raw',
541 564 repo_name=HG_REPO, commit_id='tip',
542 565 params=dict(api_key=auth_token)),
543 566 status=code)
544 567
545 568 def test_access_page_via_extra_auth_token(self):
546 569 whitelist = self._get_api_whitelist(whitelist_view)
547 570 with mock.patch.dict('rhodecode.CONFIG', whitelist):
548 571 assert whitelist_view == \
549 572 whitelist['api_access_controllers_whitelist']
550 573
551 574 new_auth_token = AuthTokenModel().create(
552 575 TEST_USER_ADMIN_LOGIN, 'test')
553 576 Session().commit()
554 577 with fixture.anon_access(False):
578 # webtest uses linter to check if response is bytes,
579 # and we use memoryview here as a wrapper, quick turn-off
580 self.app.lint = False
555 581 self.app.get(
556 582 route_path('repo_commit_raw',
557 583 repo_name=HG_REPO, commit_id='tip',
558 584 params=dict(api_key=new_auth_token.api_key)),
559 585 status=200)
560 586
561 587 def test_access_page_via_expired_auth_token(self):
562 588 whitelist = self._get_api_whitelist(whitelist_view)
563 589 with mock.patch.dict('rhodecode.CONFIG', whitelist):
564 590 assert whitelist_view == \
565 591 whitelist['api_access_controllers_whitelist']
566 592
567 593 new_auth_token = AuthTokenModel().create(
568 594 TEST_USER_ADMIN_LOGIN, 'test')
569 595 Session().commit()
570 596 # patch the api key and make it expired
571 597 new_auth_token.expires = 0
572 598 Session().add(new_auth_token)
573 599 Session().commit()
574 600 with fixture.anon_access(False):
601 # webtest uses linter to check if response is bytes,
602 # and we use memoryview here as a wrapper, quick turn-off
603 self.app.lint = False
575 604 self.app.get(
576 605 route_path('repo_commit_raw',
577 606 repo_name=HG_REPO, commit_id='tip',
578 607 params=dict(api_key=new_auth_token.api_key)),
579 608 status=302)
@@ -1,117 +1,119 b''
1 1
2 2 # Copyright (C) 2010-2020 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 import helpers as h
23 23 from rhodecode.tests import (
24 24 TestController, clear_cache_regions,
25 25 TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS)
26 26 from rhodecode.tests.fixture import Fixture
27 27 from rhodecode.tests.utils import AssertResponse
28 28
29 29 fixture = Fixture()
30 30
31 31
32 32 def route_path(name, params=None, **kwargs):
33 import urllib.request, urllib.parse, urllib.error
33 import urllib.request
34 import urllib.parse
35 import urllib.error
34 36 from rhodecode.apps._base import ADMIN_PREFIX
35 37
36 38 base_url = {
37 39 'login': ADMIN_PREFIX + '/login',
38 40 'logout': ADMIN_PREFIX + '/logout',
39 41 'register': ADMIN_PREFIX + '/register',
40 42 'reset_password':
41 43 ADMIN_PREFIX + '/password_reset',
42 44 'reset_password_confirmation':
43 45 ADMIN_PREFIX + '/password_reset_confirmation',
44 46
45 47 'admin_permissions_application':
46 48 ADMIN_PREFIX + '/permissions/application',
47 49 'admin_permissions_application_update':
48 50 ADMIN_PREFIX + '/permissions/application/update',
49 51 }[name].format(**kwargs)
50 52
51 53 if params:
52 54 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
53 55 return base_url
54 56
55 57
56 58 class TestPasswordReset(TestController):
57 59
58 60 @pytest.mark.parametrize(
59 61 'pwd_reset_setting, show_link, show_reset', [
60 62 ('hg.password_reset.enabled', True, True),
61 63 ('hg.password_reset.hidden', False, True),
62 64 ('hg.password_reset.disabled', False, False),
63 65 ])
64 66 def test_password_reset_settings(
65 67 self, pwd_reset_setting, show_link, show_reset):
66 68 clear_cache_regions()
67 69 self.log_user(TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS)
68 70 params = {
69 71 'csrf_token': self.csrf_token,
70 72 'anonymous': 'True',
71 73 'default_register': 'hg.register.auto_activate',
72 74 'default_register_message': '',
73 75 'default_password_reset': pwd_reset_setting,
74 76 'default_extern_activate': 'hg.extern_activate.auto',
75 77 }
76 78 resp = self.app.post(
77 79 route_path('admin_permissions_application_update'), params=params)
78 80 self.logout_user()
79 81
80 82 login_page = self.app.get(route_path('login'))
81 83 asr_login = AssertResponse(login_page)
82 84
83 85 if show_link:
84 86 asr_login.one_element_exists('a.pwd_reset')
85 87 else:
86 88 asr_login.no_element_exists('a.pwd_reset')
87 89
88 90 response = self.app.get(route_path('reset_password'))
89 91
90 92 assert_response = response.assert_response()
91 93 if show_reset:
92 94 response.mustcontain('Send password reset email')
93 95 assert_response.one_element_exists('#email')
94 96 assert_response.one_element_exists('#send')
95 97 else:
96 98 response.mustcontain('Password reset is disabled.')
97 99 assert_response.no_element_exists('#email')
98 100 assert_response.no_element_exists('#send')
99 101
100 102 def test_password_form_disabled(self):
101 103 self.log_user(TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS)
102 104 params = {
103 105 'csrf_token': self.csrf_token,
104 106 'anonymous': 'True',
105 107 'default_register': 'hg.register.auto_activate',
106 108 'default_register_message': '',
107 109 'default_password_reset': 'hg.password_reset.disabled',
108 110 'default_extern_activate': 'hg.extern_activate.auto',
109 111 }
110 112 self.app.post(route_path('admin_permissions_application_update'), params=params)
111 113 self.logout_user()
112 114
113 115 response = self.app.post(
114 116 route_path('reset_password'), {'email': 'lisa@rhodecode.com',}
115 117 )
116 118 response = response.follow()
117 119 response.mustcontain('Password reset is disabled.')
@@ -1,205 +1,208 b''
1 1
2 2 # Copyright (C) 2010-2020 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._base import ADMIN_PREFIX
23 23 from rhodecode.tests import (
24 24 TestController, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS,
25 25 TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS)
26 26 from rhodecode.tests.fixture import Fixture
27 27
28 28 from rhodecode.model.db import Notification, User
29 29 from rhodecode.model.user import UserModel
30 30 from rhodecode.model.notification import NotificationModel
31 31 from rhodecode.model.meta import Session
32 32
33 33 fixture = Fixture()
34 34
35 35
36 36 def route_path(name, params=None, **kwargs):
37 import urllib.request, urllib.parse, urllib.error
37 import urllib.request
38 import urllib.parse
39 import urllib.error
38 40 from rhodecode.apps._base import ADMIN_PREFIX
39 41
40 42 base_url = {
41 43 'notifications_show_all': ADMIN_PREFIX + '/notifications',
42 44 'notifications_mark_all_read': ADMIN_PREFIX + '/notifications_mark_all_read',
43 45 'notifications_show': ADMIN_PREFIX + '/notifications/{notification_id}',
44 46 'notifications_update': ADMIN_PREFIX + '/notifications/{notification_id}/update',
45 47 'notifications_delete': ADMIN_PREFIX + '/notifications/{notification_id}/delete',
46 48
47 49 }[name].format(**kwargs)
48 50
49 51 if params:
50 52 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
51 53 return base_url
52 54
53 55
54 56 class TestNotificationsController(TestController):
55 57
56 58 def teardown_method(self, method):
57 59 for n in Notification.query().all():
58 60 inst = Notification.get(n.notification_id)
59 61 Session().delete(inst)
60 62 Session().commit()
61 63
62 64 def test_mark_all_read(self, user_util):
63 65 user = user_util.create_user(password='qweqwe')
64 66 self.log_user(user.username, 'qweqwe')
65 67
66 68 self.app.post(
67 69 route_path('notifications_mark_all_read'), status=302,
68 70 params={'csrf_token': self.csrf_token}
69 71 )
70 72
71 73 def test_show_all(self, user_util):
72 74 user = user_util.create_user(password='qweqwe')
73 75 user_id = user.user_id
74 76 self.log_user(user.username, 'qweqwe')
75 77
76 78 response = self.app.get(
77 79 route_path('notifications_show_all', params={'type': 'all'}))
78 80 response.mustcontain(
79 81 '<div class="table">No notifications here yet</div>')
80 82
81 83 notification = NotificationModel().create(
82 84 created_by=user_id, notification_subject=u'test_notification_1',
83 85 notification_body=u'notification_1', recipients=[user_id])
84 86 Session().commit()
85 87 notification_id = notification.notification_id
86 88
87 89 response = self.app.get(route_path('notifications_show_all',
88 90 params={'type': 'all'}))
89 91 response.mustcontain('id="notification_%s"' % notification_id)
90 92
91 93 def test_show_unread(self, user_util):
92 94 user = user_util.create_user(password='qweqwe')
93 95 user_id = user.user_id
94 96 self.log_user(user.username, 'qweqwe')
95 97
96 98 response = self.app.get(route_path('notifications_show_all'))
97 99 response.mustcontain(
98 100 '<div class="table">No notifications here yet</div>')
99 101
100 102 notification = NotificationModel().create(
101 103 created_by=user_id, notification_subject=u'test_notification_1',
102 104 notification_body=u'notification_1', recipients=[user_id])
103 105
104 106 # mark the USER notification as unread
105 107 user_notification = NotificationModel().get_user_notification(
106 108 user_id, notification)
107 109 user_notification.read = False
108 110
109 111 Session().commit()
110 112 notification_id = notification.notification_id
111 113
112 114 response = self.app.get(route_path('notifications_show_all'))
113 115 response.mustcontain('id="notification_%s"' % notification_id)
114 116 response.mustcontain('<div class="desc unread')
115 117
116 118 @pytest.mark.parametrize('user,password', [
117 119 (TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS),
118 120 (TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS),
119 121 ])
120 122 def test_delete(self, user, password, user_util):
121 123 self.log_user(user, password)
122 124 cur_user = self._get_logged_user()
123 125
124 126 u1 = user_util.create_user()
125 127 u2 = user_util.create_user()
126 128
127 129 # make notifications
128 130 notification = NotificationModel().create(
129 131 created_by=cur_user, notification_subject=u'test',
130 132 notification_body=u'hi there', recipients=[cur_user, u1, u2])
131 133 Session().commit()
132 134 u1 = User.get(u1.user_id)
133 135 u2 = User.get(u2.user_id)
134 136
135 137 # check DB
136 get_notif = lambda un: [x.notification for x in un]
138 def get_notif(un):
139 return [x.notification for x in un]
137 140 assert get_notif(cur_user.notifications) == [notification]
138 141 assert get_notif(u1.notifications) == [notification]
139 142 assert get_notif(u2.notifications) == [notification]
140 143 cur_usr_id = cur_user.user_id
141 144
142 145 response = self.app.post(
143 146 route_path('notifications_delete',
144 147 notification_id=notification.notification_id),
145 148 params={'csrf_token': self.csrf_token})
146 149 assert response.json == 'ok'
147 150
148 151 cur_user = User.get(cur_usr_id)
149 152 assert cur_user.notifications == []
150 153
151 154 @pytest.mark.parametrize('user,password', [
152 155 (TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS),
153 156 (TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS),
154 157 ])
155 158 def test_show(self, user, password, user_util):
156 159 self.log_user(user, password)
157 160 cur_user = self._get_logged_user()
158 161 u1 = user_util.create_user()
159 162 u2 = user_util.create_user()
160 163
161 164 subject = u'test'
162 165 notif_body = u'hi there'
163 166 notification = NotificationModel().create(
164 167 created_by=cur_user, notification_subject=subject,
165 168 notification_body=notif_body, recipients=[cur_user, u1, u2])
166 169 Session().commit()
167 170
168 171 response = self.app.get(
169 172 route_path('notifications_show',
170 173 notification_id=notification.notification_id))
171 174
172 175 response.mustcontain(subject)
173 176 response.mustcontain(notif_body)
174 177
175 178 @pytest.mark.parametrize('user,password', [
176 179 (TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS),
177 180 (TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS),
178 181 ])
179 182 def test_update(self, user, password, user_util):
180 183 self.log_user(user, password)
181 184 cur_user = self._get_logged_user()
182 185 u1 = user_util.create_user()
183 186 u2 = user_util.create_user()
184 187
185 188 # make notifications
186 189 recipients = [cur_user, u1, u2]
187 190 notification = NotificationModel().create(
188 191 created_by=cur_user, notification_subject=u'test',
189 192 notification_body=u'hi there', recipients=recipients)
190 193 Session().commit()
191 194
192 195 for u_obj in recipients:
193 196 # if it's current user, he has his message already read
194 197 read = u_obj.username == user
195 198 assert len(u_obj.notifications) == 1
196 199 assert u_obj.notifications[0].read == read
197 200
198 201 response = self.app.post(
199 202 route_path('notifications_update',
200 203 notification_id=notification.notification_id),
201 204 params={'csrf_token': self.csrf_token})
202 205 assert response.json == 'ok'
203 206
204 207 cur_user = self._get_logged_user()
205 208 assert True is cur_user.notifications[0].read
@@ -1,162 +1,164 b''
1 1
2 2 # Copyright (C) 2010-2020 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 26
27 27 fixture = Fixture()
28 28
29 29
30 30 def route_path(name, params=None, **kwargs):
31 import urllib.request, urllib.parse, urllib.error
31 import urllib.request
32 import urllib.parse
33 import urllib.error
32 34 from rhodecode.apps._base import ADMIN_PREFIX
33 35
34 36 base_url = {
35 37 'my_account_ssh_keys':
36 38 ADMIN_PREFIX + '/my_account/ssh_keys',
37 39 'my_account_ssh_keys_generate':
38 40 ADMIN_PREFIX + '/my_account/ssh_keys/generate',
39 41 'my_account_ssh_keys_add':
40 42 ADMIN_PREFIX + '/my_account/ssh_keys/new',
41 43 'my_account_ssh_keys_delete':
42 44 ADMIN_PREFIX + '/my_account/ssh_keys/delete',
43 45 }[name].format(**kwargs)
44 46
45 47 if params:
46 48 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
47 49 return base_url
48 50
49 51
50 52 class TestMyAccountSshKeysView(TestController):
51 53 INVALID_KEY = """\
52 54 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDk+77sjDzVeB6vevJsuZds1iNU5
53 55 LANOa5CU5G/9JYIA6RYsWWMO7mbsR82IUckdqOHmxSykfR1D1TdluyIpQLrwgH5kb
54 56 n8FkVI8zBMCKakxowvN67B0R7b1BT4PPzW2JlOXei/m9W12ZY484VTow6/B+kf2Q8
55 57 cP8tmCJmKWZma5Em7OTUhvjyQVNz3v7HfeY5Hq0Ci4ECJ59hepFDabJvtAXg9XrI6
56 58 jvdphZTc30I4fG8+hBHzpeFxUGvSGNtXPUbwaAY8j/oHYrTpMgkj6pUEFsiKfC5zP
57 59 qPFR5HyKTCHW0nFUJnZsbyFT5hMiF/hZkJc9A0ZbdSvJwCRQ/g3bmdL
58 60 your_email@example.com
59 61 """
60 62 VALID_KEY = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDk+77sjDzVeB6vev' \
61 63 'JsuZds1iNU5LANOa5CU5G/9JYIA6RYsWWMO7mbsR82IUckdqOHmxSy' \
62 64 'kfR1D1TdluyIpQLrwgH5kbn8FkVI8zBMCKakxowvN67B0R7b1BT4PP' \
63 65 'zW2JlOXei/m9W12ZY484VTow6/B+kf2Q8cP8tmCJmKWZma5Em7OTUh' \
64 66 'vjyQVNz3v7HfeY5Hq0Ci4ECJ59hepFDabJvtAXg9XrI6jvdphZTc30' \
65 67 'I4fG8+hBHzpeFxUGvSGNtXPUbwaAY8j/oHYrTpMgkj6pUEFsiKfC5zPq' \
66 68 'PFR5HyKTCHW0nFUJnZsbyFT5hMiF/hZkJc9A0ZbdSvJwCRQ/g3bmdL ' \
67 69 'your_email@example.com'
68 70 FINGERPRINT = 'MD5:01:4f:ad:29:22:6e:01:37:c9:d2:52:26:52:b0:2d:93'
69 71
70 72 def test_add_ssh_key_error(self, user_util):
71 73 user = user_util.create_user(password='qweqwe')
72 74 self.log_user(user.username, 'qweqwe')
73 75
74 76 key_data = self.INVALID_KEY
75 77
76 78 desc = 'MY SSH KEY'
77 79 response = self.app.post(
78 80 route_path('my_account_ssh_keys_add'),
79 81 {'description': desc, 'key_data': key_data,
80 82 'csrf_token': self.csrf_token})
81 83 assert_session_flash(response, 'An error occurred during ssh '
82 84 'key saving: Unable to decode the key')
83 85
84 86 def test_ssh_key_duplicate(self, user_util):
85 87 user = user_util.create_user(password='qweqwe')
86 88 self.log_user(user.username, 'qweqwe')
87 89 key_data = self.VALID_KEY
88 90
89 91 desc = 'MY SSH KEY'
90 92 response = self.app.post(
91 93 route_path('my_account_ssh_keys_add'),
92 94 {'description': desc, 'key_data': key_data,
93 95 'csrf_token': self.csrf_token})
94 96 assert_session_flash(response, 'Ssh Key successfully created')
95 97 response.follow() # flush session flash
96 98
97 99 # add the same key AGAIN
98 100 desc = 'MY SSH KEY'
99 101 response = self.app.post(
100 102 route_path('my_account_ssh_keys_add'),
101 103 {'description': desc, 'key_data': key_data,
102 104 'csrf_token': self.csrf_token})
103 105
104 106 err = 'Such key with fingerprint `{}` already exists, ' \
105 107 'please use a different one'.format(self.FINGERPRINT)
106 108 assert_session_flash(response, 'An error occurred during ssh key '
107 109 'saving: {}'.format(err))
108 110
109 111 def test_add_ssh_key(self, user_util):
110 112 user = user_util.create_user(password='qweqwe')
111 113 self.log_user(user.username, 'qweqwe')
112 114
113 115 key_data = self.VALID_KEY
114 116
115 117 desc = 'MY SSH KEY'
116 118 response = self.app.post(
117 119 route_path('my_account_ssh_keys_add'),
118 120 {'description': desc, 'key_data': key_data,
119 121 'csrf_token': self.csrf_token})
120 122 assert_session_flash(response, 'Ssh Key successfully created')
121 123
122 124 response = response.follow()
123 125 response.mustcontain(desc)
124 126
125 127 def test_delete_ssh_key(self, user_util):
126 128 user = user_util.create_user(password='qweqwe')
127 129 user_id = user.user_id
128 130 self.log_user(user.username, 'qweqwe')
129 131
130 132 key_data = self.VALID_KEY
131 133
132 134 desc = 'MY SSH KEY'
133 135 response = self.app.post(
134 136 route_path('my_account_ssh_keys_add'),
135 137 {'description': desc, 'key_data': key_data,
136 138 'csrf_token': self.csrf_token})
137 139 assert_session_flash(response, 'Ssh Key successfully created')
138 140 response = response.follow() # flush the Session flash
139 141
140 142 # now delete our key
141 143 keys = UserSshKeys.query().filter(UserSshKeys.user_id == user_id).all()
142 144 assert 1 == len(keys)
143 145
144 146 response = self.app.post(
145 147 route_path('my_account_ssh_keys_delete'),
146 148 {'del_ssh_key': keys[0].ssh_key_id,
147 149 'csrf_token': self.csrf_token})
148 150
149 151 assert_session_flash(response, 'Ssh key successfully deleted')
150 152 keys = UserSshKeys.query().filter(UserSshKeys.user_id == user_id).all()
151 153 assert 0 == len(keys)
152 154
153 155 def test_generate_keypair(self, user_util):
154 156 user = user_util.create_user(password='qweqwe')
155 157 self.log_user(user.username, 'qweqwe')
156 158
157 159 response = self.app.get(
158 160 route_path('my_account_ssh_keys_generate'))
159 161
160 162 response.mustcontain('Private key')
161 163 response.mustcontain('Public key')
162 164 response.mustcontain('-----BEGIN PRIVATE KEY-----')
@@ -1,88 +1,90 b''
1 1
2 2 # Copyright (C) 2010-2020 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 23
24 24
25 25 def route_path(name, params=None, **kwargs):
26 import urllib.request, urllib.parse, urllib.error
26 import urllib.request
27 import urllib.parse
28 import urllib.error
27 29
28 30 base_url = {
29 31 'edit_repo_group_advanced':
30 32 '/{repo_group_name}/_settings/advanced',
31 33 'edit_repo_group_advanced_delete':
32 34 '/{repo_group_name}/_settings/advanced/delete',
33 35 }[name].format(**kwargs)
34 36
35 37 if params:
36 38 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
37 39 return base_url
38 40
39 41
40 42 @pytest.mark.usefixtures("app")
41 43 class TestRepoGroupsAdvancedView(object):
42 44
43 45 @pytest.mark.parametrize('repo_group_name', [
44 46 'gro',
45 47 '12345',
46 48 ])
47 49 def test_show_advanced_settings(self, autologin_user, user_util, repo_group_name):
48 50 user_util._test_name = repo_group_name
49 51 gr = user_util.create_repo_group()
50 52 self.app.get(
51 53 route_path('edit_repo_group_advanced',
52 54 repo_group_name=gr.group_name))
53 55
54 56 def test_show_advanced_settings_delete(self, autologin_user, user_util,
55 57 csrf_token):
56 58 gr = user_util.create_repo_group(auto_cleanup=False)
57 59 repo_group_name = gr.group_name
58 60
59 61 params = dict(
60 62 csrf_token=csrf_token
61 63 )
62 64 response = self.app.post(
63 65 route_path('edit_repo_group_advanced_delete',
64 66 repo_group_name=repo_group_name), params=params)
65 67 assert_session_flash(
66 68 response, 'Removed repository group `{}`'.format(repo_group_name))
67 69
68 70 def test_delete_not_possible_with_objects_inside(self, autologin_user,
69 71 repo_groups, csrf_token):
70 72 zombie_group, parent_group, child_group = repo_groups
71 73
72 74 response = self.app.get(
73 75 route_path('edit_repo_group_advanced',
74 76 repo_group_name=parent_group.group_name))
75 77
76 78 response.mustcontain(
77 79 'This repository group includes 1 children repository group')
78 80
79 81 params = dict(
80 82 csrf_token=csrf_token
81 83 )
82 84 response = self.app.post(
83 85 route_path('edit_repo_group_advanced_delete',
84 86 repo_group_name=parent_group.group_name), params=params)
85 87
86 88 assert_session_flash(
87 89 response, 'This repository group contains 1 subgroup '
88 90 'and cannot be deleted')
@@ -1,85 +1,87 b''
1 1
2 2 # Copyright (C) 2010-2020 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 23
24 24
25 25 def route_path(name, params=None, **kwargs):
26 import urllib.request, urllib.parse, urllib.error
26 import urllib.request
27 import urllib.parse
28 import urllib.error
27 29
28 30 base_url = {
29 31 'edit_repo_group_perms':
30 32 '/{repo_group_name:}/_settings/permissions',
31 33 'edit_repo_group_perms_update':
32 34 '/{repo_group_name}/_settings/permissions/update',
33 35 }[name].format(**kwargs)
34 36
35 37 if params:
36 38 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
37 39 return base_url
38 40
39 41
40 42 @pytest.mark.usefixtures("app")
41 43 class TestRepoGroupPermissionsView(object):
42 44
43 45 def test_edit_perms_view(self, user_util, autologin_user):
44 46 repo_group = user_util.create_repo_group()
45 47
46 48 self.app.get(
47 49 route_path('edit_repo_group_perms',
48 50 repo_group_name=repo_group.group_name), status=200)
49 51
50 52 def test_update_permissions(self, csrf_token, user_util):
51 53 repo_group = user_util.create_repo_group()
52 54 repo_group_name = repo_group.group_name
53 55 user = user_util.create_user()
54 56 user_id = user.user_id
55 57 username = user.username
56 58
57 59 # grant new
58 60 form_data = permission_update_data_generator(
59 61 csrf_token,
60 62 default='group.write',
61 63 grant=[(user_id, 'group.write', username, 'user')])
62 64
63 65 # recursive flag required for repo groups
64 66 form_data.extend([('recursive', u'none')])
65 67
66 68 response = self.app.post(
67 69 route_path('edit_repo_group_perms_update',
68 70 repo_group_name=repo_group_name), form_data).follow()
69 71
70 72 assert 'Repository Group permissions updated' in response
71 73
72 74 # revoke given
73 75 form_data = permission_update_data_generator(
74 76 csrf_token,
75 77 default='group.read',
76 78 revoke=[(user_id, 'user')])
77 79
78 80 # recursive flag required for repo groups
79 81 form_data.extend([('recursive', u'none')])
80 82
81 83 response = self.app.post(
82 84 route_path('edit_repo_group_perms_update',
83 85 repo_group_name=repo_group_name), form_data).follow()
84 86
85 87 assert 'Repository Group permissions updated' in response
@@ -1,89 +1,91 b''
1 1
2 2 # Copyright (C) 2010-2020 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 23
24 24
25 25 def route_path(name, params=None, **kwargs):
26 import urllib.request, urllib.parse, urllib.error
26 import urllib.request
27 import urllib.parse
28 import urllib.error
27 29
28 30 base_url = {
29 31 'edit_repo_group': '/{repo_group_name}/_edit',
30 32 # Update is POST to the above url
31 33 }[name].format(**kwargs)
32 34
33 35 if params:
34 36 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
35 37 return base_url
36 38
37 39
38 40 @pytest.mark.usefixtures("app")
39 41 class TestRepoGroupsSettingsView(object):
40 42
41 43 @pytest.mark.parametrize('repo_group_name', [
42 44 'gro',
43 45 u'12345',
44 46 ])
45 47 def test_edit(self, user_util, autologin_user, repo_group_name):
46 48 user_util._test_name = repo_group_name
47 49 repo_group = user_util.create_repo_group()
48 50
49 51 self.app.get(
50 52 route_path('edit_repo_group', repo_group_name=repo_group.group_name),
51 53 status=200)
52 54
53 55 def test_update(self, csrf_token, autologin_user, user_util, rc_fixture):
54 56 repo_group = user_util.create_repo_group()
55 57 repo_group_name = repo_group.group_name
56 58
57 59 description = 'description for newly created repo group'
58 60 form_data = rc_fixture._get_group_create_params(
59 61 group_name=repo_group.group_name,
60 62 group_description=description,
61 63 csrf_token=csrf_token,
62 64 repo_group_name=repo_group.group_name,
63 65 repo_group_owner=repo_group.user.username)
64 66
65 67 response = self.app.post(
66 68 route_path('edit_repo_group',
67 69 repo_group_name=repo_group.group_name),
68 70 form_data,
69 71 status=302)
70 72
71 73 assert_session_flash(
72 74 response, 'Repository Group `{}` updated successfully'.format(
73 75 repo_group_name))
74 76
75 77 def test_update_fails_when_parent_pointing_to_self(
76 78 self, csrf_token, user_util, autologin_user, rc_fixture):
77 79 group = user_util.create_repo_group()
78 80 response = self.app.post(
79 81 route_path('edit_repo_group', repo_group_name=group.group_name),
80 82 rc_fixture._get_group_create_params(
81 83 repo_group_name=group.group_name,
82 84 repo_group_owner=group.user.username,
83 85 repo_group=group.group_id,
84 86 csrf_token=csrf_token),
85 87 status=200
86 88 )
87 89 response.mustcontain(
88 90 '<span class="error-message">"{}" is not one of -1'.format(
89 91 group.group_id))
@@ -1,83 +1,85 b''
1 1
2 2 # Copyright (C) 2010-2020 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.db import Repository
22 22
23 23
24 24 def route_path(name, params=None, **kwargs):
25 import urllib.request, urllib.parse, urllib.error
25 import urllib.request
26 import urllib.parse
27 import urllib.error
26 28
27 29 base_url = {
28 30 'pullrequest_show_all': '/{repo_name}/pull-request',
29 31 'pullrequest_show_all_data': '/{repo_name}/pull-request-data',
30 32 }[name].format(**kwargs)
31 33
32 34 if params:
33 35 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
34 36 return base_url
35 37
36 38
37 39 @pytest.mark.backends("git", "hg")
38 40 @pytest.mark.usefixtures('autologin_user', 'app')
39 41 class TestPullRequestList(object):
40 42
41 43 @pytest.mark.parametrize('params, expected_title', [
42 44 ({'source': 0, 'closed': 1}, 'Closed'),
43 45 ({'source': 0, 'my': 1}, 'Created by me'),
44 46 ({'source': 0, 'awaiting_review': 1}, 'Awaiting review'),
45 47 ({'source': 0, 'awaiting_my_review': 1}, 'Awaiting my review'),
46 48 ({'source': 1}, 'From this repo'),
47 49 ])
48 50 def test_showing_list_page(self, backend, pr_util, params, expected_title):
49 51 pull_request = pr_util.create_pull_request()
50 52
51 53 response = self.app.get(
52 54 route_path('pullrequest_show_all',
53 55 repo_name=pull_request.target_repo.repo_name,
54 56 params=params))
55 57
56 58 assert_response = response.assert_response()
57 59
58 60 element = assert_response.get_element('.title .active')
59 61 element_text = element.text_content()
60 62 assert expected_title == element_text
61 63
62 64 def test_showing_list_page_data(self, backend, pr_util, xhr_header):
63 65 pull_request = pr_util.create_pull_request()
64 66 response = self.app.get(
65 67 route_path('pullrequest_show_all_data',
66 68 repo_name=pull_request.target_repo.repo_name),
67 69 extra_environ=xhr_header)
68 70
69 71 assert response.json['recordsTotal'] == 1
70 72 assert response.json['data'][0]['description'] == 'Description'
71 73
72 74 def test_description_is_escaped_on_index_page(self, backend, pr_util, xhr_header):
73 75 xss_description = "<script>alert('Hi!')</script>"
74 76 pull_request = pr_util.create_pull_request(description=xss_description)
75 77
76 78 response = self.app.get(
77 79 route_path('pullrequest_show_all_data',
78 80 repo_name=pull_request.target_repo.repo_name),
79 81 extra_environ=xhr_header)
80 82
81 83 assert response.json['recordsTotal'] == 1
82 84 assert response.json['data'][0]['description'] == \
83 85 "&lt;script&gt;alert(&#39;Hi!&#39;)&lt;/script&gt;"
@@ -1,51 +1,53 b''
1 1
2 2 # Copyright (C) 2010-2020 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.db import Repository
22 22
23 23
24 24 def route_path(name, params=None, **kwargs):
25 import urllib.request, urllib.parse, urllib.error
25 import urllib.request
26 import urllib.parse
27 import urllib.error
26 28
27 29 base_url = {
28 30 'bookmarks_home': '/{repo_name}/bookmarks',
29 31 }[name].format(**kwargs)
30 32
31 33 if params:
32 34 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
33 35 return base_url
34 36
35 37
36 38 @pytest.mark.usefixtures('autologin_user', 'app')
37 39 class TestBookmarks(object):
38 40
39 41 def test_index(self, backend):
40 42 if backend.alias == 'hg':
41 43 response = self.app.get(
42 44 route_path('bookmarks_home', repo_name=backend.repo_name))
43 45
44 46 repo = Repository.get_by_repo_name(backend.repo_name)
45 47 for commit_id, obj_name in repo.scm_instance().bookmarks.items():
46 48 assert commit_id in response
47 49 assert obj_name in response
48 50 else:
49 51 self.app.get(
50 52 route_path('bookmarks_home', repo_name=backend.repo_name),
51 53 status=404)
@@ -1,47 +1,49 b''
1 1
2 2 # Copyright (C) 2010-2020 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.db import Repository
22 22
23 23
24 24 def route_path(name, params=None, **kwargs):
25 import urllib.request, urllib.parse, urllib.error
25 import urllib.request
26 import urllib.parse
27 import urllib.error
26 28
27 29 base_url = {
28 30 'branches_home': '/{repo_name}/branches',
29 31 }[name].format(**kwargs)
30 32
31 33 if params:
32 34 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
33 35 return base_url
34 36
35 37
36 38 @pytest.mark.usefixtures('autologin_user', 'app')
37 39 class TestBranchesController(object):
38 40
39 41 def test_index(self, backend):
40 42 response = self.app.get(
41 43 route_path('branches_home', repo_name=backend.repo_name))
42 44
43 45 repo = Repository.get_by_repo_name(backend.repo_name)
44 46
45 47 for commit_id, obj_name in repo.scm_instance().branches.items():
46 48 assert commit_id in response
47 49 assert obj_name in response
@@ -1,219 +1,220 b''
1 1
2 2 # Copyright (C) 2010-2020 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 re
21 21
22 22 import pytest
23 23
24 24 from rhodecode.apps.repository.views.repo_changelog import DEFAULT_CHANGELOG_SIZE
25 25 from rhodecode.tests import TestController
26 26
27 27 MATCH_HASH = re.compile(r'<span class="commit_hash">r(\d+):[\da-f]+</span>')
28 28
29 29
30 30 def route_path(name, params=None, **kwargs):
31 import urllib.request, urllib.parse, urllib.error
31 import urllib.request
32 import urllib.parse
33 import urllib.error
32 34
33 35 base_url = {
34 36 'repo_changelog': '/{repo_name}/changelog',
35 37 'repo_commits': '/{repo_name}/commits',
36 38 'repo_commits_file': '/{repo_name}/commits/{commit_id}/{f_path}',
37 39 'repo_commits_elements': '/{repo_name}/commits_elements',
38 40 }[name].format(**kwargs)
39 41
40 42 if params:
41 43 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
42 44 return base_url
43 45
44 46
45 47 def assert_commits_on_page(response, indexes):
46 found_indexes = [int(idx) for idx in MATCH_HASH.findall(response.body)]
48 found_indexes = [int(idx) for idx in MATCH_HASH.findall(response.text)]
47 49 assert found_indexes == indexes
48 50
49 51
50 52 class TestChangelogController(TestController):
51 53
52 54 def test_commits_page(self, backend):
53 55 self.log_user()
54 56 response = self.app.get(
55 57 route_path('repo_commits', repo_name=backend.repo_name))
56 58
57 59 first_idx = -1
58 60 last_idx = -DEFAULT_CHANGELOG_SIZE
59 61 self.assert_commit_range_on_page(response, first_idx, last_idx, backend)
60 62
61 63 def test_changelog(self, backend):
62 64 self.log_user()
63 65 response = self.app.get(
64 66 route_path('repo_changelog', repo_name=backend.repo_name))
65 67
66 68 first_idx = -1
67 69 last_idx = -DEFAULT_CHANGELOG_SIZE
68 70 self.assert_commit_range_on_page(
69 71 response, first_idx, last_idx, backend)
70 72
71 73 @pytest.mark.backends("hg", "git")
72 74 def test_changelog_filtered_by_branch(self, backend):
73 75 self.log_user()
74 76 self.app.get(
75 77 route_path('repo_changelog', repo_name=backend.repo_name,
76 78 params=dict(branch=backend.default_branch_name)),
77 79 status=200)
78 80
79 81 @pytest.mark.backends("hg", "git")
80 82 def test_commits_filtered_by_branch(self, backend):
81 83 self.log_user()
82 84 self.app.get(
83 85 route_path('repo_commits', repo_name=backend.repo_name,
84 86 params=dict(branch=backend.default_branch_name)),
85 87 status=200)
86 88
87 89 @pytest.mark.backends("svn")
88 90 def test_changelog_filtered_by_branch_svn(self, autologin_user, backend):
89 91 repo = backend['svn-simple-layout']
90 92 response = self.app.get(
91 93 route_path('repo_changelog', repo_name=repo.repo_name,
92 94 params=dict(branch='trunk')),
93 95 status=200)
94 96
95 97 assert_commits_on_page(response, indexes=[15, 12, 7, 3, 2, 1])
96 98
97 99 def test_commits_filtered_by_wrong_branch(self, backend):
98 100 self.log_user()
99 101 branch = 'wrong-branch-name'
100 102 response = self.app.get(
101 103 route_path('repo_commits', repo_name=backend.repo_name,
102 104 params=dict(branch=branch)),
103 105 status=302)
104 106 expected_url = '/{repo}/commits/{branch}'.format(
105 107 repo=backend.repo_name, branch=branch)
106 108 assert expected_url in response.location
107 109 response = response.follow()
108 110 expected_warning = 'Branch {} is not found.'.format(branch)
109 111 assert expected_warning in response.text
110 112
111 113 @pytest.mark.xfail_backends("svn", reason="Depends on branch support")
112 def test_changelog_filtered_by_branch_with_merges(
113 self, autologin_user, backend):
114 def test_changelog_filtered_by_branch_with_merges(self, autologin_user, backend):
114 115
115 116 # Note: The changelog of branch "b" does not contain the commit "a1"
116 117 # although this is a parent of commit "b1". And branch "b" has commits
117 118 # which have a smaller index than commit "a1".
118 119 commits = [
119 120 {'message': 'a'},
120 121 {'message': 'b', 'branch': 'b'},
121 122 {'message': 'a1', 'parents': ['a']},
122 123 {'message': 'b1', 'branch': 'b', 'parents': ['b', 'a1']},
123 124 ]
124 125 backend.create_repo(commits)
125 126
126 127 self.app.get(
127 128 route_path('repo_changelog', repo_name=backend.repo_name,
128 129 params=dict(branch='b')),
129 130 status=200)
130 131
131 132 @pytest.mark.backends("hg")
132 133 def test_commits_closed_branches(self, autologin_user, backend):
133 134 repo = backend['closed_branch']
134 135 response = self.app.get(
135 136 route_path('repo_commits', repo_name=repo.repo_name,
136 137 params=dict(branch='experimental')),
137 138 status=200)
138 139
139 140 assert_commits_on_page(response, indexes=[3, 1])
140 141
141 142 def test_changelog_pagination(self, backend):
142 143 self.log_user()
143 144 # pagination, walk up to page 6
144 145 changelog_url = route_path(
145 146 'repo_commits', repo_name=backend.repo_name)
146 147
147 148 for page in range(1, 7):
148 149 response = self.app.get(changelog_url, {'page': page})
149 150
150 151 first_idx = -DEFAULT_CHANGELOG_SIZE * (page - 1) - 1
151 152 last_idx = -DEFAULT_CHANGELOG_SIZE * page
152 153 self.assert_commit_range_on_page(response, first_idx, last_idx, backend)
153 154
154 155 def assert_commit_range_on_page(
155 156 self, response, first_idx, last_idx, backend):
156 157 input_template = (
157 158 """<input class="commit-range" """
158 159 """data-commit-id="%(raw_id)s" data-commit-idx="%(idx)s" """
159 160 """data-short-id="%(short_id)s" id="%(raw_id)s" """
160 161 """name="%(raw_id)s" type="checkbox" value="1" />"""
161 162 )
162 163
163 164 commit_span_template = """<span class="commit_hash">r%s:%s</span>"""
164 165 repo = backend.repo
165 166
166 167 first_commit_on_page = repo.get_commit(commit_idx=first_idx)
167 168 response.mustcontain(
168 169 input_template % {'raw_id': first_commit_on_page.raw_id,
169 170 'idx': first_commit_on_page.idx,
170 171 'short_id': first_commit_on_page.short_id})
171 172
172 173 response.mustcontain(commit_span_template % (
173 174 first_commit_on_page.idx, first_commit_on_page.short_id)
174 175 )
175 176
176 177 last_commit_on_page = repo.get_commit(commit_idx=last_idx)
177 178 response.mustcontain(
178 179 input_template % {'raw_id': last_commit_on_page.raw_id,
179 180 'idx': last_commit_on_page.idx,
180 181 'short_id': last_commit_on_page.short_id})
181 182 response.mustcontain(commit_span_template % (
182 183 last_commit_on_page.idx, last_commit_on_page.short_id)
183 184 )
184 185
185 186 first_commit_of_next_page = repo.get_commit(commit_idx=last_idx - 1)
186 187 first_span_of_next_page = commit_span_template % (
187 188 first_commit_of_next_page.idx, first_commit_of_next_page.short_id)
188 189 assert first_span_of_next_page not in response
189 190
190 191 @pytest.mark.parametrize('test_path', [
191 192 'vcs/exceptions.py',
192 193 '/vcs/exceptions.py',
193 194 '//vcs/exceptions.py'
194 195 ])
195 196 def test_commits_with_filenode(self, backend, test_path):
196 197 self.log_user()
197 198 response = self.app.get(
198 199 route_path('repo_commits_file', repo_name=backend.repo_name,
199 200 commit_id='tip', f_path=test_path),
200 201 )
201 202
202 203 # history commits messages
203 204 response.mustcontain('Added exceptions module, this time for real')
204 205 response.mustcontain('Added not implemented hg backend test case')
205 206 response.mustcontain('Added BaseChangeset class')
206 207
207 208 def test_commits_with_filenode_that_is_dirnode(self, backend):
208 209 self.log_user()
209 210 self.app.get(
210 211 route_path('repo_commits_file', repo_name=backend.repo_name,
211 212 commit_id='tip', f_path='/tests'),
212 213 status=302)
213 214
214 215 def test_commits_with_filenode_not_existing(self, backend):
215 216 self.log_user()
216 217 self.app.get(
217 218 route_path('repo_commits_file', repo_name=backend.repo_name,
218 219 commit_id='tip', f_path='wrong_path'),
219 220 status=302)
@@ -1,493 +1,495 b''
1 1
2 2 # Copyright (C) 2010-2020 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
24 24 from rhodecode.model.db import ChangesetComment, Notification
25 25 from rhodecode.model.meta import Session
26 26 from rhodecode.lib import helpers as h
27 27
28 28
29 29 def route_path(name, params=None, **kwargs):
30 import urllib.request, urllib.parse, urllib.error
30 import urllib.request
31 import urllib.parse
32 import urllib.error
31 33
32 34 base_url = {
33 35 'repo_commit': '/{repo_name}/changeset/{commit_id}',
34 36 'repo_commit_comment_create': '/{repo_name}/changeset/{commit_id}/comment/create',
35 37 'repo_commit_comment_preview': '/{repo_name}/changeset/{commit_id}/comment/preview',
36 38 'repo_commit_comment_delete': '/{repo_name}/changeset/{commit_id}/comment/{comment_id}/delete',
37 39 'repo_commit_comment_edit': '/{repo_name}/changeset/{commit_id}/comment/{comment_id}/edit',
38 40 }[name].format(**kwargs)
39 41
40 42 if params:
41 43 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
42 44 return base_url
43 45
44 46
45 47 @pytest.mark.backends("git", "hg", "svn")
46 48 class TestRepoCommitCommentsView(TestController):
47 49
48 50 @pytest.fixture(autouse=True)
49 51 def prepare(self, request, baseapp):
50 52 for x in ChangesetComment.query().all():
51 53 Session().delete(x)
52 54 Session().commit()
53 55
54 56 for x in Notification.query().all():
55 57 Session().delete(x)
56 58 Session().commit()
57 59
58 60 request.addfinalizer(self.cleanup)
59 61
60 62 def cleanup(self):
61 63 for x in ChangesetComment.query().all():
62 64 Session().delete(x)
63 65 Session().commit()
64 66
65 67 for x in Notification.query().all():
66 68 Session().delete(x)
67 69 Session().commit()
68 70
69 71 @pytest.mark.parametrize('comment_type', ChangesetComment.COMMENT_TYPES)
70 72 def test_create(self, comment_type, backend):
71 73 self.log_user()
72 74 commit = backend.repo.get_commit('300')
73 75 commit_id = commit.raw_id
74 text = u'CommentOnCommit'
76 text = 'CommentOnCommit'
75 77
76 78 params = {'text': text, 'csrf_token': self.csrf_token,
77 79 'comment_type': comment_type}
78 80 self.app.post(
79 81 route_path('repo_commit_comment_create',
80 82 repo_name=backend.repo_name, commit_id=commit_id),
81 83 params=params)
82 84
83 85 response = self.app.get(
84 86 route_path('repo_commit',
85 87 repo_name=backend.repo_name, commit_id=commit_id))
86 88
87 89 # test DB
88 90 assert ChangesetComment.query().count() == 1
89 91 assert_comment_links(response, ChangesetComment.query().count(), 0)
90 92
91 93 assert Notification.query().count() == 1
92 94 assert ChangesetComment.query().count() == 1
93 95
94 96 notification = Notification.query().all()[0]
95 97
96 98 comment_id = ChangesetComment.query().first().comment_id
97 99 assert notification.type_ == Notification.TYPE_CHANGESET_COMMENT
98 100
99 101 author = notification.created_by_user.username_and_name
100 102 sbj = '@{0} left a {1} on commit `{2}` in the `{3}` repository'.format(
101 103 author, comment_type, h.show_id(commit), backend.repo_name)
102 104 assert sbj == notification.subject
103 105
104 lnk = (u'/{0}/changeset/{1}#comment-{2}'.format(
106 lnk = ('/{0}/changeset/{1}#comment-{2}'.format(
105 107 backend.repo_name, commit_id, comment_id))
106 108 assert lnk in notification.body
107 109
108 110 @pytest.mark.parametrize('comment_type', ChangesetComment.COMMENT_TYPES)
109 111 def test_create_inline(self, comment_type, backend):
110 112 self.log_user()
111 113 commit = backend.repo.get_commit('300')
112 114 commit_id = commit.raw_id
113 text = u'CommentOnCommit'
115 text = 'CommentOnCommit'
114 116 f_path = 'vcs/web/simplevcs/views/repository.py'
115 117 line = 'n1'
116 118
117 119 params = {'text': text, 'f_path': f_path, 'line': line,
118 120 'comment_type': comment_type,
119 121 'csrf_token': self.csrf_token}
120 122
121 123 self.app.post(
122 124 route_path('repo_commit_comment_create',
123 125 repo_name=backend.repo_name, commit_id=commit_id),
124 126 params=params)
125 127
126 128 response = self.app.get(
127 129 route_path('repo_commit',
128 130 repo_name=backend.repo_name, commit_id=commit_id))
129 131
130 132 # test DB
131 133 assert ChangesetComment.query().count() == 1
132 134 assert_comment_links(response, 0, ChangesetComment.query().count())
133 135
134 136 if backend.alias == 'svn':
135 137 response.mustcontain(
136 138 '''data-f-path="vcs/commands/summary.py" '''
137 139 '''data-anchor-id="c-300-ad05457a43f8"'''
138 140 )
139 141 if backend.alias == 'git':
140 142 response.mustcontain(
141 143 '''data-f-path="vcs/backends/hg.py" '''
142 144 '''data-anchor-id="c-883e775e89ea-9c390eb52cd6"'''
143 145 )
144 146
145 147 if backend.alias == 'hg':
146 148 response.mustcontain(
147 149 '''data-f-path="vcs/backends/hg.py" '''
148 150 '''data-anchor-id="c-e58d85a3973b-9c390eb52cd6"'''
149 151 )
150 152
151 153 assert Notification.query().count() == 1
152 154 assert ChangesetComment.query().count() == 1
153 155
154 156 notification = Notification.query().all()[0]
155 157 comment = ChangesetComment.query().first()
156 158 assert notification.type_ == Notification.TYPE_CHANGESET_COMMENT
157 159
158 160 assert comment.revision == commit_id
159 161
160 162 author = notification.created_by_user.username_and_name
161 163 sbj = '@{0} left a {1} on file `{2}` in commit `{3}` in the `{4}` repository'.format(
162 164 author, comment_type, f_path, h.show_id(commit), backend.repo_name)
163 165
164 166 assert sbj == notification.subject
165 167
166 lnk = (u'/{0}/changeset/{1}#comment-{2}'.format(
168 lnk = ('/{0}/changeset/{1}#comment-{2}'.format(
167 169 backend.repo_name, commit_id, comment.comment_id))
168 170 assert lnk in notification.body
169 171 assert 'on line n1' in notification.body
170 172
171 173 def test_create_with_mention(self, backend):
172 174 self.log_user()
173 175
174 176 commit_id = backend.repo.get_commit('300').raw_id
175 text = u'@test_regular check CommentOnCommit'
177 text = '@test_regular check CommentOnCommit'
176 178
177 179 params = {'text': text, 'csrf_token': self.csrf_token}
178 180 self.app.post(
179 181 route_path('repo_commit_comment_create',
180 182 repo_name=backend.repo_name, commit_id=commit_id),
181 183 params=params)
182 184
183 185 response = self.app.get(
184 186 route_path('repo_commit',
185 187 repo_name=backend.repo_name, commit_id=commit_id))
186 188 # test DB
187 189 assert ChangesetComment.query().count() == 1
188 190 assert_comment_links(response, ChangesetComment.query().count(), 0)
189 191
190 192 notification = Notification.query().one()
191 193
192 194 assert len(notification.recipients) == 2
193 195 users = [x.username for x in notification.recipients]
194 196
195 197 # test_regular gets notification by @mention
196 assert sorted(users) == [u'test_admin', u'test_regular']
198 assert sorted(users) == ['test_admin', 'test_regular']
197 199
198 200 def test_create_with_status_change(self, backend):
199 201 self.log_user()
200 202 commit = backend.repo.get_commit('300')
201 203 commit_id = commit.raw_id
202 text = u'CommentOnCommit'
204 text = 'CommentOnCommit'
203 205 f_path = 'vcs/web/simplevcs/views/repository.py'
204 206 line = 'n1'
205 207
206 208 params = {'text': text, 'changeset_status': 'approved',
207 209 'csrf_token': self.csrf_token}
208 210
209 211 self.app.post(
210 212 route_path(
211 213 'repo_commit_comment_create',
212 214 repo_name=backend.repo_name, commit_id=commit_id),
213 215 params=params)
214 216
215 217 response = self.app.get(
216 218 route_path('repo_commit',
217 219 repo_name=backend.repo_name, commit_id=commit_id))
218 220
219 221 # test DB
220 222 assert ChangesetComment.query().count() == 1
221 223 assert_comment_links(response, ChangesetComment.query().count(), 0)
222 224
223 225 assert Notification.query().count() == 1
224 226 assert ChangesetComment.query().count() == 1
225 227
226 228 notification = Notification.query().all()[0]
227 229
228 230 comment_id = ChangesetComment.query().first().comment_id
229 231 assert notification.type_ == Notification.TYPE_CHANGESET_COMMENT
230 232
231 233 author = notification.created_by_user.username_and_name
232 234 sbj = '[status: Approved] @{0} left a note on commit `{1}` in the `{2}` repository'.format(
233 235 author, h.show_id(commit), backend.repo_name)
234 236 assert sbj == notification.subject
235 237
236 lnk = (u'/{0}/changeset/{1}#comment-{2}'.format(
238 lnk = ('/{0}/changeset/{1}#comment-{2}'.format(
237 239 backend.repo_name, commit_id, comment_id))
238 240 assert lnk in notification.body
239 241
240 242 def test_delete(self, backend):
241 243 self.log_user()
242 244 commit_id = backend.repo.get_commit('300').raw_id
243 text = u'CommentOnCommit'
245 text = 'CommentOnCommit'
244 246
245 247 params = {'text': text, 'csrf_token': self.csrf_token}
246 248 self.app.post(
247 249 route_path(
248 250 'repo_commit_comment_create',
249 251 repo_name=backend.repo_name, commit_id=commit_id),
250 252 params=params)
251 253
252 254 comments = ChangesetComment.query().all()
253 255 assert len(comments) == 1
254 256 comment_id = comments[0].comment_id
255 257
256 258 self.app.post(
257 259 route_path('repo_commit_comment_delete',
258 260 repo_name=backend.repo_name,
259 261 commit_id=commit_id,
260 262 comment_id=comment_id),
261 263 params={'csrf_token': self.csrf_token})
262 264
263 265 comments = ChangesetComment.query().all()
264 266 assert len(comments) == 0
265 267
266 268 response = self.app.get(
267 269 route_path('repo_commit',
268 270 repo_name=backend.repo_name, commit_id=commit_id))
269 271 assert_comment_links(response, 0, 0)
270 272
271 273 def test_edit(self, backend):
272 274 self.log_user()
273 275 commit_id = backend.repo.get_commit('300').raw_id
274 text = u'CommentOnCommit'
276 text = 'CommentOnCommit'
275 277
276 278 params = {'text': text, 'csrf_token': self.csrf_token}
277 279 self.app.post(
278 280 route_path(
279 281 'repo_commit_comment_create',
280 282 repo_name=backend.repo_name, commit_id=commit_id),
281 283 params=params)
282 284
283 285 comments = ChangesetComment.query().all()
284 286 assert len(comments) == 1
285 287 comment_id = comments[0].comment_id
286 288 test_text = 'test_text'
287 289 self.app.post(
288 290 route_path(
289 291 'repo_commit_comment_edit',
290 292 repo_name=backend.repo_name,
291 293 commit_id=commit_id,
292 294 comment_id=comment_id,
293 295 ),
294 296 params={
295 297 'csrf_token': self.csrf_token,
296 298 'text': test_text,
297 299 'version': '0',
298 300 })
299 301
300 302 text_form_db = ChangesetComment.query().filter(
301 303 ChangesetComment.comment_id == comment_id).first().text
302 304 assert test_text == text_form_db
303 305
304 306 def test_edit_without_change(self, backend):
305 307 self.log_user()
306 308 commit_id = backend.repo.get_commit('300').raw_id
307 text = u'CommentOnCommit'
309 text = 'CommentOnCommit'
308 310
309 311 params = {'text': text, 'csrf_token': self.csrf_token}
310 312 self.app.post(
311 313 route_path(
312 314 'repo_commit_comment_create',
313 315 repo_name=backend.repo_name, commit_id=commit_id),
314 316 params=params)
315 317
316 318 comments = ChangesetComment.query().all()
317 319 assert len(comments) == 1
318 320 comment_id = comments[0].comment_id
319 321
320 322 response = self.app.post(
321 323 route_path(
322 324 'repo_commit_comment_edit',
323 325 repo_name=backend.repo_name,
324 326 commit_id=commit_id,
325 327 comment_id=comment_id,
326 328 ),
327 329 params={
328 330 'csrf_token': self.csrf_token,
329 331 'text': text,
330 332 'version': '0',
331 333 },
332 334 status=404,
333 335 )
334 336 assert response.status_int == 404
335 337
336 338 def test_edit_try_edit_already_edited(self, backend):
337 339 self.log_user()
338 340 commit_id = backend.repo.get_commit('300').raw_id
339 text = u'CommentOnCommit'
341 text = 'CommentOnCommit'
340 342
341 343 params = {'text': text, 'csrf_token': self.csrf_token}
342 344 self.app.post(
343 345 route_path(
344 346 'repo_commit_comment_create',
345 347 repo_name=backend.repo_name, commit_id=commit_id
346 348 ),
347 349 params=params,
348 350 )
349 351
350 352 comments = ChangesetComment.query().all()
351 353 assert len(comments) == 1
352 354 comment_id = comments[0].comment_id
353 355 test_text = 'test_text'
354 356 self.app.post(
355 357 route_path(
356 358 'repo_commit_comment_edit',
357 359 repo_name=backend.repo_name,
358 360 commit_id=commit_id,
359 361 comment_id=comment_id,
360 362 ),
361 363 params={
362 364 'csrf_token': self.csrf_token,
363 365 'text': test_text,
364 366 'version': '0',
365 367 }
366 368 )
367 369 test_text_v2 = 'test_v2'
368 370 response = self.app.post(
369 371 route_path(
370 372 'repo_commit_comment_edit',
371 373 repo_name=backend.repo_name,
372 374 commit_id=commit_id,
373 375 comment_id=comment_id,
374 376 ),
375 377 params={
376 378 'csrf_token': self.csrf_token,
377 379 'text': test_text_v2,
378 380 'version': '0',
379 381 },
380 382 status=409,
381 383 )
382 384 assert response.status_int == 409
383 385
384 386 text_form_db = ChangesetComment.query().filter(
385 387 ChangesetComment.comment_id == comment_id).first().text
386 388
387 389 assert test_text == text_form_db
388 390 assert test_text_v2 != text_form_db
389 391
390 392 def test_edit_forbidden_for_immutable_comments(self, backend):
391 393 self.log_user()
392 394 commit_id = backend.repo.get_commit('300').raw_id
393 text = u'CommentOnCommit'
395 text = 'CommentOnCommit'
394 396
395 397 params = {'text': text, 'csrf_token': self.csrf_token, 'version': '0'}
396 398 self.app.post(
397 399 route_path(
398 400 'repo_commit_comment_create',
399 401 repo_name=backend.repo_name,
400 402 commit_id=commit_id,
401 403 ),
402 404 params=params
403 405 )
404 406
405 407 comments = ChangesetComment.query().all()
406 408 assert len(comments) == 1
407 409 comment_id = comments[0].comment_id
408 410
409 411 comment = ChangesetComment.get(comment_id)
410 412 comment.immutable_state = ChangesetComment.OP_IMMUTABLE
411 413 Session().add(comment)
412 414 Session().commit()
413 415
414 416 response = self.app.post(
415 417 route_path(
416 418 'repo_commit_comment_edit',
417 419 repo_name=backend.repo_name,
418 420 commit_id=commit_id,
419 421 comment_id=comment_id,
420 422 ),
421 423 params={
422 424 'csrf_token': self.csrf_token,
423 425 'text': 'test_text',
424 426 },
425 427 status=403,
426 428 )
427 429 assert response.status_int == 403
428 430
429 431 def test_delete_forbidden_for_immutable_comments(self, backend):
430 432 self.log_user()
431 433 commit_id = backend.repo.get_commit('300').raw_id
432 text = u'CommentOnCommit'
434 text = 'CommentOnCommit'
433 435
434 436 params = {'text': text, 'csrf_token': self.csrf_token}
435 437 self.app.post(
436 438 route_path(
437 439 'repo_commit_comment_create',
438 440 repo_name=backend.repo_name, commit_id=commit_id),
439 441 params=params)
440 442
441 443 comments = ChangesetComment.query().all()
442 444 assert len(comments) == 1
443 445 comment_id = comments[0].comment_id
444 446
445 447 comment = ChangesetComment.get(comment_id)
446 448 comment.immutable_state = ChangesetComment.OP_IMMUTABLE
447 449 Session().add(comment)
448 450 Session().commit()
449 451
450 452 self.app.post(
451 453 route_path('repo_commit_comment_delete',
452 454 repo_name=backend.repo_name,
453 455 commit_id=commit_id,
454 456 comment_id=comment_id),
455 457 params={'csrf_token': self.csrf_token},
456 458 status=403)
457 459
458 460 @pytest.mark.parametrize('renderer, text_input, output', [
459 461 ('rst', 'plain text', '<p>plain text</p>'),
460 462 ('rst', 'header\n======', '<h1 class="title">header</h1>'),
461 463 ('rst', '*italics*', '<em>italics</em>'),
462 464 ('rst', '**bold**', '<strong>bold</strong>'),
463 465 ('markdown', 'plain text', '<p>plain text</p>'),
464 466 ('markdown', '# header', '<h1>header</h1>'),
465 467 ('markdown', '*italics*', '<em>italics</em>'),
466 468 ('markdown', '**bold**', '<strong>bold</strong>'),
467 469 ], ids=['rst-plain', 'rst-header', 'rst-italics', 'rst-bold', 'md-plain',
468 470 'md-header', 'md-italics', 'md-bold', ])
469 471 def test_preview(self, renderer, text_input, output, backend, xhr_header):
470 472 self.log_user()
471 473 params = {
472 474 'renderer': renderer,
473 475 'text': text_input,
474 476 'csrf_token': self.csrf_token
475 477 }
476 478 commit_id = '0' * 16 # fake this for tests
477 479 response = self.app.post(
478 480 route_path('repo_commit_comment_preview',
479 481 repo_name=backend.repo_name, commit_id=commit_id,),
480 482 params=params,
481 483 extra_environ=xhr_header)
482 484
483 485 response.mustcontain(output)
484 486
485 487
486 488 def assert_comment_links(response, comments, inline_comments):
487 489 response.mustcontain(
488 490 '<span class="display-none" id="general-comments-count">{}</span>'.format(comments))
489 491 response.mustcontain(
490 492 '<span class="display-none" id="inline-comments-count">{}</span>'.format(inline_comments))
491 493
492 494
493 495
@@ -1,326 +1,336 b''
1 1
2 2 # Copyright (C) 2010-2020 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 24
25 25
26 26 def route_path(name, params=None, **kwargs):
27 import urllib.request, urllib.parse, urllib.error
27 import urllib.request
28 import urllib.parse
29 import urllib.error
28 30
29 31 base_url = {
30 32 'repo_commit': '/{repo_name}/changeset/{commit_id}',
31 33 'repo_commit_children': '/{repo_name}/changeset_children/{commit_id}',
32 34 'repo_commit_parents': '/{repo_name}/changeset_parents/{commit_id}',
33 35 'repo_commit_raw': '/{repo_name}/changeset-diff/{commit_id}',
34 36 'repo_commit_patch': '/{repo_name}/changeset-patch/{commit_id}',
35 37 'repo_commit_download': '/{repo_name}/changeset-download/{commit_id}',
36 38 'repo_commit_data': '/{repo_name}/changeset-data/{commit_id}',
37 39 'repo_compare': '/{repo_name}/compare/{source_ref_type}@{source_ref}...{target_ref_type}@{target_ref}',
38 40 }[name].format(**kwargs)
39 41
40 42 if params:
41 43 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
42 44 return base_url
43 45
44 46
45 47 @pytest.mark.usefixtures("app")
46 48 class TestRepoCommitView(object):
47 49
48 50 def test_show_commit(self, backend):
49 51 commit_id = self.commit_id[backend.alias]
50 52 response = self.app.get(route_path(
51 53 'repo_commit', repo_name=backend.repo_name, commit_id=commit_id))
52 54 response.mustcontain('Added a symlink')
53 55 response.mustcontain(commit_id)
54 56 response.mustcontain('No newline at end of file')
55 57
56 58 def test_show_raw(self, backend):
57 59 commit_id = self.commit_id[backend.alias]
60 # webtest uses linter to check if response is bytes,
61 # and we use memoryview here as a wrapper, quick turn-off
62 self.app.lint = False
63
58 64 response = self.app.get(route_path(
59 65 'repo_commit_raw',
60 66 repo_name=backend.repo_name, commit_id=commit_id))
61 assert response.text == self.diffs[backend.alias]
67 assert response.body == self.diffs[backend.alias]
62 68
63 69 def test_show_raw_patch(self, backend):
64 70 response = self.app.get(route_path(
65 71 'repo_commit_patch', repo_name=backend.repo_name,
66 72 commit_id=self.commit_id[backend.alias]))
67 assert response.text == self.patches[backend.alias]
73 assert response.body == self.patches[backend.alias]
68 74
69 75 def test_commit_download(self, backend):
76 # webtest uses linter to check if response is bytes,
77 # and we use memoryview here as a wrapper, quick turn-off
78 self.app.lint = False
79
70 80 response = self.app.get(route_path(
71 81 'repo_commit_download',
72 82 repo_name=backend.repo_name,
73 83 commit_id=self.commit_id[backend.alias]))
74 assert response.text == self.diffs[backend.alias]
84 assert response.body == self.diffs[backend.alias]
75 85
76 86 def test_single_commit_page_different_ops(self, backend):
77 87 commit_id = {
78 88 'hg': '603d6c72c46d953420c89d36372f08d9f305f5dd',
79 89 'git': '03fa803d7e9fb14daa9a3089e0d1494eda75d986',
80 90 'svn': '337',
81 91 }
82 92 diff_stat = {
83 93 'hg': (21, 943, 288),
84 94 'git': (20, 941, 286),
85 95 'svn': (21, 943, 288),
86 96 }
87 97
88 98 commit_id = commit_id[backend.alias]
89 99 response = self.app.get(route_path(
90 100 'repo_commit',
91 101 repo_name=backend.repo_name, commit_id=commit_id))
92 102
93 103 response.mustcontain(_shorten_commit_id(commit_id))
94 104
95 105 compare_page = ComparePage(response)
96 106 file_changes = diff_stat[backend.alias]
97 107 compare_page.contains_change_summary(*file_changes)
98 108
99 109 # files op files
100 110 response.mustcontain('File not present at commit: %s' %
101 111 _shorten_commit_id(commit_id))
102 112
103 113 # svn uses a different filename
104 114 if backend.alias == 'svn':
105 115 response.mustcontain('new file 10644')
106 116 else:
107 117 response.mustcontain('new file 100644')
108 118 response.mustcontain('Changed theme to ADC theme') # commit msg
109 119
110 120 self._check_new_diff_menus(response, right_menu=True)
111 121
112 122 def test_commit_range_page_different_ops(self, backend):
113 123 commit_id_range = {
114 124 'hg': (
115 125 '25d7e49c18b159446cadfa506a5cf8ad1cb04067',
116 126 '603d6c72c46d953420c89d36372f08d9f305f5dd'),
117 127 'git': (
118 128 '6fc9270775aaf5544c1deb014f4ddd60c952fcbb',
119 129 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'),
120 130 'svn': (
121 131 '335',
122 132 '337'),
123 133 }
124 134 commit_ids = commit_id_range[backend.alias]
125 135 commit_id = '%s...%s' % (commit_ids[0], commit_ids[1])
126 136 response = self.app.get(route_path(
127 137 'repo_commit',
128 138 repo_name=backend.repo_name, commit_id=commit_id))
129 139
130 140 response.mustcontain(_shorten_commit_id(commit_ids[0]))
131 141 response.mustcontain(_shorten_commit_id(commit_ids[1]))
132 142
133 143 compare_page = ComparePage(response)
134 144
135 145 # svn is special
136 146 if backend.alias == 'svn':
137 147 response.mustcontain('new file 10644')
138 148 for file_changes in [(1, 5, 1), (12, 236, 22), (21, 943, 288)]:
139 149 compare_page.contains_change_summary(*file_changes)
140 150 elif backend.alias == 'git':
141 151 response.mustcontain('new file 100644')
142 152 for file_changes in [(12, 222, 20), (20, 941, 286)]:
143 153 compare_page.contains_change_summary(*file_changes)
144 154 else:
145 155 response.mustcontain('new file 100644')
146 156 for file_changes in [(12, 222, 20), (21, 943, 288)]:
147 157 compare_page.contains_change_summary(*file_changes)
148 158
149 159 # files op files
150 160 response.mustcontain('File not present at commit: %s' % _shorten_commit_id(commit_ids[1]))
151 161 response.mustcontain('Added docstrings to vcs.cli') # commit msg
152 162 response.mustcontain('Changed theme to ADC theme') # commit msg
153 163
154 164 self._check_new_diff_menus(response)
155 165
156 166 def test_combined_compare_commit_page_different_ops(self, backend):
157 167 commit_id_range = {
158 168 'hg': (
159 169 '4fdd71e9427417b2e904e0464c634fdee85ec5a7',
160 170 '603d6c72c46d953420c89d36372f08d9f305f5dd'),
161 171 'git': (
162 172 'f5fbf9cfd5f1f1be146f6d3b38bcd791a7480c13',
163 173 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'),
164 174 'svn': (
165 175 '335',
166 176 '337'),
167 177 }
168 178 commit_ids = commit_id_range[backend.alias]
169 179 response = self.app.get(route_path(
170 180 'repo_compare',
171 181 repo_name=backend.repo_name,
172 182 source_ref_type='rev', source_ref=commit_ids[0],
173 183 target_ref_type='rev', target_ref=commit_ids[1], ))
174 184
175 185 response.mustcontain(_shorten_commit_id(commit_ids[0]))
176 186 response.mustcontain(_shorten_commit_id(commit_ids[1]))
177 187
178 188 # files op files
179 189 response.mustcontain('File not present at commit: %s' %
180 190 _shorten_commit_id(commit_ids[1]))
181 191
182 192 compare_page = ComparePage(response)
183 193
184 194 # svn is special
185 195 if backend.alias == 'svn':
186 196 response.mustcontain('new file 10644')
187 197 file_changes = (32, 1179, 310)
188 198 compare_page.contains_change_summary(*file_changes)
189 199 elif backend.alias == 'git':
190 200 response.mustcontain('new file 100644')
191 201 file_changes = (31, 1163, 306)
192 202 compare_page.contains_change_summary(*file_changes)
193 203 else:
194 204 response.mustcontain('new file 100644')
195 205 file_changes = (32, 1165, 308)
196 206 compare_page.contains_change_summary(*file_changes)
197 207
198 208 response.mustcontain('Added docstrings to vcs.cli') # commit msg
199 209 response.mustcontain('Changed theme to ADC theme') # commit msg
200 210
201 211 self._check_new_diff_menus(response)
202 212
203 213 def test_changeset_range(self, backend):
204 214 self._check_changeset_range(
205 215 backend, self.commit_id_range, self.commit_id_range_result)
206 216
207 217 def test_changeset_range_with_initial_commit(self, backend):
208 218 commit_id_range = {
209 219 'hg': (
210 220 'b986218ba1c9b0d6a259fac9b050b1724ed8e545'
211 221 '...6cba7170863a2411822803fa77a0a264f1310b35'),
212 222 'git': (
213 223 'c1214f7e79e02fc37156ff215cd71275450cffc3'
214 224 '...fa6600f6848800641328adbf7811fd2372c02ab2'),
215 225 'svn': '1...3',
216 226 }
217 227 commit_id_range_result = {
218 228 'hg': ['b986218ba1c9', '3d8f361e72ab', '6cba7170863a'],
219 229 'git': ['c1214f7e79e0', '38b5fe81f109', 'fa6600f68488'],
220 230 'svn': ['1', '2', '3'],
221 231 }
222 232 self._check_changeset_range(
223 233 backend, commit_id_range, commit_id_range_result)
224 234
225 235 def _check_changeset_range(
226 236 self, backend, commit_id_ranges, commit_id_range_result):
227 237 response = self.app.get(
228 238 route_path('repo_commit',
229 239 repo_name=backend.repo_name,
230 240 commit_id=commit_id_ranges[backend.alias]))
231 241
232 242 expected_result = commit_id_range_result[backend.alias]
233 243 response.mustcontain('{} commits'.format(len(expected_result)))
234 244 for commit_id in expected_result:
235 245 response.mustcontain(commit_id)
236 246
237 247 commit_id = {
238 248 'hg': '2062ec7beeeaf9f44a1c25c41479565040b930b2',
239 249 'svn': '393',
240 250 'git': 'fd627b9e0dd80b47be81af07c4a98518244ed2f7',
241 251 }
242 252
243 253 commit_id_range = {
244 254 'hg': (
245 255 'a53d9201d4bc278910d416d94941b7ea007ecd52'
246 256 '...2062ec7beeeaf9f44a1c25c41479565040b930b2'),
247 257 'git': (
248 258 '7ab37bc680b4aa72c34d07b230c866c28e9fc204'
249 259 '...fd627b9e0dd80b47be81af07c4a98518244ed2f7'),
250 260 'svn': '391...393',
251 261 }
252 262
253 263 commit_id_range_result = {
254 264 'hg': ['a53d9201d4bc', '96507bd11ecc', '2062ec7beeea'],
255 265 'git': ['7ab37bc680b4', '5f2c6ee19592', 'fd627b9e0dd8'],
256 266 'svn': ['391', '392', '393'],
257 267 }
258 268
259 269 diffs = {
260 'hg': r"""diff --git a/README b/README
270 'hg': br"""diff --git a/README b/README
261 271 new file mode 120000
262 272 --- /dev/null
263 273 +++ b/README
264 274 @@ -0,0 +1,1 @@
265 275 +README.rst
266 276 \ No newline at end of file
267 277 """,
268 'git': r"""diff --git a/README b/README
278 'git': br"""diff --git a/README b/README
269 279 new file mode 120000
270 280 index 0000000..92cacd2
271 281 --- /dev/null
272 282 +++ b/README
273 283 @@ -0,0 +1 @@
274 284 +README.rst
275 285 \ No newline at end of file
276 286 """,
277 'svn': """Index: README
287 'svn': b"""Index: README
278 288 ===================================================================
279 289 diff --git a/README b/README
280 290 new file mode 10644
281 291 --- /dev/null\t(revision 0)
282 292 +++ b/README\t(revision 393)
283 293 @@ -0,0 +1 @@
284 294 +link README.rst
285 295 \\ No newline at end of file
286 296 """,
287 297 }
288 298
289 299 patches = {
290 'hg': r"""# HG changeset patch
300 'hg': br"""# HG changeset patch
291 301 # User Marcin Kuzminski <marcin@python-works.com>
292 302 # Date 2014-01-07 12:21:40
293 303 # Node ID 2062ec7beeeaf9f44a1c25c41479565040b930b2
294 304 # Parent 96507bd11ecc815ebc6270fdf6db110928c09c1e
295 305
296 306 Added a symlink
297 307
298 308 """ + diffs['hg'],
299 'git': r"""From fd627b9e0dd80b47be81af07c4a98518244ed2f7 2014-01-07 12:22:20
309 'git': br"""From fd627b9e0dd80b47be81af07c4a98518244ed2f7 2014-01-07 12:22:20
300 310 From: Marcin Kuzminski <marcin@python-works.com>
301 311 Date: 2014-01-07 12:22:20
302 312 Subject: [PATCH] Added a symlink
303 313
304 314 ---
305 315
306 316 """ + diffs['git'],
307 'svn': r"""# SVN changeset patch
317 'svn': br"""# SVN changeset patch
308 318 # User marcin
309 319 # Date 2014-09-02 12:25:22.071142
310 320 # Revision 393
311 321
312 322 Added a symlink
313 323
314 324 """ + diffs['svn'],
315 325 }
316 326
317 327 def _check_new_diff_menus(self, response, right_menu=False,):
318 328 # individual file diff menus
319 329 for elem in ['Show file before', 'Show file after']:
320 330 response.mustcontain(elem)
321 331
322 332 # right pane diff menus
323 333 if right_menu:
324 334 for elem in ['Hide whitespace changes', 'Toggle wide diff',
325 335 'Show full context diff']:
326 336 response.mustcontain(elem)
@@ -1,671 +1,670 b''
1 1
2 2 # Copyright (C) 2010-2020 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 27
28 28
29 29 def route_path(name, params=None, **kwargs):
30 import urllib.request, urllib.parse, urllib.error
30 import urllib.request
31 import urllib.parse
32 import urllib.error
31 33
32 34 base_url = {
33 35 'repo_compare_select': '/{repo_name}/compare',
34 36 'repo_compare': '/{repo_name}/compare/{source_ref_type}@{source_ref}...{target_ref_type}@{target_ref}',
35 37 }[name].format(**kwargs)
36 38
37 39 if params:
38 40 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
39 41 return base_url
40 42
41 43
42 44 @pytest.mark.usefixtures("autologin_user", "app")
43 45 class TestCompareView(object):
44 46
45 47 def test_compare_index_is_reached_at_least_once(self, backend):
46 48 repo = backend.repo
47 49 self.app.get(
48 50 route_path('repo_compare_select', repo_name=repo.repo_name))
49 51
50 52 @pytest.mark.xfail_backends("svn", reason="Requires pull")
51 53 def test_compare_remote_with_different_commit_indexes(self, backend):
52 54 # Preparing the following repository structure:
53 55 #
54 56 # Origin repository has two commits:
55 57 #
56 58 # 0 1
57 59 # A -- D
58 60 #
59 61 # The fork of it has a few more commits and "D" has a commit index
60 62 # which does not exist in origin.
61 63 #
62 64 # 0 1 2 3 4
63 65 # A -- -- -- D -- E
64 66 # \- B -- C
65 67 #
66 68
67 69 fork = backend.create_repo()
70 origin = backend.create_repo()
68 71
69 72 # prepare fork
70 73 commit0 = commit_change(
71 fork.repo_name, filename='file1', content='A',
72 message='A', vcs_type=backend.alias, parent=None, newfile=True)
74 fork.repo_name, filename=b'file1', content=b'A',
75 message='A - Initial Commit', vcs_type=backend.alias, parent=None, newfile=True)
73 76
74 77 commit1 = commit_change(
75 fork.repo_name, filename='file1', content='B',
78 fork.repo_name, filename=b'file1', content=b'B',
76 79 message='B, child of A', vcs_type=backend.alias, parent=commit0)
77 80
78 81 commit_change( # commit 2
79 fork.repo_name, filename='file1', content='C',
82 fork.repo_name, filename=b'file1', content=b'C',
80 83 message='C, child of B', vcs_type=backend.alias, parent=commit1)
81 84
82 85 commit3 = commit_change(
83 fork.repo_name, filename='file1', content='D',
86 fork.repo_name, filename=b'file1', content=b'D',
84 87 message='D, child of A', vcs_type=backend.alias, parent=commit0)
85 88
86 89 commit4 = commit_change(
87 fork.repo_name, filename='file1', content='E',
90 fork.repo_name, filename=b'file1', content=b'E',
88 91 message='E, child of D', vcs_type=backend.alias, parent=commit3)
89 92
90 93 # prepare origin repository, taking just the history up to D
91 origin = backend.create_repo()
92 94
93 95 origin_repo = origin.scm_instance(cache=False)
94 96 origin_repo.config.clear_section('hooks')
95 97 origin_repo.pull(fork.repo_full_path, commit_ids=[commit3.raw_id])
96 98 origin_repo = origin.scm_instance(cache=False) # cache rebuild
97 99
98 100 # Verify test fixture setup
99 101 # This does not work for git
100 102 if backend.alias != 'git':
101 assert 5 == len(fork.scm_instance().commit_ids)
103 assert 5 == len(fork.scm_instance(cache=False).commit_ids)
102 104 assert 2 == len(origin_repo.commit_ids)
103 105
104 106 # Comparing the revisions
105 107 response = self.app.get(
106 108 route_path('repo_compare',
107 109 repo_name=origin.repo_name,
108 110 source_ref_type="rev", source_ref=commit3.raw_id,
109 111 target_ref_type="rev", target_ref=commit4.raw_id,
110 112 params=dict(merge='1', target_repo=fork.repo_name)
111 ))
113 ),
114 status=200)
112 115
113 116 compare_page = ComparePage(response)
114 117 compare_page.contains_commits([commit4])
115 118
116 119 @pytest.mark.xfail_backends("svn", reason="Depends on branch support")
117 120 def test_compare_forks_on_branch_extra_commits(self, backend):
118 121 repo1 = backend.create_repo()
119 122
120 123 # commit something !
121 124 commit0 = commit_change(
122 repo1.repo_name, filename='file1', content='line1\n',
125 repo1.repo_name, filename=b'file1', content=b'line1\n',
123 126 message='commit1', vcs_type=backend.alias, parent=None,
124 127 newfile=True)
125 128
126 129 # fork this repo
127 130 repo2 = backend.create_fork()
128 131
129 132 # add two extra commit into fork
130 133 commit1 = commit_change(
131 repo2.repo_name, filename='file1', content='line1\nline2\n',
134 repo2.repo_name, filename=b'file1', content=b'line1\nline2\n',
132 135 message='commit2', vcs_type=backend.alias, parent=commit0)
133 136
134 137 commit2 = commit_change(
135 repo2.repo_name, filename='file1', content='line1\nline2\nline3\n',
138 repo2.repo_name, filename=b'file1', content=b'line1\nline2\nline3\n',
136 139 message='commit3', vcs_type=backend.alias, parent=commit1)
137 140
138 141 commit_id1 = repo1.scm_instance().DEFAULT_BRANCH_NAME
139 142 commit_id2 = repo2.scm_instance().DEFAULT_BRANCH_NAME
140 143
141 144 response = self.app.get(
142 145 route_path('repo_compare',
143 146 repo_name=repo1.repo_name,
144 147 source_ref_type="branch", source_ref=commit_id2,
145 148 target_ref_type="branch", target_ref=commit_id1,
146 149 params=dict(merge='1', target_repo=repo2.repo_name)
147 150 ))
148 151
149 152 response.mustcontain('%s@%s' % (repo1.repo_name, commit_id2))
150 153 response.mustcontain('%s@%s' % (repo2.repo_name, commit_id1))
151 154
152 155 compare_page = ComparePage(response)
153 156 compare_page.contains_change_summary(1, 2, 0)
154 157 compare_page.contains_commits([commit1, commit2])
155 158
156 159 anchor = 'a_c-{}-826e8142e6ba'.format(commit0.short_id)
157 160 compare_page.contains_file_links_and_anchors([('file1', anchor), ])
158 161
159 162 # Swap is removed when comparing branches since it's a PR feature and
160 163 # it is then a preview mode
161 164 compare_page.swap_is_hidden()
162 165 compare_page.target_source_are_disabled()
163 166
164 167 @pytest.mark.xfail_backends("svn", reason="Depends on branch support")
165 168 def test_compare_forks_on_branch_extra_commits_origin_has_incomming(self, backend):
166 169 repo1 = backend.create_repo()
167 170
168 171 # commit something !
169 172 commit0 = commit_change(
170 repo1.repo_name, filename='file1', content='line1\n',
173 repo1.repo_name, filename=b'file1', content=b'line1\n',
171 174 message='commit1', vcs_type=backend.alias, parent=None,
172 175 newfile=True)
173 176
174 177 # fork this repo
175 178 repo2 = backend.create_fork()
176 179
177 180 # now commit something to origin repo
178 181 commit_change(
179 repo1.repo_name, filename='file2', content='line1file2\n',
182 repo1.repo_name, filename=b'file2', content=b'line1file2\n',
180 183 message='commit2', vcs_type=backend.alias, parent=commit0,
181 184 newfile=True)
182 185
183 186 # add two extra commit into fork
184 187 commit1 = commit_change(
185 repo2.repo_name, filename='file1', content='line1\nline2\n',
188 repo2.repo_name, filename=b'file1', content=b'line1\nline2\n',
186 189 message='commit2', vcs_type=backend.alias, parent=commit0)
187 190
188 191 commit2 = commit_change(
189 repo2.repo_name, filename='file1', content='line1\nline2\nline3\n',
192 repo2.repo_name, filename=b'file1', content=b'line1\nline2\nline3\n',
190 193 message='commit3', vcs_type=backend.alias, parent=commit1)
191 194
192 195 commit_id1 = repo1.scm_instance().DEFAULT_BRANCH_NAME
193 196 commit_id2 = repo2.scm_instance().DEFAULT_BRANCH_NAME
194 197
195 198 response = self.app.get(
196 199 route_path('repo_compare',
197 200 repo_name=repo1.repo_name,
198 201 source_ref_type="branch", source_ref=commit_id2,
199 202 target_ref_type="branch", target_ref=commit_id1,
200 203 params=dict(merge='1', target_repo=repo2.repo_name),
201 204 ))
202 205
203 206 response.mustcontain('%s@%s' % (repo1.repo_name, commit_id2))
204 207 response.mustcontain('%s@%s' % (repo2.repo_name, commit_id1))
205 208
206 209 compare_page = ComparePage(response)
207 210 compare_page.contains_change_summary(1, 2, 0)
208 211 compare_page.contains_commits([commit1, commit2])
209 212 anchor = 'a_c-{}-826e8142e6ba'.format(commit0.short_id)
210 213 compare_page.contains_file_links_and_anchors([('file1', anchor), ])
211 214
212 215 # Swap is removed when comparing branches since it's a PR feature and
213 216 # it is then a preview mode
214 217 compare_page.swap_is_hidden()
215 218 compare_page.target_source_are_disabled()
216 219
217 220 @pytest.mark.xfail_backends("svn")
218 221 # TODO(marcink): no svn support for compare two seperate repos
219 222 def test_compare_of_unrelated_forks(self, backend):
220 223 orig = backend.create_repo(number_of_commits=1)
221 224 fork = backend.create_repo(number_of_commits=1)
222 225
223 226 response = self.app.get(
224 227 route_path('repo_compare',
225 228 repo_name=orig.repo_name,
226 229 source_ref_type="rev", source_ref="tip",
227 230 target_ref_type="rev", target_ref="tip",
228 231 params=dict(merge='1', target_repo=fork.repo_name),
229 232 ),
230 233 status=302)
231 234 response = response.follow()
232 235 response.mustcontain("Repositories unrelated.")
233 236
234 237 @pytest.mark.xfail_backends("svn")
235 238 def test_compare_cherry_pick_commits_from_bottom(self, backend):
236 239
237 240 # repo1:
238 241 # commit0:
239 242 # commit1:
240 243 # repo1-fork- in which we will cherry pick bottom commits
241 244 # commit0:
242 245 # commit1:
243 246 # commit2: x
244 247 # commit3: x
245 248 # commit4: x
246 249 # commit5:
247 250 # make repo1, and commit1+commit2
248 251
249 252 repo1 = backend.create_repo()
250 253
251 254 # commit something !
252 255 commit0 = commit_change(
253 repo1.repo_name, filename='file1', content='line1\n',
256 repo1.repo_name, filename=b'file1', content=b'line1\n',
254 257 message='commit1', vcs_type=backend.alias, parent=None,
255 258 newfile=True)
256 259 commit1 = commit_change(
257 repo1.repo_name, filename='file1', content='line1\nline2\n',
260 repo1.repo_name, filename=b'file1', content=b'line1\nline2\n',
258 261 message='commit2', vcs_type=backend.alias, parent=commit0)
259 262
260 263 # fork this repo
261 264 repo2 = backend.create_fork()
262 265
263 266 # now make commit3-6
264 267 commit2 = commit_change(
265 repo1.repo_name, filename='file1', content='line1\nline2\nline3\n',
268 repo1.repo_name, filename=b'file1', content=b'line1\nline2\nline3\n',
266 269 message='commit3', vcs_type=backend.alias, parent=commit1)
267 270 commit3 = commit_change(
268 repo1.repo_name, filename='file1',
269 content='line1\nline2\nline3\nline4\n', message='commit4',
270 vcs_type=backend.alias, parent=commit2)
271 repo1.repo_name, filename=b'file1',content=b'line1\nline2\nline3\nline4\n',
272 message='commit4', vcs_type=backend.alias, parent=commit2)
271 273 commit4 = commit_change(
272 repo1.repo_name, filename='file1',
273 content='line1\nline2\nline3\nline4\nline5\n', message='commit5',
274 vcs_type=backend.alias, parent=commit3)
274 repo1.repo_name, filename=b'file1', content=b'line1\nline2\nline3\nline4\nline5\n',
275 message='commit5', vcs_type=backend.alias, parent=commit3)
275 276 commit_change( # commit 5
276 repo1.repo_name, filename='file1',
277 content='line1\nline2\nline3\nline4\nline5\nline6\n',
277 repo1.repo_name, filename=b'file1', content=b'line1\nline2\nline3\nline4\nline5\nline6\n',
278 278 message='commit6', vcs_type=backend.alias, parent=commit4)
279 279
280 280 response = self.app.get(
281 281 route_path('repo_compare',
282 282 repo_name=repo2.repo_name,
283 283 # parent of commit2, in target repo2
284 284 source_ref_type="rev", source_ref=commit1.raw_id,
285 285 target_ref_type="rev", target_ref=commit4.raw_id,
286 286 params=dict(merge='1', target_repo=repo1.repo_name),
287 287 ))
288 288 response.mustcontain('%s@%s' % (repo2.repo_name, commit1.short_id))
289 289 response.mustcontain('%s@%s' % (repo1.repo_name, commit4.short_id))
290 290
291 291 # files
292 292 compare_page = ComparePage(response)
293 293 compare_page.contains_change_summary(1, 3, 0)
294 294 compare_page.contains_commits([commit2, commit3, commit4])
295 295 anchor = 'a_c-{}-826e8142e6ba'.format(commit1.short_id)
296 296 compare_page.contains_file_links_and_anchors([('file1', anchor),])
297 297
298 298 @pytest.mark.xfail_backends("svn")
299 299 def test_compare_cherry_pick_commits_from_top(self, backend):
300 300 # repo1:
301 301 # commit0:
302 302 # commit1:
303 303 # repo1-fork- in which we will cherry pick bottom commits
304 304 # commit0:
305 305 # commit1:
306 306 # commit2:
307 307 # commit3: x
308 308 # commit4: x
309 309 # commit5: x
310 310
311 311 # make repo1, and commit1+commit2
312 312 repo1 = backend.create_repo()
313 313
314 314 # commit something !
315 315 commit0 = commit_change(
316 repo1.repo_name, filename='file1', content='line1\n',
316 repo1.repo_name, filename=b'file1', content=b'line1\n',
317 317 message='commit1', vcs_type=backend.alias, parent=None,
318 318 newfile=True)
319 319 commit1 = commit_change(
320 repo1.repo_name, filename='file1', content='line1\nline2\n',
320 repo1.repo_name, filename=b'file1', content=b'line1\nline2\n',
321 321 message='commit2', vcs_type=backend.alias, parent=commit0)
322 322
323 323 # fork this repo
324 324 backend.create_fork()
325 325
326 326 # now make commit3-6
327 327 commit2 = commit_change(
328 repo1.repo_name, filename='file1', content='line1\nline2\nline3\n',
328 repo1.repo_name, filename=b'file1', content=b'line1\nline2\nline3\n',
329 329 message='commit3', vcs_type=backend.alias, parent=commit1)
330 330 commit3 = commit_change(
331 repo1.repo_name, filename='file1',
332 content='line1\nline2\nline3\nline4\n', message='commit4',
331 repo1.repo_name, filename=b'file1',
332 content=b'line1\nline2\nline3\nline4\n', message='commit4',
333 333 vcs_type=backend.alias, parent=commit2)
334 334 commit4 = commit_change(
335 repo1.repo_name, filename='file1',
336 content='line1\nline2\nline3\nline4\nline5\n', message='commit5',
335 repo1.repo_name, filename=b'file1',
336 content=b'line1\nline2\nline3\nline4\nline5\n', message='commit5',
337 337 vcs_type=backend.alias, parent=commit3)
338 338 commit5 = commit_change(
339 repo1.repo_name, filename='file1',
340 content='line1\nline2\nline3\nline4\nline5\nline6\n',
339 repo1.repo_name, filename=b'file1',
340 content=b'line1\nline2\nline3\nline4\nline5\nline6\n',
341 341 message='commit6', vcs_type=backend.alias, parent=commit4)
342 342
343 343 response = self.app.get(
344 344 route_path('repo_compare',
345 345 repo_name=repo1.repo_name,
346 346 # parent of commit3, not in source repo2
347 347 source_ref_type="rev", source_ref=commit2.raw_id,
348 348 target_ref_type="rev", target_ref=commit5.raw_id,
349 349 params=dict(merge='1'),))
350 350
351 351 response.mustcontain('%s@%s' % (repo1.repo_name, commit2.short_id))
352 352 response.mustcontain('%s@%s' % (repo1.repo_name, commit5.short_id))
353 353
354 354 compare_page = ComparePage(response)
355 355 compare_page.contains_change_summary(1, 3, 0)
356 356 compare_page.contains_commits([commit3, commit4, commit5])
357 357
358 358 # files
359 359 anchor = 'a_c-{}-826e8142e6ba'.format(commit2.short_id)
360 360 compare_page.contains_file_links_and_anchors([('file1', anchor),])
361 361
362 362 @pytest.mark.xfail_backends("svn")
363 363 def test_compare_remote_branches(self, backend):
364 364 repo1 = backend.repo
365 365 repo2 = backend.create_fork()
366 366
367 367 commit_id1 = repo1.get_commit(commit_idx=3).raw_id
368 368 commit_id1_short = repo1.get_commit(commit_idx=3).short_id
369 369 commit_id2 = repo1.get_commit(commit_idx=6).raw_id
370 370 commit_id2_short = repo1.get_commit(commit_idx=6).short_id
371 371
372 372 response = self.app.get(
373 373 route_path('repo_compare',
374 374 repo_name=repo1.repo_name,
375 375 source_ref_type="rev", source_ref=commit_id1,
376 376 target_ref_type="rev", target_ref=commit_id2,
377 377 params=dict(merge='1', target_repo=repo2.repo_name),
378 378 ))
379 379
380 380 response.mustcontain('%s@%s' % (repo1.repo_name, commit_id1))
381 381 response.mustcontain('%s@%s' % (repo2.repo_name, commit_id2))
382 382
383 383 compare_page = ComparePage(response)
384 384
385 385 # outgoing commits between those commits
386 386 compare_page.contains_commits(
387 387 [repo2.get_commit(commit_idx=x) for x in [4, 5, 6]])
388 388
389 389 # files
390 390 compare_page.contains_file_links_and_anchors([
391 391 ('vcs/backends/hg.py', 'a_c-{}-9c390eb52cd6'.format(commit_id2_short)),
392 392 ('vcs/backends/__init__.py', 'a_c-{}-41b41c1f2796'.format(commit_id1_short)),
393 393 ('vcs/backends/base.py', 'a_c-{}-2f574d260608'.format(commit_id1_short)),
394 394 ])
395 395
396 396 @pytest.mark.xfail_backends("svn")
397 397 def test_source_repo_new_commits_after_forking_simple_diff(self, backend):
398 398 repo1 = backend.create_repo()
399 399 r1_name = repo1.repo_name
400 400
401 401 commit0 = commit_change(
402 repo=r1_name, filename='file1',
403 content='line1', message='commit1', vcs_type=backend.alias,
402 repo=r1_name, filename=b'file1',
403 content=b'line1', message='commit1', vcs_type=backend.alias,
404 404 newfile=True)
405 405 assert repo1.scm_instance().commit_ids == [commit0.raw_id]
406 406
407 407 # fork the repo1
408 408 repo2 = backend.create_fork()
409 409 assert repo2.scm_instance().commit_ids == [commit0.raw_id]
410 410
411 411 self.r2_id = repo2.repo_id
412 412 r2_name = repo2.repo_name
413 413
414 414 commit1 = commit_change(
415 repo=r2_name, filename='file1-fork',
416 content='file1-line1-from-fork', message='commit1-fork',
415 repo=r2_name, filename=b'file1-fork',
416 content=b'file1-line1-from-fork', message='commit1-fork',
417 417 vcs_type=backend.alias, parent=repo2.scm_instance()[-1],
418 418 newfile=True)
419 419
420 420 commit2 = commit_change(
421 repo=r2_name, filename='file2-fork',
422 content='file2-line1-from-fork', message='commit2-fork',
421 repo=r2_name, filename=b'file2-fork',
422 content=b'file2-line1-from-fork', message='commit2-fork',
423 423 vcs_type=backend.alias, parent=commit1,
424 424 newfile=True)
425 425
426 426 commit_change( # commit 3
427 repo=r2_name, filename='file3-fork',
428 content='file3-line1-from-fork', message='commit3-fork',
427 repo=r2_name, filename=b'file3-fork',
428 content=b'file3-line1-from-fork', message='commit3-fork',
429 429 vcs_type=backend.alias, parent=commit2, newfile=True)
430 430
431 431 # compare !
432 432 commit_id1 = repo1.scm_instance().DEFAULT_BRANCH_NAME
433 433 commit_id2 = repo2.scm_instance().DEFAULT_BRANCH_NAME
434 434
435 435 response = self.app.get(
436 436 route_path('repo_compare',
437 437 repo_name=r2_name,
438 438 source_ref_type="branch", source_ref=commit_id1,
439 439 target_ref_type="branch", target_ref=commit_id2,
440 440 params=dict(merge='1', target_repo=r1_name),
441 441 ))
442 442
443 443 response.mustcontain('%s@%s' % (r2_name, commit_id1))
444 444 response.mustcontain('%s@%s' % (r1_name, commit_id2))
445 445 response.mustcontain('No files')
446 446 response.mustcontain('No commits in this compare')
447 447
448 448 commit0 = commit_change(
449 repo=r1_name, filename='file2',
450 content='line1-added-after-fork', message='commit2-parent',
449 repo=r1_name, filename=b'file2',
450 content=b'line1-added-after-fork', message='commit2-parent',
451 451 vcs_type=backend.alias, parent=None, newfile=True)
452 452
453 453 # compare !
454 454 response = self.app.get(
455 455 route_path('repo_compare',
456 456 repo_name=r2_name,
457 457 source_ref_type="branch", source_ref=commit_id1,
458 458 target_ref_type="branch", target_ref=commit_id2,
459 459 params=dict(merge='1', target_repo=r1_name),
460 460 ))
461 461
462 462 response.mustcontain('%s@%s' % (r2_name, commit_id1))
463 463 response.mustcontain('%s@%s' % (r1_name, commit_id2))
464 464
465 465 response.mustcontain("""commit2-parent""")
466 466 response.mustcontain("""line1-added-after-fork""")
467 467 compare_page = ComparePage(response)
468 468 compare_page.contains_change_summary(1, 1, 0)
469 469
470 470 @pytest.mark.xfail_backends("svn")
471 471 def test_compare_commits(self, backend, xhr_header):
472 472 commit0 = backend.repo.get_commit(commit_idx=0)
473 473 commit1 = backend.repo.get_commit(commit_idx=1)
474 474
475 475 response = self.app.get(
476 476 route_path('repo_compare',
477 477 repo_name=backend.repo_name,
478 478 source_ref_type="rev", source_ref=commit0.raw_id,
479 479 target_ref_type="rev", target_ref=commit1.raw_id,
480 480 params=dict(merge='1')
481 481 ),
482 482 extra_environ=xhr_header, )
483 483
484 484 # outgoing commits between those commits
485 485 compare_page = ComparePage(response)
486 486 compare_page.contains_commits(commits=[commit1])
487 487
488 488 def test_errors_when_comparing_unknown_source_repo(self, backend):
489 489 repo = backend.repo
490 badrepo = 'badrepo'
491 490
492 response = self.app.get(
491 self.app.get(
493 492 route_path('repo_compare',
494 repo_name=badrepo,
493 repo_name='badrepo',
495 494 source_ref_type="rev", source_ref='tip',
496 495 target_ref_type="rev", target_ref='tip',
497 496 params=dict(merge='1', target_repo=repo.repo_name)
498 497 ),
499 498 status=404)
500 499
501 500 def test_errors_when_comparing_unknown_target_repo(self, backend):
502 501 repo = backend.repo
503 502 badrepo = 'badrepo'
504 503
505 504 response = self.app.get(
506 505 route_path('repo_compare',
507 506 repo_name=repo.repo_name,
508 507 source_ref_type="rev", source_ref='tip',
509 508 target_ref_type="rev", target_ref='tip',
510 509 params=dict(merge='1', target_repo=badrepo),
511 510 ),
512 511 status=302)
513 512 redirected = response.follow()
514 513 redirected.mustcontain(
515 514 'Could not find the target repo: `{}`'.format(badrepo))
516 515
517 516 def test_compare_not_in_preview_mode(self, backend_stub):
518 517 commit0 = backend_stub.repo.get_commit(commit_idx=0)
519 518 commit1 = backend_stub.repo.get_commit(commit_idx=1)
520 519
521 520 response = self.app.get(
522 521 route_path('repo_compare',
523 522 repo_name=backend_stub.repo_name,
524 523 source_ref_type="rev", source_ref=commit0.raw_id,
525 524 target_ref_type="rev", target_ref=commit1.raw_id,
526 525 ))
527 526
528 527 # outgoing commits between those commits
529 528 compare_page = ComparePage(response)
530 529 compare_page.swap_is_visible()
531 530 compare_page.target_source_are_enabled()
532 531
533 532 def test_compare_of_fork_with_largefiles(self, backend_hg, settings_util):
534 533 orig = backend_hg.create_repo(number_of_commits=1)
535 534 fork = backend_hg.create_fork()
536 535
537 536 settings_util.create_repo_rhodecode_ui(
538 537 orig, 'extensions', value='', key='largefiles', active=False)
539 538 settings_util.create_repo_rhodecode_ui(
540 539 fork, 'extensions', value='', key='largefiles', active=True)
541 540
542 541 compare_module = ('rhodecode.lib.vcs.backends.hg.repository.'
543 542 'MercurialRepository.compare')
544 543 with mock.patch(compare_module) as compare_mock:
545 544 compare_mock.side_effect = RepositoryRequirementError()
546 545
547 546 response = self.app.get(
548 547 route_path('repo_compare',
549 548 repo_name=orig.repo_name,
550 549 source_ref_type="rev", source_ref="tip",
551 550 target_ref_type="rev", target_ref="tip",
552 551 params=dict(merge='1', target_repo=fork.repo_name),
553 552 ),
554 553 status=302)
555 554
556 555 assert_session_flash(
557 556 response,
558 557 'Could not compare repos with different large file settings')
559 558
560 559
561 560 @pytest.mark.usefixtures("autologin_user")
562 561 class TestCompareControllerSvn(object):
563 562
564 563 def test_supports_references_with_path(self, app, backend_svn):
565 564 repo = backend_svn['svn-simple-layout']
566 565 commit_id = repo.get_commit(commit_idx=-1).raw_id
567 566 response = app.get(
568 567 route_path('repo_compare',
569 568 repo_name=repo.repo_name,
570 569 source_ref_type="tag",
571 570 source_ref="%s@%s" % ('tags/v0.1', commit_id),
572 571 target_ref_type="tag",
573 572 target_ref="%s@%s" % ('tags/v0.2', commit_id),
574 573 params=dict(merge='1'),
575 574 ),
576 575 status=200)
577 576
578 577 # Expecting no commits, since both paths are at the same revision
579 578 response.mustcontain('No commits in this compare')
580 579
581 580 # Should find only one file changed when comparing those two tags
582 581 response.mustcontain('example.py')
583 582 compare_page = ComparePage(response)
584 583 compare_page.contains_change_summary(1, 5, 1)
585 584
586 585 def test_shows_commits_if_different_ids(self, app, backend_svn):
587 586 repo = backend_svn['svn-simple-layout']
588 587 source_id = repo.get_commit(commit_idx=-6).raw_id
589 588 target_id = repo.get_commit(commit_idx=-1).raw_id
590 589 response = app.get(
591 590 route_path('repo_compare',
592 591 repo_name=repo.repo_name,
593 592 source_ref_type="tag",
594 593 source_ref="%s@%s" % ('tags/v0.1', source_id),
595 594 target_ref_type="tag",
596 595 target_ref="%s@%s" % ('tags/v0.2', target_id),
597 596 params=dict(merge='1')
598 597 ),
599 598 status=200)
600 599
601 600 # It should show commits
602 601 assert 'No commits in this compare' not in response.text
603 602
604 603 # Should find only one file changed when comparing those two tags
605 604 response.mustcontain('example.py')
606 605 compare_page = ComparePage(response)
607 606 compare_page.contains_change_summary(1, 5, 1)
608 607
609 608
610 609 class ComparePage(AssertResponse):
611 610 """
612 611 Abstracts the page template from the tests
613 612 """
614 613
615 614 def contains_file_links_and_anchors(self, files):
616 615 doc = lxml.html.fromstring(self.response.body)
617 616 for filename, file_id in files:
618 617 self.contains_one_anchor(file_id)
619 618 diffblock = doc.cssselect('[data-f-path="%s"]' % filename)
620 619 assert len(diffblock) == 2
621 620 for lnk in diffblock[0].cssselect('a'):
622 621 if 'permalink' in lnk.text:
623 622 assert '#{}'.format(file_id) in lnk.attrib['href']
624 623 break
625 624 else:
626 625 pytest.fail('Unable to find permalink')
627 626
628 627 def contains_change_summary(self, files_changed, inserted, deleted):
629 628 template = (
630 629 '{files_changed} file{plural} changed: '
631 630 '<span class="op-added">{inserted} inserted</span>, <span class="op-deleted">{deleted} deleted</span>')
632 631 self.response.mustcontain(template.format(
633 632 files_changed=files_changed,
634 633 plural="s" if files_changed > 1 else "",
635 634 inserted=inserted,
636 635 deleted=deleted))
637 636
638 637 def contains_commits(self, commits, ancestors=None):
639 638 response = self.response
640 639
641 640 for commit in commits:
642 641 # Expecting to see the commit message in an element which
643 642 # has the ID "c-{commit.raw_id}"
644 643 self.element_contains('#c-' + commit.raw_id, commit.message)
645 644 self.contains_one_link(
646 645 'r%s:%s' % (commit.idx, commit.short_id),
647 646 self._commit_url(commit))
648 647
649 648 if ancestors:
650 649 response.mustcontain('Ancestor')
651 650 for ancestor in ancestors:
652 651 self.contains_one_link(
653 652 ancestor.short_id, self._commit_url(ancestor))
654 653
655 654 def _commit_url(self, commit):
656 655 return '/%s/changeset/%s' % (commit.repository.name, commit.raw_id)
657 656
658 657 def swap_is_hidden(self):
659 658 assert '<a id="btn-swap"' not in self.response.text
660 659
661 660 def swap_is_visible(self):
662 661 assert '<a id="btn-swap"' in self.response.text
663 662
664 663 def target_source_are_disabled(self):
665 664 response = self.response
666 665 response.mustcontain("var enable_fields = false;")
667 666 response.mustcontain('.select2("enable", enable_fields)')
668 667
669 668 def target_source_are_enabled(self):
670 669 response = self.response
671 670 response.mustcontain("var enable_fields = true;")
@@ -1,166 +1,168 b''
1 1
2 2 # Copyright (C) 2010-2020 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 23
24 24
25 25 def route_path(name, params=None, **kwargs):
26 import urllib.request, urllib.parse, urllib.error
26 import urllib.request
27 import urllib.parse
28 import urllib.error
27 29
28 30 base_url = {
29 31 'repo_compare_select': '/{repo_name}/compare',
30 32 'repo_compare': '/{repo_name}/compare/{source_ref_type}@{source_ref}...{target_ref_type}@{target_ref}',
31 33 }[name].format(**kwargs)
32 34
33 35 if params:
34 36 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
35 37 return base_url
36 38
37 39
38 40 @pytest.mark.usefixtures("autologin_user", "app")
39 41 class TestCompareView(object):
40 42
41 43 @pytest.mark.xfail_backends("svn", msg="Depends on branch and tag support")
42 44 def test_compare_tag(self, backend):
43 45 tag1 = 'v0.1.2'
44 46 tag2 = 'v0.1.3'
45 47 response = self.app.get(
46 48 route_path(
47 49 'repo_compare',
48 50 repo_name=backend.repo_name,
49 51 source_ref_type="tag", source_ref=tag1,
50 52 target_ref_type="tag", target_ref=tag2),
51 53 status=200)
52 54
53 55 response.mustcontain('%s@%s' % (backend.repo_name, tag1))
54 56 response.mustcontain('%s@%s' % (backend.repo_name, tag2))
55 57
56 58 # outgoing commits between tags
57 59 commit_indexes = {
58 'git': [113] + range(115, 121),
59 'hg': [112] + range(115, 121),
60 'git': [113] + list(range(115, 121)),
61 'hg': [112] + list(range(115, 121)),
60 62 }
61 63 repo = backend.repo
62 64 commits = (repo.get_commit(commit_idx=idx)
63 65 for idx in commit_indexes[backend.alias])
64 66 compare_page = ComparePage(response)
65 67 compare_page.contains_change_summary(11, 94, 64)
66 68 compare_page.contains_commits(commits)
67 69
68 70 # files diff
69 71 short_id = short_id_new = ''
70 72 if backend.alias == 'git':
71 73 short_id = '5a3a8fb00555'
72 74 short_id_new = '0ba5f8a46600'
73 75 if backend.alias == 'hg':
74 76 short_id = '17544fbfcd33'
75 77 short_id_new = 'a7e60bff65d5'
76 78
77 79 compare_page.contains_file_links_and_anchors([
78 80 # modified
79 81 ('docs/api/utils/index.rst', 'a_c-{}-1c5cf9e91c12'.format(short_id)),
80 82 ('test_and_report.sh', 'a_c-{}-e3305437df55'.format(short_id)),
81 83 # added
82 84 ('.hgignore', 'a_c-{}-c8e92ef85cd1'.format(short_id_new)),
83 85 ('.hgtags', 'a_c-{}-6e08b694d687'.format(short_id_new)),
84 86 ('docs/api/index.rst', 'a_c-{}-2c14b00f3393'.format(short_id_new)),
85 87 ('vcs/__init__.py', 'a_c-{}-430ccbc82bdf'.format(short_id_new)),
86 88 ('vcs/backends/hg.py', 'a_c-{}-9c390eb52cd6'.format(short_id_new)),
87 89 ('vcs/utils/__init__.py', 'a_c-{}-ebb592c595c0'.format(short_id_new)),
88 90 ('vcs/utils/annotate.py', 'a_c-{}-7abc741b5052'.format(short_id_new)),
89 91 ('vcs/utils/diffs.py', 'a_c-{}-2ef0ef106c56'.format(short_id_new)),
90 92 ('vcs/utils/lazy.py', 'a_c-{}-3150cb87d4b7'.format(short_id_new)),
91 93 ])
92 94
93 95 @pytest.mark.xfail_backends("svn", msg="Depends on branch and tag support")
94 96 def test_compare_tag_branch(self, backend):
95 97 revisions = {
96 98 'hg': {
97 99 'tag': 'v0.2.0',
98 100 'branch': 'default',
99 101 'response': (147, 5701, 10177)
100 102 },
101 103 'git': {
102 104 'tag': 'v0.2.2',
103 105 'branch': 'master',
104 106 'response': (70, 1855, 3002)
105 107 },
106 108 }
107 109
108 110 # Backend specific data, depends on the test repository for
109 111 # functional tests.
110 112 data = revisions[backend.alias]
111 113
112 114 response = self.app.get(
113 115 route_path(
114 116 'repo_compare',
115 117 repo_name=backend.repo_name,
116 118 source_ref_type='branch', source_ref=data['branch'],
117 119 target_ref_type="tag", target_ref=data['tag'],
118 120 ))
119 121
120 122 response.mustcontain('%s@%s' % (backend.repo_name, data['branch']))
121 123 response.mustcontain('%s@%s' % (backend.repo_name, data['tag']))
122 124 compare_page = ComparePage(response)
123 125 compare_page.contains_change_summary(*data['response'])
124 126
125 127 def test_index_branch(self, backend):
126 128 head_id = backend.default_head_id
127 129 response = self.app.get(
128 130 route_path(
129 131 'repo_compare',
130 132 repo_name=backend.repo_name,
131 133 source_ref_type="branch", source_ref=head_id,
132 134 target_ref_type="branch", target_ref=head_id,
133 135 ))
134 136
135 137 response.mustcontain('%s@%s' % (backend.repo_name, head_id))
136 138
137 139 # branches are equal
138 140 response.mustcontain('No files')
139 141 response.mustcontain('No commits in this compare')
140 142
141 143 def test_compare_commits(self, backend):
142 144 repo = backend.repo
143 145 commit1 = repo.get_commit(commit_idx=0)
144 146 commit1_short_id = commit1.short_id
145 147 commit2 = repo.get_commit(commit_idx=1)
146 148 commit2_short_id = commit2.short_id
147 149
148 150 response = self.app.get(
149 151 route_path(
150 152 'repo_compare',
151 153 repo_name=backend.repo_name,
152 154 source_ref_type="rev", source_ref=commit1.raw_id,
153 155 target_ref_type="rev", target_ref=commit2.raw_id,
154 156 ))
155 157 response.mustcontain('%s@%s' % (backend.repo_name, commit1.raw_id))
156 158 response.mustcontain('%s@%s' % (backend.repo_name, commit2.raw_id))
157 159 compare_page = ComparePage(response)
158 160
159 161 # files
160 162 compare_page.contains_change_summary(1, 7, 0)
161 163
162 164 # outgoing commits between those commits
163 165 compare_page.contains_commits([commit2])
164 166 anchor = 'a_c-{}-c8e92ef85cd1'.format(commit2_short_id)
165 167 response.mustcontain(anchor)
166 168 compare_page.contains_file_links_and_anchors([('.hgignore', anchor),])
@@ -1,290 +1,292 b''
1 1
2 2 # Copyright (C) 2010-2020 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 27
28 28 fixture = Fixture()
29 29
30 30
31 31 def route_path(name, params=None, **kwargs):
32 import urllib.request, urllib.parse, urllib.error
32 import urllib.request
33 import urllib.parse
34 import urllib.error
33 35
34 36 base_url = {
35 37 'repo_compare_select': '/{repo_name}/compare',
36 38 'repo_compare': '/{repo_name}/compare/{source_ref_type}@{source_ref}...{target_ref_type}@{target_ref}',
37 39 }[name].format(**kwargs)
38 40
39 41 if params:
40 42 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
41 43 return base_url
42 44
43 45
44 46 @pytest.mark.usefixtures("autologin_user", "app")
45 47 class TestSideBySideDiff(object):
46 48
47 49 def test_diff_sidebyside_single_commit(self, app, backend):
48 50 commit_id_range = {
49 51 'hg': {
50 52 'commits': ['25d7e49c18b159446cadfa506a5cf8ad1cb04067',
51 53 '603d6c72c46d953420c89d36372f08d9f305f5dd'],
52 54 'changes': (21, 943, 288),
53 55 },
54 56 'git': {
55 57 'commits': ['6fc9270775aaf5544c1deb014f4ddd60c952fcbb',
56 58 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'],
57 59 'changes': (20, 941, 286),
58 60 },
59 61
60 62 'svn': {
61 63 'commits': ['336',
62 64 '337'],
63 65 'changes': (21, 943, 288),
64 66 },
65 67 }
66 68
67 69 commit_info = commit_id_range[backend.alias]
68 70 commit2, commit1 = commit_info['commits']
69 71 file_changes = commit_info['changes']
70 72
71 73 response = self.app.get(route_path(
72 74 'repo_compare',
73 75 repo_name=backend.repo_name,
74 76 source_ref_type='rev',
75 77 source_ref=commit2,
76 78 target_repo=backend.repo_name,
77 79 target_ref_type='rev',
78 80 target_ref=commit1,
79 81 params=dict(target_repo=backend.repo_name, diffmode='sidebyside')
80 82 ))
81 83
82 84 compare_page = ComparePage(response)
83 85 compare_page.contains_change_summary(*file_changes)
84 86 response.mustcontain('Collapse 1 commit')
85 87
86 88 def test_diff_sidebyside_two_commits(self, app, backend):
87 89 commit_id_range = {
88 90 'hg': {
89 91 'commits': ['4fdd71e9427417b2e904e0464c634fdee85ec5a7',
90 92 '603d6c72c46d953420c89d36372f08d9f305f5dd'],
91 93 'changes': (32, 1165, 308),
92 94 },
93 95 'git': {
94 96 'commits': ['f5fbf9cfd5f1f1be146f6d3b38bcd791a7480c13',
95 97 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'],
96 98 'changes': (31, 1163, 306),
97 99 },
98 100
99 101 'svn': {
100 102 'commits': ['335',
101 103 '337'],
102 104 'changes': (32, 1179, 310),
103 105 },
104 106 }
105 107
106 108 commit_info = commit_id_range[backend.alias]
107 109 commit2, commit1 = commit_info['commits']
108 110 file_changes = commit_info['changes']
109 111
110 112 response = self.app.get(route_path(
111 113 'repo_compare',
112 114 repo_name=backend.repo_name,
113 115 source_ref_type='rev',
114 116 source_ref=commit2,
115 117 target_repo=backend.repo_name,
116 118 target_ref_type='rev',
117 119 target_ref=commit1,
118 120 params=dict(target_repo=backend.repo_name, diffmode='sidebyside')
119 121 ))
120 122
121 123 compare_page = ComparePage(response)
122 124 compare_page.contains_change_summary(*file_changes)
123 125
124 126 response.mustcontain('Collapse 2 commits')
125 127
126 128 def test_diff_sidebyside_collapsed_commits(self, app, backend_svn):
127 129 commit_id_range = {
128 130
129 131 'svn': {
130 132 'commits': ['330',
131 133 '337'],
132 134
133 135 },
134 136 }
135 137
136 138 commit_info = commit_id_range['svn']
137 139 commit2, commit1 = commit_info['commits']
138 140
139 141 response = self.app.get(route_path(
140 142 'repo_compare',
141 143 repo_name=backend_svn.repo_name,
142 144 source_ref_type='rev',
143 145 source_ref=commit2,
144 146 target_repo=backend_svn.repo_name,
145 147 target_ref_type='rev',
146 148 target_ref=commit1,
147 149 params=dict(target_repo=backend_svn.repo_name, diffmode='sidebyside')
148 150 ))
149 151
150 152 response.mustcontain('Expand 7 commits')
151 153
152 154 @pytest.mark.xfail(reason='GIT does not handle empty commit compare correct (missing 1 commit)')
153 155 def test_diff_side_by_side_from_0_commit(self, app, backend, backend_stub):
154 f_path = 'test_sidebyside_file.py'
155 commit1_content = 'content-25d7e49c18b159446c\n'
156 commit2_content = 'content-603d6c72c46d953420\n'
156 f_path = b'test_sidebyside_file.py'
157 commit1_content = b'content-25d7e49c18b159446c\n'
158 commit2_content = b'content-603d6c72c46d953420\n'
157 159 repo = backend.create_repo()
158 160
159 161 commit1 = commit_change(
160 162 repo.repo_name, filename=f_path, content=commit1_content,
161 163 message='A', vcs_type=backend.alias, parent=None, newfile=True)
162 164
163 165 commit2 = commit_change(
164 166 repo.repo_name, filename=f_path, content=commit2_content,
165 167 message='B, child of A', vcs_type=backend.alias, parent=commit1)
166 168
167 169 response = self.app.get(route_path(
168 170 'repo_compare',
169 171 repo_name=repo.repo_name,
170 172 source_ref_type='rev',
171 173 source_ref=EmptyCommit().raw_id,
172 174 target_ref_type='rev',
173 175 target_ref=commit2.raw_id,
174 176 params=dict(diffmode='sidebyside')
175 177 ))
176 178
177 179 response.mustcontain('Collapse 2 commits')
178 180 response.mustcontain('123 file changed')
179 181
180 182 response.mustcontain(
181 183 'r%s:%s...r%s:%s' % (
182 184 commit1.idx, commit1.short_id, commit2.idx, commit2.short_id))
183 185
184 186 response.mustcontain(f_path)
185 187
186 188 @pytest.mark.xfail(reason='GIT does not handle empty commit compare correct (missing 1 commit)')
187 189 def test_diff_side_by_side_from_0_commit_with_file_filter(self, app, backend, backend_stub):
188 f_path = 'test_sidebyside_file.py'
189 commit1_content = 'content-25d7e49c18b159446c\n'
190 commit2_content = 'content-603d6c72c46d953420\n'
190 f_path = b'test_sidebyside_file.py'
191 commit1_content = b'content-25d7e49c18b159446c\n'
192 commit2_content = b'content-603d6c72c46d953420\n'
191 193 repo = backend.create_repo()
192 194
193 195 commit1 = commit_change(
194 196 repo.repo_name, filename=f_path, content=commit1_content,
195 197 message='A', vcs_type=backend.alias, parent=None, newfile=True)
196 198
197 199 commit2 = commit_change(
198 200 repo.repo_name, filename=f_path, content=commit2_content,
199 201 message='B, child of A', vcs_type=backend.alias, parent=commit1)
200 202
201 203 response = self.app.get(route_path(
202 204 'repo_compare',
203 205 repo_name=repo.repo_name,
204 206 source_ref_type='rev',
205 207 source_ref=EmptyCommit().raw_id,
206 208 target_ref_type='rev',
207 209 target_ref=commit2.raw_id,
208 210 params=dict(f_path=f_path, target_repo=repo.repo_name, diffmode='sidebyside')
209 211 ))
210 212
211 213 response.mustcontain('Collapse 2 commits')
212 214 response.mustcontain('1 file changed')
213 215
214 216 response.mustcontain(
215 217 'r%s:%s...r%s:%s' % (
216 218 commit1.idx, commit1.short_id, commit2.idx, commit2.short_id))
217 219
218 220 response.mustcontain(f_path)
219 221
220 222 def test_diff_side_by_side_with_empty_file(self, app, backend, backend_stub):
221 223 commits = [
222 224 {'message': 'First commit'},
223 225 {'message': 'Second commit'},
224 226 {'message': 'Commit with binary',
225 'added': [nodes.FileNode('file.empty', content='')]},
227 'added': [nodes.FileNode(b'file.empty', content=b'')]},
226 228 ]
227 229 f_path = 'file.empty'
228 230 repo = backend.create_repo(commits=commits)
229 231 commit1 = repo.get_commit(commit_idx=0)
230 232 commit2 = repo.get_commit(commit_idx=1)
231 233 commit3 = repo.get_commit(commit_idx=2)
232 234
233 235 response = self.app.get(route_path(
234 236 'repo_compare',
235 237 repo_name=repo.repo_name,
236 238 source_ref_type='rev',
237 239 source_ref=commit1.raw_id,
238 240 target_ref_type='rev',
239 241 target_ref=commit3.raw_id,
240 242 params=dict(f_path=f_path, target_repo=repo.repo_name, diffmode='sidebyside')
241 243 ))
242 244
243 245 response.mustcontain('Collapse 2 commits')
244 246 response.mustcontain('1 file changed')
245 247
246 248 response.mustcontain(
247 249 'r%s:%s...r%s:%s' % (
248 250 commit2.idx, commit2.short_id, commit3.idx, commit3.short_id))
249 251
250 252 response.mustcontain(f_path)
251 253
252 254 def test_diff_sidebyside_two_commits_with_file_filter(self, app, backend):
253 255 commit_id_range = {
254 256 'hg': {
255 257 'commits': ['4fdd71e9427417b2e904e0464c634fdee85ec5a7',
256 258 '603d6c72c46d953420c89d36372f08d9f305f5dd'],
257 259 'changes': (1, 3, 3)
258 260 },
259 261 'git': {
260 262 'commits': ['f5fbf9cfd5f1f1be146f6d3b38bcd791a7480c13',
261 263 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'],
262 264 'changes': (1, 3, 3)
263 265 },
264 266
265 267 'svn': {
266 268 'commits': ['335',
267 269 '337'],
268 270 'changes': (1, 3, 3)
269 271 },
270 272 }
271 273 f_path = 'docs/conf.py'
272 274
273 275 commit_info = commit_id_range[backend.alias]
274 276 commit2, commit1 = commit_info['commits']
275 277 file_changes = commit_info['changes']
276 278
277 279 response = self.app.get(route_path(
278 280 'repo_compare',
279 281 repo_name=backend.repo_name,
280 282 source_ref_type='rev',
281 283 source_ref=commit2,
282 284 target_ref_type='rev',
283 285 target_ref=commit1,
284 286 params=dict(f_path=f_path, target_repo=backend.repo_name, diffmode='sidebyside')
285 287 ))
286 288
287 289 response.mustcontain('Collapse 2 commits')
288 290
289 291 compare_page = ComparePage(response)
290 292 compare_page.contains_change_summary(*file_changes)
@@ -1,137 +1,138 b''
1 # -*- coding: utf-8 -*-
2 1
3 2 # Copyright (C) 2010-2020 RhodeCode GmbH
4 3 #
5 4 # This program is free software: you can redistribute it and/or modify
6 5 # it under the terms of the GNU Affero General Public License, version 3
7 6 # (only), as published by the Free Software Foundation.
8 7 #
9 8 # This program is distributed in the hope that it will be useful,
10 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 11 # GNU General Public License for more details.
13 12 #
14 13 # You should have received a copy of the GNU Affero General Public License
15 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 15 #
17 16 # This program is dual-licensed. If you wish to learn more about the
18 17 # RhodeCode Enterprise Edition, including its added features, Support services,
19 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 19
21 20 import pytest
22 21 from rhodecode.model.auth_token import AuthTokenModel
23 22 from rhodecode.tests import TestController
24 23
25 24
26 25 def route_path(name, params=None, **kwargs):
27 import urllib.request, urllib.parse, urllib.error
26 import urllib.request
27 import urllib.parse
28 import urllib.error
28 29
29 30 base_url = {
30 31 'rss_feed_home': '/{repo_name}/feed-rss',
31 32 'atom_feed_home': '/{repo_name}/feed-atom',
32 33 'rss_feed_home_old': '/{repo_name}/feed/rss',
33 34 'atom_feed_home_old': '/{repo_name}/feed/atom',
34 35 }[name].format(**kwargs)
35 36
36 37 if params:
37 38 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
38 39 return base_url
39 40
40 41
41 42 class TestFeedView(TestController):
42 43
43 44 @pytest.mark.parametrize("feed_type,response_types,content_type",[
44 45 ('rss', ['<rss version="2.0"'],
45 46 "application/rss+xml"),
46 47 ('atom', ['xmlns="http://www.w3.org/2005/Atom"', 'xml:lang="en-us"'],
47 48 "application/atom+xml"),
48 49 ])
49 50 def test_feed(self, backend, feed_type, response_types, content_type):
50 51 self.log_user()
51 52 response = self.app.get(
52 53 route_path('{}_feed_home'.format(feed_type),
53 54 repo_name=backend.repo_name))
54 55
55 56 for content in response_types:
56 57 response.mustcontain(content)
57 58
58 59 assert response.content_type == content_type
59 60
60 61 @pytest.mark.parametrize("feed_type, content_type", [
61 62 ('rss', "application/rss+xml"),
62 63 ('atom', "application/atom+xml")
63 64 ])
64 65 def test_feed_with_auth_token(
65 66 self, backend, user_admin, feed_type, content_type):
66 67 auth_token = user_admin.feed_token
67 68 assert auth_token != ''
68 69
69 70 response = self.app.get(
70 71 route_path(
71 72 '{}_feed_home'.format(feed_type),
72 73 repo_name=backend.repo_name,
73 74 params=dict(auth_token=auth_token)),
74 75 status=200)
75 76
76 77 assert response.content_type == content_type
77 78
78 79 @pytest.mark.parametrize("feed_type, content_type", [
79 80 ('rss', "application/rss+xml"),
80 81 ('atom', "application/atom+xml")
81 82 ])
82 83 def test_feed_with_auth_token_by_uid(
83 84 self, backend, user_admin, feed_type, content_type):
84 85 auth_token = user_admin.feed_token
85 86 assert auth_token != ''
86 87
87 88 response = self.app.get(
88 89 route_path(
89 90 '{}_feed_home'.format(feed_type),
90 91 repo_name='_{}'.format(backend.repo.repo_id),
91 92 params=dict(auth_token=auth_token)),
92 93 status=200)
93 94
94 95 assert response.content_type == content_type
95 96
96 97 @pytest.mark.parametrize("feed_type, content_type", [
97 98 ('rss', "application/rss+xml"),
98 99 ('atom', "application/atom+xml")
99 100 ])
100 101 def test_feed_old_urls_with_auth_token(
101 102 self, backend, user_admin, feed_type, content_type):
102 103 auth_token = user_admin.feed_token
103 104 assert auth_token != ''
104 105
105 106 response = self.app.get(
106 107 route_path(
107 108 '{}_feed_home_old'.format(feed_type),
108 109 repo_name=backend.repo_name,
109 110 params=dict(auth_token=auth_token)),
110 111 status=200)
111 112
112 113 assert response.content_type == content_type
113 114
114 115 @pytest.mark.parametrize("feed_type", ['rss', 'atom'])
115 116 def test_feed_with_auth_token_of_wrong_type(
116 117 self, backend, user_util, feed_type):
117 118 user = user_util.create_user()
118 119 auth_token = AuthTokenModel().create(
119 120 user.user_id, u'test-token', -1, AuthTokenModel.cls.ROLE_API)
120 121 auth_token = auth_token.api_key
121 122
122 123 self.app.get(
123 124 route_path(
124 125 '{}_feed_home'.format(feed_type),
125 126 repo_name=backend.repo_name,
126 127 params=dict(auth_token=auth_token)),
127 128 status=302)
128 129
129 130 auth_token = AuthTokenModel().create(
130 131 user.user_id, u'test-token', -1, AuthTokenModel.cls.ROLE_FEED)
131 132 auth_token = auth_token.api_key
132 133 self.app.get(
133 134 route_path(
134 135 '{}_feed_home'.format(feed_type),
135 136 repo_name=backend.repo_name,
136 137 params=dict(auth_token=auth_token)),
137 138 status=200)
@@ -1,1091 +1,1100 b''
1 1
2 2 # Copyright (C) 2010-2020 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
25 25 from rhodecode.apps.repository.tests.test_repo_compare import ComparePage
26 from rhodecode.apps.repository.views.repo_files import RepoFilesView
26 from rhodecode.apps.repository.views.repo_files import RepoFilesView, get_archive_name, get_path_sha
27 27 from rhodecode.lib import helpers as h
28 28 from collections import OrderedDict
29 29 from rhodecode.lib.ext_json import json
30 from rhodecode.lib.str_utils import safe_str
30 31 from rhodecode.lib.vcs import nodes
31 32
32 33 from rhodecode.lib.vcs.conf import settings
33 34 from rhodecode.tests import assert_session_flash
34 35 from rhodecode.tests.fixture import Fixture
35 36 from rhodecode.model.db import Session
36 37
37 38 fixture = Fixture()
38 39
39 40
40 41 def get_node_history(backend_type):
41 42 return {
42 43 'hg': json.loads(fixture.load_resource('hg_node_history_response.json')),
43 44 'git': json.loads(fixture.load_resource('git_node_history_response.json')),
44 45 'svn': json.loads(fixture.load_resource('svn_node_history_response.json')),
45 46 }[backend_type]
46 47
47 48
48 49 def route_path(name, params=None, **kwargs):
49 import urllib.request, urllib.parse, urllib.error
50 import urllib.request
51 import urllib.parse
52 import urllib.error
50 53
51 54 base_url = {
52 55 'repo_summary': '/{repo_name}',
53 56 'repo_archivefile': '/{repo_name}/archive/{fname}',
54 57 'repo_files_diff': '/{repo_name}/diff/{f_path}',
55 58 'repo_files_diff_2way_redirect': '/{repo_name}/diff-2way/{f_path}',
56 59 'repo_files': '/{repo_name}/files/{commit_id}/{f_path}',
57 60 'repo_files:default_path': '/{repo_name}/files/{commit_id}/',
58 61 'repo_files:default_commit': '/{repo_name}/files',
59 62 'repo_files:rendered': '/{repo_name}/render/{commit_id}/{f_path}',
60 63 'repo_files:annotated': '/{repo_name}/annotate/{commit_id}/{f_path}',
61 64 'repo_files:annotated_previous': '/{repo_name}/annotate-previous/{commit_id}/{f_path}',
62 65 'repo_files_nodelist': '/{repo_name}/nodelist/{commit_id}/{f_path}',
63 66 'repo_file_raw': '/{repo_name}/raw/{commit_id}/{f_path}',
64 67 'repo_file_download': '/{repo_name}/download/{commit_id}/{f_path}',
65 68 'repo_file_history': '/{repo_name}/history/{commit_id}/{f_path}',
66 69 'repo_file_authors': '/{repo_name}/authors/{commit_id}/{f_path}',
67 70 'repo_files_remove_file': '/{repo_name}/remove_file/{commit_id}/{f_path}',
68 71 'repo_files_delete_file': '/{repo_name}/delete_file/{commit_id}/{f_path}',
69 72 'repo_files_edit_file': '/{repo_name}/edit_file/{commit_id}/{f_path}',
70 73 'repo_files_update_file': '/{repo_name}/update_file/{commit_id}/{f_path}',
71 74 'repo_files_add_file': '/{repo_name}/add_file/{commit_id}/{f_path}',
72 75 'repo_files_create_file': '/{repo_name}/create_file/{commit_id}/{f_path}',
73 76 'repo_nodetree_full': '/{repo_name}/nodetree_full/{commit_id}/{f_path}',
74 77 'repo_nodetree_full:default_path': '/{repo_name}/nodetree_full/{commit_id}/',
75 78 }[name].format(**kwargs)
76 79
77 80 if params:
78 81 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
79 82 return base_url
80 83
81 84
82 85 def assert_files_in_response(response, files, params):
83 86 template = (
84 87 'href="/%(repo_name)s/files/%(commit_id)s/%(name)s"')
85 88 _assert_items_in_response(response, files, template, params)
86 89
87 90
88 91 def assert_dirs_in_response(response, dirs, params):
89 92 template = (
90 93 'href="/%(repo_name)s/files/%(commit_id)s/%(name)s"')
91 94 _assert_items_in_response(response, dirs, template, params)
92 95
93 96
94 97 def _assert_items_in_response(response, items, template, params):
95 98 for item in items:
96 99 item_params = {'name': item}
97 100 item_params.update(params)
98 101 response.mustcontain(template % item_params)
99 102
100 103
101 104 def assert_timeago_in_response(response, items, params):
102 105 for item in items:
103 106 response.mustcontain(h.age_component(params['date']))
104 107
105 108
106 109 @pytest.mark.usefixtures("app")
107 110 class TestFilesViews(object):
108 111
109 112 def test_show_files(self, backend):
110 113 response = self.app.get(
111 114 route_path('repo_files',
112 115 repo_name=backend.repo_name,
113 116 commit_id='tip', f_path='/'))
114 117 commit = backend.repo.get_commit()
115 118
116 119 params = {
117 120 'repo_name': backend.repo_name,
118 121 'commit_id': commit.raw_id,
119 122 'date': commit.date
120 123 }
121 124 assert_dirs_in_response(response, ['docs', 'vcs'], params)
122 125 files = [
123 126 '.gitignore',
124 127 '.hgignore',
125 128 '.hgtags',
126 129 # TODO: missing in Git
127 130 # '.travis.yml',
128 131 'MANIFEST.in',
129 132 'README.rst',
130 133 # TODO: File is missing in svn repository
131 134 # 'run_test_and_report.sh',
132 135 'setup.cfg',
133 136 'setup.py',
134 137 'test_and_report.sh',
135 138 'tox.ini',
136 139 ]
137 140 assert_files_in_response(response, files, params)
138 141 assert_timeago_in_response(response, files, params)
139 142
140 143 def test_show_files_links_submodules_with_absolute_url(self, backend_hg):
141 144 repo = backend_hg['subrepos']
142 145 response = self.app.get(
143 146 route_path('repo_files',
144 147 repo_name=repo.repo_name,
145 148 commit_id='tip', f_path='/'))
146 149 assert_response = response.assert_response()
147 150 assert_response.contains_one_link(
148 151 'absolute-path @ 000000000000', 'http://example.com/absolute-path')
149 152
150 153 def test_show_files_links_submodules_with_absolute_url_subpaths(
151 154 self, backend_hg):
152 155 repo = backend_hg['subrepos']
153 156 response = self.app.get(
154 157 route_path('repo_files',
155 158 repo_name=repo.repo_name,
156 159 commit_id='tip', f_path='/'))
157 160 assert_response = response.assert_response()
158 161 assert_response.contains_one_link(
159 162 'subpaths-path @ 000000000000',
160 163 'http://sub-base.example.com/subpaths-path')
161 164
162 165 @pytest.mark.xfail_backends("svn", reason="Depends on branch support")
163 166 def test_files_menu(self, backend):
164 167 new_branch = "temp_branch_name"
165 168 commits = [
166 169 {'message': 'a'},
167 170 {'message': 'b', 'branch': new_branch}
168 171 ]
169 172 backend.create_repo(commits)
170 173 backend.repo.landing_rev = "branch:%s" % new_branch
171 174 Session().commit()
172 175
173 176 # get response based on tip and not new commit
174 177 response = self.app.get(
175 178 route_path('repo_files',
176 179 repo_name=backend.repo_name,
177 180 commit_id='tip', f_path='/'))
178 181
179 182 # make sure Files menu url is not tip but new commit
180 183 landing_rev = backend.repo.landing_ref_name
181 184 files_url = route_path('repo_files:default_path',
182 185 repo_name=backend.repo_name,
183 186 commit_id=landing_rev, params={'at': landing_rev})
184 187
185 188 assert landing_rev != 'tip'
186 189 response.mustcontain(
187 190 '<li class="active"><a class="menulink" href="%s">' % files_url)
188 191
189 192 def test_show_files_commit(self, backend):
190 193 commit = backend.repo.get_commit(commit_idx=32)
191 194
192 195 response = self.app.get(
193 196 route_path('repo_files',
194 197 repo_name=backend.repo_name,
195 198 commit_id=commit.raw_id, f_path='/'))
196 199
197 200 dirs = ['docs', 'tests']
198 201 files = ['README.rst']
199 202 params = {
200 203 'repo_name': backend.repo_name,
201 204 'commit_id': commit.raw_id,
202 205 }
203 206 assert_dirs_in_response(response, dirs, params)
204 207 assert_files_in_response(response, files, params)
205 208
206 209 def test_show_files_different_branch(self, backend):
207 210 branches = dict(
208 211 hg=(150, ['git']),
209 212 # TODO: Git test repository does not contain other branches
210 213 git=(633, ['master']),
211 214 # TODO: Branch support in Subversion
212 215 svn=(150, [])
213 216 )
214 217 idx, branches = branches[backend.alias]
215 218 commit = backend.repo.get_commit(commit_idx=idx)
216 219 response = self.app.get(
217 220 route_path('repo_files',
218 221 repo_name=backend.repo_name,
219 222 commit_id=commit.raw_id, f_path='/'))
220 223
221 224 assert_response = response.assert_response()
222 225 for branch in branches:
223 226 assert_response.element_contains('.tags .branchtag', branch)
224 227
225 228 def test_show_files_paging(self, backend):
226 229 repo = backend.repo
227 230 indexes = [73, 92, 109, 1, 0]
228 231 idx_map = [(rev, repo.get_commit(commit_idx=rev).raw_id)
229 232 for rev in indexes]
230 233
231 234 for idx in idx_map:
232 235 response = self.app.get(
233 236 route_path('repo_files',
234 237 repo_name=backend.repo_name,
235 238 commit_id=idx[1], f_path='/'))
236 239
237 240 response.mustcontain("""r%s:%s""" % (idx[0], idx[1][:8]))
238 241
239 242 def test_file_source(self, backend):
240 243 commit = backend.repo.get_commit(commit_idx=167)
241 244 response = self.app.get(
242 245 route_path('repo_files',
243 246 repo_name=backend.repo_name,
244 247 commit_id=commit.raw_id, f_path='vcs/nodes.py'))
245 248
246 249 msgbox = """<div class="commit">%s</div>"""
247 250 response.mustcontain(msgbox % (commit.message, ))
248 251
249 252 assert_response = response.assert_response()
250 253 if commit.branch:
251 254 assert_response.element_contains(
252 255 '.tags.tags-main .branchtag', commit.branch)
253 256 if commit.tags:
254 257 for tag in commit.tags:
255 258 assert_response.element_contains('.tags.tags-main .tagtag', tag)
256 259
257 260 def test_file_source_annotated(self, backend):
258 261 response = self.app.get(
259 262 route_path('repo_files:annotated',
260 263 repo_name=backend.repo_name,
261 264 commit_id='tip', f_path='vcs/nodes.py'))
262 265 expected_commits = {
263 266 'hg': 'r356',
264 267 'git': 'r345',
265 268 'svn': 'r208',
266 269 }
267 270 response.mustcontain(expected_commits[backend.alias])
268 271
269 272 def test_file_source_authors(self, backend):
270 273 response = self.app.get(
271 274 route_path('repo_file_authors',
272 275 repo_name=backend.repo_name,
273 276 commit_id='tip', f_path='vcs/nodes.py'))
274 277 expected_authors = {
275 278 'hg': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
276 279 'git': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
277 280 'svn': ('marcin', 'lukasz'),
278 281 }
279 282
280 283 for author in expected_authors[backend.alias]:
281 284 response.mustcontain(author)
282 285
283 286 def test_file_source_authors_with_annotation(self, backend):
284 287 response = self.app.get(
285 288 route_path('repo_file_authors',
286 289 repo_name=backend.repo_name,
287 290 commit_id='tip', f_path='vcs/nodes.py',
288 291 params=dict(annotate=1)))
289 292 expected_authors = {
290 293 'hg': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
291 294 'git': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
292 295 'svn': ('marcin', 'lukasz'),
293 296 }
294 297
295 298 for author in expected_authors[backend.alias]:
296 299 response.mustcontain(author)
297 300
298 301 def test_file_source_history(self, backend, xhr_header):
299 302 response = self.app.get(
300 303 route_path('repo_file_history',
301 304 repo_name=backend.repo_name,
302 305 commit_id='tip', f_path='vcs/nodes.py'),
303 306 extra_environ=xhr_header)
304 307 assert get_node_history(backend.alias) == json.loads(response.body)
305 308
306 309 def test_file_source_history_svn(self, backend_svn, xhr_header):
307 310 simple_repo = backend_svn['svn-simple-layout']
308 311 response = self.app.get(
309 312 route_path('repo_file_history',
310 313 repo_name=simple_repo.repo_name,
311 314 commit_id='tip', f_path='trunk/example.py'),
312 315 extra_environ=xhr_header)
313 316
314 317 expected_data = json.loads(
315 318 fixture.load_resource('svn_node_history_branches.json'))
316 319
317 320 assert expected_data == response.json
318 321
319 322 def test_file_source_history_with_annotation(self, backend, xhr_header):
320 323 response = self.app.get(
321 324 route_path('repo_file_history',
322 325 repo_name=backend.repo_name,
323 326 commit_id='tip', f_path='vcs/nodes.py',
324 327 params=dict(annotate=1)),
325 328
326 329 extra_environ=xhr_header)
327 330 assert get_node_history(backend.alias) == json.loads(response.body)
328 331
329 332 def test_tree_search_top_level(self, backend, xhr_header):
330 333 commit = backend.repo.get_commit(commit_idx=173)
331 334 response = self.app.get(
332 335 route_path('repo_files_nodelist',
333 336 repo_name=backend.repo_name,
334 337 commit_id=commit.raw_id, f_path='/'),
335 338 extra_environ=xhr_header)
336 339 assert 'nodes' in response.json
337 340 assert {'name': 'docs', 'type': 'dir'} in response.json['nodes']
338 341
339 342 def test_tree_search_missing_xhr(self, backend):
340 343 self.app.get(
341 344 route_path('repo_files_nodelist',
342 345 repo_name=backend.repo_name,
343 346 commit_id='tip', f_path='/'),
344 347 status=404)
345 348
346 349 def test_tree_search_at_path(self, backend, xhr_header):
347 350 commit = backend.repo.get_commit(commit_idx=173)
348 351 response = self.app.get(
349 352 route_path('repo_files_nodelist',
350 353 repo_name=backend.repo_name,
351 354 commit_id=commit.raw_id, f_path='/docs'),
352 355 extra_environ=xhr_header)
353 356 assert 'nodes' in response.json
354 357 nodes = response.json['nodes']
355 358 assert {'name': 'docs/api', 'type': 'dir'} in nodes
356 359 assert {'name': 'docs/index.rst', 'type': 'file'} in nodes
357 360
358 361 def test_tree_search_at_path_2nd_level(self, backend, xhr_header):
359 362 commit = backend.repo.get_commit(commit_idx=173)
360 363 response = self.app.get(
361 364 route_path('repo_files_nodelist',
362 365 repo_name=backend.repo_name,
363 366 commit_id=commit.raw_id, f_path='/docs/api'),
364 367 extra_environ=xhr_header)
365 368 assert 'nodes' in response.json
366 369 nodes = response.json['nodes']
367 370 assert {'name': 'docs/api/index.rst', 'type': 'file'} in nodes
368 371
369 372 def test_tree_search_at_path_missing_xhr(self, backend):
370 373 self.app.get(
371 374 route_path('repo_files_nodelist',
372 375 repo_name=backend.repo_name,
373 376 commit_id='tip', f_path='/docs'),
374 377 status=404)
375 378
376 379 def test_nodetree(self, backend, xhr_header):
377 380 commit = backend.repo.get_commit(commit_idx=173)
378 381 response = self.app.get(
379 382 route_path('repo_nodetree_full',
380 383 repo_name=backend.repo_name,
381 384 commit_id=commit.raw_id, f_path='/'),
382 385 extra_environ=xhr_header)
383 386
384 387 assert_response = response.assert_response()
385 388
386 389 for attr in ['data-commit-id', 'data-date', 'data-author']:
387 390 elements = assert_response.get_elements('[{}]'.format(attr))
388 391 assert len(elements) > 1
389 392
390 393 for element in elements:
391 394 assert element.get(attr)
392 395
393 396 def test_nodetree_if_file(self, backend, xhr_header):
394 397 commit = backend.repo.get_commit(commit_idx=173)
395 398 response = self.app.get(
396 399 route_path('repo_nodetree_full',
397 400 repo_name=backend.repo_name,
398 401 commit_id=commit.raw_id, f_path='README.rst'),
399 402 extra_environ=xhr_header)
400 403 assert response.text == ''
401 404
402 405 def test_nodetree_wrong_path(self, backend, xhr_header):
403 406 commit = backend.repo.get_commit(commit_idx=173)
404 407 response = self.app.get(
405 408 route_path('repo_nodetree_full',
406 409 repo_name=backend.repo_name,
407 410 commit_id=commit.raw_id, f_path='/dont-exist'),
408 411 extra_environ=xhr_header)
409 412
410 413 err = 'error: There is no file nor ' \
411 414 'directory at the given path'
412 415 assert err in response.text
413 416
414 417 def test_nodetree_missing_xhr(self, backend):
415 418 self.app.get(
416 419 route_path('repo_nodetree_full',
417 420 repo_name=backend.repo_name,
418 421 commit_id='tip', f_path='/'),
419 422 status=404)
420 423
421 424
422 425 @pytest.mark.usefixtures("app", "autologin_user")
423 426 class TestRawFileHandling(object):
424 427
425 428 def test_download_file(self, backend):
426 429 commit = backend.repo.get_commit(commit_idx=173)
427 430 response = self.app.get(
428 431 route_path('repo_file_download',
429 432 repo_name=backend.repo_name,
430 433 commit_id=commit.raw_id, f_path='vcs/nodes.py'),)
431 434
432 435 assert response.content_disposition == 'attachment; filename="nodes.py"; filename*=UTF-8\'\'nodes.py'
433 436 assert response.content_type == "text/x-python"
434 437
435 438 def test_download_file_wrong_cs(self, backend):
436 439 raw_id = u'ERRORce30c96924232dffcd24178a07ffeb5dfc'
437 440
438 441 response = self.app.get(
439 442 route_path('repo_file_download',
440 443 repo_name=backend.repo_name,
441 444 commit_id=raw_id, f_path='vcs/nodes.svg'),
442 445 status=404)
443 446
444 447 msg = """No such commit exists for this repository"""
445 448 response.mustcontain(msg)
446 449
447 450 def test_download_file_wrong_f_path(self, backend):
448 451 commit = backend.repo.get_commit(commit_idx=173)
449 452 f_path = 'vcs/ERRORnodes.py'
450 453
451 454 response = self.app.get(
452 455 route_path('repo_file_download',
453 456 repo_name=backend.repo_name,
454 457 commit_id=commit.raw_id, f_path=f_path),
455 458 status=404)
456 459
457 460 msg = (
458 461 "There is no file nor directory at the given path: "
459 462 "`%s` at commit %s" % (f_path, commit.short_id))
460 463 response.mustcontain(msg)
461 464
462 465 def test_file_raw(self, backend):
463 466 commit = backend.repo.get_commit(commit_idx=173)
464 467 response = self.app.get(
465 468 route_path('repo_file_raw',
466 469 repo_name=backend.repo_name,
467 470 commit_id=commit.raw_id, f_path='vcs/nodes.py'),)
468 471
469 472 assert response.content_type == "text/plain"
470 473
471 474 def test_file_raw_binary(self, backend):
472 475 commit = backend.repo.get_commit()
473 476 response = self.app.get(
474 477 route_path('repo_file_raw',
475 478 repo_name=backend.repo_name,
476 479 commit_id=commit.raw_id,
477 480 f_path='docs/theme/ADC/static/breadcrumb_background.png'),)
478 481
479 482 assert response.content_disposition == 'inline'
480 483
481 484 def test_raw_file_wrong_cs(self, backend):
482 485 raw_id = u'ERRORcce30c96924232dffcd24178a07ffeb5dfc'
483 486
484 487 response = self.app.get(
485 488 route_path('repo_file_raw',
486 489 repo_name=backend.repo_name,
487 490 commit_id=raw_id, f_path='vcs/nodes.svg'),
488 491 status=404)
489 492
490 493 msg = """No such commit exists for this repository"""
491 494 response.mustcontain(msg)
492 495
493 496 def test_raw_wrong_f_path(self, backend):
494 497 commit = backend.repo.get_commit(commit_idx=173)
495 498 f_path = 'vcs/ERRORnodes.py'
496 499 response = self.app.get(
497 500 route_path('repo_file_raw',
498 501 repo_name=backend.repo_name,
499 502 commit_id=commit.raw_id, f_path=f_path),
500 503 status=404)
501 504
502 505 msg = (
503 506 "There is no file nor directory at the given path: "
504 507 "`%s` at commit %s" % (f_path, commit.short_id))
505 508 response.mustcontain(msg)
506 509
507 510 def test_raw_svg_should_not_be_rendered(self, backend):
508 511 backend.create_repo()
509 backend.ensure_file("xss.svg")
512 backend.ensure_file(b"xss.svg")
510 513 response = self.app.get(
511 514 route_path('repo_file_raw',
512 515 repo_name=backend.repo_name,
513 516 commit_id='tip', f_path='xss.svg'),)
514 517 # If the content type is image/svg+xml then it allows to render HTML
515 518 # and malicious SVG.
516 519 assert response.content_type == "text/plain"
517 520
518 521
519 522 @pytest.mark.usefixtures("app")
520 523 class TestRepositoryArchival(object):
521 524
522 525 def test_archival(self, backend):
523 526 backend.enable_downloads()
524 527 commit = backend.repo.get_commit(commit_idx=173)
525 528 for a_type, content_type, extension in settings.ARCHIVE_SPECS:
529 path_sha = get_path_sha('/')
530 filename = get_archive_name(backend.repo_name, commit_sha=commit.short_id, ext=extension, path_sha=path_sha)
526 531
527 short = commit.short_id + extension
528 532 fname = commit.raw_id + extension
529 filename = '%s-%s' % (backend.repo_name, short)
530 533 response = self.app.get(
531 534 route_path('repo_archivefile',
532 535 repo_name=backend.repo_name,
533 536 fname=fname))
534 537
535 538 assert response.status == '200 OK'
536 539 headers = [
537 540 ('Content-Disposition', 'attachment; filename=%s' % filename),
538 541 ('Content-Type', '%s' % content_type),
539 542 ]
540 543
541 544 for header in headers:
542 545 assert header in response.headers.items()
543 546
544 547 def test_archival_no_hash(self, backend):
545 548 backend.enable_downloads()
546 549 commit = backend.repo.get_commit(commit_idx=173)
547 550 for a_type, content_type, extension in settings.ARCHIVE_SPECS:
551 path_sha = get_path_sha('/')
552 filename = get_archive_name(backend.repo_name, commit_sha=commit.short_id, ext=extension, path_sha=path_sha, with_hash=False)
548 553
549 short = 'plain' + extension
550 554 fname = commit.raw_id + extension
551 filename = '%s-%s' % (backend.repo_name, short)
552 555 response = self.app.get(
553 556 route_path('repo_archivefile',
554 557 repo_name=backend.repo_name,
555 558 fname=fname, params={'with_hash': 0}))
556 559
557 560 assert response.status == '200 OK'
558 561 headers = [
559 562 ('Content-Disposition', 'attachment; filename=%s' % filename),
560 563 ('Content-Type', '%s' % content_type),
561 564 ]
562 565
563 566 for header in headers:
564 567 assert header in response.headers.items()
565 568
566 569 @pytest.mark.parametrize('arch_ext',[
567 570 'tar', 'rar', 'x', '..ax', '.zipz', 'tar.gz.tar'])
568 571 def test_archival_wrong_ext(self, backend, arch_ext):
569 572 backend.enable_downloads()
570 573 commit = backend.repo.get_commit(commit_idx=173)
571 574
572 575 fname = commit.raw_id + '.' + arch_ext
573 576
574 577 response = self.app.get(
575 578 route_path('repo_archivefile',
576 579 repo_name=backend.repo_name,
577 580 fname=fname))
578 581 response.mustcontain(
579 582 'Unknown archive type for: `{}`'.format(fname))
580 583
581 584 @pytest.mark.parametrize('commit_id', [
582 585 '00x000000', 'tar', 'wrong', '@$@$42413232', '232dffcd'])
583 586 def test_archival_wrong_commit_id(self, backend, commit_id):
584 587 backend.enable_downloads()
585 588 fname = '%s.zip' % commit_id
586 589
587 590 response = self.app.get(
588 591 route_path('repo_archivefile',
589 592 repo_name=backend.repo_name,
590 593 fname=fname))
591 594 response.mustcontain('Unknown commit_id')
592 595
593 596
594 597 @pytest.mark.usefixtures("app")
595 598 class TestFilesDiff(object):
596 599
597 600 @pytest.mark.parametrize("diff", ['diff', 'download', 'raw'])
598 601 def test_file_full_diff(self, backend, diff):
599 602 commit1 = backend.repo.get_commit(commit_idx=-1)
600 603 commit2 = backend.repo.get_commit(commit_idx=-2)
601 604
602 605 response = self.app.get(
603 606 route_path('repo_files_diff',
604 607 repo_name=backend.repo_name,
605 608 f_path='README'),
606 609 params={
607 610 'diff1': commit2.raw_id,
608 611 'diff2': commit1.raw_id,
609 612 'fulldiff': '1',
610 613 'diff': diff,
611 614 })
612 615
613 616 if diff == 'diff':
614 617 # use redirect since this is OLD view redirecting to compare page
615 618 response = response.follow()
616 619
617 620 # It's a symlink to README.rst
618 621 response.mustcontain('README.rst')
619 622 response.mustcontain('No newline at end of file')
620 623
621 624 def test_file_binary_diff(self, backend):
622 625 commits = [
623 626 {'message': 'First commit'},
624 627 {'message': 'Commit with binary',
625 'added': [nodes.FileNode('file.bin', content='\0BINARY\0')]},
628 'added': [nodes.FileNode(b'file.bin', content='\0BINARY\0')]},
626 629 ]
627 630 repo = backend.create_repo(commits=commits)
628 631
629 632 response = self.app.get(
630 633 route_path('repo_files_diff',
631 634 repo_name=backend.repo_name,
632 635 f_path='file.bin'),
633 636 params={
634 637 'diff1': repo.get_commit(commit_idx=0).raw_id,
635 638 'diff2': repo.get_commit(commit_idx=1).raw_id,
636 639 'fulldiff': '1',
637 640 'diff': 'diff',
638 641 })
639 642 # use redirect since this is OLD view redirecting to compare page
640 643 response = response.follow()
641 644 response.mustcontain('Collapse 1 commit')
642 645 file_changes = (1, 0, 0)
643 646
644 647 compare_page = ComparePage(response)
645 648 compare_page.contains_change_summary(*file_changes)
646 649
647 650 if backend.alias == 'svn':
648 651 response.mustcontain('new file 10644')
649 652 # TODO(marcink): SVN doesn't yet detect binary changes
650 653 else:
651 654 response.mustcontain('new file 100644')
652 655 response.mustcontain('binary diff hidden')
653 656
654 657 def test_diff_2way(self, backend):
655 658 commit1 = backend.repo.get_commit(commit_idx=-1)
656 659 commit2 = backend.repo.get_commit(commit_idx=-2)
657 660 response = self.app.get(
658 661 route_path('repo_files_diff_2way_redirect',
659 662 repo_name=backend.repo_name,
660 663 f_path='README'),
661 664 params={
662 665 'diff1': commit2.raw_id,
663 666 'diff2': commit1.raw_id,
664 667 })
665 668 # use redirect since this is OLD view redirecting to compare page
666 669 response = response.follow()
667 670
668 671 # It's a symlink to README.rst
669 672 response.mustcontain('README.rst')
670 673 response.mustcontain('No newline at end of file')
671 674
672 675 def test_requires_one_commit_id(self, backend, autologin_user):
673 676 response = self.app.get(
674 677 route_path('repo_files_diff',
675 678 repo_name=backend.repo_name,
676 679 f_path='README.rst'),
677 680 status=400)
678 681 response.mustcontain(
679 682 'Need query parameter', 'diff1', 'diff2', 'to generate a diff.')
680 683
681 684 def test_returns_no_files_if_file_does_not_exist(self, vcsbackend):
682 685 repo = vcsbackend.repo
683 686 response = self.app.get(
684 687 route_path('repo_files_diff',
685 688 repo_name=repo.name,
686 689 f_path='does-not-exist-in-any-commit'),
687 690 params={
688 691 'diff1': repo[0].raw_id,
689 692 'diff2': repo[1].raw_id
690 693 })
691 694
692 695 response = response.follow()
693 696 response.mustcontain('No files')
694 697
695 698 def test_returns_redirect_if_file_not_changed(self, backend):
696 699 commit = backend.repo.get_commit(commit_idx=-1)
697 700 response = self.app.get(
698 701 route_path('repo_files_diff_2way_redirect',
699 702 repo_name=backend.repo_name,
700 703 f_path='README'),
701 704 params={
702 705 'diff1': commit.raw_id,
703 706 'diff2': commit.raw_id,
704 707 })
705 708
706 709 response = response.follow()
707 710 response.mustcontain('No files')
708 711 response.mustcontain('No commits in this compare')
709 712
710 713 def test_supports_diff_to_different_path_svn(self, backend_svn):
711 714 #TODO: check this case
712 715 return
713 716
714 717 repo = backend_svn['svn-simple-layout'].scm_instance()
715 718 commit_id_1 = '24'
716 719 commit_id_2 = '26'
717 720
718 721 response = self.app.get(
719 722 route_path('repo_files_diff',
720 723 repo_name=backend_svn.repo_name,
721 724 f_path='trunk/example.py'),
722 725 params={
723 726 'diff1': 'tags/v0.2/example.py@' + commit_id_1,
724 727 'diff2': commit_id_2,
725 728 })
726 729
727 730 response = response.follow()
728 731 response.mustcontain(
729 732 # diff contains this
730 733 "Will print out a useful message on invocation.")
731 734
732 735 # Note: Expecting that we indicate the user what's being compared
733 736 response.mustcontain("trunk/example.py")
734 737 response.mustcontain("tags/v0.2/example.py")
735 738
736 739 def test_show_rev_redirects_to_svn_path(self, backend_svn):
737 740 #TODO: check this case
738 741 return
739 742
740 743 repo = backend_svn['svn-simple-layout'].scm_instance()
741 744 commit_id = repo[-1].raw_id
742 745
743 746 response = self.app.get(
744 747 route_path('repo_files_diff',
745 748 repo_name=backend_svn.repo_name,
746 749 f_path='trunk/example.py'),
747 750 params={
748 751 'diff1': 'branches/argparse/example.py@' + commit_id,
749 752 'diff2': commit_id,
750 753 },
751 754 status=302)
752 755 response = response.follow()
753 756 assert response.headers['Location'].endswith(
754 757 'svn-svn-simple-layout/files/26/branches/argparse/example.py')
755 758
756 759 def test_show_rev_and_annotate_redirects_to_svn_path(self, backend_svn):
757 760 #TODO: check this case
758 761 return
759 762
760 763 repo = backend_svn['svn-simple-layout'].scm_instance()
761 764 commit_id = repo[-1].raw_id
762 765 response = self.app.get(
763 766 route_path('repo_files_diff',
764 767 repo_name=backend_svn.repo_name,
765 768 f_path='trunk/example.py'),
766 769 params={
767 770 'diff1': 'branches/argparse/example.py@' + commit_id,
768 771 'diff2': commit_id,
769 772 'show_rev': 'Show at Revision',
770 773 'annotate': 'true',
771 774 },
772 775 status=302)
773 776 response = response.follow()
774 777 assert response.headers['Location'].endswith(
775 778 'svn-svn-simple-layout/annotate/26/branches/argparse/example.py')
776 779
777 780
778 781 @pytest.mark.usefixtures("app", "autologin_user")
779 782 class TestModifyFilesWithWebInterface(object):
780 783
781 784 def test_add_file_view(self, backend):
782 785 self.app.get(
783 786 route_path('repo_files_add_file',
784 787 repo_name=backend.repo_name,
785 788 commit_id='tip', f_path='/')
786 789 )
787 790
788 791 @pytest.mark.xfail_backends("svn", reason="Depends on online editing")
789 792 def test_add_file_into_repo_missing_content(self, backend, csrf_token):
790 793 backend.create_repo()
791 794 filename = 'init.py'
792 795 response = self.app.post(
793 796 route_path('repo_files_create_file',
794 797 repo_name=backend.repo_name,
795 798 commit_id='tip', f_path='/'),
796 799 params={
797 800 'content': "",
798 801 'filename': filename,
799 802 'csrf_token': csrf_token,
800 803 },
801 804 status=302)
802 805 expected_msg = 'Successfully committed new file `{}`'.format(os.path.join(filename))
803 806 assert_session_flash(response, expected_msg)
804 807
805 808 def test_add_file_into_repo_missing_filename(self, backend, csrf_token):
806 809 commit_id = backend.repo.get_commit().raw_id
807 810 response = self.app.post(
808 811 route_path('repo_files_create_file',
809 812 repo_name=backend.repo_name,
810 813 commit_id=commit_id, f_path='/'),
811 814 params={
812 815 'content': "foo",
813 816 'csrf_token': csrf_token,
814 817 },
815 818 status=302)
816 819
817 820 assert_session_flash(response, 'No filename specified')
818 821
819 822 def test_add_file_into_repo_errors_and_no_commits(
820 823 self, backend, csrf_token):
821 824 repo = backend.create_repo()
822 825 # Create a file with no filename, it will display an error but
823 826 # the repo has no commits yet
824 827 response = self.app.post(
825 828 route_path('repo_files_create_file',
826 829 repo_name=repo.repo_name,
827 830 commit_id='tip', f_path='/'),
828 831 params={
829 832 'content': "foo",
830 833 'csrf_token': csrf_token,
831 834 },
832 835 status=302)
833 836
834 837 assert_session_flash(response, 'No filename specified')
835 838
836 839 # Not allowed, redirect to the summary
837 840 redirected = response.follow()
838 841 summary_url = h.route_path('repo_summary', repo_name=repo.repo_name)
839 842
840 843 # As there are no commits, displays the summary page with the error of
841 844 # creating a file with no filename
842 845
843 846 assert redirected.request.path == summary_url
844 847
845 848 @pytest.mark.parametrize("filename, clean_filename", [
846 849 ('/abs/foo', 'abs/foo'),
847 850 ('../rel/foo', 'rel/foo'),
848 851 ('file/../foo/foo', 'file/foo/foo'),
849 852 ])
850 853 def test_add_file_into_repo_bad_filenames(self, filename, clean_filename, backend, csrf_token):
851 854 repo = backend.create_repo()
852 855 commit_id = repo.get_commit().raw_id
853 856
854 857 response = self.app.post(
855 858 route_path('repo_files_create_file',
856 859 repo_name=repo.repo_name,
857 860 commit_id=commit_id, f_path='/'),
858 861 params={
859 862 'content': "foo",
860 863 'filename': filename,
861 864 'csrf_token': csrf_token,
862 865 },
863 866 status=302)
864 867
865 868 expected_msg = 'Successfully committed new file `{}`'.format(clean_filename)
866 869 assert_session_flash(response, expected_msg)
867 870
868 871 @pytest.mark.parametrize("cnt, filename, content", [
869 872 (1, 'foo.txt', "Content"),
870 873 (2, 'dir/foo.rst', "Content"),
871 874 (3, 'dir/foo-second.rst', "Content"),
872 875 (4, 'rel/dir/foo.bar', "Content"),
873 876 ])
874 877 def test_add_file_into_empty_repo(self, cnt, filename, content, backend, csrf_token):
875 878 repo = backend.create_repo()
876 879 commit_id = repo.get_commit().raw_id
877 880 response = self.app.post(
878 881 route_path('repo_files_create_file',
879 882 repo_name=repo.repo_name,
880 883 commit_id=commit_id, f_path='/'),
881 884 params={
882 885 'content': content,
883 886 'filename': filename,
884 887 'csrf_token': csrf_token,
885 888 },
886 889 status=302)
887 890
888 891 expected_msg = 'Successfully committed new file `{}`'.format(filename)
889 892 assert_session_flash(response, expected_msg)
890 893
891 894 def test_edit_file_view(self, backend):
892 895 response = self.app.get(
893 896 route_path('repo_files_edit_file',
894 897 repo_name=backend.repo_name,
895 898 commit_id=backend.default_head_id,
896 899 f_path='vcs/nodes.py'),
897 900 status=200)
898 901 response.mustcontain("Module holding everything related to vcs nodes.")
899 902
900 903 def test_edit_file_view_not_on_branch(self, backend):
901 904 repo = backend.create_repo()
902 backend.ensure_file("vcs/nodes.py")
905 backend.ensure_file(b"vcs/nodes.py")
903 906
904 907 response = self.app.get(
905 908 route_path('repo_files_edit_file',
906 909 repo_name=repo.repo_name,
907 910 commit_id='tip',
908 911 f_path='vcs/nodes.py'),
909 912 status=302)
910 913 assert_session_flash(
911 914 response, 'Cannot modify file. Given commit `tip` is not head of a branch.')
912 915
913 916 def test_edit_file_view_commit_changes(self, backend, csrf_token):
914 917 repo = backend.create_repo()
915 backend.ensure_file("vcs/nodes.py", content="print 'hello'")
918 backend.ensure_file(b"vcs/nodes.py", content=b"print 'hello'")
916 919
917 920 response = self.app.post(
918 921 route_path('repo_files_update_file',
919 922 repo_name=repo.repo_name,
920 923 commit_id=backend.default_head_id,
921 924 f_path='vcs/nodes.py'),
922 925 params={
923 926 'content': "print 'hello world'",
924 927 'message': 'I committed',
925 928 'filename': "vcs/nodes.py",
926 929 'csrf_token': csrf_token,
927 930 },
928 931 status=302)
929 932 assert_session_flash(
930 933 response, 'Successfully committed changes to file `vcs/nodes.py`')
931 934 tip = repo.get_commit(commit_idx=-1)
932 935 assert tip.message == 'I committed'
933 936
934 937 def test_edit_file_view_commit_changes_default_message(self, backend,
935 938 csrf_token):
936 939 repo = backend.create_repo()
937 backend.ensure_file("vcs/nodes.py", content="print 'hello'")
940 backend.ensure_file(b"vcs/nodes.py", content=b"print 'hello'")
938 941
939 942 commit_id = (
940 943 backend.default_branch_name or
941 944 backend.repo.scm_instance().commit_ids[-1])
942 945
943 946 response = self.app.post(
944 947 route_path('repo_files_update_file',
945 948 repo_name=repo.repo_name,
946 949 commit_id=commit_id,
947 950 f_path='vcs/nodes.py'),
948 951 params={
949 952 'content': "print 'hello world'",
950 953 'message': '',
951 954 'filename': "vcs/nodes.py",
952 955 'csrf_token': csrf_token,
953 956 },
954 957 status=302)
955 958 assert_session_flash(
956 959 response, 'Successfully committed changes to file `vcs/nodes.py`')
957 960 tip = repo.get_commit(commit_idx=-1)
958 961 assert tip.message == 'Edited file vcs/nodes.py via RhodeCode Enterprise'
959 962
960 963 def test_delete_file_view(self, backend):
961 964 self.app.get(
962 965 route_path('repo_files_remove_file',
963 966 repo_name=backend.repo_name,
964 967 commit_id=backend.default_head_id,
965 968 f_path='vcs/nodes.py'),
966 969 status=200)
967 970
968 971 def test_delete_file_view_not_on_branch(self, backend):
969 972 repo = backend.create_repo()
970 backend.ensure_file('vcs/nodes.py')
973 backend.ensure_file(b'vcs/nodes.py')
971 974
972 975 response = self.app.get(
973 976 route_path('repo_files_remove_file',
974 977 repo_name=repo.repo_name,
975 978 commit_id='tip',
976 979 f_path='vcs/nodes.py'),
977 980 status=302)
978 981 assert_session_flash(
979 982 response, 'Cannot modify file. Given commit `tip` is not head of a branch.')
980 983
981 984 def test_delete_file_view_commit_changes(self, backend, csrf_token):
982 985 repo = backend.create_repo()
983 backend.ensure_file("vcs/nodes.py")
986 backend.ensure_file(b"vcs/nodes.py")
984 987
985 988 response = self.app.post(
986 989 route_path('repo_files_delete_file',
987 990 repo_name=repo.repo_name,
988 991 commit_id=backend.default_head_id,
989 992 f_path='vcs/nodes.py'),
990 993 params={
991 994 'message': 'i committed',
992 995 'csrf_token': csrf_token,
993 996 },
994 997 status=302)
995 998 assert_session_flash(
996 999 response, 'Successfully deleted file `vcs/nodes.py`')
997 1000
998 1001
999 1002 @pytest.mark.usefixtures("app")
1000 1003 class TestFilesViewOtherCases(object):
1001 1004
1002 1005 def test_access_empty_repo_redirect_to_summary_with_alert_write_perms(
1003 1006 self, backend_stub, autologin_regular_user, user_regular,
1004 1007 user_util):
1005 1008
1006 1009 repo = backend_stub.create_repo()
1007 1010 user_util.grant_user_permission_to_repo(
1008 1011 repo, user_regular, 'repository.write')
1009 1012 response = self.app.get(
1010 1013 route_path('repo_files',
1011 1014 repo_name=repo.repo_name,
1012 1015 commit_id='tip', f_path='/'))
1013 1016
1014 1017 repo_file_add_url = route_path(
1015 1018 'repo_files_add_file',
1016 1019 repo_name=repo.repo_name,
1017 1020 commit_id=0, f_path='')
1021 add_new = f'<a class="alert-link" href="{repo_file_add_url}">add a new file</a>'
1022
1023 repo_file_upload_url = route_path(
1024 'repo_files_upload_file',
1025 repo_name=repo.repo_name,
1026 commit_id=0, f_path='')
1027 upload_new = f'<a class="alert-link" href="{repo_file_upload_url}">upload a new file</a>'
1018 1028
1019 1029 assert_session_flash(
1020 1030 response,
1021 'There are no files yet. <a class="alert-link" '
1022 'href="{}">Click here to add a new file.</a>'
1023 .format(repo_file_add_url))
1031 'There are no files yet. Click here to %s or %s.' % (add_new, upload_new)
1032 )
1024 1033
1025 1034 def test_access_empty_repo_redirect_to_summary_with_alert_no_write_perms(
1026 1035 self, backend_stub, autologin_regular_user):
1027 1036 repo = backend_stub.create_repo()
1028 1037 # init session for anon user
1029 1038 route_path('repo_summary', repo_name=repo.repo_name)
1030 1039
1031 1040 repo_file_add_url = route_path(
1032 1041 'repo_files_add_file',
1033 1042 repo_name=repo.repo_name,
1034 1043 commit_id=0, f_path='')
1035 1044
1036 1045 response = self.app.get(
1037 1046 route_path('repo_files',
1038 1047 repo_name=repo.repo_name,
1039 1048 commit_id='tip', f_path='/'))
1040 1049
1041 1050 assert_session_flash(response, no_=repo_file_add_url)
1042 1051
1043 1052 @pytest.mark.parametrize('file_node', [
1044 'archive/file.zip',
1045 'diff/my-file.txt',
1046 'render.py',
1047 'render',
1048 'remove_file',
1049 'remove_file/to-delete.txt',
1053 b'archive/file.zip',
1054 b'diff/my-file.txt',
1055 b'render.py',
1056 b'render',
1057 b'remove_file',
1058 b'remove_file/to-delete.txt',
1050 1059 ])
1051 1060 def test_file_names_equal_to_routes_parts(self, backend, file_node):
1052 1061 backend.create_repo()
1053 1062 backend.ensure_file(file_node)
1054 1063
1055 1064 self.app.get(
1056 1065 route_path('repo_files',
1057 1066 repo_name=backend.repo_name,
1058 commit_id='tip', f_path=file_node),
1067 commit_id='tip', f_path=safe_str(file_node)),
1059 1068 status=200)
1060 1069
1061 1070
1062 1071 class TestAdjustFilePathForSvn(object):
1063 1072 """
1064 1073 SVN specific adjustments of node history in RepoFilesView.
1065 1074 """
1066 1075
1067 1076 def test_returns_path_relative_to_matched_reference(self):
1068 1077 repo = self._repo(branches=['trunk'])
1069 1078 self.assert_file_adjustment('trunk/file', 'file', repo)
1070 1079
1071 1080 def test_does_not_modify_file_if_no_reference_matches(self):
1072 1081 repo = self._repo(branches=['trunk'])
1073 1082 self.assert_file_adjustment('notes/file', 'notes/file', repo)
1074 1083
1075 1084 def test_does_not_adjust_partial_directory_names(self):
1076 1085 repo = self._repo(branches=['trun'])
1077 1086 self.assert_file_adjustment('trunk/file', 'trunk/file', repo)
1078 1087
1079 1088 def test_is_robust_to_patterns_which_prefix_other_patterns(self):
1080 1089 repo = self._repo(branches=['trunk', 'trunk/new', 'trunk/old'])
1081 1090 self.assert_file_adjustment('trunk/new/file', 'file', repo)
1082 1091
1083 1092 def assert_file_adjustment(self, f_path, expected, repo):
1084 1093 result = RepoFilesView.adjust_file_path_for_svn(f_path, repo)
1085 1094 assert result == expected
1086 1095
1087 1096 def _repo(self, branches=None):
1088 1097 repo = mock.Mock()
1089 1098 repo.branches = OrderedDict((name, '0') for name in branches or [])
1090 1099 repo.tags = {}
1091 1100 return repo
@@ -1,332 +1,334 b''
1 1
2 2 # Copyright (C) 2010-2020 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 31
32 32 fixture = Fixture()
33 33
34 34
35 35 def route_path(name, params=None, **kwargs):
36 import urllib.request, urllib.parse, urllib.error
36 import urllib.request
37 import urllib.parse
38 import urllib.error
37 39
38 40 base_url = {
39 41 'repo_summary': '/{repo_name}',
40 42 'repo_creating_check': '/{repo_name}/repo_creating_check',
41 43 'repo_fork_new': '/{repo_name}/fork',
42 44 'repo_fork_create': '/{repo_name}/fork/create',
43 45 'repo_forks_show_all': '/{repo_name}/forks',
44 46 'repo_forks_data': '/{repo_name}/forks/data',
45 47 }[name].format(**kwargs)
46 48
47 49 if params:
48 50 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
49 51 return base_url
50 52
51 53
52 54 FORK_NAME = {
53 55 'hg': HG_FORK,
54 56 'git': GIT_FORK
55 57 }
56 58
57 59
58 60 @pytest.mark.skip_backends('svn')
59 61 class TestRepoForkViewTests(TestController):
60 62
61 63 def test_show_forks(self, backend, xhr_header):
62 64 self.log_user()
63 65 response = self.app.get(
64 66 route_path('repo_forks_data', repo_name=backend.repo_name),
65 67 extra_environ=xhr_header)
66 68
67 69 assert response.json == {u'data': [], u'draw': None,
68 70 u'recordsFiltered': 0, u'recordsTotal': 0}
69 71
70 72 def test_no_permissions_to_fork_page(self, backend, user_util):
71 73 user = user_util.create_user(password='qweqwe')
72 74 user_id = user.user_id
73 75 self.log_user(user.username, 'qweqwe')
74 76
75 77 user_model = UserModel()
76 78 user_model.revoke_perm(user_id, 'hg.fork.repository')
77 79 user_model.grant_perm(user_id, 'hg.fork.none')
78 80 u = UserModel().get(user_id)
79 81 u.inherit_default_permissions = False
80 82 Session().commit()
81 83 # try create a fork
82 84 self.app.get(
83 85 route_path('repo_fork_new', repo_name=backend.repo_name),
84 86 status=404)
85 87
86 88 def test_no_permissions_to_fork_submit(self, backend, csrf_token, user_util):
87 89 user = user_util.create_user(password='qweqwe')
88 90 user_id = user.user_id
89 91 self.log_user(user.username, 'qweqwe')
90 92
91 93 user_model = UserModel()
92 94 user_model.revoke_perm(user_id, 'hg.fork.repository')
93 95 user_model.grant_perm(user_id, 'hg.fork.none')
94 96 u = UserModel().get(user_id)
95 97 u.inherit_default_permissions = False
96 98 Session().commit()
97 99 # try create a fork
98 100 self.app.post(
99 101 route_path('repo_fork_create', repo_name=backend.repo_name),
100 102 {'csrf_token': csrf_token},
101 103 status=404)
102 104
103 105 def test_fork_missing_data(self, autologin_user, backend, csrf_token):
104 106 # try create a fork
105 107 response = self.app.post(
106 108 route_path('repo_fork_create', repo_name=backend.repo_name),
107 109 {'csrf_token': csrf_token},
108 110 status=200)
109 111 # test if html fill works fine
110 112 response.mustcontain('Missing value')
111 113
112 114 def test_create_fork_page(self, autologin_user, backend):
113 115 self.app.get(
114 116 route_path('repo_fork_new', repo_name=backend.repo_name),
115 117 status=200)
116 118
117 119 def test_create_and_show_fork(
118 120 self, autologin_user, backend, csrf_token, xhr_header):
119 121
120 122 # create a fork
121 123 fork_name = FORK_NAME[backend.alias]
122 124 description = 'fork of vcs test'
123 125 repo_name = backend.repo_name
124 126 source_repo = Repository.get_by_repo_name(repo_name)
125 127 creation_args = {
126 128 'repo_name': fork_name,
127 129 'repo_group': '',
128 130 'fork_parent_id': source_repo.repo_id,
129 131 'repo_type': backend.alias,
130 132 'description': description,
131 133 'private': 'False',
132 134 'csrf_token': csrf_token,
133 135 }
134 136
135 137 self.app.post(
136 138 route_path('repo_fork_create', repo_name=repo_name), creation_args)
137 139
138 140 response = self.app.get(
139 141 route_path('repo_forks_data', repo_name=repo_name),
140 142 extra_environ=xhr_header)
141 143
142 144 assert response.json['data'][0]['fork_name'] == \
143 145 """<a href="/%s">%s</a>""" % (fork_name, fork_name)
144 146
145 147 # remove this fork
146 148 fixture.destroy_repo(fork_name)
147 149
148 150 def test_fork_create(self, autologin_user, backend, csrf_token):
149 151 fork_name = FORK_NAME[backend.alias]
150 152 description = 'fork of vcs test'
151 153 repo_name = backend.repo_name
152 154 source_repo = Repository.get_by_repo_name(repo_name)
153 155 creation_args = {
154 156 'repo_name': fork_name,
155 157 'repo_group': '',
156 158 'fork_parent_id': source_repo.repo_id,
157 159 'repo_type': backend.alias,
158 160 'description': description,
159 161 'private': 'False',
160 162 'csrf_token': csrf_token,
161 163 }
162 164 self.app.post(
163 165 route_path('repo_fork_create', repo_name=repo_name), creation_args)
164 166 repo = Repository.get_by_repo_name(FORK_NAME[backend.alias])
165 167 assert repo.fork.repo_name == backend.repo_name
166 168
167 169 # run the check page that triggers the flash message
168 170 response = self.app.get(
169 171 route_path('repo_creating_check', repo_name=fork_name))
170 172 # test if we have a message that fork is ok
171 173 assert_session_flash(response,
172 174 'Forked repository %s as <a href="/%s">%s</a>' % (
173 175 repo_name, fork_name, fork_name))
174 176
175 177 # test if the fork was created in the database
176 178 fork_repo = Session().query(Repository)\
177 179 .filter(Repository.repo_name == fork_name).one()
178 180
179 181 assert fork_repo.repo_name == fork_name
180 182 assert fork_repo.fork.repo_name == repo_name
181 183
182 184 # test if the repository is visible in the list ?
183 185 response = self.app.get(
184 186 h.route_path('repo_summary', repo_name=fork_name))
185 187 response.mustcontain(fork_name)
186 188 response.mustcontain(backend.alias)
187 189 response.mustcontain('Fork of')
188 190 response.mustcontain('<a href="/%s">%s</a>' % (repo_name, repo_name))
189 191
190 192 def test_fork_create_into_group(self, autologin_user, backend, csrf_token):
191 193 group = fixture.create_repo_group('vc')
192 194 group_id = group.group_id
193 195 fork_name = FORK_NAME[backend.alias]
194 196 fork_name_full = 'vc/%s' % fork_name
195 197 description = 'fork of vcs test'
196 198 repo_name = backend.repo_name
197 199 source_repo = Repository.get_by_repo_name(repo_name)
198 200 creation_args = {
199 201 'repo_name': fork_name,
200 202 'repo_group': group_id,
201 203 'fork_parent_id': source_repo.repo_id,
202 204 'repo_type': backend.alias,
203 205 'description': description,
204 206 'private': 'False',
205 207 'csrf_token': csrf_token,
206 208 }
207 209 self.app.post(
208 210 route_path('repo_fork_create', repo_name=repo_name), creation_args)
209 211 repo = Repository.get_by_repo_name(fork_name_full)
210 212 assert repo.fork.repo_name == backend.repo_name
211 213
212 214 # run the check page that triggers the flash message
213 215 response = self.app.get(
214 216 route_path('repo_creating_check', repo_name=fork_name_full))
215 217 # test if we have a message that fork is ok
216 218 assert_session_flash(response,
217 219 'Forked repository %s as <a href="/%s">%s</a>' % (
218 220 repo_name, fork_name_full, fork_name_full))
219 221
220 222 # test if the fork was created in the database
221 223 fork_repo = Session().query(Repository)\
222 224 .filter(Repository.repo_name == fork_name_full).one()
223 225
224 226 assert fork_repo.repo_name == fork_name_full
225 227 assert fork_repo.fork.repo_name == repo_name
226 228
227 229 # test if the repository is visible in the list ?
228 230 response = self.app.get(
229 231 h.route_path('repo_summary', repo_name=fork_name_full))
230 232 response.mustcontain(fork_name_full)
231 233 response.mustcontain(backend.alias)
232 234
233 235 response.mustcontain('Fork of')
234 236 response.mustcontain('<a href="/%s">%s</a>' % (repo_name, repo_name))
235 237
236 238 fixture.destroy_repo(fork_name_full)
237 239 fixture.destroy_repo_group(group_id)
238 240
239 241 def test_fork_read_permission(self, backend, xhr_header, user_util):
240 242 user = user_util.create_user(password='qweqwe')
241 243 user_id = user.user_id
242 244 self.log_user(user.username, 'qweqwe')
243 245
244 246 # create a fake fork
245 247 fork = user_util.create_repo(repo_type=backend.alias)
246 248 source = user_util.create_repo(repo_type=backend.alias)
247 249 repo_name = source.repo_name
248 250
249 251 fork.fork_id = source.repo_id
250 252 fork_name = fork.repo_name
251 253 Session().commit()
252 254
253 255 forks = Repository.query()\
254 256 .filter(Repository.repo_type == backend.alias)\
255 257 .filter(Repository.fork_id == source.repo_id).all()
256 258 assert 1 == len(forks)
257 259
258 260 # set read permissions for this
259 261 RepoModel().grant_user_permission(
260 262 repo=forks[0], user=user_id, perm='repository.read')
261 263 Session().commit()
262 264
263 265 response = self.app.get(
264 266 route_path('repo_forks_data', repo_name=repo_name),
265 267 extra_environ=xhr_header)
266 268
267 269 assert response.json['data'][0]['fork_name'] == \
268 270 """<a href="/%s">%s</a>""" % (fork_name, fork_name)
269 271
270 272 def test_fork_none_permission(self, backend, xhr_header, user_util):
271 273 user = user_util.create_user(password='qweqwe')
272 274 user_id = user.user_id
273 275 self.log_user(user.username, 'qweqwe')
274 276
275 277 # create a fake fork
276 278 fork = user_util.create_repo(repo_type=backend.alias)
277 279 source = user_util.create_repo(repo_type=backend.alias)
278 280 repo_name = source.repo_name
279 281
280 282 fork.fork_id = source.repo_id
281 283
282 284 Session().commit()
283 285
284 286 forks = Repository.query()\
285 287 .filter(Repository.repo_type == backend.alias)\
286 288 .filter(Repository.fork_id == source.repo_id).all()
287 289 assert 1 == len(forks)
288 290
289 291 # set none
290 292 RepoModel().grant_user_permission(
291 293 repo=forks[0], user=user_id, perm='repository.none')
292 294 Session().commit()
293 295
294 296 # fork shouldn't be there
295 297 response = self.app.get(
296 298 route_path('repo_forks_data', repo_name=repo_name),
297 299 extra_environ=xhr_header)
298 300
299 301 assert response.json == {u'data': [], u'draw': None,
300 302 u'recordsFiltered': 0, u'recordsTotal': 0}
301 303
302 304 @pytest.mark.parametrize('url_type', [
303 305 'repo_fork_new',
304 306 'repo_fork_create'
305 307 ])
306 308 def test_fork_is_forbidden_on_archived_repo(self, backend, xhr_header, user_util, url_type):
307 309 user = user_util.create_user(password='qweqwe')
308 310 self.log_user(user.username, 'qweqwe')
309 311
310 312 # create a temporary repo
311 313 source = user_util.create_repo(repo_type=backend.alias)
312 314 repo_name = source.repo_name
313 315 repo = Repository.get_by_repo_name(repo_name)
314 316 repo.archived = True
315 317 Session().commit()
316 318
317 319 response = self.app.get(
318 320 route_path(url_type, repo_name=repo_name), status=302)
319 321
320 322 msg = 'Action not supported for archived repository.'
321 323 assert_session_flash(response, msg)
322 324
323 325
324 326 class TestSVNFork(TestController):
325 327 @pytest.mark.parametrize('route_name', [
326 328 'repo_fork_create', 'repo_fork_new'
327 329 ])
328 330 def test_fork_redirects(self, autologin_user, backend_svn, route_name):
329 331
330 332 self.app.get(route_path(
331 333 route_name, repo_name=backend_svn.repo_name),
332 334 status=404)
@@ -1,148 +1,150 b''
1 1
2 2 # Copyright (C) 2010-2020 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 from rhodecode.lib.utils2 import md5
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 26
27 27
28 28 def route_path(name, params=None, **kwargs):
29 import urllib.request, urllib.parse, urllib.error
29 import urllib.request
30 import urllib.parse
31 import urllib.error
30 32
31 33 base_url = {
32 34 'repo_summary': '/{repo_name}',
33 35 'edit_repo_issuetracker': '/{repo_name}/settings/issue_trackers',
34 36 'edit_repo_issuetracker_test': '/{repo_name}/settings/issue_trackers/test',
35 37 'edit_repo_issuetracker_delete': '/{repo_name}/settings/issue_trackers/delete',
36 38 'edit_repo_issuetracker_update': '/{repo_name}/settings/issue_trackers/update',
37 39 }[name].format(**kwargs)
38 40
39 41 if params:
40 42 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
41 43 return base_url
42 44
43 45
44 46 @pytest.mark.usefixtures("app")
45 47 class TestRepoIssueTracker(object):
46 48 def test_issuetracker_index(self, autologin_user, backend):
47 49 repo = backend.create_repo()
48 50 response = self.app.get(route_path('edit_repo_issuetracker',
49 51 repo_name=repo.repo_name))
50 52 assert response.status_code == 200
51 53
52 54 def test_add_and_test_issuetracker_patterns(
53 55 self, autologin_user, backend, csrf_token, request, xhr_header):
54 56 pattern = 'issuetracker_pat'
55 57 another_pattern = pattern+'1'
56 58 post_url = route_path(
57 59 'edit_repo_issuetracker_update', repo_name=backend.repo.repo_name)
58 60 post_data = {
59 61 'new_pattern_pattern_0': pattern,
60 62 'new_pattern_url_0': 'http://url',
61 63 'new_pattern_prefix_0': 'prefix',
62 64 'new_pattern_description_0': 'description',
63 65 'new_pattern_pattern_1': another_pattern,
64 66 'new_pattern_url_1': '/url1',
65 67 'new_pattern_prefix_1': 'prefix1',
66 68 'new_pattern_description_1': 'description1',
67 69 'csrf_token': csrf_token
68 70 }
69 71 self.app.post(post_url, post_data, status=302)
70 72 self.settings_model = IssueTrackerSettingsModel(repo=backend.repo)
71 73 settings = self.settings_model.get_repo_settings()
72 self.uid = md5(pattern)
74 self.uid = md5_safe(pattern)
73 75 assert settings[self.uid]['pat'] == pattern
74 self.another_uid = md5(another_pattern)
76 self.another_uid = md5_safe(another_pattern)
75 77 assert settings[self.another_uid]['pat'] == another_pattern
76 78
77 79 # test pattern
78 80 data = {'test_text': 'example of issuetracker_pat replacement',
79 81 'csrf_token': csrf_token}
80 82 response = self.app.post(
81 83 route_path('edit_repo_issuetracker_test',
82 84 repo_name=backend.repo.repo_name),
83 85 extra_environ=xhr_header, params=data)
84 86
85 87 assert response.text == \
86 88 'example of <a class="tooltip issue-tracker-link" href="http://url" title="description">prefix</a> replacement'
87 89
88 90 @request.addfinalizer
89 91 def cleanup():
90 92 self.settings_model.delete_entries(self.uid)
91 93 self.settings_model.delete_entries(self.another_uid)
92 94
93 95 def test_edit_issuetracker_pattern(
94 96 self, autologin_user, backend, csrf_token, request):
95 97 entry_key = 'issuetracker_pat_'
96 98 pattern = 'issuetracker_pat2'
97 99 old_pattern = 'issuetracker_pat'
98 old_uid = md5(old_pattern)
100 old_uid = md5_safe(old_pattern)
99 101
100 102 sett = SettingsModel(repo=backend.repo).create_or_update_setting(
101 103 entry_key+old_uid, old_pattern, 'unicode')
102 104 Session().add(sett)
103 105 Session().commit()
104 106 post_url = route_path(
105 107 'edit_repo_issuetracker_update', repo_name=backend.repo.repo_name)
106 108 post_data = {
107 109 'new_pattern_pattern_0': pattern,
108 110 'new_pattern_url_0': '/url',
109 111 'new_pattern_prefix_0': 'prefix',
110 112 'new_pattern_description_0': 'description',
111 113 'uid': old_uid,
112 114 'csrf_token': csrf_token
113 115 }
114 116 self.app.post(post_url, post_data, status=302)
115 117 self.settings_model = IssueTrackerSettingsModel(repo=backend.repo)
116 118 settings = self.settings_model.get_repo_settings()
117 self.uid = md5(pattern)
119 self.uid = md5_safe(pattern)
118 120 assert settings[self.uid]['pat'] == pattern
119 121 with pytest.raises(KeyError):
120 122 key = settings[old_uid]
121 123
122 124 @request.addfinalizer
123 125 def cleanup():
124 126 self.settings_model.delete_entries(self.uid)
125 127
126 128 def test_delete_issuetracker_pattern(
127 129 self, autologin_user, backend, csrf_token, settings_util, xhr_header):
128 130 repo = backend.create_repo()
129 131 repo_name = repo.repo_name
130 132 entry_key = 'issuetracker_pat_'
131 133 pattern = 'issuetracker_pat3'
132 uid = md5(pattern)
134 uid = md5_safe(pattern)
133 135 settings_util.create_repo_rhodecode_setting(
134 136 repo=backend.repo, name=entry_key+uid,
135 137 value=entry_key, type_='unicode', cleanup=False)
136 138
137 139 self.app.post(
138 140 route_path(
139 141 'edit_repo_issuetracker_delete',
140 142 repo_name=backend.repo.repo_name),
141 143 {
142 144 'uid': uid,
143 145 'csrf_token': csrf_token,
144 146 '': ''
145 147 }, extra_environ=xhr_header, status=200)
146 148 settings = IssueTrackerSettingsModel(
147 149 repo=Repository.get_by_repo_name(repo_name)).get_repo_settings()
148 150 assert 'rhodecode_%s%s' % (entry_key, uid) not in settings
@@ -1,73 +1,75 b''
1 1
2 2 # Copyright (C) 2010-2020 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 from rhodecode.lib.utils2 import str2bool
24 24 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
25 25 from rhodecode.model.db import Repository, UserRepoToPerm, Permission, User
26 26 from rhodecode.model.meta import Session
27 27 from rhodecode.tests import (
28 28 TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN, assert_session_flash)
29 29 from rhodecode.tests.fixture import Fixture
30 30
31 31 fixture = Fixture()
32 32
33 33
34 34 def route_path(name, params=None, **kwargs):
35 import urllib.request, urllib.parse, urllib.error
35 import urllib.request
36 import urllib.parse
37 import urllib.error
36 38
37 39 base_url = {
38 40 'edit_repo_maintenance': '/{repo_name}/settings/maintenance',
39 41 'edit_repo_maintenance_execute': '/{repo_name}/settings/maintenance/execute',
40 42
41 43 }[name].format(**kwargs)
42 44
43 45 if params:
44 46 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
45 47 return base_url
46 48
47 49
48 50 def _get_permission_for_user(user, repo):
49 51 perm = UserRepoToPerm.query()\
50 52 .filter(UserRepoToPerm.repository ==
51 53 Repository.get_by_repo_name(repo))\
52 54 .filter(UserRepoToPerm.user == User.get_by_username(user))\
53 55 .all()
54 56 return perm
55 57
56 58
57 59 @pytest.mark.usefixtures('autologin_user', 'app')
58 60 class TestAdminRepoMaintenance(object):
59 61 @pytest.mark.parametrize('urlname', [
60 62 'edit_repo_maintenance',
61 63 ])
62 64 def test_show_page(self, urlname, app, backend):
63 65 app.get(route_path(urlname, repo_name=backend.repo_name), status=200)
64 66
65 67 def test_execute_maintenance_for_repo_hg(self, app, backend_hg, autologin_user, xhr_header):
66 68 repo_name = backend_hg.repo_name
67 69
68 70 response = app.get(
69 71 route_path('edit_repo_maintenance_execute',
70 72 repo_name=repo_name,),
71 73 extra_environ=xhr_header)
72 74
73 75 assert "HG Verify repo" in ''.join(response.json)
@@ -1,76 +1,78 b''
1 1
2 2 # Copyright (C) 2010-2020 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 23
24 24
25 25 def route_path(name, params=None, **kwargs):
26 import urllib.request, urllib.parse, urllib.error
26 import urllib.request
27 import urllib.parse
28 import urllib.error
27 29
28 30 base_url = {
29 31 'edit_repo_perms': '/{repo_name}/settings/permissions'
30 32 # update is the same url
31 33 }[name].format(**kwargs)
32 34
33 35 if params:
34 36 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
35 37 return base_url
36 38
37 39
38 40 @pytest.mark.usefixtures("app")
39 41 class TestRepoPermissionsView(object):
40 42
41 43 def test_edit_perms_view(self, user_util, autologin_user):
42 44 repo = user_util.create_repo()
43 45 self.app.get(
44 46 route_path('edit_repo_perms',
45 47 repo_name=repo.repo_name), status=200)
46 48
47 49 def test_update_permissions(self, csrf_token, user_util):
48 50 repo = user_util.create_repo()
49 51 repo_name = repo.repo_name
50 52 user = user_util.create_user()
51 53 user_id = user.user_id
52 54 username = user.username
53 55
54 56 # grant new
55 57 form_data = permission_update_data_generator(
56 58 csrf_token,
57 59 default='repository.write',
58 60 grant=[(user_id, 'repository.write', username, 'user')])
59 61
60 62 response = self.app.post(
61 63 route_path('edit_repo_perms',
62 64 repo_name=repo_name), form_data).follow()
63 65
64 66 assert 'Repository access permissions updated' in response
65 67
66 68 # revoke given
67 69 form_data = permission_update_data_generator(
68 70 csrf_token,
69 71 default='repository.read',
70 72 revoke=[(user_id, 'user')])
71 73
72 74 response = self.app.post(
73 75 route_path('edit_repo_perms',
74 76 repo_name=repo_name), form_data).follow()
75 77
76 78 assert 'Repository access permissions updated' in response
@@ -1,1679 +1,1680 b''
1 1
2 2 # Copyright (C) 2010-2020 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 import mock
20 20 import pytest
21 21
22 22 import rhodecode
23 23 from rhodecode.lib import helpers as h
24 24 from rhodecode.lib.vcs.backends.base import MergeResponse, MergeFailureReason
25 25 from rhodecode.lib.vcs.nodes import FileNode
26 26 from rhodecode.lib.ext_json import json
27 27 from rhodecode.model.changeset_status import ChangesetStatusModel
28 28 from rhodecode.model.db import (
29 29 PullRequest, ChangesetStatus, UserLog, Notification, ChangesetComment, Repository)
30 30 from rhodecode.model.meta import Session
31 31 from rhodecode.model.pull_request import PullRequestModel
32 32 from rhodecode.model.user import UserModel
33 33 from rhodecode.model.comment import CommentsModel
34 34 from rhodecode.tests import (
35 35 assert_session_flash, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN)
36 36
37 37
38 38 def route_path(name, params=None, **kwargs):
39 import urllib.request, urllib.parse, urllib.error
39 import urllib.request
40 import urllib.parse
41 import urllib.error
40 42
41 43 base_url = {
42 44 'repo_changelog': '/{repo_name}/changelog',
43 45 'repo_changelog_file': '/{repo_name}/changelog/{commit_id}/{f_path}',
44 46 'repo_commits': '/{repo_name}/commits',
45 47 'repo_commits_file': '/{repo_name}/commits/{commit_id}/{f_path}',
46 48 'pullrequest_show': '/{repo_name}/pull-request/{pull_request_id}',
47 49 'pullrequest_show_all': '/{repo_name}/pull-request',
48 50 'pullrequest_show_all_data': '/{repo_name}/pull-request-data',
49 51 'pullrequest_repo_refs': '/{repo_name}/pull-request/refs/{target_repo_name:.*?[^/]}',
50 52 'pullrequest_repo_targets': '/{repo_name}/pull-request/repo-destinations',
51 53 'pullrequest_new': '/{repo_name}/pull-request/new',
52 54 'pullrequest_create': '/{repo_name}/pull-request/create',
53 55 'pullrequest_update': '/{repo_name}/pull-request/{pull_request_id}/update',
54 56 'pullrequest_merge': '/{repo_name}/pull-request/{pull_request_id}/merge',
55 57 'pullrequest_delete': '/{repo_name}/pull-request/{pull_request_id}/delete',
56 58 'pullrequest_comment_create': '/{repo_name}/pull-request/{pull_request_id}/comment',
57 59 'pullrequest_comment_delete': '/{repo_name}/pull-request/{pull_request_id}/comment/{comment_id}/delete',
58 60 'pullrequest_comment_edit': '/{repo_name}/pull-request/{pull_request_id}/comment/{comment_id}/edit',
59 61 }[name].format(**kwargs)
60 62
61 63 if params:
62 64 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
63 65 return base_url
64 66
65 67
66 68 @pytest.mark.usefixtures('app', 'autologin_user')
67 69 @pytest.mark.backends("git", "hg")
68 70 class TestPullrequestsView(object):
69 71
70 72 def test_index(self, backend):
71 73 self.app.get(route_path(
72 74 'pullrequest_new',
73 75 repo_name=backend.repo_name))
74 76
75 77 def test_option_menu_create_pull_request_exists(self, backend):
76 78 repo_name = backend.repo_name
77 79 response = self.app.get(h.route_path('repo_summary', repo_name=repo_name))
78 80
79 81 create_pr_link = '<a href="%s">Create Pull Request</a>' % route_path(
80 82 'pullrequest_new', repo_name=repo_name)
81 83 response.mustcontain(create_pr_link)
82 84
83 85 def test_create_pr_form_with_raw_commit_id(self, backend):
84 86 repo = backend.repo
85 87
86 88 self.app.get(
87 89 route_path('pullrequest_new', repo_name=repo.repo_name,
88 90 commit=repo.get_commit().raw_id),
89 91 status=200)
90 92
91 93 @pytest.mark.parametrize('pr_merge_enabled', [True, False])
92 94 @pytest.mark.parametrize('range_diff', ["0", "1"])
93 95 def test_show(self, pr_util, pr_merge_enabled, range_diff):
94 96 pull_request = pr_util.create_pull_request(
95 97 mergeable=pr_merge_enabled, enable_notifications=False)
96 98
97 99 response = self.app.get(route_path(
98 100 'pullrequest_show',
99 101 repo_name=pull_request.target_repo.scm_instance().name,
100 102 pull_request_id=pull_request.pull_request_id,
101 103 params={'range-diff': range_diff}))
102 104
103 105 for commit_id in pull_request.revisions:
104 106 response.mustcontain(commit_id)
105 107
106 108 response.mustcontain(pull_request.target_ref_parts.type)
107 109 response.mustcontain(pull_request.target_ref_parts.name)
108 110
109 111 response.mustcontain('class="pull-request-merge"')
110 112
111 113 if pr_merge_enabled:
112 114 response.mustcontain('Pull request reviewer approval is pending')
113 115 else:
114 116 response.mustcontain('Server-side pull request merging is disabled.')
115 117
116 118 if range_diff == "1":
117 119 response.mustcontain('Turn off: Show the diff as commit range')
118 120
119 121 def test_show_versions_of_pr(self, backend, csrf_token):
120 122 commits = [
121 123 {'message': 'initial-commit',
122 'added': [FileNode('test-file.txt', 'LINE1\n')]},
124 'added': [FileNode(b'test-file.txt', b'LINE1\n')]},
123 125
124 126 {'message': 'commit-1',
125 'changed': [FileNode('test-file.txt', 'LINE1\nLINE2\n')]},
127 'changed': [FileNode(b'test-file.txt', b'LINE1\nLINE2\n')]},
126 128 # Above is the initial version of PR that changes a single line
127 129
128 130 # from now on we'll add 3x commit adding a nother line on each step
129 131 {'message': 'commit-2',
130 'changed': [FileNode('test-file.txt', 'LINE1\nLINE2\nLINE3\n')]},
132 'changed': [FileNode(b'test-file.txt', b'LINE1\nLINE2\nLINE3\n')]},
131 133
132 134 {'message': 'commit-3',
133 'changed': [FileNode('test-file.txt', 'LINE1\nLINE2\nLINE3\nLINE4\n')]},
135 'changed': [FileNode(b'test-file.txt', b'LINE1\nLINE2\nLINE3\nLINE4\n')]},
134 136
135 137 {'message': 'commit-4',
136 'changed': [FileNode('test-file.txt', 'LINE1\nLINE2\nLINE3\nLINE4\nLINE5\n')]},
138 'changed': [FileNode(b'test-file.txt', b'LINE1\nLINE2\nLINE3\nLINE4\nLINE5\n')]},
137 139 ]
138 140
139 141 commit_ids = backend.create_master_repo(commits)
140 142 target = backend.create_repo(heads=['initial-commit'])
141 143 source = backend.create_repo(heads=['commit-1'])
142 144 source_repo_name = source.repo_name
143 145 target_repo_name = target.repo_name
144 146
145 147 target_ref = 'branch:{branch}:{commit_id}'.format(
146 148 branch=backend.default_branch_name, commit_id=commit_ids['initial-commit'])
147 149 source_ref = 'branch:{branch}:{commit_id}'.format(
148 150 branch=backend.default_branch_name, commit_id=commit_ids['commit-1'])
149 151
150 152 response = self.app.post(
151 153 route_path('pullrequest_create', repo_name=source.repo_name),
152 154 [
153 155 ('source_repo', source_repo_name),
154 156 ('source_ref', source_ref),
155 157 ('target_repo', target_repo_name),
156 158 ('target_ref', target_ref),
157 159 ('common_ancestor', commit_ids['initial-commit']),
158 160 ('pullrequest_title', 'Title'),
159 161 ('pullrequest_desc', 'Description'),
160 162 ('description_renderer', 'markdown'),
161 163 ('__start__', 'review_members:sequence'),
162 164 ('__start__', 'reviewer:mapping'),
163 165 ('user_id', '1'),
164 166 ('__start__', 'reasons:sequence'),
165 167 ('reason', 'Some reason'),
166 168 ('__end__', 'reasons:sequence'),
167 169 ('__start__', 'rules:sequence'),
168 170 ('__end__', 'rules:sequence'),
169 171 ('mandatory', 'False'),
170 172 ('__end__', 'reviewer:mapping'),
171 173 ('__end__', 'review_members:sequence'),
172 174 ('__start__', 'revisions:sequence'),
173 175 ('revisions', commit_ids['commit-1']),
174 176 ('__end__', 'revisions:sequence'),
175 177 ('user', ''),
176 178 ('csrf_token', csrf_token),
177 179 ],
178 180 status=302)
179 181
180 182 location = response.headers['Location']
181 183
182 184 pull_request_id = location.rsplit('/', 1)[1]
183 185 assert pull_request_id != 'new'
184 186 pull_request = PullRequest.get(int(pull_request_id))
185 187
186 188 pull_request_id = pull_request.pull_request_id
187 189
188 190 # Show initial version of PR
189 191 response = self.app.get(
190 192 route_path('pullrequest_show',
191 193 repo_name=target_repo_name,
192 194 pull_request_id=pull_request_id))
193 195
194 196 response.mustcontain('commit-1')
195 197 response.mustcontain(no=['commit-2'])
196 198 response.mustcontain(no=['commit-3'])
197 199 response.mustcontain(no=['commit-4'])
198 200
199 201 response.mustcontain('cb-addition"></span><span>LINE2</span>')
200 202 response.mustcontain(no=['LINE3'])
201 203 response.mustcontain(no=['LINE4'])
202 204 response.mustcontain(no=['LINE5'])
203 205
204 206 # update PR #1
205 207 source_repo = Repository.get_by_repo_name(source_repo_name)
206 208 backend.pull_heads(source_repo, heads=['commit-2'])
207 209 response = self.app.post(
208 210 route_path('pullrequest_update',
209 211 repo_name=target_repo_name, pull_request_id=pull_request_id),
210 212 params={'update_commits': 'true', 'csrf_token': csrf_token})
211 213
212 214 # update PR #2
213 215 source_repo = Repository.get_by_repo_name(source_repo_name)
214 216 backend.pull_heads(source_repo, heads=['commit-3'])
215 217 response = self.app.post(
216 218 route_path('pullrequest_update',
217 219 repo_name=target_repo_name, pull_request_id=pull_request_id),
218 220 params={'update_commits': 'true', 'csrf_token': csrf_token})
219 221
220 222 # update PR #3
221 223 source_repo = Repository.get_by_repo_name(source_repo_name)
222 224 backend.pull_heads(source_repo, heads=['commit-4'])
223 225 response = self.app.post(
224 226 route_path('pullrequest_update',
225 227 repo_name=target_repo_name, pull_request_id=pull_request_id),
226 228 params={'update_commits': 'true', 'csrf_token': csrf_token})
227 229
228 230 # Show final version !
229 231 response = self.app.get(
230 232 route_path('pullrequest_show',
231 233 repo_name=target_repo_name,
232 234 pull_request_id=pull_request_id))
233 235
234 236 # 3 updates, and the latest == 4
235 237 response.mustcontain('4 versions available for this pull request')
236 238 response.mustcontain(no=['rhodecode diff rendering error'])
237 239
238 240 # initial show must have 3 commits, and 3 adds
239 241 response.mustcontain('commit-1')
240 242 response.mustcontain('commit-2')
241 243 response.mustcontain('commit-3')
242 244 response.mustcontain('commit-4')
243 245
244 246 response.mustcontain('cb-addition"></span><span>LINE2</span>')
245 247 response.mustcontain('cb-addition"></span><span>LINE3</span>')
246 248 response.mustcontain('cb-addition"></span><span>LINE4</span>')
247 249 response.mustcontain('cb-addition"></span><span>LINE5</span>')
248 250
249 251 # fetch versions
250 252 pr = PullRequest.get(pull_request_id)
251 253 versions = [x.pull_request_version_id for x in pr.versions.all()]
252 254 assert len(versions) == 3
253 255
254 256 # show v1,v2,v3,v4
255 257 def cb_line(text):
256 258 return 'cb-addition"></span><span>{}</span>'.format(text)
257 259
258 260 def cb_context(text):
259 261 return '<span class="cb-code"><span class="cb-action cb-context">' \
260 262 '</span><span>{}</span></span>'.format(text)
261 263
262 264 commit_tests = {
263 265 # in response, not in response
264 266 1: (['commit-1'], ['commit-2', 'commit-3', 'commit-4']),
265 267 2: (['commit-1', 'commit-2'], ['commit-3', 'commit-4']),
266 268 3: (['commit-1', 'commit-2', 'commit-3'], ['commit-4']),
267 269 4: (['commit-1', 'commit-2', 'commit-3', 'commit-4'], []),
268 270 }
269 271 diff_tests = {
270 272 1: (['LINE2'], ['LINE3', 'LINE4', 'LINE5']),
271 273 2: (['LINE2', 'LINE3'], ['LINE4', 'LINE5']),
272 274 3: (['LINE2', 'LINE3', 'LINE4'], ['LINE5']),
273 275 4: (['LINE2', 'LINE3', 'LINE4', 'LINE5'], []),
274 276 }
275 277 for idx, ver in enumerate(versions, 1):
276 278
277 279 response = self.app.get(
278 280 route_path('pullrequest_show',
279 281 repo_name=target_repo_name,
280 282 pull_request_id=pull_request_id,
281 283 params={'version': ver}))
282 284
283 285 response.mustcontain(no=['rhodecode diff rendering error'])
284 286 response.mustcontain('Showing changes at v{}'.format(idx))
285 287
286 288 yes, no = commit_tests[idx]
287 289 for y in yes:
288 290 response.mustcontain(y)
289 291 for n in no:
290 292 response.mustcontain(no=n)
291 293
292 294 yes, no = diff_tests[idx]
293 295 for y in yes:
294 296 response.mustcontain(cb_line(y))
295 297 for n in no:
296 298 response.mustcontain(no=n)
297 299
298 300 # show diff between versions
299 301 diff_compare_tests = {
300 302 1: (['LINE3'], ['LINE1', 'LINE2']),
301 303 2: (['LINE3', 'LINE4'], ['LINE1', 'LINE2']),
302 304 3: (['LINE3', 'LINE4', 'LINE5'], ['LINE1', 'LINE2']),
303 305 }
304 306 for idx, ver in enumerate(versions, 1):
305 307 adds, context = diff_compare_tests[idx]
306 308
307 309 to_ver = ver+1
308 310 if idx == 3:
309 311 to_ver = 'latest'
310 312
311 313 response = self.app.get(
312 314 route_path('pullrequest_show',
313 315 repo_name=target_repo_name,
314 316 pull_request_id=pull_request_id,
315 317 params={'from_version': versions[0], 'version': to_ver}))
316 318
317 319 response.mustcontain(no=['rhodecode diff rendering error'])
318 320
319 321 for a in adds:
320 322 response.mustcontain(cb_line(a))
321 323 for c in context:
322 324 response.mustcontain(cb_context(c))
323 325
324 326 # test version v2 -> v3
325 327 response = self.app.get(
326 328 route_path('pullrequest_show',
327 329 repo_name=target_repo_name,
328 330 pull_request_id=pull_request_id,
329 331 params={'from_version': versions[1], 'version': versions[2]}))
330 332
331 333 response.mustcontain(cb_context('LINE1'))
332 334 response.mustcontain(cb_context('LINE2'))
333 335 response.mustcontain(cb_context('LINE3'))
334 336 response.mustcontain(cb_line('LINE4'))
335 337
336 338 def test_close_status_visibility(self, pr_util, user_util, csrf_token):
337 339 # Logout
338 340 response = self.app.post(
339 341 h.route_path('logout'),
340 342 params={'csrf_token': csrf_token})
341 343 # Login as regular user
342 344 response = self.app.post(h.route_path('login'),
343 345 {'username': TEST_USER_REGULAR_LOGIN,
344 346 'password': 'test12'})
345 347
346 348 pull_request = pr_util.create_pull_request(
347 349 author=TEST_USER_REGULAR_LOGIN)
348 350
349 351 response = self.app.get(route_path(
350 352 'pullrequest_show',
351 353 repo_name=pull_request.target_repo.scm_instance().name,
352 354 pull_request_id=pull_request.pull_request_id))
353 355
354 356 response.mustcontain('Server-side pull request merging is disabled.')
355 357
356 358 assert_response = response.assert_response()
357 359 # for regular user without a merge permissions, we don't see it
358 360 assert_response.no_element_exists('#close-pull-request-action')
359 361
360 362 user_util.grant_user_permission_to_repo(
361 363 pull_request.target_repo,
362 364 UserModel().get_by_username(TEST_USER_REGULAR_LOGIN),
363 365 'repository.write')
364 366 response = self.app.get(route_path(
365 367 'pullrequest_show',
366 368 repo_name=pull_request.target_repo.scm_instance().name,
367 369 pull_request_id=pull_request.pull_request_id))
368 370
369 371 response.mustcontain('Server-side pull request merging is disabled.')
370 372
371 373 assert_response = response.assert_response()
372 374 # now regular user has a merge permissions, we have CLOSE button
373 375 assert_response.one_element_exists('#close-pull-request-action')
374 376
375 377 def test_show_invalid_commit_id(self, pr_util):
376 378 # Simulating invalid revisions which will cause a lookup error
377 379 pull_request = pr_util.create_pull_request()
378 380 pull_request.revisions = ['invalid']
379 381 Session().add(pull_request)
380 382 Session().commit()
381 383
382 384 response = self.app.get(route_path(
383 385 'pullrequest_show',
384 386 repo_name=pull_request.target_repo.scm_instance().name,
385 387 pull_request_id=pull_request.pull_request_id))
386 388
387 389 for commit_id in pull_request.revisions:
388 390 response.mustcontain(commit_id)
389 391
390 392 def test_show_invalid_source_reference(self, pr_util):
391 393 pull_request = pr_util.create_pull_request()
392 394 pull_request.source_ref = 'branch:b:invalid'
393 395 Session().add(pull_request)
394 396 Session().commit()
395 397
396 398 self.app.get(route_path(
397 399 'pullrequest_show',
398 400 repo_name=pull_request.target_repo.scm_instance().name,
399 401 pull_request_id=pull_request.pull_request_id))
400 402
401 403 def test_edit_title_description(self, pr_util, csrf_token):
402 404 pull_request = pr_util.create_pull_request()
403 405 pull_request_id = pull_request.pull_request_id
404 406
405 407 response = self.app.post(
406 408 route_path('pullrequest_update',
407 repo_name=pull_request.target_repo.repo_name,
408 pull_request_id=pull_request_id),
409 repo_name=pull_request.target_repo.repo_name,
410 pull_request_id=pull_request_id),
409 411 params={
410 412 'edit_pull_request': 'true',
411 413 'title': 'New title',
412 414 'description': 'New description',
413 415 'csrf_token': csrf_token})
414 416
415 417 assert_session_flash(
416 response, u'Pull request title & description updated.',
418 response, 'Pull request title & description updated.',
417 419 category='success')
418 420
419 421 pull_request = PullRequest.get(pull_request_id)
420 422 assert pull_request.title == 'New title'
421 423 assert pull_request.description == 'New description'
422 424
423 def test_edit_title_description(self, pr_util, csrf_token):
425 def test_edit_title_description_special(self, pr_util, csrf_token):
424 426 pull_request = pr_util.create_pull_request()
425 427 pull_request_id = pull_request.pull_request_id
426 428
427 429 response = self.app.post(
428 430 route_path('pullrequest_update',
429 repo_name=pull_request.target_repo.repo_name,
430 pull_request_id=pull_request_id),
431 repo_name=pull_request.target_repo.repo_name,
432 pull_request_id=pull_request_id),
431 433 params={
432 434 'edit_pull_request': 'true',
433 435 'title': 'New title {} {2} {foo}',
434 436 'description': 'New description',
435 437 'csrf_token': csrf_token})
436 438
437 439 assert_session_flash(
438 response, u'Pull request title & description updated.',
440 response, 'Pull request title & description updated.',
439 441 category='success')
440 442
441 443 pull_request = PullRequest.get(pull_request_id)
442 444 assert pull_request.title_safe == 'New title {{}} {{2}} {{foo}}'
443 445
444 446 def test_edit_title_description_closed(self, pr_util, csrf_token):
445 447 pull_request = pr_util.create_pull_request()
446 448 pull_request_id = pull_request.pull_request_id
447 449 repo_name = pull_request.target_repo.repo_name
448 450 pr_util.close()
449 451
450 452 response = self.app.post(
451 453 route_path('pullrequest_update',
452 454 repo_name=repo_name, pull_request_id=pull_request_id),
453 455 params={
454 456 'edit_pull_request': 'true',
455 457 'title': 'New title',
456 458 'description': 'New description',
457 459 'csrf_token': csrf_token}, status=200)
458 460 assert_session_flash(
459 response, u'Cannot update closed pull requests.',
461 response, 'Cannot update closed pull requests.',
460 462 category='error')
461 463
462 464 def test_update_invalid_source_reference(self, pr_util, csrf_token):
463 465 from rhodecode.lib.vcs.backends.base import UpdateFailureReason
464 466
465 467 pull_request = pr_util.create_pull_request()
466 468 pull_request.source_ref = 'branch:invalid-branch:invalid-commit-id'
467 469 Session().add(pull_request)
468 470 Session().commit()
469 471
470 472 pull_request_id = pull_request.pull_request_id
471 473
472 474 response = self.app.post(
473 475 route_path('pullrequest_update',
474 476 repo_name=pull_request.target_repo.repo_name,
475 477 pull_request_id=pull_request_id),
476 478 params={'update_commits': 'true', 'csrf_token': csrf_token})
477 479
478 480 expected_msg = str(PullRequestModel.UPDATE_STATUS_MESSAGES[
479 481 UpdateFailureReason.MISSING_SOURCE_REF])
480 482 assert_session_flash(response, expected_msg, category='error')
481 483
482 484 def test_missing_target_reference(self, pr_util, csrf_token):
483 485 from rhodecode.lib.vcs.backends.base import MergeFailureReason
484 486 pull_request = pr_util.create_pull_request(
485 487 approved=True, mergeable=True)
486 unicode_reference = u'branch:invalid-branch:invalid-commit-id'
488 unicode_reference = 'branch:invalid-branch:invalid-commit-id'
487 489 pull_request.target_ref = unicode_reference
488 490 Session().add(pull_request)
489 491 Session().commit()
490 492
491 493 pull_request_id = pull_request.pull_request_id
492 494 pull_request_url = route_path(
493 495 'pullrequest_show',
494 496 repo_name=pull_request.target_repo.repo_name,
495 497 pull_request_id=pull_request_id)
496 498
497 499 response = self.app.get(pull_request_url)
498 500 target_ref_id = 'invalid-branch'
499 501 merge_resp = MergeResponse(
500 502 True, True, '', MergeFailureReason.MISSING_TARGET_REF,
501 503 metadata={'target_ref': PullRequest.unicode_to_reference(unicode_reference)})
502 504 response.assert_response().element_contains(
503 505 'div[data-role="merge-message"]', merge_resp.merge_status_message)
504 506
505 507 def test_comment_and_close_pull_request_custom_message_approved(
506 508 self, pr_util, csrf_token, xhr_header):
507 509
508 510 pull_request = pr_util.create_pull_request(approved=True)
509 511 pull_request_id = pull_request.pull_request_id
510 512 author = pull_request.user_id
511 513 repo = pull_request.target_repo.repo_id
512 514
513 515 self.app.post(
514 516 route_path('pullrequest_comment_create',
515 517 repo_name=pull_request.target_repo.scm_instance().name,
516 518 pull_request_id=pull_request_id),
517 519 params={
518 520 'close_pull_request': '1',
519 521 'text': 'Closing a PR',
520 522 'csrf_token': csrf_token},
521 523 extra_environ=xhr_header,)
522 524
523 525 journal = UserLog.query()\
524 526 .filter(UserLog.user_id == author)\
525 527 .filter(UserLog.repository_id == repo) \
526 528 .order_by(UserLog.user_log_id.asc()) \
527 529 .all()
528 530 assert journal[-1].action == 'repo.pull_request.close'
529 531
530 532 pull_request = PullRequest.get(pull_request_id)
531 533 assert pull_request.is_closed()
532 534
533 535 status = ChangesetStatusModel().get_status(
534 536 pull_request.source_repo, pull_request=pull_request)
535 537 assert status == ChangesetStatus.STATUS_APPROVED
536 538 comments = ChangesetComment().query() \
537 539 .filter(ChangesetComment.pull_request == pull_request) \
538 540 .order_by(ChangesetComment.comment_id.asc())\
539 541 .all()
540 542 assert comments[-1].text == 'Closing a PR'
541 543
542 544 def test_comment_force_close_pull_request_rejected(
543 545 self, pr_util, csrf_token, xhr_header):
544 546 pull_request = pr_util.create_pull_request()
545 547 pull_request_id = pull_request.pull_request_id
546 548 PullRequestModel().update_reviewers(
547 549 pull_request_id, [
548 550 (1, ['reason'], False, 'reviewer', []),
549 551 (2, ['reason2'], False, 'reviewer', [])],
550 552 pull_request.author)
551 553 author = pull_request.user_id
552 554 repo = pull_request.target_repo.repo_id
553 555
554 556 self.app.post(
555 557 route_path('pullrequest_comment_create',
556 558 repo_name=pull_request.target_repo.scm_instance().name,
557 559 pull_request_id=pull_request_id),
558 560 params={
559 561 'close_pull_request': '1',
560 562 'csrf_token': csrf_token},
561 563 extra_environ=xhr_header)
562 564
563 565 pull_request = PullRequest.get(pull_request_id)
564 566
565 567 journal = UserLog.query()\
566 568 .filter(UserLog.user_id == author, UserLog.repository_id == repo) \
567 569 .order_by(UserLog.user_log_id.asc()) \
568 570 .all()
569 571 assert journal[-1].action == 'repo.pull_request.close'
570 572
571 573 # check only the latest status, not the review status
572 574 status = ChangesetStatusModel().get_status(
573 575 pull_request.source_repo, pull_request=pull_request)
574 576 assert status == ChangesetStatus.STATUS_REJECTED
575 577
576 578 def test_comment_and_close_pull_request(
577 579 self, pr_util, csrf_token, xhr_header):
578 580 pull_request = pr_util.create_pull_request()
579 581 pull_request_id = pull_request.pull_request_id
580 582
581 583 response = self.app.post(
582 584 route_path('pullrequest_comment_create',
583 585 repo_name=pull_request.target_repo.scm_instance().name,
584 586 pull_request_id=pull_request.pull_request_id),
585 587 params={
586 588 'close_pull_request': 'true',
587 589 'csrf_token': csrf_token},
588 590 extra_environ=xhr_header)
589 591
590 592 assert response.json
591 593
592 594 pull_request = PullRequest.get(pull_request_id)
593 595 assert pull_request.is_closed()
594 596
595 597 # check only the latest status, not the review status
596 598 status = ChangesetStatusModel().get_status(
597 599 pull_request.source_repo, pull_request=pull_request)
598 600 assert status == ChangesetStatus.STATUS_REJECTED
599 601
600 602 def test_comment_and_close_pull_request_try_edit_comment(
601 603 self, pr_util, csrf_token, xhr_header
602 604 ):
603 605 pull_request = pr_util.create_pull_request()
604 606 pull_request_id = pull_request.pull_request_id
605 607 target_scm = pull_request.target_repo.scm_instance()
606 608 target_scm_name = target_scm.name
607 609
608 610 response = self.app.post(
609 611 route_path(
610 612 'pullrequest_comment_create',
611 613 repo_name=target_scm_name,
612 614 pull_request_id=pull_request_id,
613 615 ),
614 616 params={
615 617 'close_pull_request': 'true',
616 618 'csrf_token': csrf_token,
617 619 },
618 620 extra_environ=xhr_header)
619 621
620 622 assert response.json
621 623
622 624 pull_request = PullRequest.get(pull_request_id)
623 625 target_scm = pull_request.target_repo.scm_instance()
624 626 target_scm_name = target_scm.name
625 627 assert pull_request.is_closed()
626 628
627 629 # check only the latest status, not the review status
628 630 status = ChangesetStatusModel().get_status(
629 631 pull_request.source_repo, pull_request=pull_request)
630 632 assert status == ChangesetStatus.STATUS_REJECTED
631 633
632 634 for comment_id in response.json.keys():
633 635 test_text = 'test'
634 636 response = self.app.post(
635 637 route_path(
636 638 'pullrequest_comment_edit',
637 639 repo_name=target_scm_name,
638 640 pull_request_id=pull_request_id,
639 641 comment_id=comment_id,
640 642 ),
641 643 extra_environ=xhr_header,
642 644 params={
643 645 'csrf_token': csrf_token,
644 646 'text': test_text,
645 647 },
646 648 status=403,
647 649 )
648 650 assert response.status_int == 403
649 651
650 652 def test_comment_and_comment_edit(self, pr_util, csrf_token, xhr_header):
651 653 pull_request = pr_util.create_pull_request()
652 654 target_scm = pull_request.target_repo.scm_instance()
653 655 target_scm_name = target_scm.name
654 656
655 657 response = self.app.post(
656 658 route_path(
657 659 'pullrequest_comment_create',
658 660 repo_name=target_scm_name,
659 661 pull_request_id=pull_request.pull_request_id),
660 662 params={
661 663 'csrf_token': csrf_token,
662 664 'text': 'init',
663 665 },
664 666 extra_environ=xhr_header,
665 667 )
666 668 assert response.json
667 669
668 670 for comment_id in response.json.keys():
669 671 assert comment_id
670 672 test_text = 'test'
671 673 self.app.post(
672 674 route_path(
673 675 'pullrequest_comment_edit',
674 676 repo_name=target_scm_name,
675 677 pull_request_id=pull_request.pull_request_id,
676 678 comment_id=comment_id,
677 679 ),
678 680 extra_environ=xhr_header,
679 681 params={
680 682 'csrf_token': csrf_token,
681 683 'text': test_text,
682 684 'version': '0',
683 685 },
684 686
685 687 )
686 688 text_form_db = ChangesetComment.query().filter(
687 689 ChangesetComment.comment_id == comment_id).first().text
688 690 assert test_text == text_form_db
689 691
690 def test_comment_and_comment_edit(self, pr_util, csrf_token, xhr_header):
692 def test_comment_and_comment_edit_special(self, pr_util, csrf_token, xhr_header):
691 693 pull_request = pr_util.create_pull_request()
692 694 target_scm = pull_request.target_repo.scm_instance()
693 695 target_scm_name = target_scm.name
694 696
695 697 response = self.app.post(
696 698 route_path(
697 699 'pullrequest_comment_create',
698 700 repo_name=target_scm_name,
699 701 pull_request_id=pull_request.pull_request_id),
700 702 params={
701 703 'csrf_token': csrf_token,
702 704 'text': 'init',
703 705 },
704 706 extra_environ=xhr_header,
705 707 )
706 708 assert response.json
707 709
708 710 for comment_id in response.json.keys():
709 711 test_text = 'init'
710 712 response = self.app.post(
711 713 route_path(
712 714 'pullrequest_comment_edit',
713 715 repo_name=target_scm_name,
714 716 pull_request_id=pull_request.pull_request_id,
715 717 comment_id=comment_id,
716 718 ),
717 719 extra_environ=xhr_header,
718 720 params={
719 721 'csrf_token': csrf_token,
720 722 'text': test_text,
721 723 'version': '0',
722 724 },
723 725 status=404,
724 726
725 727 )
726 728 assert response.status_int == 404
727 729
728 730 def test_comment_and_try_edit_already_edited(self, pr_util, csrf_token, xhr_header):
729 731 pull_request = pr_util.create_pull_request()
730 732 target_scm = pull_request.target_repo.scm_instance()
731 733 target_scm_name = target_scm.name
732 734
733 735 response = self.app.post(
734 736 route_path(
735 737 'pullrequest_comment_create',
736 738 repo_name=target_scm_name,
737 739 pull_request_id=pull_request.pull_request_id),
738 740 params={
739 741 'csrf_token': csrf_token,
740 742 'text': 'init',
741 743 },
742 744 extra_environ=xhr_header,
743 745 )
744 746 assert response.json
745 747 for comment_id in response.json.keys():
746 748 test_text = 'test'
747 749 self.app.post(
748 750 route_path(
749 751 'pullrequest_comment_edit',
750 752 repo_name=target_scm_name,
751 753 pull_request_id=pull_request.pull_request_id,
752 754 comment_id=comment_id,
753 755 ),
754 756 extra_environ=xhr_header,
755 757 params={
756 758 'csrf_token': csrf_token,
757 759 'text': test_text,
758 760 'version': '0',
759 761 },
760 762
761 763 )
762 764 test_text_v2 = 'test_v2'
763 765 response = self.app.post(
764 766 route_path(
765 767 'pullrequest_comment_edit',
766 768 repo_name=target_scm_name,
767 769 pull_request_id=pull_request.pull_request_id,
768 770 comment_id=comment_id,
769 771 ),
770 772 extra_environ=xhr_header,
771 773 params={
772 774 'csrf_token': csrf_token,
773 775 'text': test_text_v2,
774 776 'version': '0',
775 777 },
776 778 status=409,
777 779 )
778 780 assert response.status_int == 409
779 781
780 782 text_form_db = ChangesetComment.query().filter(
781 783 ChangesetComment.comment_id == comment_id).first().text
782 784
783 785 assert test_text == text_form_db
784 786 assert test_text_v2 != text_form_db
785 787
786 788 def test_comment_and_comment_edit_permissions_forbidden(
787 789 self, autologin_regular_user, user_regular, user_admin, pr_util,
788 790 csrf_token, xhr_header):
789 791 pull_request = pr_util.create_pull_request(
790 792 author=user_admin.username, enable_notifications=False)
791 793 comment = CommentsModel().create(
792 794 text='test',
793 795 repo=pull_request.target_repo.scm_instance().name,
794 796 user=user_admin,
795 797 pull_request=pull_request,
796 798 )
797 799 response = self.app.post(
798 800 route_path(
799 801 'pullrequest_comment_edit',
800 802 repo_name=pull_request.target_repo.scm_instance().name,
801 803 pull_request_id=pull_request.pull_request_id,
802 804 comment_id=comment.comment_id,
803 805 ),
804 806 extra_environ=xhr_header,
805 807 params={
806 808 'csrf_token': csrf_token,
807 809 'text': 'test_text',
808 810 },
809 811 status=403,
810 812 )
811 813 assert response.status_int == 403
812 814
813 815 def test_create_pull_request(self, backend, csrf_token):
814 816 commits = [
815 817 {'message': 'ancestor'},
816 818 {'message': 'change'},
817 819 {'message': 'change2'},
818 820 ]
819 821 commit_ids = backend.create_master_repo(commits)
820 822 target = backend.create_repo(heads=['ancestor'])
821 823 source = backend.create_repo(heads=['change2'])
822 824
823 825 response = self.app.post(
824 826 route_path('pullrequest_create', repo_name=source.repo_name),
825 827 [
826 828 ('source_repo', source.repo_name),
827 829 ('source_ref', 'branch:default:' + commit_ids['change2']),
828 830 ('target_repo', target.repo_name),
829 831 ('target_ref', 'branch:default:' + commit_ids['ancestor']),
830 832 ('common_ancestor', commit_ids['ancestor']),
831 833 ('pullrequest_title', 'Title'),
832 834 ('pullrequest_desc', 'Description'),
833 835 ('description_renderer', 'markdown'),
834 836 ('__start__', 'review_members:sequence'),
835 837 ('__start__', 'reviewer:mapping'),
836 838 ('user_id', '1'),
837 839 ('__start__', 'reasons:sequence'),
838 840 ('reason', 'Some reason'),
839 841 ('__end__', 'reasons:sequence'),
840 842 ('__start__', 'rules:sequence'),
841 843 ('__end__', 'rules:sequence'),
842 844 ('mandatory', 'False'),
843 845 ('__end__', 'reviewer:mapping'),
844 846 ('__end__', 'review_members:sequence'),
845 847 ('__start__', 'revisions:sequence'),
846 848 ('revisions', commit_ids['change']),
847 849 ('revisions', commit_ids['change2']),
848 850 ('__end__', 'revisions:sequence'),
849 851 ('user', ''),
850 852 ('csrf_token', csrf_token),
851 853 ],
852 854 status=302)
853 855
854 856 location = response.headers['Location']
855 857 pull_request_id = location.rsplit('/', 1)[1]
856 858 assert pull_request_id != 'new'
857 859 pull_request = PullRequest.get(int(pull_request_id))
858 860
859 861 # check that we have now both revisions
860 862 assert pull_request.revisions == [commit_ids['change2'], commit_ids['change']]
861 863 assert pull_request.source_ref == 'branch:default:' + commit_ids['change2']
862 864 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
863 865 assert pull_request.target_ref == expected_target_ref
864 866
865 867 def test_reviewer_notifications(self, backend, csrf_token):
866 868 # We have to use the app.post for this test so it will create the
867 869 # notifications properly with the new PR
868 870 commits = [
869 871 {'message': 'ancestor',
870 'added': [FileNode('file_A', content='content_of_ancestor')]},
872 'added': [FileNode(b'file_A', content=b'content_of_ancestor')]},
871 873 {'message': 'change',
872 'added': [FileNode('file_a', content='content_of_change')]},
874 'added': [FileNode(b'file_a', content=b'content_of_change')]},
873 875 {'message': 'change-child'},
874 876 {'message': 'ancestor-child', 'parents': ['ancestor'],
875 'added': [
876 FileNode('file_B', content='content_of_ancestor_child')]},
877 'added': [ FileNode(b'file_B', content=b'content_of_ancestor_child')]},
877 878 {'message': 'ancestor-child-2'},
878 879 ]
879 880 commit_ids = backend.create_master_repo(commits)
880 881 target = backend.create_repo(heads=['ancestor-child'])
881 882 source = backend.create_repo(heads=['change'])
882 883
883 884 response = self.app.post(
884 885 route_path('pullrequest_create', repo_name=source.repo_name),
885 886 [
886 887 ('source_repo', source.repo_name),
887 888 ('source_ref', 'branch:default:' + commit_ids['change']),
888 889 ('target_repo', target.repo_name),
889 890 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
890 891 ('common_ancestor', commit_ids['ancestor']),
891 892 ('pullrequest_title', 'Title'),
892 893 ('pullrequest_desc', 'Description'),
893 894 ('description_renderer', 'markdown'),
894 895 ('__start__', 'review_members:sequence'),
895 896 ('__start__', 'reviewer:mapping'),
896 897 ('user_id', '2'),
897 898 ('__start__', 'reasons:sequence'),
898 899 ('reason', 'Some reason'),
899 900 ('__end__', 'reasons:sequence'),
900 901 ('__start__', 'rules:sequence'),
901 902 ('__end__', 'rules:sequence'),
902 903 ('mandatory', 'False'),
903 904 ('__end__', 'reviewer:mapping'),
904 905 ('__end__', 'review_members:sequence'),
905 906 ('__start__', 'revisions:sequence'),
906 907 ('revisions', commit_ids['change']),
907 908 ('__end__', 'revisions:sequence'),
908 909 ('user', ''),
909 910 ('csrf_token', csrf_token),
910 911 ],
911 912 status=302)
912 913
913 914 location = response.headers['Location']
914 915
915 916 pull_request_id = location.rsplit('/', 1)[1]
916 917 assert pull_request_id != 'new'
917 918 pull_request = PullRequest.get(int(pull_request_id))
918 919
919 920 # Check that a notification was made
920 921 notifications = Notification.query()\
921 922 .filter(Notification.created_by == pull_request.author.user_id,
922 923 Notification.type_ == Notification.TYPE_PULL_REQUEST,
923 924 Notification.subject.contains(
924 925 "requested a pull request review. !%s" % pull_request_id))
925 926 assert len(notifications.all()) == 1
926 927
927 928 # Change reviewers and check that a notification was made
928 929 PullRequestModel().update_reviewers(
929 930 pull_request.pull_request_id, [
930 931 (1, [], False, 'reviewer', [])
931 932 ],
932 933 pull_request.author)
933 934 assert len(notifications.all()) == 2
934 935
935 936 def test_create_pull_request_stores_ancestor_commit_id(self, backend, csrf_token):
936 937 commits = [
937 938 {'message': 'ancestor',
938 'added': [FileNode('file_A', content='content_of_ancestor')]},
939 'added': [FileNode(b'file_A', content=b'content_of_ancestor')]},
939 940 {'message': 'change',
940 'added': [FileNode('file_a', content='content_of_change')]},
941 'added': [FileNode(b'file_a', content=b'content_of_change')]},
941 942 {'message': 'change-child'},
942 943 {'message': 'ancestor-child', 'parents': ['ancestor'],
943 944 'added': [
944 FileNode('file_B', content='content_of_ancestor_child')]},
945 FileNode(b'file_B', content=b'content_of_ancestor_child')]},
945 946 {'message': 'ancestor-child-2'},
946 947 ]
947 948 commit_ids = backend.create_master_repo(commits)
948 949 target = backend.create_repo(heads=['ancestor-child'])
949 950 source = backend.create_repo(heads=['change'])
950 951
951 952 response = self.app.post(
952 953 route_path('pullrequest_create', repo_name=source.repo_name),
953 954 [
954 955 ('source_repo', source.repo_name),
955 956 ('source_ref', 'branch:default:' + commit_ids['change']),
956 957 ('target_repo', target.repo_name),
957 958 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
958 959 ('common_ancestor', commit_ids['ancestor']),
959 960 ('pullrequest_title', 'Title'),
960 961 ('pullrequest_desc', 'Description'),
961 962 ('description_renderer', 'markdown'),
962 963 ('__start__', 'review_members:sequence'),
963 964 ('__start__', 'reviewer:mapping'),
964 965 ('user_id', '1'),
965 966 ('__start__', 'reasons:sequence'),
966 967 ('reason', 'Some reason'),
967 968 ('__end__', 'reasons:sequence'),
968 969 ('__start__', 'rules:sequence'),
969 970 ('__end__', 'rules:sequence'),
970 971 ('mandatory', 'False'),
971 972 ('__end__', 'reviewer:mapping'),
972 973 ('__end__', 'review_members:sequence'),
973 974 ('__start__', 'revisions:sequence'),
974 975 ('revisions', commit_ids['change']),
975 976 ('__end__', 'revisions:sequence'),
976 977 ('user', ''),
977 978 ('csrf_token', csrf_token),
978 979 ],
979 980 status=302)
980 981
981 982 location = response.headers['Location']
982 983
983 984 pull_request_id = location.rsplit('/', 1)[1]
984 985 assert pull_request_id != 'new'
985 986 pull_request = PullRequest.get(int(pull_request_id))
986 987
987 988 # target_ref has to point to the ancestor's commit_id in order to
988 989 # show the correct diff
989 990 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
990 991 assert pull_request.target_ref == expected_target_ref
991 992
992 993 # Check generated diff contents
993 994 response = response.follow()
994 995 response.mustcontain(no=['content_of_ancestor'])
995 996 response.mustcontain(no=['content_of_ancestor-child'])
996 997 response.mustcontain('content_of_change')
997 998
998 999 def test_merge_pull_request_enabled(self, pr_util, csrf_token):
999 1000 # Clear any previous calls to rcextensions
1000 1001 rhodecode.EXTENSIONS.calls.clear()
1001 1002
1002 1003 pull_request = pr_util.create_pull_request(
1003 1004 approved=True, mergeable=True)
1004 1005 pull_request_id = pull_request.pull_request_id
1005 1006 repo_name = pull_request.target_repo.scm_instance().name,
1006 1007
1007 1008 url = route_path('pullrequest_merge',
1008 1009 repo_name=str(repo_name[0]),
1009 1010 pull_request_id=pull_request_id)
1010 1011 response = self.app.post(url, params={'csrf_token': csrf_token}).follow()
1011 1012
1012 1013 pull_request = PullRequest.get(pull_request_id)
1013 1014
1014 1015 assert response.status_int == 200
1015 1016 assert pull_request.is_closed()
1016 1017 assert_pull_request_status(
1017 1018 pull_request, ChangesetStatus.STATUS_APPROVED)
1018 1019
1019 1020 # Check the relevant log entries were added
1020 1021 user_logs = UserLog.query().order_by(UserLog.user_log_id.desc()).limit(3)
1021 1022 actions = [log.action for log in user_logs]
1022 1023 pr_commit_ids = PullRequestModel()._get_commit_ids(pull_request)
1023 1024 expected_actions = [
1024 u'repo.pull_request.close',
1025 u'repo.pull_request.merge',
1026 u'repo.pull_request.comment.create'
1025 'repo.pull_request.close',
1026 'repo.pull_request.merge',
1027 'repo.pull_request.comment.create'
1027 1028 ]
1028 1029 assert actions == expected_actions
1029 1030
1030 1031 user_logs = UserLog.query().order_by(UserLog.user_log_id.desc()).limit(4)
1031 1032 actions = [log for log in user_logs]
1032 1033 assert actions[-1].action == 'user.push'
1033 1034 assert actions[-1].action_data['commit_ids'] == pr_commit_ids
1034 1035
1035 1036 # Check post_push rcextension was really executed
1036 1037 push_calls = rhodecode.EXTENSIONS.calls['_push_hook']
1037 1038 assert len(push_calls) == 1
1038 1039 unused_last_call_args, last_call_kwargs = push_calls[0]
1039 1040 assert last_call_kwargs['action'] == 'push'
1040 1041 assert last_call_kwargs['commit_ids'] == pr_commit_ids
1041 1042
1042 1043 def test_merge_pull_request_disabled(self, pr_util, csrf_token):
1043 1044 pull_request = pr_util.create_pull_request(mergeable=False)
1044 1045 pull_request_id = pull_request.pull_request_id
1045 1046 pull_request = PullRequest.get(pull_request_id)
1046 1047
1047 1048 response = self.app.post(
1048 1049 route_path('pullrequest_merge',
1049 repo_name=pull_request.target_repo.scm_instance().name,
1050 pull_request_id=pull_request.pull_request_id),
1050 repo_name=pull_request.target_repo.scm_instance().name,
1051 pull_request_id=pull_request.pull_request_id),
1051 1052 params={'csrf_token': csrf_token}).follow()
1052 1053
1053 1054 assert response.status_int == 200
1054 1055 response.mustcontain(
1055 1056 'Merge is not currently possible because of below failed checks.')
1056 1057 response.mustcontain('Server-side pull request merging is disabled.')
1057 1058
1058 1059 @pytest.mark.skip_backends('svn')
1059 1060 def test_merge_pull_request_not_approved(self, pr_util, csrf_token):
1060 1061 pull_request = pr_util.create_pull_request(mergeable=True)
1061 1062 pull_request_id = pull_request.pull_request_id
1062 1063 repo_name = pull_request.target_repo.scm_instance().name
1063 1064
1064 1065 response = self.app.post(
1065 1066 route_path('pullrequest_merge',
1066 1067 repo_name=repo_name, pull_request_id=pull_request_id),
1067 1068 params={'csrf_token': csrf_token}).follow()
1068 1069
1069 1070 assert response.status_int == 200
1070 1071
1071 1072 response.mustcontain(
1072 1073 'Merge is not currently possible because of below failed checks.')
1073 1074 response.mustcontain('Pull request reviewer approval is pending.')
1074 1075
1075 1076 def test_merge_pull_request_renders_failure_reason(
1076 1077 self, user_regular, csrf_token, pr_util):
1077 1078 pull_request = pr_util.create_pull_request(mergeable=True, approved=True)
1078 1079 pull_request_id = pull_request.pull_request_id
1079 1080 repo_name = pull_request.target_repo.scm_instance().name
1080 1081
1081 1082 merge_resp = MergeResponse(True, False, 'STUB_COMMIT_ID',
1082 1083 MergeFailureReason.PUSH_FAILED,
1083 1084 metadata={'target': 'shadow repo',
1084 1085 'merge_commit': 'xxx'})
1085 1086 model_patcher = mock.patch.multiple(
1086 1087 PullRequestModel,
1087 1088 merge_repo=mock.Mock(return_value=merge_resp),
1088 1089 merge_status=mock.Mock(return_value=(None, True, 'WRONG_MESSAGE')))
1089 1090
1090 1091 with model_patcher:
1091 1092 response = self.app.post(
1092 1093 route_path('pullrequest_merge',
1093 1094 repo_name=repo_name,
1094 1095 pull_request_id=pull_request_id),
1095 1096 params={'csrf_token': csrf_token}, status=302)
1096 1097
1097 1098 merge_resp = MergeResponse(True, True, '', MergeFailureReason.PUSH_FAILED,
1098 1099 metadata={'target': 'shadow repo',
1099 1100 'merge_commit': 'xxx'})
1100 1101 assert_session_flash(response, merge_resp.merge_status_message)
1101 1102
1102 1103 def test_update_source_revision(self, backend, csrf_token):
1103 1104 commits = [
1104 1105 {'message': 'ancestor'},
1105 1106 {'message': 'change'},
1106 1107 {'message': 'change-2'},
1107 1108 ]
1108 1109 commit_ids = backend.create_master_repo(commits)
1109 1110 target = backend.create_repo(heads=['ancestor'])
1110 1111 source = backend.create_repo(heads=['change'])
1111 1112
1112 1113 # create pr from a in source to A in target
1113 1114 pull_request = PullRequest()
1114 1115
1115 1116 pull_request.source_repo = source
1116 1117 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1117 1118 branch=backend.default_branch_name, commit_id=commit_ids['change'])
1118 1119
1119 1120 pull_request.target_repo = target
1120 1121 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1121 1122 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
1122 1123
1123 1124 pull_request.revisions = [commit_ids['change']]
1124 pull_request.title = u"Test"
1125 pull_request.description = u"Description"
1125 pull_request.title = "Test"
1126 pull_request.description = "Description"
1126 1127 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1127 1128 pull_request.pull_request_state = PullRequest.STATE_CREATED
1128 1129 Session().add(pull_request)
1129 1130 Session().commit()
1130 1131 pull_request_id = pull_request.pull_request_id
1131 1132
1132 1133 # source has ancestor - change - change-2
1133 1134 backend.pull_heads(source, heads=['change-2'])
1134 1135 target_repo_name = target.repo_name
1135 1136
1136 1137 # update PR
1137 1138 self.app.post(
1138 1139 route_path('pullrequest_update',
1139 1140 repo_name=target_repo_name, pull_request_id=pull_request_id),
1140 1141 params={'update_commits': 'true', 'csrf_token': csrf_token})
1141 1142
1142 1143 response = self.app.get(
1143 1144 route_path('pullrequest_show',
1144 1145 repo_name=target_repo_name,
1145 1146 pull_request_id=pull_request.pull_request_id))
1146 1147
1147 1148 assert response.status_int == 200
1148 1149 response.mustcontain('Pull request updated to')
1149 1150 response.mustcontain('with 1 added, 0 removed commits.')
1150 1151
1151 1152 # check that we have now both revisions
1152 1153 pull_request = PullRequest.get(pull_request_id)
1153 1154 assert pull_request.revisions == [commit_ids['change-2'], commit_ids['change']]
1154 1155
1155 1156 def test_update_target_revision(self, backend, csrf_token):
1156 1157 commits = [
1157 1158 {'message': 'ancestor'},
1158 1159 {'message': 'change'},
1159 1160 {'message': 'ancestor-new', 'parents': ['ancestor']},
1160 1161 {'message': 'change-rebased'},
1161 1162 ]
1162 1163 commit_ids = backend.create_master_repo(commits)
1163 1164 target = backend.create_repo(heads=['ancestor'])
1164 1165 source = backend.create_repo(heads=['change'])
1165 1166
1166 1167 # create pr from a in source to A in target
1167 1168 pull_request = PullRequest()
1168 1169
1169 1170 pull_request.source_repo = source
1170 1171 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1171 1172 branch=backend.default_branch_name, commit_id=commit_ids['change'])
1172 1173
1173 1174 pull_request.target_repo = target
1174 1175 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1175 1176 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
1176 1177
1177 1178 pull_request.revisions = [commit_ids['change']]
1178 pull_request.title = u"Test"
1179 pull_request.description = u"Description"
1179 pull_request.title = "Test"
1180 pull_request.description = "Description"
1180 1181 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1181 1182 pull_request.pull_request_state = PullRequest.STATE_CREATED
1182 1183
1183 1184 Session().add(pull_request)
1184 1185 Session().commit()
1185 1186 pull_request_id = pull_request.pull_request_id
1186 1187
1187 1188 # target has ancestor - ancestor-new
1188 1189 # source has ancestor - ancestor-new - change-rebased
1189 1190 backend.pull_heads(target, heads=['ancestor-new'])
1190 1191 backend.pull_heads(source, heads=['change-rebased'])
1191 1192 target_repo_name = target.repo_name
1192 1193
1193 1194 # update PR
1194 1195 url = route_path('pullrequest_update',
1195 1196 repo_name=target_repo_name,
1196 1197 pull_request_id=pull_request_id)
1197 1198 self.app.post(url,
1198 1199 params={'update_commits': 'true', 'csrf_token': csrf_token},
1199 1200 status=200)
1200 1201
1201 1202 # check that we have now both revisions
1202 1203 pull_request = PullRequest.get(pull_request_id)
1203 1204 assert pull_request.revisions == [commit_ids['change-rebased']]
1204 1205 assert pull_request.target_ref == 'branch:{branch}:{commit_id}'.format(
1205 1206 branch=backend.default_branch_name, commit_id=commit_ids['ancestor-new'])
1206 1207
1207 1208 response = self.app.get(
1208 1209 route_path('pullrequest_show',
1209 1210 repo_name=target_repo_name,
1210 1211 pull_request_id=pull_request.pull_request_id))
1211 1212 assert response.status_int == 200
1212 1213 response.mustcontain('Pull request updated to')
1213 1214 response.mustcontain('with 1 added, 1 removed commits.')
1214 1215
1215 1216 def test_update_target_revision_with_removal_of_1_commit_git(self, backend_git, csrf_token):
1216 1217 backend = backend_git
1217 1218 commits = [
1218 1219 {'message': 'master-commit-1'},
1219 1220 {'message': 'master-commit-2-change-1'},
1220 1221 {'message': 'master-commit-3-change-2'},
1221 1222
1222 1223 {'message': 'feat-commit-1', 'parents': ['master-commit-1']},
1223 1224 {'message': 'feat-commit-2'},
1224 1225 ]
1225 1226 commit_ids = backend.create_master_repo(commits)
1226 1227 target = backend.create_repo(heads=['master-commit-3-change-2'])
1227 1228 source = backend.create_repo(heads=['feat-commit-2'])
1228 1229
1229 1230 # create pr from a in source to A in target
1230 1231 pull_request = PullRequest()
1231 1232 pull_request.source_repo = source
1232 1233
1233 1234 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1234 1235 branch=backend.default_branch_name,
1235 1236 commit_id=commit_ids['master-commit-3-change-2'])
1236 1237
1237 1238 pull_request.target_repo = target
1238 1239 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1239 1240 branch=backend.default_branch_name, commit_id=commit_ids['feat-commit-2'])
1240 1241
1241 1242 pull_request.revisions = [
1242 1243 commit_ids['feat-commit-1'],
1243 1244 commit_ids['feat-commit-2']
1244 1245 ]
1245 pull_request.title = u"Test"
1246 pull_request.description = u"Description"
1246 pull_request.title = "Test"
1247 pull_request.description = "Description"
1247 1248 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1248 1249 pull_request.pull_request_state = PullRequest.STATE_CREATED
1249 1250 Session().add(pull_request)
1250 1251 Session().commit()
1251 1252 pull_request_id = pull_request.pull_request_id
1252 1253
1253 1254 # PR is created, now we simulate a force-push into target,
1254 1255 # that drops a 2 last commits
1255 1256 vcsrepo = target.scm_instance()
1256 1257 vcsrepo.config.clear_section('hooks')
1257 1258 vcsrepo.run_git_command(['reset', '--soft', 'HEAD~2'])
1258 1259 target_repo_name = target.repo_name
1259 1260
1260 1261 # update PR
1261 1262 url = route_path('pullrequest_update',
1262 1263 repo_name=target_repo_name,
1263 1264 pull_request_id=pull_request_id)
1264 1265 self.app.post(url,
1265 1266 params={'update_commits': 'true', 'csrf_token': csrf_token},
1266 1267 status=200)
1267 1268
1268 1269 response = self.app.get(route_path('pullrequest_new', repo_name=target_repo_name))
1269 1270 assert response.status_int == 200
1270 1271 response.mustcontain('Pull request updated to')
1271 1272 response.mustcontain('with 0 added, 0 removed commits.')
1272 1273
1273 1274 def test_update_of_ancestor_reference(self, backend, csrf_token):
1274 1275 commits = [
1275 1276 {'message': 'ancestor'},
1276 1277 {'message': 'change'},
1277 1278 {'message': 'change-2'},
1278 1279 {'message': 'ancestor-new', 'parents': ['ancestor']},
1279 1280 {'message': 'change-rebased'},
1280 1281 ]
1281 1282 commit_ids = backend.create_master_repo(commits)
1282 1283 target = backend.create_repo(heads=['ancestor'])
1283 1284 source = backend.create_repo(heads=['change'])
1284 1285
1285 1286 # create pr from a in source to A in target
1286 1287 pull_request = PullRequest()
1287 1288 pull_request.source_repo = source
1288 1289
1289 1290 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1290 1291 branch=backend.default_branch_name, commit_id=commit_ids['change'])
1291 1292 pull_request.target_repo = target
1292 1293 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1293 1294 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
1294 1295 pull_request.revisions = [commit_ids['change']]
1295 pull_request.title = u"Test"
1296 pull_request.description = u"Description"
1296 pull_request.title = "Test"
1297 pull_request.description = "Description"
1297 1298 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1298 1299 pull_request.pull_request_state = PullRequest.STATE_CREATED
1299 1300 Session().add(pull_request)
1300 1301 Session().commit()
1301 1302 pull_request_id = pull_request.pull_request_id
1302 1303
1303 1304 # target has ancestor - ancestor-new
1304 1305 # source has ancestor - ancestor-new - change-rebased
1305 1306 backend.pull_heads(target, heads=['ancestor-new'])
1306 1307 backend.pull_heads(source, heads=['change-rebased'])
1307 1308 target_repo_name = target.repo_name
1308 1309
1309 1310 # update PR
1310 1311 self.app.post(
1311 1312 route_path('pullrequest_update',
1312 1313 repo_name=target_repo_name, pull_request_id=pull_request_id),
1313 1314 params={'update_commits': 'true', 'csrf_token': csrf_token},
1314 1315 status=200)
1315 1316
1316 1317 # Expect the target reference to be updated correctly
1317 1318 pull_request = PullRequest.get(pull_request_id)
1318 1319 assert pull_request.revisions == [commit_ids['change-rebased']]
1319 1320 expected_target_ref = 'branch:{branch}:{commit_id}'.format(
1320 1321 branch=backend.default_branch_name,
1321 1322 commit_id=commit_ids['ancestor-new'])
1322 1323 assert pull_request.target_ref == expected_target_ref
1323 1324
1324 1325 def test_remove_pull_request_branch(self, backend_git, csrf_token):
1325 1326 branch_name = 'development'
1326 1327 commits = [
1327 1328 {'message': 'initial-commit'},
1328 1329 {'message': 'old-feature'},
1329 1330 {'message': 'new-feature', 'branch': branch_name},
1330 1331 ]
1331 1332 repo = backend_git.create_repo(commits)
1332 1333 repo_name = repo.repo_name
1333 1334 commit_ids = backend_git.commit_ids
1334 1335
1335 1336 pull_request = PullRequest()
1336 1337 pull_request.source_repo = repo
1337 1338 pull_request.target_repo = repo
1338 1339 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1339 1340 branch=branch_name, commit_id=commit_ids['new-feature'])
1340 1341 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1341 1342 branch=backend_git.default_branch_name, commit_id=commit_ids['old-feature'])
1342 1343 pull_request.revisions = [commit_ids['new-feature']]
1343 pull_request.title = u"Test"
1344 pull_request.description = u"Description"
1344 pull_request.title = "Test"
1345 pull_request.description = "Description"
1345 1346 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1346 1347 pull_request.pull_request_state = PullRequest.STATE_CREATED
1347 1348 Session().add(pull_request)
1348 1349 Session().commit()
1349 1350
1350 1351 pull_request_id = pull_request.pull_request_id
1351 1352
1352 1353 vcs = repo.scm_instance()
1353 1354 vcs.remove_ref('refs/heads/{}'.format(branch_name))
1354 1355 # NOTE(marcink): run GC to ensure the commits are gone
1355 1356 vcs.run_gc()
1356 1357
1357 1358 response = self.app.get(route_path(
1358 1359 'pullrequest_show',
1359 1360 repo_name=repo_name,
1360 1361 pull_request_id=pull_request_id))
1361 1362
1362 1363 assert response.status_int == 200
1363 1364
1364 1365 response.assert_response().element_contains(
1365 1366 '#changeset_compare_view_content .alert strong',
1366 1367 'Missing commits')
1367 1368 response.assert_response().element_contains(
1368 1369 '#changeset_compare_view_content .alert',
1369 1370 'This pull request cannot be displayed, because one or more'
1370 1371 ' commits no longer exist in the source repository.')
1371 1372
1372 1373 def test_strip_commits_from_pull_request(
1373 1374 self, backend, pr_util, csrf_token):
1374 1375 commits = [
1375 1376 {'message': 'initial-commit'},
1376 1377 {'message': 'old-feature'},
1377 1378 {'message': 'new-feature', 'parents': ['initial-commit']},
1378 1379 ]
1379 1380 pull_request = pr_util.create_pull_request(
1380 1381 commits, target_head='initial-commit', source_head='new-feature',
1381 1382 revisions=['new-feature'])
1382 1383
1383 1384 vcs = pr_util.source_repository.scm_instance()
1384 1385 if backend.alias == 'git':
1385 1386 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
1386 1387 else:
1387 1388 vcs.strip(pr_util.commit_ids['new-feature'])
1388 1389
1389 1390 response = self.app.get(route_path(
1390 1391 'pullrequest_show',
1391 1392 repo_name=pr_util.target_repository.repo_name,
1392 1393 pull_request_id=pull_request.pull_request_id))
1393 1394
1394 1395 assert response.status_int == 200
1395 1396
1396 1397 response.assert_response().element_contains(
1397 1398 '#changeset_compare_view_content .alert strong',
1398 1399 'Missing commits')
1399 1400 response.assert_response().element_contains(
1400 1401 '#changeset_compare_view_content .alert',
1401 1402 'This pull request cannot be displayed, because one or more'
1402 1403 ' commits no longer exist in the source repository.')
1403 1404 response.assert_response().element_contains(
1404 1405 '#update_commits',
1405 1406 'Update commits')
1406 1407
1407 1408 def test_strip_commits_and_update(
1408 1409 self, backend, pr_util, csrf_token):
1409 1410 commits = [
1410 1411 {'message': 'initial-commit'},
1411 1412 {'message': 'old-feature'},
1412 1413 {'message': 'new-feature', 'parents': ['old-feature']},
1413 1414 ]
1414 1415 pull_request = pr_util.create_pull_request(
1415 1416 commits, target_head='old-feature', source_head='new-feature',
1416 1417 revisions=['new-feature'], mergeable=True)
1417 1418 pr_id = pull_request.pull_request_id
1418 1419 target_repo_name = pull_request.target_repo.repo_name
1419 1420
1420 1421 vcs = pr_util.source_repository.scm_instance()
1421 1422 if backend.alias == 'git':
1422 1423 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
1423 1424 else:
1424 1425 vcs.strip(pr_util.commit_ids['new-feature'])
1425 1426
1426 1427 url = route_path('pullrequest_update',
1427 1428 repo_name=target_repo_name,
1428 1429 pull_request_id=pr_id)
1429 1430 response = self.app.post(url,
1430 1431 params={'update_commits': 'true',
1431 1432 'csrf_token': csrf_token})
1432 1433
1433 1434 assert response.status_int == 200
1434 1435 assert json.loads(response.body) == json.loads('{"response": true, "redirect_url": null}')
1435 1436
1436 1437 # Make sure that after update, it won't raise 500 errors
1437 1438 response = self.app.get(route_path(
1438 1439 'pullrequest_show',
1439 1440 repo_name=target_repo_name,
1440 1441 pull_request_id=pr_id))
1441 1442
1442 1443 assert response.status_int == 200
1443 1444 response.assert_response().element_contains(
1444 1445 '#changeset_compare_view_content .alert strong',
1445 1446 'Missing commits')
1446 1447
1447 1448 def test_branch_is_a_link(self, pr_util):
1448 1449 pull_request = pr_util.create_pull_request()
1449 1450 pull_request.source_ref = 'branch:origin:1234567890abcdef'
1450 1451 pull_request.target_ref = 'branch:target:abcdef1234567890'
1451 1452 Session().add(pull_request)
1452 1453 Session().commit()
1453 1454
1454 1455 response = self.app.get(route_path(
1455 1456 'pullrequest_show',
1456 1457 repo_name=pull_request.target_repo.scm_instance().name,
1457 1458 pull_request_id=pull_request.pull_request_id))
1458 1459 assert response.status_int == 200
1459 1460
1460 1461 source = response.assert_response().get_element('.pr-source-info')
1461 1462 source_parent = source.getparent()
1462 1463 assert len(source_parent) == 1
1463 1464
1464 1465 target = response.assert_response().get_element('.pr-target-info')
1465 1466 target_parent = target.getparent()
1466 1467 assert len(target_parent) == 1
1467 1468
1468 1469 expected_origin_link = route_path(
1469 1470 'repo_commits',
1470 1471 repo_name=pull_request.source_repo.scm_instance().name,
1471 1472 params=dict(branch='origin'))
1472 1473 expected_target_link = route_path(
1473 1474 'repo_commits',
1474 1475 repo_name=pull_request.target_repo.scm_instance().name,
1475 1476 params=dict(branch='target'))
1476 1477 assert source_parent.attrib['href'] == expected_origin_link
1477 1478 assert target_parent.attrib['href'] == expected_target_link
1478 1479
1479 1480 def test_bookmark_is_not_a_link(self, pr_util):
1480 1481 pull_request = pr_util.create_pull_request()
1481 1482 pull_request.source_ref = 'bookmark:origin:1234567890abcdef'
1482 1483 pull_request.target_ref = 'bookmark:target:abcdef1234567890'
1483 1484 Session().add(pull_request)
1484 1485 Session().commit()
1485 1486
1486 1487 response = self.app.get(route_path(
1487 1488 'pullrequest_show',
1488 1489 repo_name=pull_request.target_repo.scm_instance().name,
1489 1490 pull_request_id=pull_request.pull_request_id))
1490 1491 assert response.status_int == 200
1491 1492
1492 1493 source = response.assert_response().get_element('.pr-source-info')
1493 1494 assert source.text.strip() == 'bookmark:origin'
1494 1495 assert source.getparent().attrib.get('href') is None
1495 1496
1496 1497 target = response.assert_response().get_element('.pr-target-info')
1497 1498 assert target.text.strip() == 'bookmark:target'
1498 1499 assert target.getparent().attrib.get('href') is None
1499 1500
1500 1501 def test_tag_is_not_a_link(self, pr_util):
1501 1502 pull_request = pr_util.create_pull_request()
1502 1503 pull_request.source_ref = 'tag:origin:1234567890abcdef'
1503 1504 pull_request.target_ref = 'tag:target:abcdef1234567890'
1504 1505 Session().add(pull_request)
1505 1506 Session().commit()
1506 1507
1507 1508 response = self.app.get(route_path(
1508 1509 'pullrequest_show',
1509 1510 repo_name=pull_request.target_repo.scm_instance().name,
1510 1511 pull_request_id=pull_request.pull_request_id))
1511 1512 assert response.status_int == 200
1512 1513
1513 1514 source = response.assert_response().get_element('.pr-source-info')
1514 1515 assert source.text.strip() == 'tag:origin'
1515 1516 assert source.getparent().attrib.get('href') is None
1516 1517
1517 1518 target = response.assert_response().get_element('.pr-target-info')
1518 1519 assert target.text.strip() == 'tag:target'
1519 1520 assert target.getparent().attrib.get('href') is None
1520 1521
1521 1522 @pytest.mark.parametrize('mergeable', [True, False])
1522 1523 def test_shadow_repository_link(
1523 1524 self, mergeable, pr_util, http_host_only_stub):
1524 1525 """
1525 1526 Check that the pull request summary page displays a link to the shadow
1526 1527 repository if the pull request is mergeable. If it is not mergeable
1527 1528 the link should not be displayed.
1528 1529 """
1529 1530 pull_request = pr_util.create_pull_request(
1530 1531 mergeable=mergeable, enable_notifications=False)
1531 1532 target_repo = pull_request.target_repo.scm_instance()
1532 1533 pr_id = pull_request.pull_request_id
1533 1534 shadow_url = '{host}/{repo}/pull-request/{pr_id}/repository'.format(
1534 1535 host=http_host_only_stub, repo=target_repo.name, pr_id=pr_id)
1535 1536
1536 1537 response = self.app.get(route_path(
1537 1538 'pullrequest_show',
1538 1539 repo_name=target_repo.name,
1539 1540 pull_request_id=pr_id))
1540 1541
1541 1542 if mergeable:
1542 1543 response.assert_response().element_value_contains(
1543 1544 'input.pr-mergeinfo', shadow_url)
1544 1545 response.assert_response().element_value_contains(
1545 1546 'input.pr-mergeinfo ', 'pr-merge')
1546 1547 else:
1547 1548 response.assert_response().no_element_exists('.pr-mergeinfo')
1548 1549
1549 1550
1550 1551 @pytest.mark.usefixtures('app')
1551 1552 @pytest.mark.backends("git", "hg")
1552 1553 class TestPullrequestsControllerDelete(object):
1553 1554 def test_pull_request_delete_button_permissions_admin(
1554 1555 self, autologin_user, user_admin, pr_util):
1555 1556 pull_request = pr_util.create_pull_request(
1556 1557 author=user_admin.username, enable_notifications=False)
1557 1558
1558 1559 response = self.app.get(route_path(
1559 1560 'pullrequest_show',
1560 1561 repo_name=pull_request.target_repo.scm_instance().name,
1561 1562 pull_request_id=pull_request.pull_request_id))
1562 1563
1563 1564 response.mustcontain('id="delete_pullrequest"')
1564 1565 response.mustcontain('Confirm to delete this pull request')
1565 1566
1566 1567 def test_pull_request_delete_button_permissions_owner(
1567 1568 self, autologin_regular_user, user_regular, pr_util):
1568 1569 pull_request = pr_util.create_pull_request(
1569 1570 author=user_regular.username, enable_notifications=False)
1570 1571
1571 1572 response = self.app.get(route_path(
1572 1573 'pullrequest_show',
1573 1574 repo_name=pull_request.target_repo.scm_instance().name,
1574 1575 pull_request_id=pull_request.pull_request_id))
1575 1576
1576 1577 response.mustcontain('id="delete_pullrequest"')
1577 1578 response.mustcontain('Confirm to delete this pull request')
1578 1579
1579 1580 def test_pull_request_delete_button_permissions_forbidden(
1580 1581 self, autologin_regular_user, user_regular, user_admin, pr_util):
1581 1582 pull_request = pr_util.create_pull_request(
1582 1583 author=user_admin.username, enable_notifications=False)
1583 1584
1584 1585 response = self.app.get(route_path(
1585 1586 'pullrequest_show',
1586 1587 repo_name=pull_request.target_repo.scm_instance().name,
1587 1588 pull_request_id=pull_request.pull_request_id))
1588 1589 response.mustcontain(no=['id="delete_pullrequest"'])
1589 1590 response.mustcontain(no=['Confirm to delete this pull request'])
1590 1591
1591 1592 def test_pull_request_delete_button_permissions_can_update_cannot_delete(
1592 1593 self, autologin_regular_user, user_regular, user_admin, pr_util,
1593 1594 user_util):
1594 1595
1595 1596 pull_request = pr_util.create_pull_request(
1596 1597 author=user_admin.username, enable_notifications=False)
1597 1598
1598 1599 user_util.grant_user_permission_to_repo(
1599 1600 pull_request.target_repo, user_regular,
1600 1601 'repository.write')
1601 1602
1602 1603 response = self.app.get(route_path(
1603 1604 'pullrequest_show',
1604 1605 repo_name=pull_request.target_repo.scm_instance().name,
1605 1606 pull_request_id=pull_request.pull_request_id))
1606 1607
1607 1608 response.mustcontain('id="open_edit_pullrequest"')
1608 1609 response.mustcontain('id="delete_pullrequest"')
1609 1610 response.mustcontain(no=['Confirm to delete this pull request'])
1610 1611
1611 1612 def test_delete_comment_returns_404_if_comment_does_not_exist(
1612 1613 self, autologin_user, pr_util, user_admin, csrf_token, xhr_header):
1613 1614
1614 1615 pull_request = pr_util.create_pull_request(
1615 1616 author=user_admin.username, enable_notifications=False)
1616 1617
1617 1618 self.app.post(
1618 1619 route_path(
1619 1620 'pullrequest_comment_delete',
1620 1621 repo_name=pull_request.target_repo.scm_instance().name,
1621 1622 pull_request_id=pull_request.pull_request_id,
1622 1623 comment_id=1024404),
1623 1624 extra_environ=xhr_header,
1624 1625 params={'csrf_token': csrf_token},
1625 1626 status=404
1626 1627 )
1627 1628
1628 1629 def test_delete_comment(
1629 1630 self, autologin_user, pr_util, user_admin, csrf_token, xhr_header):
1630 1631
1631 1632 pull_request = pr_util.create_pull_request(
1632 1633 author=user_admin.username, enable_notifications=False)
1633 1634 comment = pr_util.create_comment()
1634 1635 comment_id = comment.comment_id
1635 1636
1636 1637 response = self.app.post(
1637 1638 route_path(
1638 1639 'pullrequest_comment_delete',
1639 1640 repo_name=pull_request.target_repo.scm_instance().name,
1640 1641 pull_request_id=pull_request.pull_request_id,
1641 1642 comment_id=comment_id),
1642 1643 extra_environ=xhr_header,
1643 1644 params={'csrf_token': csrf_token},
1644 1645 status=200
1645 1646 )
1646 1647 assert response.text == 'true'
1647 1648
1648 1649 @pytest.mark.parametrize('url_type', [
1649 1650 'pullrequest_new',
1650 1651 'pullrequest_create',
1651 1652 'pullrequest_update',
1652 1653 'pullrequest_merge',
1653 1654 ])
1654 1655 def test_pull_request_is_forbidden_on_archived_repo(
1655 1656 self, autologin_user, backend, xhr_header, user_util, url_type):
1656 1657
1657 1658 # create a temporary repo
1658 1659 source = user_util.create_repo(repo_type=backend.alias)
1659 1660 repo_name = source.repo_name
1660 1661 repo = Repository.get_by_repo_name(repo_name)
1661 1662 repo.archived = True
1662 1663 Session().commit()
1663 1664
1664 1665 response = self.app.get(
1665 1666 route_path(url_type, repo_name=repo_name, pull_request_id=1), status=302)
1666 1667
1667 1668 msg = 'Action not supported for archived repository.'
1668 1669 assert_session_flash(response, msg)
1669 1670
1670 1671
1671 1672 def assert_pull_request_status(pull_request, expected_status):
1672 1673 status = ChangesetStatusModel().calculated_review_status(pull_request=pull_request)
1673 1674 assert status == expected_status
1674 1675
1675 1676
1676 1677 @pytest.mark.parametrize('route', ['pullrequest_new', 'pullrequest_create'])
1677 1678 @pytest.mark.usefixtures("autologin_user")
1678 1679 def test_forbidde_to_repo_summary_for_svn_repositories(backend_svn, app, route):
1679 1680 app.get(route_path(route, repo_name=backend_svn.repo_name), status=404)
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
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
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
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
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
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
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
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
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